From fa8aca27c035136d5441889cd004dce073de5334 Mon Sep 17 00:00:00 2001 From: Dan Martin <93075337+Mingqwqqaq@users.noreply.github.com> Date: Thu, 13 Aug 2026 17:57:45 +0800 Subject: [PATCH] feat(StructuralOptimization): add composite laminate stacking benchmark --- TASK_DETAILS.md | 6 +- TASK_DETAILS_zh-CN.md | 6 +- .../CompositeLaminateStacking/README.md | 53 ++++ .../CompositeLaminateStacking/README_zh-CN.md | 52 ++++ .../CompositeLaminateStacking/Task.md | 127 ++++++++ .../CompositeLaminateStacking/Task_zh-CN.md | 87 ++++++ .../baseline/result_log.txt | 32 ++ .../baseline/solution.py | 10 + .../frontier_eval/agent_files.txt | 7 + .../frontier_eval/artifact_files.txt | 2 + .../frontier_eval/candidate_destination.txt | 1 + .../frontier_eval/constraints.txt | 12 + .../frontier_eval/copy_files.txt | 12 + .../frontier_eval/eval_command.txt | 1 + .../frontier_eval/initial_program.txt | 1 + .../frontier_eval/readonly_files.txt | 13 + .../references/config.json | 120 ++++++++ .../references/design_notes.md | 78 +++++ .../CompositeLaminateStacking/scripts/init.py | 14 + .../verification/design_runtime.py | 276 ++++++++++++++++++ .../verification/design_worker.py | 98 +++++++ .../verification/evaluator.py | 221 ++++++++++++++ .../verification/mechanics.py | 266 +++++++++++++++++ .../verification/requirements.txt | 1 + .../verification/test_evaluator.py | 103 +++++++ .../verification/test_mechanics.py | 71 +++++ benchmarks/StructuralOptimization/README.md | 19 +- .../StructuralOptimization/README_zh-CN.md | 19 +- .../task/composite_laminate_stacking.yaml | 6 + 29 files changed, 1694 insertions(+), 20 deletions(-) create mode 100644 benchmarks/StructuralOptimization/CompositeLaminateStacking/README.md create mode 100644 benchmarks/StructuralOptimization/CompositeLaminateStacking/README_zh-CN.md create mode 100644 benchmarks/StructuralOptimization/CompositeLaminateStacking/Task.md create mode 100644 benchmarks/StructuralOptimization/CompositeLaminateStacking/Task_zh-CN.md create mode 100644 benchmarks/StructuralOptimization/CompositeLaminateStacking/baseline/result_log.txt create mode 100644 benchmarks/StructuralOptimization/CompositeLaminateStacking/baseline/solution.py create mode 100644 benchmarks/StructuralOptimization/CompositeLaminateStacking/frontier_eval/agent_files.txt create mode 100644 benchmarks/StructuralOptimization/CompositeLaminateStacking/frontier_eval/artifact_files.txt create mode 100644 benchmarks/StructuralOptimization/CompositeLaminateStacking/frontier_eval/candidate_destination.txt create mode 100644 benchmarks/StructuralOptimization/CompositeLaminateStacking/frontier_eval/constraints.txt create mode 100644 benchmarks/StructuralOptimization/CompositeLaminateStacking/frontier_eval/copy_files.txt create mode 100644 benchmarks/StructuralOptimization/CompositeLaminateStacking/frontier_eval/eval_command.txt create mode 100644 benchmarks/StructuralOptimization/CompositeLaminateStacking/frontier_eval/initial_program.txt create mode 100644 benchmarks/StructuralOptimization/CompositeLaminateStacking/frontier_eval/readonly_files.txt create mode 100644 benchmarks/StructuralOptimization/CompositeLaminateStacking/references/config.json create mode 100644 benchmarks/StructuralOptimization/CompositeLaminateStacking/references/design_notes.md create mode 100644 benchmarks/StructuralOptimization/CompositeLaminateStacking/scripts/init.py create mode 100644 benchmarks/StructuralOptimization/CompositeLaminateStacking/verification/design_runtime.py create mode 100644 benchmarks/StructuralOptimization/CompositeLaminateStacking/verification/design_worker.py create mode 100644 benchmarks/StructuralOptimization/CompositeLaminateStacking/verification/evaluator.py create mode 100644 benchmarks/StructuralOptimization/CompositeLaminateStacking/verification/mechanics.py create mode 100644 benchmarks/StructuralOptimization/CompositeLaminateStacking/verification/requirements.txt create mode 100644 benchmarks/StructuralOptimization/CompositeLaminateStacking/verification/test_evaluator.py create mode 100644 benchmarks/StructuralOptimization/CompositeLaminateStacking/verification/test_mechanics.py create mode 100644 frontier_eval/conf/task/composite_laminate_stacking.yaml diff --git a/TASK_DETAILS.md b/TASK_DETAILS.md index 7475c4cc..7a9344a2 100644 --- a/TASK_DETAILS.md +++ b/TASK_DETAILS.md @@ -286,7 +286,7 @@ We welcome new engineering problem ideas — even without complete verification JSSP YN family (Yamada and Nakano 1992) - StructuralOptimization + StructuralOptimization ISCSO2015 Minimize weight of 45-bar 2D truss under stress / displacement constraints @@ -302,6 +302,10 @@ We welcome new engineering problem ideas — even without complete verification PyMOTOSIMPCompliance pyMOTO-based 2D beam topology optimization (SIMP + OC/MMA) under a volume-fraction constraint + + CompositeLaminateStacking + Optimize balanced, symmetric 48-ply composite laminates for buckling and maximum-strain failure across ten plate cases + Robotics DynamicObstacleAvoidanceNavigation diff --git a/TASK_DETAILS_zh-CN.md b/TASK_DETAILS_zh-CN.md index e2a070a2..ef1be0c3 100644 --- a/TASK_DETAILS_zh-CN.md +++ b/TASK_DETAILS_zh-CN.md @@ -286,7 +286,7 @@ Frontier-Eng 目前已覆盖以下领域的任务。每个任务均配有可运 JSSP YN 家族(Yamada & Nakano 1992) - StructuralOptimization + StructuralOptimization ISCSO2015 在应力 / 位移约束下最小化 45 杆 2D 桁架重量 @@ -302,6 +302,10 @@ Frontier-Eng 目前已覆盖以下领域的任务。每个任务均配有可运 PyMOTOSIMPCompliance 基于 pyMOTO 的 2D 梁拓扑优化(SIMP + OC/MMA),体积分数约束 + + CompositeLaminateStacking + 在十个板工况下优化平衡、对称的 48 层复合材料铺层,同时提高屈曲和最大应变失效性能 + Robotics DynamicObstacleAvoidanceNavigation diff --git a/benchmarks/StructuralOptimization/CompositeLaminateStacking/README.md b/benchmarks/StructuralOptimization/CompositeLaminateStacking/README.md new file mode 100644 index 00000000..2a9d8374 --- /dev/null +++ b/benchmarks/StructuralOptimization/CompositeLaminateStacking/README.md @@ -0,0 +1,53 @@ +# CompositeLaminateStacking + +Optimize balanced, symmetric 48-ply composite laminates across ten simply supported +plate cases derived from an MIT-licensed published benchmark. + +## What the agent edits + +Only [`scripts/init.py`](scripts/init.py) is editable. Implement: + +```python +def design_laminates(cases: list[dict]) -> dict[str, list[int]]: + ... +``` + +Return one list of 12 integer base angles in `[0, 90]` for every `case_id`. The evaluator +expands the list into a balanced, symmetric 48-ply stack. See [`Task.md`](Task.md) for the +complete contract and [`references/design_notes.md`](references/design_notes.md) for the +mechanics and source cross-check. + +## Requirements + +- Python 3.10+ +- NumPy +- no GPU, Docker, external data, or proprietary solver + +Install task-local requirements if needed: + +```bash +python -m pip install -r verification/requirements.txt +``` + +## Direct evaluation + +From this task directory: + +```bash +python verification/evaluator.py scripts/init.py +python -m unittest discover -s verification -p "test_*.py" -v +``` + +## Unified evaluation + +From the repository root: + +```bash +python -m frontier_eval \ + task=unified \ + task.benchmark=StructuralOptimization/CompositeLaminateStacking \ + algorithm=openevolve \ + algorithm.iterations=0 +``` + +The baseline is feasible in all ten cases and scores 50. Higher is better. diff --git a/benchmarks/StructuralOptimization/CompositeLaminateStacking/README_zh-CN.md b/benchmarks/StructuralOptimization/CompositeLaminateStacking/README_zh-CN.md new file mode 100644 index 00000000..8fea7f7d --- /dev/null +++ b/benchmarks/StructuralOptimization/CompositeLaminateStacking/README_zh-CN.md @@ -0,0 +1,52 @@ +# CompositeLaminateStacking + +在十个简支板工况上优化平衡、对称的 48 层复合材料铺层。任务参数来自一个采用 +MIT 许可证发布的真实研究 benchmark。 + +## Agent 修改内容 + +仅允许修改 [`scripts/init.py`](scripts/init.py),并实现: + +```python +def design_laminates(cases: list[dict]) -> dict[str, list[int]]: + ... +``` + +必须为每个 `case_id` 返回一个包含 12 个 `[0, 90]` 区间整数角度的列表。评测器会 +把它展开为平衡、对称的 48 层铺层。完整接口见 [`Task_zh-CN.md`](Task_zh-CN.md), +力学模型与来源交叉验证见 [`references/design_notes.md`](references/design_notes.md)。 + +## 环境要求 + +- Python 3.10+ +- NumPy +- 不需要 GPU、Docker、外部数据或商业求解器 + +如有需要,可安装任务依赖: + +```bash +python -m pip install -r verification/requirements.txt +``` + +## 直接评测 + +在当前任务目录运行: + +```bash +python verification/evaluator.py scripts/init.py +python -m unittest discover -s verification -p "test_*.py" -v +``` + +## Unified 评测 + +在仓库根目录运行: + +```bash +python -m frontier_eval \ + task=unified \ + task.benchmark=StructuralOptimization/CompositeLaminateStacking \ + algorithm=openevolve \ + algorithm.iterations=0 +``` + +baseline 在全部十个工况中均可行,得分为 50;分数越高越好。 diff --git a/benchmarks/StructuralOptimization/CompositeLaminateStacking/Task.md b/benchmarks/StructuralOptimization/CompositeLaminateStacking/Task.md new file mode 100644 index 00000000..af55edc0 --- /dev/null +++ b/benchmarks/StructuralOptimization/CompositeLaminateStacking/Task.md @@ -0,0 +1,127 @@ +# Task: Composite Laminate Stacking Optimization + +## 1. Engineering problem + +Design the stacking sequence of a 48-ply orthotropic composite plate. A good laminate +must resist both material failure and plate buckling under several geometries and load +ratios. This is a discrete, coupled structural-design problem: moving stiff plies toward +the surfaces can help buckling, while changing fiber directions alters in-plane strength +and can hurt another load case. + +The task is derived from the MIT-licensed Zenodo release *Beyond Double-Double Theory: +n-Directional Stacking Sequence Optimisation in Composite Laminates* +([doi:10.5281/zenodo.15864525](https://doi.org/10.5281/zenodo.15864525)). It preserves +the released 48-ply construction, orthotropic material, ply thickness, five aspect ratios, +uniaxial/biaxial compression, strain allowables, and Haftka reference layup. The evaluator +is an independent NumPy implementation documented in `references/design_notes.md`. + +## 2. Candidate API + +Implement in `scripts/init.py`: + +```python +def design_laminates(cases: list[dict]) -> dict[str, list[int]]: + ... +``` + +The function receives all public cases in one call. Each case contains only: + +```python +{ + "case_id": str, + "aspect_ratio": float, + "a_mm": float, + "b_mm": float, + "Nx_N_per_mm": float, + "Ny_N_per_mm": float, +} +``` + +Compression is negative. `Ny_N_per_mm == 0` denotes uniaxial loading; a negative value +denotes biaxial compression. + +Return a dictionary with exactly the supplied `case_id` values. Each value must be a JSON +list containing exactly 12 finite integer angles in the inclusive interval `[0, 90]`. + +## 3. Layup construction + +For candidate angles `theta_1 ... theta_12`, the verifier constructs the first half as: + +```text +[theta_1, -theta_1, theta_2, -theta_2, ..., theta_12, -theta_12] +``` + +and appends its reverse. The result has 48 plies and is balanced and symmetric by +construction. The candidate therefore optimizes both angle selection and through-thickness +ordering without needing to implement manufacturing-constraint repair. + +## 4. Evaluation cases + +Ten deterministic cases combine aspect ratios `0.5, 1, 2, 3, 4` with: + +- uniaxial compression: `Nx = -0.175126835 N/mm`, `Ny = 0`; +- biaxial compression: `Nx = -0.175126835 N/mm`, `Ny = -0.087563418 N/mm`. + +The plate widths are fixed at `127 mm`; lengths vary with aspect ratio. Material and +allowable values are committed in `references/config.json`. + +## 5. Physics + +The verifier applies classical lamination theory to compute `A`, `B`, and `D`. It then +computes: + +1. a maximum-strain failure load factor from the most critical longitudinal, transverse, + or shear ply strain under the released 1.5 design-load factor; +2. a simply supported plate buckling load factor from a double-sine Ritz basis, numerical + quadrature, and a symmetric generalized eigenproblem. + +The governing reserve factor is: + +```text +reserve = min(failure_load_factor, buckling_load_factor) +``` + +## 6. Score + +Each case is normalized against the released Haftka reference stacking sequence: + +```text +case_score = clip(50 + 50*tanh(log(reserve/anchor_reserve)/0.5), 0, 100) +``` + +The final diagnostic score is: + +```text +0.75 * mean(case_scores) + 0.25 * percentile(case_scores, 20) +``` + +A valid candidate receives that value as `combined_score`. Missing cases, extra case ids, +bad types, non-integer/out-of-range angles, timeouts, import failures, or invalid mechanics +make the candidate invalid and set `combined_score` to zero. Per-case diagnostic feedback +remains available. + +## 7. Runtime and integrity contract + +- Keep `design_laminates` deterministic and self-contained. +- Candidate import and execution occur in a separate Python process with a bounded runtime. +- The process receives cases and returns designs through a JSON-lines protocol. +- Candidate stdout is discarded so it cannot corrupt the evaluator protocol. +- This is process isolation, not an operating-system security sandbox. +- Do not read or modify evaluator, reference, output, or environment-secret files. +- Edit only the region between the `EVOLVE-BLOCK` markers in `scripts/init.py`. + +## 8. Commands + +Direct evaluation, from this directory: + +```bash +python verification/evaluator.py scripts/init.py +``` + +Unified evaluation, from the repository root: + +```bash +python -m frontier_eval task=unified \ + task.benchmark=StructuralOptimization/CompositeLaminateStacking \ + algorithm=openevolve algorithm.iterations=0 +``` diff --git a/benchmarks/StructuralOptimization/CompositeLaminateStacking/Task_zh-CN.md b/benchmarks/StructuralOptimization/CompositeLaminateStacking/Task_zh-CN.md new file mode 100644 index 00000000..90074496 --- /dev/null +++ b/benchmarks/StructuralOptimization/CompositeLaminateStacking/Task_zh-CN.md @@ -0,0 +1,87 @@ +# 任务:复合材料层合板铺层优化 + +## 1. 工程问题 + +设计一个 48 层正交各向异性复合材料板的铺层顺序。铺层需要在多种几何尺寸和载荷 +比例下同时抵抗材料失效和板屈曲。这是一个耦合的离散结构设计问题:把高刚度方向 +的铺层放到外侧可能提高屈曲性能,但改变纤维方向也会改变面内强度,并可能损害另一 +载荷工况。 + +本任务源自采用 MIT 许可证发布的 Zenodo 数据集 *Beyond Double-Double Theory: +n-Directional Stacking Sequence Optimisation in Composite Laminates* +([doi:10.5281/zenodo.15864525](https://doi.org/10.5281/zenodo.15864525))。任务保留了 +原发布代码中的 48 层构造、正交各向异性材料、单层厚度、五种长宽比、单轴/双轴压缩、 +应变许用值和 Haftka 参考铺层。评测器采用独立的 NumPy 实现,详见 +`references/design_notes.md`。 + +## 2. 候选程序接口 + +在 `scripts/init.py` 中实现: + +```python +def design_laminates(cases: list[dict]) -> dict[str, list[int]]: + ... +``` + +函数一次收到所有公开工况。每个工况仅包含 `case_id`、长宽比、板长宽和 `Nx/Ny` +膜内载荷。负值表示压缩;`Ny_N_per_mm == 0` 表示单轴载荷。 + +返回字典必须恰好包含输入给出的全部 `case_id`。每个值必须是可 JSON 序列化的列表, +包含恰好 12 个 `[0, 90]` 闭区间内的有限整数角度。 + +## 3. 铺层构造 + +对于候选角度 `theta_1 ... theta_12`,评测器先构造: + +```text +[theta_1, -theta_1, theta_2, -theta_2, ..., theta_12, -theta_12] +``` + +再追加其逆序,得到平衡、对称的 48 层铺层。候选程序需要优化角度选择和厚度方向顺序, +但不需要自行修复制造约束。 + +## 4. 评测工况与物理模型 + +十个确定性工况由长宽比 `0.5, 1, 2, 3, 4` 与单轴、双轴压缩组合而成。板宽固定为 +`127 mm`,长度随长宽比变化。材料、载荷和许用值记录在 `references/config.json`。 + +评测器使用经典层合板理论计算 `A`、`B`、`D`,然后求得: + +1. 由最危险的纵向、横向或剪切层内应变确定的最大应变失效载荷因子; +2. 通过双正弦 Ritz 基、数值积分和对称广义特征值问题得到的简支板屈曲载荷因子。 + +控制储备因子为: + +```text +reserve = min(failure_load_factor, buckling_load_factor) +``` + +## 5. 评分 + +每个工况相对于已发布的 Haftka 参考铺层归一化: + +```text +case_score = clip(50 + 50*tanh(log(reserve/anchor_reserve)/0.5), 0, 100) +``` + +最终诊断分数为 `75%` 平均工况分加 `25%` 的第 20 百分位工况分。缺少工况、多余 ID、 +错误类型、非整数或越界角度、超时、导入失败或非法力学结果都会令 `combined_score=0`; +逐工况诊断反馈仍会保留。 + +## 6. 运行与完整性约束 + +- `design_laminates` 必须确定性、可独立运行。 +- 候选程序在受时间限制的独立 Python 进程中导入和执行。 +- 工况和设计通过 JSON-lines 协议交换;候选 stdout 会被丢弃。 +- 这是进程隔离,并非操作系统安全沙箱。 +- 不得读取或修改 evaluator、参考参数、输出或环境机密文件。 +- 仅修改 `scripts/init.py` 内 `EVOLVE-BLOCK` 标记之间的区域。 + +## 7. 运行命令 + +```bash +python verification/evaluator.py scripts/init.py +python -m frontier_eval task=unified \ + task.benchmark=StructuralOptimization/CompositeLaminateStacking \ + algorithm=openevolve algorithm.iterations=0 +``` diff --git a/benchmarks/StructuralOptimization/CompositeLaminateStacking/baseline/result_log.txt b/benchmarks/StructuralOptimization/CompositeLaminateStacking/baseline/result_log.txt new file mode 100644 index 00000000..00e9943a --- /dev/null +++ b/benchmarks/StructuralOptimization/CompositeLaminateStacking/baseline/result_log.txt @@ -0,0 +1,32 @@ +CompositeLaminateStacking baseline verification +================================================ + +Reference dataset: + Dataset for Beyond Double-Double Theory: n-Directional Stacking Sequence + Optimisation in Composite Laminates + DOI: 10.5281/zenodo.15864525 + License: MIT + +Verification date: 2026-08-13 + +Direct evaluator: + python verification/evaluator.py scripts/init.py + feasible_cases: 10 / 10 + diagnostic_score: 50.0 + combined_score: 50.0 + +Unit tests: + python -m unittest discover -s verification -p "test_*.py" -v + result: 13 tests passed + +Unified evaluator: + python -m frontier_eval task=unified \ + task.benchmark=StructuralOptimization/CompositeLaminateStacking \ + algorithm=openevolve algorithm.iterations=0 + benchmark_returncode: 0 + valid: 1 + feasible_cases: 10 + combined_score: 50.0 + +The unified run was also exercised on Windows with platform-specific shell and +Python-path overrides; the benchmark output and score matched the direct run. diff --git a/benchmarks/StructuralOptimization/CompositeLaminateStacking/baseline/solution.py b/benchmarks/StructuralOptimization/CompositeLaminateStacking/baseline/solution.py new file mode 100644 index 00000000..83ea527f --- /dev/null +++ b/benchmarks/StructuralOptimization/CompositeLaminateStacking/baseline/solution.py @@ -0,0 +1,10 @@ +"""Recorded baseline; intentionally identical to scripts/init.py.""" + +from __future__ import annotations + +from typing import Any + + +def design_laminates(cases: list[dict[str, Any]]) -> dict[str, list[int]]: + haftka_reference = [90, 45, 45, 90, 45, 90, 45, 45, 45, 45, 45, 45] + return {str(case["case_id"]): list(haftka_reference) for case in cases} diff --git a/benchmarks/StructuralOptimization/CompositeLaminateStacking/frontier_eval/agent_files.txt b/benchmarks/StructuralOptimization/CompositeLaminateStacking/frontier_eval/agent_files.txt new file mode 100644 index 00000000..e8a78865 --- /dev/null +++ b/benchmarks/StructuralOptimization/CompositeLaminateStacking/frontier_eval/agent_files.txt @@ -0,0 +1,7 @@ +README.md +README_zh-CN.md +Task.md +Task_zh-CN.md +scripts/init.py +references/design_notes.md +frontier_eval/constraints.txt diff --git a/benchmarks/StructuralOptimization/CompositeLaminateStacking/frontier_eval/artifact_files.txt b/benchmarks/StructuralOptimization/CompositeLaminateStacking/frontier_eval/artifact_files.txt new file mode 100644 index 00000000..76dc893a --- /dev/null +++ b/benchmarks/StructuralOptimization/CompositeLaminateStacking/frontier_eval/artifact_files.txt @@ -0,0 +1,2 @@ +metrics.json +artifacts.json diff --git a/benchmarks/StructuralOptimization/CompositeLaminateStacking/frontier_eval/candidate_destination.txt b/benchmarks/StructuralOptimization/CompositeLaminateStacking/frontier_eval/candidate_destination.txt new file mode 100644 index 00000000..b9411b3d --- /dev/null +++ b/benchmarks/StructuralOptimization/CompositeLaminateStacking/frontier_eval/candidate_destination.txt @@ -0,0 +1 @@ +scripts/init.py diff --git a/benchmarks/StructuralOptimization/CompositeLaminateStacking/frontier_eval/constraints.txt b/benchmarks/StructuralOptimization/CompositeLaminateStacking/frontier_eval/constraints.txt new file mode 100644 index 00000000..7e9a961b --- /dev/null +++ b/benchmarks/StructuralOptimization/CompositeLaminateStacking/frontier_eval/constraints.txt @@ -0,0 +1,12 @@ +Composite laminate stacking constraints: +1) Edit only `scripts/init.py` inside the EVOLVE-BLOCK markers. +2) Keep `design_laminates(cases) -> dict` working. +3) Return exactly one design for every supplied case_id and no unknown case ids. +4) Each design is a JSON list of exactly 12 finite integer angles in [0, 90] degrees. +5) The evaluator expands every base angle theta to [theta, -theta], then mirrors the + 24-ply half-stack to form a balanced, symmetric 48-ply laminate. +6) Maximize the governing reserve factor, min(buckling load factor, maximum-strain + failure load factor), across all uniaxial and biaxial aspect-ratio cases. +7) Keep the design function deterministic, self-contained, and JSON serializable. +8) Do not read, write, or modify evaluator, reference, result, or environment-secret files. + Candidate code runs in a separate process with a bounded total runtime. diff --git a/benchmarks/StructuralOptimization/CompositeLaminateStacking/frontier_eval/copy_files.txt b/benchmarks/StructuralOptimization/CompositeLaminateStacking/frontier_eval/copy_files.txt new file mode 100644 index 00000000..a167c883 --- /dev/null +++ b/benchmarks/StructuralOptimization/CompositeLaminateStacking/frontier_eval/copy_files.txt @@ -0,0 +1,12 @@ +README.md +README_zh-CN.md +Task.md +Task_zh-CN.md +scripts/init.py +verification/evaluator.py +verification/mechanics.py +verification/design_runtime.py +verification/design_worker.py +verification/requirements.txt +references/config.json +references/design_notes.md diff --git a/benchmarks/StructuralOptimization/CompositeLaminateStacking/frontier_eval/eval_command.txt b/benchmarks/StructuralOptimization/CompositeLaminateStacking/frontier_eval/eval_command.txt new file mode 100644 index 00000000..527d23b5 --- /dev/null +++ b/benchmarks/StructuralOptimization/CompositeLaminateStacking/frontier_eval/eval_command.txt @@ -0,0 +1 @@ +{python} verification/evaluator.py {candidate} --metrics-out metrics.json --artifacts-out artifacts.json diff --git a/benchmarks/StructuralOptimization/CompositeLaminateStacking/frontier_eval/initial_program.txt b/benchmarks/StructuralOptimization/CompositeLaminateStacking/frontier_eval/initial_program.txt new file mode 100644 index 00000000..b9411b3d --- /dev/null +++ b/benchmarks/StructuralOptimization/CompositeLaminateStacking/frontier_eval/initial_program.txt @@ -0,0 +1 @@ +scripts/init.py diff --git a/benchmarks/StructuralOptimization/CompositeLaminateStacking/frontier_eval/readonly_files.txt b/benchmarks/StructuralOptimization/CompositeLaminateStacking/frontier_eval/readonly_files.txt new file mode 100644 index 00000000..7f0018cd --- /dev/null +++ b/benchmarks/StructuralOptimization/CompositeLaminateStacking/frontier_eval/readonly_files.txt @@ -0,0 +1,13 @@ +README.md +README_zh-CN.md +Task.md +Task_zh-CN.md +verification/evaluator.py +verification/mechanics.py +verification/design_runtime.py +verification/design_worker.py +verification/test_evaluator.py +verification/test_mechanics.py +verification/requirements.txt +references/config.json +references/design_notes.md diff --git a/benchmarks/StructuralOptimization/CompositeLaminateStacking/references/config.json b/benchmarks/StructuralOptimization/CompositeLaminateStacking/references/config.json new file mode 100644 index 00000000..56489bb5 --- /dev/null +++ b/benchmarks/StructuralOptimization/CompositeLaminateStacking/references/config.json @@ -0,0 +1,120 @@ +{ + "source": { + "title": "Dataset for Beyond Double-Double Theory: n-Directional Stacking Sequence Optimisation in Composite Laminates", + "authors": [ + "Jose Humberto S. Almeida Jr.", + "Emilia Balonek", + "Saullo G. P. Castro" + ], + "doi": "10.5281/zenodo.15864525", + "concept_doi": "10.5281/zenodo.15864524", + "license": "MIT" + }, + "material": { + "E11_MPa": 127550.0, + "E22_MPa": 13030.0, + "nu12": 0.3, + "G12_MPa": 6410.0 + }, + "laminate": { + "num_plies": 48, + "design_angles": 12, + "ply_thickness_mm": 0.127, + "angle_min_deg": 0, + "angle_max_deg": 90 + }, + "failure": { + "epsilon_1_allowable": 0.008, + "epsilon_2_allowable": 0.029, + "gamma_12_allowable": 0.015, + "design_load_factor": 1.5 + }, + "buckling": { + "modes_x": 5, + "modes_y": 5, + "quadrature_order": 12 + }, + "anchor_base_angles_deg": [90, 45, 45, 90, 45, 90, 45, 45, 45, 45, 45, 45], + "cases": [ + { + "case_id": "uniaxial_ar_0_5", + "aspect_ratio": 0.5, + "a_mm": 63.5, + "b_mm": 127.0, + "Nx_N_per_mm": -0.1751268352464764, + "Ny_N_per_mm": 0.0 + }, + { + "case_id": "uniaxial_ar_1_0", + "aspect_ratio": 1.0, + "a_mm": 127.0, + "b_mm": 127.0, + "Nx_N_per_mm": -0.1751268352464764, + "Ny_N_per_mm": 0.0 + }, + { + "case_id": "uniaxial_ar_2_0", + "aspect_ratio": 2.0, + "a_mm": 254.0, + "b_mm": 127.0, + "Nx_N_per_mm": -0.1751268352464764, + "Ny_N_per_mm": 0.0 + }, + { + "case_id": "uniaxial_ar_3_0", + "aspect_ratio": 3.0, + "a_mm": 381.0, + "b_mm": 127.0, + "Nx_N_per_mm": -0.1751268352464764, + "Ny_N_per_mm": 0.0 + }, + { + "case_id": "uniaxial_ar_4_0", + "aspect_ratio": 4.0, + "a_mm": 508.0, + "b_mm": 127.0, + "Nx_N_per_mm": -0.1751268352464764, + "Ny_N_per_mm": 0.0 + }, + { + "case_id": "biaxial_ar_0_5", + "aspect_ratio": 0.5, + "a_mm": 63.5, + "b_mm": 127.0, + "Nx_N_per_mm": -0.1751268352464764, + "Ny_N_per_mm": -0.0875634176232382 + }, + { + "case_id": "biaxial_ar_1_0", + "aspect_ratio": 1.0, + "a_mm": 127.0, + "b_mm": 127.0, + "Nx_N_per_mm": -0.1751268352464764, + "Ny_N_per_mm": -0.0875634176232382 + }, + { + "case_id": "biaxial_ar_2_0", + "aspect_ratio": 2.0, + "a_mm": 254.0, + "b_mm": 127.0, + "Nx_N_per_mm": -0.1751268352464764, + "Ny_N_per_mm": -0.0875634176232382 + }, + { + "case_id": "biaxial_ar_3_0", + "aspect_ratio": 3.0, + "a_mm": 381.0, + "b_mm": 127.0, + "Nx_N_per_mm": -0.1751268352464764, + "Ny_N_per_mm": -0.0875634176232382 + }, + { + "case_id": "biaxial_ar_4_0", + "aspect_ratio": 4.0, + "a_mm": 508.0, + "b_mm": 127.0, + "Nx_N_per_mm": -0.1751268352464764, + "Ny_N_per_mm": -0.0875634176232382 + } + ] +} diff --git a/benchmarks/StructuralOptimization/CompositeLaminateStacking/references/design_notes.md b/benchmarks/StructuralOptimization/CompositeLaminateStacking/references/design_notes.md new file mode 100644 index 00000000..be044677 --- /dev/null +++ b/benchmarks/StructuralOptimization/CompositeLaminateStacking/references/design_notes.md @@ -0,0 +1,78 @@ +# Design and verification notes + +## Provenance + +This benchmark is an independent, NumPy-only adaptation of: + +- J. H. S. Almeida Jr., E. Balonek, and S. G. P. Castro, + *Dataset for Beyond Double-Double Theory: n-Directional Stacking Sequence + Optimisation in Composite Laminates*, Zenodo, + [doi:10.5281/zenodo.15864525](https://doi.org/10.5281/zenodo.15864525), MIT License. +- R. T. Haftka, *Optimization of Composite Structures*, 1993, including the + 48-ply square-plate reference case used by the released code. + +The repository does not vendor the upstream optimizer or its dependencies. The material, +ply, geometry, loading, allowable-strain, and reference-layup values in `config.json` are +transcribed from the MIT-licensed release. The evaluator independently implements the +mechanics described below. + +## Design encoding + +The candidate returns 12 integer angles `theta_i` in `[0, 90]`. The verifier constructs +the first half of the laminate as: + +```text +[theta_1, -theta_1, theta_2, -theta_2, ..., theta_12, -theta_12] +``` + +and mirrors that sequence to obtain 48 plies. Thus every accepted design is balanced and +symmetric by construction. Angles `90` and `-90` describe the same material direction. + +## Classical lamination theory + +The lamina reduced-stiffness matrix uses the released orthotropic properties +`E11`, `E22`, `nu12`, and `G12`. Each ply stiffness is transformed into global axes and +integrated through the thickness to form the laminate `A`, `B`, and `D` matrices. The +symmetry construction makes `B` zero to numerical precision. + +## Maximum-strain failure factor + +For each nominal membrane load, the evaluator solves: + +```text +epsilon_0 = A^-1 N +``` + +after applying the released 1.5 design-load factor. Global strains are transformed into +every ply's material axes. The failure load factor is the smallest ratio between the +released allowable longitudinal, transverse, and shear strains and the corresponding +absolute ply strain. + +## Buckling factor + +The verifier approximates a simply supported plate with a double-sine Ritz basis. It +integrates bending and geometric stiffness with Gauss-Legendre quadrature, preserving +`D16` and `D26` coupling, and solves the symmetric generalized eigenproblem. Five modes +per direction are used in the committed configuration. + +For the released square biaxial anchor, the upstream code reports a failure factor of +`10394.81` and a BFSC finite-element buckling factor of `9998.19`. The independent +implementation returns approximately `10396.48` and `10430.16`, respectively: failure +agrees within 0.1%, and the lower-order Ritz buckling approximation agrees within 5%. +These bounds are enforced by regression tests. + +## Ranking score + +For each case, engineering performance is the governing reserve factor: + +```text +reserve = min(failure_load_factor, buckling_load_factor) +``` + +The case score is centered at 50 for the released Haftka reference layup and varies +smoothly with the logarithm of the candidate-to-anchor reserve ratio. The final score is +75% mean case score plus 25% twentieth-percentile case score. Invalid or incomplete +candidate output receives `combined_score = 0`, while per-case errors remain visible. + +The anchor only normalizes task difficulty. It is not an optimum: a simple load-aware +design exceeds 60 points in the regression suite, leaving measurable optimization room. diff --git a/benchmarks/StructuralOptimization/CompositeLaminateStacking/scripts/init.py b/benchmarks/StructuralOptimization/CompositeLaminateStacking/scripts/init.py new file mode 100644 index 00000000..1b9a8bdc --- /dev/null +++ b/benchmarks/StructuralOptimization/CompositeLaminateStacking/scripts/init.py @@ -0,0 +1,14 @@ +"""Feasible starting design for CompositeLaminateStacking.""" + +from __future__ import annotations + +from typing import Any + + +def design_laminates(cases: list[dict[str, Any]]) -> dict[str, list[int]]: + """Return twelve non-negative integer base angles for every requested case.""" + + # EVOLVE-BLOCK-START + haftka_reference = [90, 45, 45, 90, 45, 90, 45, 45, 45, 45, 45, 45] + return {str(case["case_id"]): list(haftka_reference) for case in cases} + # EVOLVE-BLOCK-END diff --git a/benchmarks/StructuralOptimization/CompositeLaminateStacking/verification/design_runtime.py b/benchmarks/StructuralOptimization/CompositeLaminateStacking/verification/design_runtime.py new file mode 100644 index 00000000..e347ef62 --- /dev/null +++ b/benchmarks/StructuralOptimization/CompositeLaminateStacking/verification/design_runtime.py @@ -0,0 +1,276 @@ +"""Bounded parent-side runtime for laminate candidate modules.""" + +from __future__ import annotations + +import json +import os +import queue +import shutil +import signal +import subprocess +import sys +import tempfile +import threading +import time +from collections import deque +from pathlib import Path +from typing import Any + + +MAX_RESPONSE_BYTES = 64 * 1024 +_EOF = object() + + +class DesignRuntimeError(RuntimeError): + pass + + +class DesignTimeoutError(DesignRuntimeError): + pass + + +class DesignProtocolError(DesignRuntimeError): + pass + + +class DesignCandidateError(DesignRuntimeError): + pass + + +def _clean_environment() -> dict[str, str]: + allowed = { + "COMSPEC", + "LANG", + "LC_ALL", + "NUMBER_OF_PROCESSORS", + "PATH", + "PATHEXT", + "SYSTEMDRIVE", + "SYSTEMROOT", + "TEMP", + "TMP", + "WINDIR", + } + env = {key: value for key, value in os.environ.items() if key.upper() in allowed} + env["PYTHONUTF8"] = "1" + env["PYTHONIOENCODING"] = "utf-8" + return env + + +class DesignRuntime: + def __init__( + self, + candidate_path: str | Path, + *, + startup_timeout_s: float = 3.0, + call_timeout_s: float = 8.0, + total_timeout_s: float = 12.0, + ) -> None: + if min(startup_timeout_s, call_timeout_s, total_timeout_s) <= 0: + raise ValueError("design timeouts must be positive") + source = Path(candidate_path).expanduser().resolve() + if not source.is_file(): + raise FileNotFoundError(source) + self.call_timeout_s = float(call_timeout_s) + self._deadline = time.monotonic() + float(total_timeout_s) + self._next_id = 1 + self._closed = False + self._responses: queue.Queue[Any] = queue.Queue() + self._stderr_chunks: deque[bytes] = deque() + self._stderr_size = 0 + self._tempdir = Path(tempfile.mkdtemp(prefix="laminate_design_")) + self._process: subprocess.Popen[bytes] | None = None + try: + copied_candidate = self._tempdir / "candidate.py" + shutil.copy2(source, copied_candidate) + worker = Path(__file__).with_name("design_worker.py").resolve() + popen_kwargs: dict[str, Any] = { + "cwd": str(self._tempdir), + "env": _clean_environment(), + "stdin": subprocess.PIPE, + "stdout": subprocess.PIPE, + "stderr": subprocess.PIPE, + "bufsize": 0, + } + if os.name == "nt": + popen_kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP + else: + popen_kwargs["start_new_session"] = True + self._process = subprocess.Popen( + [sys.executable, "-I", "-u", str(worker), str(copied_candidate)], + **popen_kwargs, + ) + assert self._process.stdout is not None + assert self._process.stderr is not None + threading.Thread(target=self._read_stdout, args=(self._process.stdout,), daemon=True).start() + threading.Thread(target=self._drain_stderr, args=(self._process.stderr,), daemon=True).start() + ready = self._receive(startup_timeout_s, "candidate import") + if not isinstance(ready, dict) or not ready.get("ok") or not ready.get("ready"): + self._raise_response_error(ready, "candidate import") + except BaseException: + self.close(force=True) + raise + + @property + def stderr_tail(self) -> str: + return b"".join(self._stderr_chunks).decode("utf-8", errors="replace") + + def _read_stdout(self, stream: Any) -> None: + try: + while True: + line = stream.readline(MAX_RESPONSE_BYTES + 1) + if not line: + break + self._responses.put(line) + if len(line) > MAX_RESPONSE_BYTES or not line.endswith(b"\n"): + break + finally: + self._responses.put(_EOF) + + def _drain_stderr(self, stream: Any) -> None: + while True: + chunk = stream.read(4096) + if not chunk: + return + self._stderr_chunks.append(chunk) + self._stderr_size += len(chunk) + while self._stderr_size > 16 * 1024 and self._stderr_chunks: + self._stderr_size -= len(self._stderr_chunks.popleft()) + + def _remaining(self) -> float: + return self._deadline - time.monotonic() + + def _receive(self, timeout_s: float, operation: str) -> dict[str, Any]: + timeout = min(float(timeout_s), self._remaining()) + if timeout <= 0: + self.close(force=True) + raise DesignTimeoutError("candidate total runtime budget exceeded") + try: + item = self._responses.get(timeout=timeout) + except queue.Empty as exc: + self.close(force=True) + raise DesignTimeoutError(f"{operation} timed out after {timeout:.3f}s") from exc + if item is _EOF: + code = None if self._process is None else self._process.poll() + detail = self.stderr_tail[-1000:] + raise DesignCandidateError( + f"candidate worker exited unexpectedly (code={code})" + + (f": {detail}" if detail else "") + ) + if not isinstance(item, bytes) or len(item) > MAX_RESPONSE_BYTES or not item.endswith(b"\n"): + self.close(force=True) + raise DesignProtocolError("candidate response exceeds 64 KiB or lacks newline") + try: + response = json.loads(item.decode("utf-8")) + except (UnicodeDecodeError, json.JSONDecodeError) as exc: + self.close(force=True) + raise DesignProtocolError("candidate returned invalid UTF-8 JSON") from exc + if not isinstance(response, dict): + raise DesignProtocolError("candidate response must be a JSON object") + return response + + @staticmethod + def _raise_response_error(response: Any, operation: str) -> None: + if isinstance(response, dict): + error = response.get("error") + if isinstance(error, dict): + raise DesignCandidateError( + f"{operation} failed: {error.get('type', 'CandidateError')}: " + f"{error.get('message', '')}" + ) + raise DesignProtocolError(f"malformed response during {operation}") + + def _rpc(self, operation: str, **payload: Any) -> dict[str, Any]: + if self._closed or self._process is None or self._process.poll() is not None: + raise DesignCandidateError("candidate worker is not running") + request_id = self._next_id + self._next_id += 1 + encoded = ( + json.dumps( + {"id": request_id, "op": operation, **payload}, + ensure_ascii=True, + allow_nan=False, + separators=(",", ":"), + ) + + "\n" + ).encode("utf-8") + try: + assert self._process.stdin is not None + self._process.stdin.write(encoded) + self._process.stdin.flush() + except (BrokenPipeError, OSError) as exc: + raise DesignCandidateError("candidate worker closed its input") from exc + response = self._receive(self.call_timeout_s, operation) + if response.get("id") != request_id: + self.close(force=True) + raise DesignProtocolError("candidate response id does not match request") + if not response.get("ok"): + self._raise_response_error(response, operation) + return response + + def design_laminates(self, cases: list[dict[str, Any]]) -> dict[str, Any]: + response = self._rpc("design", cases=cases) + designs = response.get("designs") + if not isinstance(designs, dict): + raise DesignProtocolError("design_laminates must return a JSON object") + return designs + + def close(self, *, force: bool = False) -> None: + if self._closed: + return + process = self._process + if process is not None and process.poll() is None and not force: + try: + self._rpc("shutdown") + process.wait(timeout=0.25) + except Exception: + force = True + if process is not None and process.poll() is None: + self._kill_process_tree(process) + self._closed = True + if process is not None: + for stream in (process.stdin, process.stdout, process.stderr): + try: + if stream is not None: + stream.close() + except OSError: + pass + shutil.rmtree(self._tempdir, ignore_errors=True) + + @staticmethod + def _kill_process_tree(process: subprocess.Popen[bytes]) -> None: + if process.poll() is not None: + return + if os.name == "nt": + try: + subprocess.run( + ["taskkill", "/PID", str(process.pid), "/T", "/F"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + timeout=2.0, + check=False, + ) + except Exception: + process.kill() + else: + try: + os.killpg(process.pid, signal.SIGKILL) + except (ProcessLookupError, PermissionError): + process.kill() + try: + process.wait(timeout=2.0) + except subprocess.TimeoutExpired: + process.kill() + process.wait(timeout=2.0) + + def __enter__(self) -> "DesignRuntime": + return self + + def __exit__(self, exc_type: Any, exc: Any, traceback: Any) -> None: + self.close() + + def __del__(self) -> None: + try: + self.close(force=True) + except Exception: + pass diff --git a/benchmarks/StructuralOptimization/CompositeLaminateStacking/verification/design_worker.py b/benchmarks/StructuralOptimization/CompositeLaminateStacking/verification/design_worker.py new file mode 100644 index 00000000..e399571b --- /dev/null +++ b/benchmarks/StructuralOptimization/CompositeLaminateStacking/verification/design_worker.py @@ -0,0 +1,98 @@ +"""JSON-lines worker for an untrusted laminate design module. + +This gives process and protocol isolation. It is not an operating-system sandbox. +""" + +from __future__ import annotations + +import importlib.util +import json +import os +import sys +from pathlib import Path +from typing import Any + + +_json_loads = json.loads +_json_dumps = json.dumps + + +def _protocol_streams() -> tuple[Any, Any]: + protocol_in = os.fdopen(os.dup(0), "r", encoding="utf-8", newline="\n") + protocol_out = os.fdopen(os.dup(1), "w", encoding="utf-8", newline="\n", buffering=1) + devnull = os.open(os.devnull, os.O_WRONLY) + try: + os.dup2(devnull, 1) + finally: + os.close(devnull) + return protocol_in, protocol_out + + +def _load_candidate(path: Path) -> Any: + spec = importlib.util.spec_from_file_location("laminate_candidate", path) + if spec is None or spec.loader is None: + raise ImportError(f"cannot load candidate from {path}") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + if not callable(getattr(module, "design_laminates", None)): + raise AttributeError("candidate must define callable design_laminates(cases)") + return module + + +def _error(request_id: Any, exc: BaseException) -> dict[str, Any]: + try: + message = str(exc)[:500] + except Exception: + message = "failed to format candidate exception" + return { + "id": request_id, + "ok": False, + "error": {"type": type(exc).__name__, "message": message}, + } + + +def _send(stream: Any, payload: dict[str, Any]) -> None: + stream.write(_json_dumps(payload, ensure_ascii=True, allow_nan=False) + "\n") + stream.flush() + + +def main() -> int: + protocol_in, protocol_out = _protocol_streams() + if len(sys.argv) != 2: + _send(protocol_out, _error(None, ValueError("expected candidate path"))) + return 2 + try: + candidate = _load_candidate(Path(sys.argv[1]).resolve()) + except BaseException as exc: + _send(protocol_out, _error(None, exc)) + return 1 + + _send(protocol_out, {"id": None, "ok": True, "ready": True}) + for line in protocol_in: + request_id: Any = None + try: + request = _json_loads(line) + if not isinstance(request, dict): + raise TypeError("request must be a JSON object") + request_id = request.get("id") + operation = request.get("op") + if operation == "design": + cases = request.get("cases") + designs = candidate.design_laminates(cases) + response = {"id": request_id, "ok": True, "designs": designs} + elif operation == "shutdown": + _send(protocol_out, {"id": request_id, "ok": True}) + return 0 + else: + raise ValueError(f"unknown operation: {operation!r}") + _send(protocol_out, response) + except BaseException as exc: + try: + _send(protocol_out, _error(request_id, exc)) + except BaseException: + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/benchmarks/StructuralOptimization/CompositeLaminateStacking/verification/evaluator.py b/benchmarks/StructuralOptimization/CompositeLaminateStacking/verification/evaluator.py new file mode 100644 index 00000000..94cdecf3 --- /dev/null +++ b/benchmarks/StructuralOptimization/CompositeLaminateStacking/verification/evaluator.py @@ -0,0 +1,221 @@ +"""Evaluator for the CompositeLaminateStacking benchmark.""" + +from __future__ import annotations + +import argparse +import json +import math +from functools import lru_cache +from pathlib import Path +from typing import Any, Mapping + +import numpy as np + +try: + from .design_runtime import DesignRuntime + from .mechanics import ( + evaluate_base_angles, + load_config, + normalize_base_angles, + public_cases, + ) +except ImportError: # Direct execution: python verification/evaluator.py ... + from design_runtime import DesignRuntime + from mechanics import ( + evaluate_base_angles, + load_config, + normalize_base_angles, + public_cases, + ) + + +ROOT = Path(__file__).resolve().parents[1] +DEFAULT_CANDIDATE = ROOT / "scripts" / "init.py" + + +@lru_cache(maxsize=1) +def _anchor_rows() -> dict[str, dict[str, float]]: + config = load_config() + anchor = list(config["anchor_base_angles_deg"]) + return { + str(case["case_id"]): evaluate_base_angles(anchor, case, config) + for case in public_cases(config) + } + + +def _case_score(reserve_factor: float, anchor_reserve: float) -> float: + if reserve_factor <= 0.0 or anchor_reserve <= 0.0: + return 0.0 + ratio = reserve_factor / anchor_reserve + return float(np.clip(50.0 + 50.0 * math.tanh(math.log(ratio) / 0.5), 0.0, 100.0)) + + +def _robust_score(scores: list[float]) -> float: + if not scores: + return 0.0 + values = np.asarray(scores, dtype=float) + return float(0.75 * np.mean(values) + 0.25 * np.quantile(values, 0.20)) + + +def _schema_rows( + designs: Any, cases: list[dict[str, Any]], config: Mapping[str, Any] +) -> tuple[dict[str, list[int]], dict[str, str]]: + if not isinstance(designs, dict): + return {}, {str(case["case_id"]): "candidate result must be a JSON object" for case in cases} + expected_ids = {str(case["case_id"]) for case in cases} + returned_ids = {str(key) for key in designs} + extra = sorted(returned_ids - expected_ids) + global_error = f"unexpected case ids: {extra}" if extra else None + normalized: dict[str, list[int]] = {} + errors: dict[str, str] = {} + for case in cases: + case_id = str(case["case_id"]) + if case_id not in designs: + errors[case_id] = f"missing design for case {case_id}" + continue + angles, error = normalize_base_angles(designs[case_id], config) + if error is not None: + errors[case_id] = error + elif global_error is not None: + errors[case_id] = global_error + else: + assert angles is not None + normalized[case_id] = angles + return normalized, errors + + +def evaluate(candidate_path: Path, *, timeout_s: float = 12.0) -> dict[str, Any]: + config = load_config() + cases = public_cases(config) + rows: list[dict[str, Any]] = [] + try: + with DesignRuntime( + candidate_path, + startup_timeout_s=min(3.0, timeout_s), + call_timeout_s=max(0.05, timeout_s - min(3.0, timeout_s) + 0.25), + total_timeout_s=timeout_s, + ) as runtime: + designs = runtime.design_laminates(cases) + normalized, schema_errors = _schema_rows(designs, cases, config) + except Exception as exc: + message = f"{type(exc).__name__}: {exc}" + normalized = {} + schema_errors = {str(case["case_id"]): message for case in cases} + + anchors = _anchor_rows() + for case in cases: + case_id = str(case["case_id"]) + if case_id in schema_errors: + rows.append( + { + "case_id": case_id, + "aspect_ratio": float(case["aspect_ratio"]), + "load_type": "biaxial" if float(case["Ny_N_per_mm"]) < 0.0 else "uniaxial", + "score": 0.0, + "feasible": False, + "error": schema_errors[case_id], + } + ) + continue + try: + candidate = evaluate_base_angles(normalized[case_id], case, config) + anchor = anchors[case_id] + score = _case_score(candidate["reserve_factor"], anchor["reserve_factor"]) + rows.append( + { + "case_id": case_id, + "aspect_ratio": float(case["aspect_ratio"]), + "load_type": "biaxial" if float(case["Ny_N_per_mm"]) < 0.0 else "uniaxial", + "score": score, + "feasible": True, + "base_angles_deg": normalized[case_id], + "candidate": candidate, + "anchor": anchor, + } + ) + except Exception as exc: + rows.append( + { + "case_id": case_id, + "aspect_ratio": float(case["aspect_ratio"]), + "load_type": "biaxial" if float(case["Ny_N_per_mm"]) < 0.0 else "uniaxial", + "score": 0.0, + "feasible": False, + "error": f"{type(exc).__name__}: {exc}", + } + ) + + scores = [float(row["score"]) for row in rows] + diagnostic_score = _robust_score(scores) + valid = all(bool(row["feasible"]) and "error" not in row for row in rows) + return { + "combined_score": diagnostic_score if valid else 0.0, + "diagnostic_score": diagnostic_score, + "valid": float(valid), + "feasible_cases": float(sum(bool(row["feasible"]) for row in rows)), + "rows": rows, + } + + +def _write_json(path: str | None, payload: dict[str, Any]) -> None: + if not path: + return + output = Path(path).expanduser().resolve() + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8") + + +def _artifacts(result: dict[str, Any], candidate_path: Path) -> dict[str, Any]: + try: + label = candidate_path.relative_to(ROOT).as_posix() + except ValueError: + label = candidate_path.name + return { + "candidate_path": label, + "source_doi": load_config()["source"]["doi"], + "rows": result["rows"], + } + + +def main() -> None: + parser = argparse.ArgumentParser(description="Evaluate balanced symmetric laminate designs") + parser.add_argument("candidate", nargs="?", default=str(DEFAULT_CANDIDATE)) + parser.add_argument("--timeout-s", type=float, default=12.0) + parser.add_argument("--metrics-out", default=None) + parser.add_argument("--artifacts-out", default=None) + args = parser.parse_args() + if args.timeout_s <= 0.0: + parser.error("--timeout-s must be positive") + + candidate_path = Path(args.candidate).expanduser().resolve() + result = evaluate(candidate_path, timeout_s=float(args.timeout_s)) + print("=== Composite Laminate Stacking ===") + for row in result["rows"]: + if "error" in row: + print(f"case={row['case_id']} score=0.00 feasible=false error={row['error']}") + else: + candidate = row["candidate"] + print( + f"case={row['case_id']} score={row['score']:.2f} feasible=true " + f"buckling={candidate['buckling_load_factor']:.3f} " + f"failure={candidate['failure_load_factor']:.3f} " + f"reserve={candidate['reserve_factor']:.3f}" + ) + print("---") + print(f"feasible_cases: {result['feasible_cases']:.0f}/{len(result['rows'])}") + print(f"diagnostic_score: {result['diagnostic_score']:.4f}") + print(f"combined_score: {result['combined_score']:.4f}") + + metrics = { + "combined_score": float(result["combined_score"]), + "diagnostic_score": float(result["diagnostic_score"]), + "valid": float(result["valid"]), + "feasible_cases": float(result["feasible_cases"]), + "num_cases": float(len(result["rows"])), + } + _write_json(args.metrics_out, metrics) + _write_json(args.artifacts_out, _artifacts(result, candidate_path)) + + +if __name__ == "__main__": + main() diff --git a/benchmarks/StructuralOptimization/CompositeLaminateStacking/verification/mechanics.py b/benchmarks/StructuralOptimization/CompositeLaminateStacking/verification/mechanics.py new file mode 100644 index 00000000..717e27b6 --- /dev/null +++ b/benchmarks/StructuralOptimization/CompositeLaminateStacking/verification/mechanics.py @@ -0,0 +1,266 @@ +"""Independent CLT and Ritz verifier for balanced, symmetric laminates. + +Units are N, mm, MPa, and dimensionless strain throughout this module. +The implementation deliberately does not depend on the candidate process. +""" + +from __future__ import annotations + +import json +import math +from functools import lru_cache +from pathlib import Path +from typing import Any, Mapping, Sequence + +import numpy as np + + +ROOT = Path(__file__).resolve().parents[1] +CONFIG_PATH = ROOT / "references" / "config.json" + + +@lru_cache(maxsize=1) +def load_config() -> dict[str, Any]: + return json.loads(CONFIG_PATH.read_text(encoding="utf-8")) + + +def public_cases(config: Mapping[str, Any] | None = None) -> list[dict[str, Any]]: + cfg = load_config() if config is None else config + return [ + { + "case_id": str(case["case_id"]), + "aspect_ratio": float(case["aspect_ratio"]), + "a_mm": float(case["a_mm"]), + "b_mm": float(case["b_mm"]), + "Nx_N_per_mm": float(case["Nx_N_per_mm"]), + "Ny_N_per_mm": float(case["Ny_N_per_mm"]), + } + for case in cfg["cases"] + ] + + +def normalize_base_angles( + value: Any, config: Mapping[str, Any] | None = None +) -> tuple[list[int] | None, str | None]: + cfg = load_config() if config is None else config + laminate = cfg["laminate"] + expected = int(laminate["design_angles"]) + if not isinstance(value, list): + return None, "design must be a JSON list" + if len(value) != expected: + return None, f"design must contain exactly {expected} base angles" + + lower = int(laminate["angle_min_deg"]) + upper = int(laminate["angle_max_deg"]) + normalized: list[int] = [] + for index, raw in enumerate(value): + if isinstance(raw, bool) or not isinstance(raw, (int, float)): + return None, f"angle[{index}] must be a finite integer" + number = float(raw) + if not math.isfinite(number) or not number.is_integer(): + return None, f"angle[{index}] must be a finite integer" + angle = int(number) + if angle < lower or angle > upper: + return None, f"angle[{index}]={angle} is outside [{lower}, {upper}]" + normalized.append(angle) + return normalized, None + + +def expand_balanced_symmetric(base_angles: Sequence[int | float]) -> list[float]: + half: list[float] = [] + for raw in base_angles: + angle = float(raw) + half.extend((angle, -angle)) + return half + list(reversed(half)) + + +def _reduced_stiffness(material: Mapping[str, Any]) -> np.ndarray: + e11 = float(material["E11_MPa"]) + e22 = float(material["E22_MPa"]) + nu12 = float(material["nu12"]) + g12 = float(material["G12_MPa"]) + nu21 = nu12 * e22 / e11 + denominator = 1.0 - nu12 * nu21 + return np.array( + [ + [e11 / denominator, nu12 * e22 / denominator, 0.0], + [nu12 * e22 / denominator, e22 / denominator, 0.0], + [0.0, 0.0, g12], + ], + dtype=float, + ) + + +def transformed_stiffness(angle_deg: float, material: Mapping[str, Any]) -> np.ndarray: + q = _reduced_stiffness(material) + q11, q12, q22, q66 = q[0, 0], q[0, 1], q[1, 1], q[2, 2] + theta = math.radians(float(angle_deg)) + m = math.cos(theta) + n = math.sin(theta) + m2, n2 = m * m, n * n + m4, n4 = m2 * m2, n2 * n2 + m3n, mn3 = m2 * m * n, m * n2 * n + qbar11 = q11 * m4 + 2.0 * (q12 + 2.0 * q66) * m2 * n2 + q22 * n4 + qbar22 = q11 * n4 + 2.0 * (q12 + 2.0 * q66) * m2 * n2 + q22 * m4 + qbar12 = (q11 + q22 - 4.0 * q66) * m2 * n2 + q12 * (m4 + n4) + qbar16 = (q11 - q12 - 2.0 * q66) * m3n - (q22 - q12 - 2.0 * q66) * mn3 + qbar26 = (q11 - q12 - 2.0 * q66) * mn3 - (q22 - q12 - 2.0 * q66) * m3n + qbar66 = (q11 + q22 - 2.0 * q12 - 2.0 * q66) * m2 * n2 + q66 * (m4 + n4) + return np.array( + [ + [qbar11, qbar12, qbar16], + [qbar12, qbar22, qbar26], + [qbar16, qbar26, qbar66], + ], + dtype=float, + ) + + +def laminate_abd( + stack_deg: Sequence[float], config: Mapping[str, Any] | None = None +) -> tuple[np.ndarray, np.ndarray, np.ndarray]: + cfg = load_config() if config is None else config + ply_t = float(cfg["laminate"]["ply_thickness_mm"]) + thickness = ply_t * len(stack_deg) + z = np.linspace(-0.5 * thickness, 0.5 * thickness, len(stack_deg) + 1) + a = np.zeros((3, 3), dtype=float) + b = np.zeros((3, 3), dtype=float) + d = np.zeros((3, 3), dtype=float) + for index, angle in enumerate(stack_deg): + qbar = transformed_stiffness(float(angle), cfg["material"]) + z0, z1 = float(z[index]), float(z[index + 1]) + a += qbar * (z1 - z0) + b += 0.5 * qbar * (z1 * z1 - z0 * z0) + d += (1.0 / 3.0) * qbar * (z1**3 - z0**3) + return a, b, d + + +def maximum_strain_load_factor( + stack_deg: Sequence[float], + case: Mapping[str, Any], + config: Mapping[str, Any] | None = None, +) -> float: + cfg = load_config() if config is None else config + a, _, _ = laminate_abd(stack_deg, cfg) + design_factor = float(cfg["failure"]["design_load_factor"]) + loads = design_factor * np.array( + [float(case["Nx_N_per_mm"]), float(case["Ny_N_per_mm"]), 0.0], + dtype=float, + ) + strain = np.linalg.solve(a, loads) + allow_1 = float(cfg["failure"]["epsilon_1_allowable"]) + allow_2 = float(cfg["failure"]["epsilon_2_allowable"]) + allow_12 = float(cfg["failure"]["gamma_12_allowable"]) + factors: list[float] = [] + for angle_deg in stack_deg: + theta = math.radians(float(angle_deg)) + m, n = math.cos(theta), math.sin(theta) + ex, ey, gxy = (float(item) for item in strain) + e1 = m * m * ex + n * n * ey + m * n * gxy + e2 = n * n * ex + m * m * ey - m * n * gxy + g12 = -2.0 * m * n * ex + 2.0 * m * n * ey + (m * m - n * n) * gxy + for allowable, actual in ((allow_1, e1), (allow_2, e2), (allow_12, g12)): + if abs(actual) > 1e-15: + factors.append(allowable / abs(actual)) + if not factors: + return float("inf") + return float(min(factors)) + + +def _ritz_matrices( + d: np.ndarray, + case: Mapping[str, Any], + modes_x: int, + modes_y: int, + quadrature_order: int, +) -> tuple[np.ndarray, np.ndarray]: + a = float(case["a_mm"]) + b = float(case["b_mm"]) + nx = max(0.0, -float(case["Nx_N_per_mm"])) + ny = max(0.0, -float(case["Ny_N_per_mm"])) + if nx + ny <= 0.0: + raise ValueError("buckling requires at least one compressive membrane load") + + nodes, weights = np.polynomial.legendre.leggauss(quadrature_order) + xs = 0.5 * a * (nodes + 1.0) + ys = 0.5 * b * (nodes + 1.0) + scaled_weights = 0.25 * a * b * np.outer(weights, weights) + basis = [(m, n) for m in range(1, modes_x + 1) for n in range(1, modes_y + 1)] + size = len(basis) + stiffness = np.zeros((size, size), dtype=float) + geometric = np.zeros((size, size), dtype=float) + + for ix, x in enumerate(xs): + for iy, y in enumerate(ys): + curvature = np.zeros((3, size), dtype=float) + grad_x = np.zeros(size, dtype=float) + grad_y = np.zeros(size, dtype=float) + for column, (mode_x, mode_y) in enumerate(basis): + alpha = mode_x * math.pi / a + beta = mode_y * math.pi / b + sin_x, cos_x = math.sin(alpha * x), math.cos(alpha * x) + sin_y, cos_y = math.sin(beta * y), math.cos(beta * y) + curvature[:, column] = ( + alpha * alpha * sin_x * sin_y, + beta * beta * sin_x * sin_y, + -2.0 * alpha * beta * cos_x * cos_y, + ) + grad_x[column] = alpha * cos_x * sin_y + grad_y[column] = beta * sin_x * cos_y + weight = float(scaled_weights[ix, iy]) + stiffness += weight * (curvature.T @ d @ curvature) + geometric += weight * ( + nx * np.outer(grad_x, grad_x) + ny * np.outer(grad_y, grad_y) + ) + return 0.5 * (stiffness + stiffness.T), 0.5 * (geometric + geometric.T) + + +def buckling_load_factor( + stack_deg: Sequence[float], + case: Mapping[str, Any], + config: Mapping[str, Any] | None = None, +) -> float: + cfg = load_config() if config is None else config + _, _, d = laminate_abd(stack_deg, cfg) + buckling = cfg["buckling"] + k, g = _ritz_matrices( + d, + case, + int(buckling["modes_x"]), + int(buckling["modes_y"]), + int(buckling["quadrature_order"]), + ) + eigenvalues_g, vectors_g = np.linalg.eigh(g) + tolerance = max(1e-12, 1e-10 * float(np.max(eigenvalues_g))) + keep = eigenvalues_g > tolerance + if not np.any(keep): + raise np.linalg.LinAlgError("geometric stiffness is singular") + transform = vectors_g[:, keep] / np.sqrt(eigenvalues_g[keep])[None, :] + reduced = transform.T @ k @ transform + eigenvalues = np.linalg.eigvalsh(0.5 * (reduced + reduced.T)) + positive = eigenvalues[eigenvalues > 1e-9] + if positive.size == 0: + raise np.linalg.LinAlgError("no positive buckling eigenvalue") + return float(positive[0]) + + +def evaluate_base_angles( + base_angles: Sequence[int | float], + case: Mapping[str, Any], + config: Mapping[str, Any] | None = None, +) -> dict[str, float]: + cfg = load_config() if config is None else config + stack = expand_balanced_symmetric(base_angles) + expected = int(cfg["laminate"]["num_plies"]) + if len(stack) != expected: + raise ValueError(f"expanded stack has {len(stack)} plies, expected {expected}") + failure = maximum_strain_load_factor(stack, case, cfg) + buckling = buckling_load_factor(stack, case, cfg) + reserve = min(failure, buckling) + if not all(math.isfinite(value) and value > 0.0 for value in (failure, buckling, reserve)): + raise ValueError("mechanics produced a non-positive or non-finite load factor") + return { + "failure_load_factor": float(failure), + "buckling_load_factor": float(buckling), + "reserve_factor": float(reserve), + } diff --git a/benchmarks/StructuralOptimization/CompositeLaminateStacking/verification/requirements.txt b/benchmarks/StructuralOptimization/CompositeLaminateStacking/verification/requirements.txt new file mode 100644 index 00000000..9f161aca --- /dev/null +++ b/benchmarks/StructuralOptimization/CompositeLaminateStacking/verification/requirements.txt @@ -0,0 +1 @@ +numpy>=1.24 diff --git a/benchmarks/StructuralOptimization/CompositeLaminateStacking/verification/test_evaluator.py b/benchmarks/StructuralOptimization/CompositeLaminateStacking/verification/test_evaluator.py new file mode 100644 index 00000000..b7e1dfc1 --- /dev/null +++ b/benchmarks/StructuralOptimization/CompositeLaminateStacking/verification/test_evaluator.py @@ -0,0 +1,103 @@ +from __future__ import annotations + +import shutil +import tempfile +import unittest +from pathlib import Path + +try: + from verification.evaluator import _case_score, evaluate +except ModuleNotFoundError: + from evaluator import _case_score, evaluate + + +class EvaluatorTests(unittest.TestCase): + def _candidate(self, source: str) -> Path: + directory = Path(tempfile.mkdtemp(prefix="laminate_candidate_test_")) + path = directory / "candidate.py" + path.write_text(source, encoding="utf-8") + self.addCleanup(lambda: shutil.rmtree(directory, ignore_errors=True)) + return path + + def test_anchor_candidate_is_valid_and_scores_fifty(self) -> None: + candidate = self._candidate( + "def design_laminates(cases):\n" + " x=[90,45,45,90,45,90,45,45,45,45,45,45]\n" + " return {c['case_id']: list(x) for c in cases}\n" + ) + result = evaluate(candidate) + self.assertEqual(result["valid"], 1.0) + self.assertAlmostEqual(result["combined_score"], 50.0, places=8) + self.assertTrue(all(abs(row["score"] - 50.0) < 1e-8 for row in result["rows"])) + + def test_load_aware_design_has_measurable_improvement_space(self) -> None: + candidate = self._candidate( + "def design_laminates(cases):\n" + " anchor=[90,45,45,90,45,90,45,45,45,45,45,45]\n" + " mixed=[0,45,90,30,60,45,0,30,60,90,45,15]\n" + " out={}\n" + " for c in cases:\n" + " if c['Ny_N_per_mm'] == 0 and c['aspect_ratio'] <= 1:\n" + " out[c['case_id']] = [0]*12\n" + " elif c['Ny_N_per_mm'] == 0:\n" + " out[c['case_id']] = list(mixed)\n" + " else:\n" + " out[c['case_id']] = list(anchor)\n" + " return out\n" + ) + result = evaluate(candidate) + self.assertEqual(result["valid"], 1.0) + self.assertGreater(result["combined_score"], 60.0) + + def test_missing_case_is_invalid_with_actionable_feedback(self) -> None: + candidate = self._candidate("def design_laminates(cases): return {}\n") + result = evaluate(candidate) + self.assertEqual(result["valid"], 0.0) + self.assertEqual(result["combined_score"], 0.0) + self.assertIn("missing design for case", result["rows"][0]["error"]) + + def test_out_of_range_angle_is_invalid(self) -> None: + candidate = self._candidate( + "def design_laminates(cases):\n" + " return {c['case_id']: [91]*12 for c in cases}\n" + ) + result = evaluate(candidate) + self.assertEqual(result["valid"], 0.0) + self.assertIn("outside [0, 90]", result["rows"][0]["error"]) + + def test_candidate_stdout_does_not_break_protocol(self) -> None: + candidate = self._candidate( + "print('noise during import')\n" + "def design_laminates(cases):\n" + " print('noise during call')\n" + " x=[90,45,45,90,45,90,45,45,45,45,45,45]\n" + " return {c['case_id']: x for c in cases}\n" + ) + result = evaluate(candidate) + self.assertEqual(result["valid"], 1.0) + + def test_candidate_cannot_monkeypatch_parent_score(self) -> None: + candidate = self._candidate( + "import sys\n" + "setattr(sys.modules['__main__'], '_case_score', lambda *a: 100.0)\n" + "def design_laminates(cases):\n" + " return {c['case_id']: [90]*12 for c in cases}\n" + ) + result = evaluate(candidate) + self.assertNotEqual(result["combined_score"], 100.0) + + def test_syntax_error_returns_invalid_result(self) -> None: + candidate = self._candidate("def broken(:\n") + result = evaluate(candidate) + self.assertEqual(result["valid"], 0.0) + self.assertEqual(result["combined_score"], 0.0) + self.assertIn("candidate import failed", result["rows"][0]["error"]) + + def test_score_is_monotone_and_anchor_is_fifty(self) -> None: + self.assertLess(_case_score(9.0, 10.0), 50.0) + self.assertAlmostEqual(_case_score(10.0, 10.0), 50.0) + self.assertGreater(_case_score(11.0, 10.0), 50.0) + + +if __name__ == "__main__": + unittest.main() diff --git a/benchmarks/StructuralOptimization/CompositeLaminateStacking/verification/test_mechanics.py b/benchmarks/StructuralOptimization/CompositeLaminateStacking/verification/test_mechanics.py new file mode 100644 index 00000000..2a2a0877 --- /dev/null +++ b/benchmarks/StructuralOptimization/CompositeLaminateStacking/verification/test_mechanics.py @@ -0,0 +1,71 @@ +from __future__ import annotations + +import unittest + +import numpy as np + +try: + from verification.mechanics import ( + evaluate_base_angles, + expand_balanced_symmetric, + laminate_abd, + load_config, + normalize_base_angles, + public_cases, + ) +except ModuleNotFoundError: + from mechanics import ( + evaluate_base_angles, + expand_balanced_symmetric, + laminate_abd, + load_config, + normalize_base_angles, + public_cases, + ) + + +class MechanicsTests(unittest.TestCase): + def setUp(self) -> None: + self.config = load_config() + self.anchor = list(self.config["anchor_base_angles_deg"]) + + def test_expansion_is_balanced_and_symmetric(self) -> None: + stack = expand_balanced_symmetric(self.anchor) + self.assertEqual(len(stack), 48) + self.assertEqual(stack, list(reversed(stack))) + for angle in set(abs(value) for value in stack): + if angle == 0: + continue + self.assertEqual(stack.count(angle), stack.count(-angle)) + + def test_symmetric_laminate_has_negligible_coupling_matrix(self) -> None: + stack = expand_balanced_symmetric(self.anchor) + _, coupling, _ = laminate_abd(stack, self.config) + self.assertLess(float(np.max(np.abs(coupling))), 1e-7) + + def test_reference_design_produces_positive_finite_factors(self) -> None: + for case in public_cases(self.config): + result = evaluate_base_angles(self.anchor, case, self.config) + for value in result.values(): + self.assertTrue(np.isfinite(value)) + self.assertGreater(value, 0.0) + + def test_square_biaxial_case_matches_upstream_reference(self) -> None: + case = next( + item for item in public_cases(self.config) if item["case_id"] == "biaxial_ar_1_0" + ) + result = evaluate_base_angles(self.anchor, case, self.config) + # The MIT-licensed source reports lambda_cs=10394.81 and lambda_cb=9998.19. + # Failure uses the same closed-form maximum-strain calculation. Buckling uses + # an independent Ritz implementation instead of the source BFSC finite element. + self.assertAlmostEqual(result["failure_load_factor"] / 10394.81, 1.0, delta=0.001) + self.assertAlmostEqual(result["buckling_load_factor"] / 9998.19, 1.0, delta=0.05) + + def test_angle_schema_reports_specific_error(self) -> None: + angles, error = normalize_base_angles([45] * 11 + [90.5], self.config) + self.assertIsNone(angles) + self.assertEqual(error, "angle[11] must be a finite integer") + + +if __name__ == "__main__": + unittest.main() diff --git a/benchmarks/StructuralOptimization/README.md b/benchmarks/StructuralOptimization/README.md index 30215494..1684fba6 100644 --- a/benchmarks/StructuralOptimization/README.md +++ b/benchmarks/StructuralOptimization/README.md @@ -1,6 +1,6 @@ # Structural Optimization -This domain covers structural engineering optimization problems derived from the **International Student Competition in Structural Optimization (ISCSO)**, organized by [Bright Optimizer](http://www.brightoptimizer.com/). +This domain covers structural engineering optimization problems, including tasks derived from the **International Student Competition in Structural Optimization (ISCSO)**, organized by [Bright Optimizer](http://www.brightoptimizer.com/), and published laminate and topology benchmarks. Structural optimization is a core discipline in civil, aerospace, and mechanical engineering, aiming to find the optimal design of load-bearing structures that minimizes material usage (weight) while satisfying safety constraints (stress and displacement limits). @@ -12,17 +12,18 @@ Structural optimization is a core discipline in civil, aerospace, and mechanical | `ISCSO2023` | 284-member 3D truss sizing optimization | 284 | Continuous, constrained, multi-load-case | | `TopologyOptimization` | MBB beam 2D topology optimization (SIMP) | 1200 | Continuous, volume-constrained, compliance minimization | | `PyMOTOSIMPCompliance` | pyMOTO-style SIMP compliance minimization for 2D beam topology design | 4800 | Continuous, volume-constrained, compliance minimization | +| `CompositeLaminateStacking` | Balanced, symmetric 48-ply laminate design across plate geometries and load ratios | 120 | Discrete, multi-case, buckling + failure constrained | ## Why These Problems Are Suitable for Frontier-Engineering -| Feature | ISCSO 2015 | ISCSO 2023 | -| :--- | :--- | :--- | -| High-dimensional continuous variables | Medium (54-D) | High (284-D) | -| Real physical model (FEM) | Yes | Yes | -| Deterministic evaluation | Yes | Yes | -| Multi-load-case constraints | Yes (2 cases) | Yes (3 cases) | -| Non-convex feasible region | Yes | Yes | -| Industrial relevance | Yes | Yes | +| Feature | ISCSO 2015 | ISCSO 2023 | Composite laminate | +| :--- | :--- | :--- | :--- | +| High-dimensional design variables | Medium (54-D) | High (284-D) | High (120 integer variables) | +| Real physical model | FEM | FEM | Classical laminate theory + Ritz buckling | +| Deterministic evaluation | Yes | Yes | Yes | +| Multi-load-case constraints | Yes (2 cases) | Yes (3 cases) | Yes (10 cases) | +| Non-convex feasible region | Yes | Yes | Yes | +| Industrial relevance | Yes | Yes | Yes | These benchmarks serve as: diff --git a/benchmarks/StructuralOptimization/README_zh-CN.md b/benchmarks/StructuralOptimization/README_zh-CN.md index a6615171..0cd1af89 100644 --- a/benchmarks/StructuralOptimization/README_zh-CN.md +++ b/benchmarks/StructuralOptimization/README_zh-CN.md @@ -1,6 +1,6 @@ # 结构优化 -本领域涵盖源自**国际结构优化学生竞赛(ISCSO)**的结构工程优化问题,竞赛由 [Bright Optimizer](http://www.brightoptimizer.com/) 组织。 +本领域涵盖结构工程优化问题,包括由 [Bright Optimizer](http://www.brightoptimizer.com/) 组织的**国际结构优化学生竞赛(ISCSO)**衍生任务,以及公开发表的复合材料铺层与拓扑优化 benchmark。 结构优化是土木、航空航天和机械工程中的核心学科,目标是在满足安全约束(应力和位移限值)的条件下,找到使材料用量(重量)最小的承载结构最优设计。 @@ -12,17 +12,18 @@ | `ISCSO2023` | 284 杆 3D 桁架尺寸优化 | 284 | 连续、有约束、多工况 | | `TopologyOptimization` | MBB 束流二维拓扑优化 (SIMP) | 1200 | 连续、体积约束的合规最小化 | | `PyMOTOSIMPCompliance` | 基于 pyMOTO 范式的二维梁 SIMP 柔度最小化拓扑设计 | 4800 | 连续、体积约束的合规最小化 | +| `CompositeLaminateStacking` | 跨板几何与载荷比例的平衡、对称 48 层铺层设计 | 120 | 离散、多工况、屈曲与失效约束 | ## 为何适合 Frontier-Engineering -| 特性 | ISCSO 2015 | ISCSO 2023 | -| :--- | :--- | :--- | -| 高维连续变量 | 中等 (54-D) | 高 (284-D) | -| 真实物理模型 (FEM) | 是 | 是 | -| 确定性评估 | 是 | 是 | -| 多工况约束 | 是 (2 工况) | 是 (3 工况) | -| 非凸可行域 | 是 | 是 | -| 工业相关性 | 是 | 是 | +| 特性 | ISCSO 2015 | ISCSO 2023 | 复合材料铺层 | +| :--- | :--- | :--- | :--- | +| 高维设计变量 | 中等 (54-D) | 高 (284-D) | 高(120 个整数变量) | +| 真实物理模型 | FEM | FEM | 经典层合板理论 + Ritz 屈曲分析 | +| 确定性评估 | 是 | 是 | 是 | +| 多工况约束 | 是 (2 工况) | 是 (3 工况) | 是(10 工况) | +| 非凸可行域 | 是 | 是 | 是 | +| 工业相关性 | 是 | 是 | 是 | 这些 benchmark 可作为: diff --git a/frontier_eval/conf/task/composite_laminate_stacking.yaml b/frontier_eval/conf/task/composite_laminate_stacking.yaml new file mode 100644 index 00000000..325e5504 --- /dev/null +++ b/frontier_eval/conf/task/composite_laminate_stacking.yaml @@ -0,0 +1,6 @@ +name: unified +benchmark: StructuralOptimization/CompositeLaminateStacking + +runtime: + env: + PYTHONUTF8: "1"