Add multi-zone residual surrogate with held-out evaluation and runnable predictor
Browse files- README.md +2 -0
- multizone-v02/README.md +30 -0
- multizone-v02/config.json +118 -0
- multizone-v02/evaluation.json +159 -0
- multizone-v02/predict.py +24 -0
- multizone-v02/residual.safetensors +3 -0
- multizone-v02/source/multizone.py +84 -0
- multizone-v02/source/rod.py +106 -0
README.md
CHANGED
|
@@ -16,6 +16,8 @@ tags:
|
|
| 16 |
|
| 17 |
# AgentFEM · Morph Rod Surrogates
|
| 18 |
|
|
|
|
|
|
|
| 19 |
**Predict a flexible rod's shape without averaging its equilibrium branches.**
|
| 20 |
|
| 21 |

|
|
|
|
| 16 |
|
| 17 |
# AgentFEM · Morph Rod Surrogates
|
| 18 |
|
| 19 |
+
**New: [multi-zone residual surrogate](multizone-v02/README.md)** — a separate 3/6/12-zone actuation model with 8-zone holdout, downloadable weights and a runnable predictor. Original branch-conditioned models below are retained unchanged.
|
| 20 |
+
|
| 21 |
**Predict a flexible rod's shape without averaging its equilibrium branches.**
|
| 22 |
|
| 23 |

|
multizone-v02/README.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Multi-zone residual surrogate · v0.2
|
| 2 |
+
|
| 3 |
+
This additional model predicts the load-induced angular correction to an intrinsic-curvature shape. It handles a fixed 48-element field representation rather than three named curvature inputs. It is a separate task from the v0.1 microforce branch-conditioned model, not a replacement or a directly comparable leaderboard result.
|
| 4 |
+
|
| 5 |
+
## Run
|
| 6 |
+
|
| 7 |
+
Install `numpy torch safetensors`, then:
|
| 8 |
+
|
| 9 |
+
```bash
|
| 10 |
+
python predict.py --curvatures 1 .5 -1 1 .2 .8
|
| 11 |
+
```
|
| 12 |
+
|
| 13 |
+
The 51 inputs are 48 element-wise intrinsic curvatures, fx/chi, fy/chi, and log10(chi). A 51→96→96→48 tanh network predicts the angle correction; analytically integrated intrinsic curvature supplies the base shape. Weights use safetensors. `config.json` contains training-only normalization statistics. Output is a surrogate estimate, not an equilibrium-certified solution; use the accompanying mechanics solver when certification is required.
|
| 14 |
+
|
| 15 |
+
## Held-out results
|
| 16 |
+
|
| 17 |
+
| Split | Intrinsic-shape baseline RMSE / L | Learned correction RMSE / L |
|
| 18 |
+
|---|---:|---:|
|
| 19 |
+
| Scenario-held-out test, 3/6/12 zones | 0.03450 | 0.002255 |
|
| 20 |
+
| Zone-count OOD, 8 zones | 0.03387 | 0.009063 |
|
| 21 |
+
|
| 22 |
+
These are centerline errors at 49 nodes, including zero-loading states. Nonzero-only errors, endpoint means and 95th percentiles are in evaluation.json. No runtime advantage over the already fast mechanical solver has been established. The web Space continues to use that solver, not this neural model.
|
| 23 |
+
|
| 24 |
+
Training uses 2,520 states, validation 540, test 540, and zone-count OOD 480. Whole six-state loading paths stay in one split. The validation-best checkpoint is selected over 201 epochs; the test split is not used for selection. No contact, dynamics, multi-branch loading or hardware control is covered by this model.
|
| 25 |
+
|
| 26 |
+
## 中文
|
| 27 |
+
|
| 28 |
+
这版模型学习的是“本征曲率形状在外力下还会变多少”,而不是从零预测整个形状。测试集误差约为杆长的 0.226%,未见过的 8 分区工况约为 0.906%。它与原来的多分支模型用途不同,不能直接比较两个版本的误差来判断进步幅度。网页仍采用实际力学求解。
|
| 29 |
+
|
| 30 |
+
Code and model: Apache-2.0. Associated data: CC BY 4.0.
|
multizone-v02/config.json
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"mean": [
|
| 3 |
+
-0.021105827763676643,
|
| 4 |
+
-0.021105827763676643,
|
| 5 |
+
-0.021105827763676643,
|
| 6 |
+
-0.021105827763676643,
|
| 7 |
+
0.010166754014790058,
|
| 8 |
+
0.010166754014790058,
|
| 9 |
+
0.010166754014790058,
|
| 10 |
+
0.010166754014790058,
|
| 11 |
+
0.005382698494940996,
|
| 12 |
+
0.005382698494940996,
|
| 13 |
+
0.005382698494940996,
|
| 14 |
+
0.005382698494940996,
|
| 15 |
+
0.01833205670118332,
|
| 16 |
+
0.01833205670118332,
|
| 17 |
+
0.01833205670118332,
|
| 18 |
+
0.01833205670118332,
|
| 19 |
+
-0.01243000291287899,
|
| 20 |
+
-0.01243000291287899,
|
| 21 |
+
-0.01243000291287899,
|
| 22 |
+
-0.01243000291287899,
|
| 23 |
+
-0.04185189679265022,
|
| 24 |
+
-0.04185189679265022,
|
| 25 |
+
-0.04185189679265022,
|
| 26 |
+
-0.04185189679265022,
|
| 27 |
+
-0.06519726663827896,
|
| 28 |
+
-0.06519726663827896,
|
| 29 |
+
-0.06519726663827896,
|
| 30 |
+
-0.06519726663827896,
|
| 31 |
+
-0.09435442835092545,
|
| 32 |
+
-0.09435442835092545,
|
| 33 |
+
-0.09435442835092545,
|
| 34 |
+
-0.09435442835092545,
|
| 35 |
+
0.03800766170024872,
|
| 36 |
+
0.03800766170024872,
|
| 37 |
+
0.03800766170024872,
|
| 38 |
+
0.03800766170024872,
|
| 39 |
+
0.005421004723757505,
|
| 40 |
+
0.005421004723757505,
|
| 41 |
+
0.005421004723757505,
|
| 42 |
+
0.005421004723757505,
|
| 43 |
+
0.02227204106748104,
|
| 44 |
+
0.02227204106748104,
|
| 45 |
+
0.02227204106748104,
|
| 46 |
+
0.02227204106748104,
|
| 47 |
+
0.013483509421348572,
|
| 48 |
+
0.013483509421348572,
|
| 49 |
+
0.013483509421348572,
|
| 50 |
+
0.013483509421348572,
|
| 51 |
+
0.15130436420440674,
|
| 52 |
+
-0.0051275817677378654,
|
| 53 |
+
-3.2511823177337646
|
| 54 |
+
],
|
| 55 |
+
"std": [
|
| 56 |
+
0.8128650188446045,
|
| 57 |
+
0.8128650188446045,
|
| 58 |
+
0.8128650188446045,
|
| 59 |
+
0.8128650188446045,
|
| 60 |
+
0.8196129202842712,
|
| 61 |
+
0.8196129202842712,
|
| 62 |
+
0.8196129202842712,
|
| 63 |
+
0.8196129202842712,
|
| 64 |
+
0.8226141333580017,
|
| 65 |
+
0.8226141333580017,
|
| 66 |
+
0.8226141333580017,
|
| 67 |
+
0.8226141333580017,
|
| 68 |
+
0.8281049728393555,
|
| 69 |
+
0.8281049728393555,
|
| 70 |
+
0.8281049728393555,
|
| 71 |
+
0.8281049728393555,
|
| 72 |
+
0.8390543460845947,
|
| 73 |
+
0.8390543460845947,
|
| 74 |
+
0.8390543460845947,
|
| 75 |
+
0.8390543460845947,
|
| 76 |
+
0.8081016540527344,
|
| 77 |
+
0.8081016540527344,
|
| 78 |
+
0.8081016540527344,
|
| 79 |
+
0.8081016540527344,
|
| 80 |
+
0.8240239024162292,
|
| 81 |
+
0.8240239024162292,
|
| 82 |
+
0.8240239024162292,
|
| 83 |
+
0.8240239024162292,
|
| 84 |
+
0.8097495436668396,
|
| 85 |
+
0.8097495436668396,
|
| 86 |
+
0.8097495436668396,
|
| 87 |
+
0.8097495436668396,
|
| 88 |
+
0.8323362469673157,
|
| 89 |
+
0.8323362469673157,
|
| 90 |
+
0.8323362469673157,
|
| 91 |
+
0.8323362469673157,
|
| 92 |
+
0.8128801584243774,
|
| 93 |
+
0.8128801584243774,
|
| 94 |
+
0.8128801584243774,
|
| 95 |
+
0.8128801584243774,
|
| 96 |
+
0.8216692805290222,
|
| 97 |
+
0.8216692805290222,
|
| 98 |
+
0.8216692805290222,
|
| 99 |
+
0.8216692805290222,
|
| 100 |
+
0.8180782794952393,
|
| 101 |
+
0.8180782794952393,
|
| 102 |
+
0.8180782794952393,
|
| 103 |
+
0.8180782794952393,
|
| 104 |
+
0.1490403264760971,
|
| 105 |
+
0.21210536360740662,
|
| 106 |
+
0.4438764750957489
|
| 107 |
+
],
|
| 108 |
+
"output_scale": 0.1,
|
| 109 |
+
"n": 48,
|
| 110 |
+
"architecture": [
|
| 111 |
+
51,
|
| 112 |
+
96,
|
| 113 |
+
96,
|
| 114 |
+
48
|
| 115 |
+
],
|
| 116 |
+
"features": "48 piecewise intrinsic curvatures, fx/chi, fy/chi, log10(chi)",
|
| 117 |
+
"prediction": "cumulative intrinsic angle + output_scale * network(standardized features)"
|
| 118 |
+
}
|
multizone-v02/evaluation.json
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"version": "0.2",
|
| 3 |
+
"states": 4080,
|
| 4 |
+
"scenes": 680,
|
| 5 |
+
"counts": {
|
| 6 |
+
"train": 2520,
|
| 7 |
+
"validation": 540,
|
| 8 |
+
"test": 540,
|
| 9 |
+
"zone_ood": 480
|
| 10 |
+
},
|
| 11 |
+
"zones_id": [
|
| 12 |
+
3,
|
| 13 |
+
6,
|
| 14 |
+
12
|
| 15 |
+
],
|
| 16 |
+
"zones_ood": [
|
| 17 |
+
8
|
| 18 |
+
],
|
| 19 |
+
"rejected": 0,
|
| 20 |
+
"mesh_checks": 34,
|
| 21 |
+
"max_mesh_tip_difference_over_L": 4.603976254440725e-05,
|
| 22 |
+
"best_epoch": 189,
|
| 23 |
+
"metrics": {
|
| 24 |
+
"train": {
|
| 25 |
+
"intrinsic_shape": {
|
| 26 |
+
"centerline_rmse_over_L": 0.03217696650573636,
|
| 27 |
+
"tip_mean_over_L": 0.04703082547593874,
|
| 28 |
+
"tip_p95_over_L": 0.1389949922209111,
|
| 29 |
+
"nonzero_loading_rmse_over_L": 0.035248100774559976
|
| 30 |
+
},
|
| 31 |
+
"learned_residual": {
|
| 32 |
+
"centerline_rmse_over_L": 0.0007707721406115093,
|
| 33 |
+
"tip_mean_over_L": 0.0010672698493170232,
|
| 34 |
+
"tip_p95_over_L": 0.0032114897744955654,
|
| 35 |
+
"nonzero_loading_rmse_over_L": 0.0008432933805167641
|
| 36 |
+
}
|
| 37 |
+
},
|
| 38 |
+
"validation": {
|
| 39 |
+
"intrinsic_shape": {
|
| 40 |
+
"centerline_rmse_over_L": 0.030841632665898296,
|
| 41 |
+
"tip_mean_over_L": 0.04443062956595426,
|
| 42 |
+
"tip_p95_over_L": 0.13412200027231663,
|
| 43 |
+
"nonzero_loading_rmse_over_L": 0.03378531584280138
|
| 44 |
+
},
|
| 45 |
+
"learned_residual": {
|
| 46 |
+
"centerline_rmse_over_L": 0.002139143213742544,
|
| 47 |
+
"tip_mean_over_L": 0.0023906575722607896,
|
| 48 |
+
"tip_p95_over_L": 0.00985920275845111,
|
| 49 |
+
"nonzero_loading_rmse_over_L": 0.002342985905624226
|
| 50 |
+
}
|
| 51 |
+
},
|
| 52 |
+
"test": {
|
| 53 |
+
"intrinsic_shape": {
|
| 54 |
+
"centerline_rmse_over_L": 0.03449966638469762,
|
| 55 |
+
"tip_mean_over_L": 0.04949992729501256,
|
| 56 |
+
"tip_p95_over_L": 0.15202171303892403,
|
| 57 |
+
"nonzero_loading_rmse_over_L": 0.037792491010603176
|
| 58 |
+
},
|
| 59 |
+
"learned_residual": {
|
| 60 |
+
"centerline_rmse_over_L": 0.0022550218060435672,
|
| 61 |
+
"tip_mean_over_L": 0.0025461652398190942,
|
| 62 |
+
"tip_p95_over_L": 0.010101520708292833,
|
| 63 |
+
"nonzero_loading_rmse_over_L": 0.002469874665768664
|
| 64 |
+
}
|
| 65 |
+
},
|
| 66 |
+
"zone_ood": {
|
| 67 |
+
"intrinsic_shape": {
|
| 68 |
+
"centerline_rmse_over_L": 0.03387460541227201,
|
| 69 |
+
"tip_mean_over_L": 0.05124979340575448,
|
| 70 |
+
"tip_p95_over_L": 0.1401572788163453,
|
| 71 |
+
"nonzero_loading_rmse_over_L": 0.03710777102177594
|
| 72 |
+
},
|
| 73 |
+
"learned_residual": {
|
| 74 |
+
"centerline_rmse_over_L": 0.009062882912828973,
|
| 75 |
+
"tip_mean_over_L": 0.012180452320820882,
|
| 76 |
+
"tip_p95_over_L": 0.040638126307767204,
|
| 77 |
+
"nonzero_loading_rmse_over_L": 0.009927801522037316
|
| 78 |
+
}
|
| 79 |
+
}
|
| 80 |
+
},
|
| 81 |
+
"seconds": 4.511781215667725,
|
| 82 |
+
"mechanics_correction_checks": [
|
| 83 |
+
{
|
| 84 |
+
"id": "z3_008_5",
|
| 85 |
+
"residual": 5.312265384527226e-15,
|
| 86 |
+
"tip_error_over_L": 2.5286101803848858e-11
|
| 87 |
+
},
|
| 88 |
+
{
|
| 89 |
+
"id": "z3_051_5",
|
| 90 |
+
"residual": 4.446925569975155e-11,
|
| 91 |
+
"tip_error_over_L": 2.999757286531255e-08
|
| 92 |
+
},
|
| 93 |
+
{
|
| 94 |
+
"id": "z3_097_5",
|
| 95 |
+
"residual": 7.789608621038927e-16,
|
| 96 |
+
"tip_error_over_L": 4.949868572479711e-13
|
| 97 |
+
},
|
| 98 |
+
{
|
| 99 |
+
"id": "z3_144_5",
|
| 100 |
+
"residual": 1.3740308793209865e-11,
|
| 101 |
+
"tip_error_over_L": 2.148295597082969e-08
|
| 102 |
+
},
|
| 103 |
+
{
|
| 104 |
+
"id": "z3_176_5",
|
| 105 |
+
"residual": 5.722921340540243e-12,
|
| 106 |
+
"tip_error_over_L": 2.746265069278408e-07
|
| 107 |
+
},
|
| 108 |
+
{
|
| 109 |
+
"id": "z6_014_5",
|
| 110 |
+
"residual": 6.901285641049137e-17,
|
| 111 |
+
"tip_error_over_L": 7.371685286925458e-13
|
| 112 |
+
},
|
| 113 |
+
{
|
| 114 |
+
"id": "z6_044_5",
|
| 115 |
+
"residual": 2.206625380615194e-11,
|
| 116 |
+
"tip_error_over_L": 1.0975236022566359e-06
|
| 117 |
+
},
|
| 118 |
+
{
|
| 119 |
+
"id": "z6_077_5",
|
| 120 |
+
"residual": 1.4517147984741103e-11,
|
| 121 |
+
"tip_error_over_L": 8.451519429810074e-07
|
| 122 |
+
},
|
| 123 |
+
{
|
| 124 |
+
"id": "z6_138_5",
|
| 125 |
+
"residual": 4.744584467966025e-17,
|
| 126 |
+
"tip_error_over_L": 3.519878195556559e-08
|
| 127 |
+
},
|
| 128 |
+
{
|
| 129 |
+
"id": "z6_156_5",
|
| 130 |
+
"residual": 3.385439432791542e-11,
|
| 131 |
+
"tip_error_over_L": 1.3208994208734372e-07
|
| 132 |
+
},
|
| 133 |
+
{
|
| 134 |
+
"id": "z12_002_5",
|
| 135 |
+
"residual": 4.696980651641702e-15,
|
| 136 |
+
"tip_error_over_L": 5.641037807459418e-07
|
| 137 |
+
},
|
| 138 |
+
{
|
| 139 |
+
"id": "z12_037_5",
|
| 140 |
+
"residual": 2.122790464208461e-12,
|
| 141 |
+
"tip_error_over_L": 1.8782719327230075e-09
|
| 142 |
+
},
|
| 143 |
+
{
|
| 144 |
+
"id": "z12_083_5",
|
| 145 |
+
"residual": 6.979108458062555e-11,
|
| 146 |
+
"tip_error_over_L": 4.442049511326813e-06
|
| 147 |
+
},
|
| 148 |
+
{
|
| 149 |
+
"id": "z12_111_5",
|
| 150 |
+
"residual": 7.040594326440894e-17,
|
| 151 |
+
"tip_error_over_L": 5.582728434790076e-10
|
| 152 |
+
},
|
| 153 |
+
{
|
| 154 |
+
"id": "z12_172_5",
|
| 155 |
+
"residual": 2.369698582393759e-12,
|
| 156 |
+
"tip_error_over_L": 2.2341706552832602e-07
|
| 157 |
+
}
|
| 158 |
+
]
|
| 159 |
+
}
|
multizone-v02/predict.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Apache-2.0. Run: python predict.py --curvatures 1 .5 -1 1 .2 .8"""
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
import argparse,json
|
| 4 |
+
import numpy as np
|
| 5 |
+
import torch
|
| 6 |
+
from torch import nn
|
| 7 |
+
from safetensors.torch import load_file
|
| 8 |
+
|
| 9 |
+
def predict(curvatures,chi=.001,fx=.0003,fy=-.0001):
|
| 10 |
+
root=Path(__file__).resolve().parent;cfg=json.loads((root/'config.json').read_text())
|
| 11 |
+
k=np.asarray(curvatures,float)
|
| 12 |
+
if k.ndim!=1 or len(k) not in [3,6,8,12] or not np.all(np.isfinite(k)) or np.max(np.abs(k))>2.4:raise ValueError('3, 6, 12 zones (8 is OOD), finite curvature in [-2.4,2.4]')
|
| 13 |
+
if chi<=0:raise ValueError('chi must be positive')
|
| 14 |
+
field=k[np.floor((np.arange(48)+.5)*len(k)/48).astype(int)]
|
| 15 |
+
features=np.r_[field,fx/chi,fy/chi,np.log10(chi)]
|
| 16 |
+
x=torch.tensor((features-cfg['mean'])/cfg['std'],dtype=torch.float32)
|
| 17 |
+
net=nn.Sequential(nn.Linear(51,96),nn.Tanh(),nn.Linear(96,96),nn.Tanh(),nn.Linear(96,48))
|
| 18 |
+
net.load_state_dict(load_file(str(root/'residual.safetensors')));net.eval()
|
| 19 |
+
with torch.no_grad():z=np.cumsum(field)/48+cfg['output_scale']*net(x).numpy()
|
| 20 |
+
angle=(np.r_[0,z[:-1]]+z)/2;stretch=1+fx*np.cos(angle)+fy*np.sin(angle)
|
| 21 |
+
xy=np.vstack([np.zeros(2),np.cumsum(stretch[:,None]*np.c_[np.cos(angle),np.sin(angle)]/48,axis=0)])
|
| 22 |
+
return {'z':z.tolist(),'xy':xy.tolist(),'tip':xy[-1].tolist(),'source':'learned_residual_surrogate','equilibrium_verified':False}
|
| 23 |
+
if __name__=='__main__':
|
| 24 |
+
p=argparse.ArgumentParser();p.add_argument('--curvatures',type=float,nargs='+',required=True);p.add_argument('--chi',type=float,default=.001);p.add_argument('--fx',type=float,default=.0003);p.add_argument('--fy',type=float,default=-.0001);a=p.parse_args();print(json.dumps(predict(a.curvatures,a.chi,a.fx,a.fy)))
|
multizone-v02/residual.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:450959ecdf0bcb0915972db42ed378b9e1feb62a73ae7fa3ad8fcb1499cf0bac
|
| 3 |
+
size 76264
|
multizone-v02/source/multizone.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Reproducible v0.2 multi-zone supplement; preserves v0.1 artifacts."""
|
| 2 |
+
import os
|
| 3 |
+
os.environ.setdefault('OPENBLAS_NUM_THREADS','1')
|
| 4 |
+
os.environ.setdefault('OMP_NUM_THREADS','1')
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
import json,gzip,time
|
| 7 |
+
import numpy as np
|
| 8 |
+
import torch
|
| 9 |
+
from torch import nn
|
| 10 |
+
from safetensors.torch import save_file,load_file
|
| 11 |
+
from rod import Rod,Controls,serial
|
| 12 |
+
|
| 13 |
+
ROOT=Path(__file__).resolve().parents[1]
|
| 14 |
+
OUT=ROOT/'release/dataset/multizone-v02'
|
| 15 |
+
MODEL=ROOT/'release/model/multizone-v02'
|
| 16 |
+
def features(ks,chi,fx,fy):
|
| 17 |
+
field=np.asarray(ks)[np.floor((np.arange(48)+.5)*len(ks)/48).astype(int)]
|
| 18 |
+
return np.r_[field,fx/chi,fy/chi,np.log10(chi)],np.cumsum(field)/48
|
| 19 |
+
def network():
|
| 20 |
+
return nn.Sequential(nn.Linear(51,96),nn.Tanh(),nn.Linear(96,96),nn.Tanh(),nn.Linear(96,48))
|
| 21 |
+
def xy(z,fx,fy):
|
| 22 |
+
angle=(np.c_[np.zeros(len(z)),z[:,:-1]]+z)/2
|
| 23 |
+
stretch=1+fx[:,None]*np.cos(angle)+fy[:,None]*np.sin(angle)
|
| 24 |
+
ds=stretch[:,:,None]*np.stack([np.cos(angle),np.sin(angle)],axis=2)/48
|
| 25 |
+
return np.concatenate([np.zeros((len(z),1,2)),np.cumsum(ds,axis=1)],axis=1)
|
| 26 |
+
def main():
|
| 27 |
+
start=time.time();OUT.mkdir(parents=True,exist_ok=True);MODEL.mkdir(parents=True,exist_ok=True)
|
| 28 |
+
rng=np.random.default_rng(20261015);rod=Rod(48);fine=Rod(96);rows=[];mesh=[];rejected=[]
|
| 29 |
+
for m in [3,6,12,8]:
|
| 30 |
+
total=200 if m!=8 else 80
|
| 31 |
+
order=rng.permutation(total);splits={int(j):('train' if i<140 else 'validation' if i<170 else 'test') for i,j in enumerate(order)}
|
| 32 |
+
for j in range(total):
|
| 33 |
+
split=splits[j] if m!=8 else 'zone_ood';ks=rng.uniform(-2.4,2.4,m);chi=float(10**rng.uniform(-4,-2.5));fx=float(chi*rng.uniform(0,.6));fy=float(chi*rng.uniform(-.6,.6))
|
| 34 |
+
for step,t in enumerate(np.linspace(0,1,6)):
|
| 35 |
+
k=ks*t;c=Controls(chi=chi,fx=fx*t,fy=fy*t,curvatures=tuple(k));_,seed=features(k,chi,c.fx,c.fy);s=rod.solve(c,seed)
|
| 36 |
+
row=dict(id=f'z{m}_{j:03d}_{step}',scene_id=f'z{m}_{j:03d}',split=split,zones=m,t=float(t),chi=chi,fx=c.fx,fy=c.fy,curvatures=k.tolist(),**serial(s))
|
| 37 |
+
if not s['accepted'] or s['stability']!='stable':rejected.append(row);continue
|
| 38 |
+
rows.append(row)
|
| 39 |
+
if step==5 and j%20==0:
|
| 40 |
+
field=k[np.floor((np.arange(96)+.5)*m/96).astype(int)];f=fine.solve(c,np.cumsum(field)/96)
|
| 41 |
+
assert f['accepted'] and f['stability']=='stable'
|
| 42 |
+
mesh.append(dict(id=row['id'],tip_difference_over_L=float(np.linalg.norm(f['tip']-s['tip'])),centerline_rmse_over_L=float(np.sqrt(np.mean(np.sum((f['centerline']-s['centerline'])**2,axis=1))))))
|
| 43 |
+
print('Generated zones',m,'states',len(rows),flush=True)
|
| 44 |
+
for split in ['train','validation','test','zone_ood']:
|
| 45 |
+
with gzip.open(OUT/f'{split}.jsonl.gz','wt') as f:
|
| 46 |
+
for row in rows:
|
| 47 |
+
if row['split']==split:f.write(json.dumps(row)+'\n')
|
| 48 |
+
(OUT/'rejected.json').write_text(json.dumps(rejected))
|
| 49 |
+
(OUT/'mesh-checks.json').write_text(json.dumps(mesh,indent=2))
|
| 50 |
+
items=[features(r['curvatures'],r['chi'],r['fx'],r['fy']) for r in rows]
|
| 51 |
+
x=np.array([a for a,b in items],np.float32);seed=np.array([b for a,b in items],np.float32);z=np.array([r['z'] for r in rows],np.float32)
|
| 52 |
+
ix={s:np.array([i for i,r in enumerate(rows) if r['split']==s]) for s in ['train','validation','test','zone_ood']}
|
| 53 |
+
mean=x[ix['train']].mean(0);std=x[ix['train']].std(0);std[std<1e-6]=1
|
| 54 |
+
X=torch.from_numpy((x-mean)/std);Y=torch.from_numpy((z-seed)/.1)
|
| 55 |
+
torch.set_num_threads(2);torch.manual_seed(15);net=network();opt=torch.optim.Adam(net.parameters(),lr=.0015);best=float('inf');best_epoch=0
|
| 56 |
+
for epoch in range(201):
|
| 57 |
+
net.train()
|
| 58 |
+
for batch in np.array_split(rng.permutation(ix['train']),12):
|
| 59 |
+
opt.zero_grad();loss=(net(X[batch])-Y[batch]).square().mean();loss.backward();opt.step()
|
| 60 |
+
net.eval()
|
| 61 |
+
with torch.no_grad():val=float((net(X[ix['validation']])-Y[ix['validation']]).square().mean())
|
| 62 |
+
if val<best:best=val;best_epoch=epoch;weights={k:v.clone().contiguous() for k,v in net.state_dict().items()}
|
| 63 |
+
if epoch%50==0:print('Epoch',epoch,'validation',val,flush=True)
|
| 64 |
+
save_file(weights,str(MODEL/'residual.safetensors'));net.load_state_dict(load_file(str(MODEL/'residual.safetensors')))
|
| 65 |
+
with torch.no_grad():pred=seed+.1*net(X).numpy()
|
| 66 |
+
fx=np.array([r['fx'] for r in rows]);fy=np.array([r['fy'] for r in rows]);truth=xy(z,fx,fy)
|
| 67 |
+
metrics={}
|
| 68 |
+
for split,idx in ix.items():
|
| 69 |
+
metrics[split]={}
|
| 70 |
+
for name,zz in [('intrinsic_shape',seed),('learned_residual',pred)]:
|
| 71 |
+
err=np.linalg.norm(xy(zz,fx,fy)[idx]-truth[idx],axis=2)
|
| 72 |
+
metrics[split][name]={'centerline_rmse_over_L':float(np.sqrt(np.mean(err**2))),'tip_mean_over_L':float(err[:,-1].mean()),'tip_p95_over_L':float(np.quantile(err[:,-1],.95))}
|
| 73 |
+
nonzero=np.array([rows[i]['t']>0 for i in idx]);metrics[split][name]['nonzero_loading_rmse_over_L']=float(np.sqrt(np.mean(err[nonzero]**2)))
|
| 74 |
+
correction=[]
|
| 75 |
+
for i in ix['test'][5::36]:
|
| 76 |
+
r=rows[i];c=Controls(chi=r['chi'],fx=r['fx'],fy=r['fy'],curvatures=tuple(r['curvatures']))
|
| 77 |
+
corrected=rod.solve(c,pred[i]);assert corrected['accepted']
|
| 78 |
+
correction.append(dict(id=r['id'],residual=corrected['residual'],tip_error_over_L=float(np.linalg.norm(corrected['tip']-r['tip']))))
|
| 79 |
+
report=dict(version='0.2',states=len(rows),scenes=len(set(r['scene_id'] for r in rows)),counts={s:len(i) for s,i in ix.items()},zones_id=[3,6,12],zones_ood=[8],rejected=len(rejected),mesh_checks=len(mesh),max_mesh_tip_difference_over_L=max(r['tip_difference_over_L'] for r in mesh),best_epoch=best_epoch,metrics=metrics,seconds=time.time()-start)
|
| 80 |
+
report['mechanics_correction_checks']=correction
|
| 81 |
+
(MODEL/'config.json').write_text(json.dumps(dict(mean=mean.tolist(),std=std.tolist(),output_scale=.1,n=48,architecture=[51,96,96,48],features='48 piecewise intrinsic curvatures, fx/chi, fy/chi, log10(chi)',prediction='cumulative intrinsic angle + output_scale * network(standardized features)'),indent=2))
|
| 82 |
+
for p in [MODEL/'evaluation.json',OUT/'validation.json',ROOT/'reports/multizone-v02.json']:p.write_text(json.dumps(report,indent=2))
|
| 83 |
+
print(json.dumps(report,indent=2),flush=True)
|
| 84 |
+
if __name__=='__main__':main()
|
multizone-v02/source/rod.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Conservative planar extensible rod. L=EA=1; chi=EI/(EA L^2).
|
| 2 |
+
|
| 3 |
+
Rod: linear angle FE, midpoint axial elimination. Chain: Ye et al. (2026)
|
| 4 |
+
cell rotations, first cell clamped; kr=chi*n. Independent implementations
|
| 5 |
+
share ONLY the reduced local energy density. No contact or dynamics.
|
| 6 |
+
SPDX-License-Identifier: Apache-2.0
|
| 7 |
+
"""
|
| 8 |
+
from dataclasses import dataclass, asdict
|
| 9 |
+
import numpy as np
|
| 10 |
+
from scipy.linalg import solve, eigh
|
| 11 |
+
from scipy.optimize import root
|
| 12 |
+
|
| 13 |
+
@dataclass
|
| 14 |
+
class Controls:
|
| 15 |
+
chi: float = .001
|
| 16 |
+
fx: float = 0.
|
| 17 |
+
fy: float = 0.
|
| 18 |
+
moment: float = 0.
|
| 19 |
+
h: float = 0.
|
| 20 |
+
delta: float = .1
|
| 21 |
+
k1: float = 0.
|
| 22 |
+
k2: float = 0.
|
| 23 |
+
k3: float = 0.
|
| 24 |
+
curvatures: tuple | None = None
|
| 25 |
+
|
| 26 |
+
class Rod:
|
| 27 |
+
def __init__(self, n=48, kind="rod"):
|
| 28 |
+
self.n, self.kind, self.ds = n, kind, 1/n
|
| 29 |
+
if kind == "rod":
|
| 30 |
+
self.d = n
|
| 31 |
+
A = np.zeros((n,n+1)); B = A.copy()
|
| 32 |
+
for i in range(n):
|
| 33 |
+
A[i,i:i+2] = .5
|
| 34 |
+
B[i,i:i+2] = [-n,n]
|
| 35 |
+
self.A,self.B = A[:,1:],B[:,1:]
|
| 36 |
+
self.mass = (A[:,1:].T@A[:,1:])/n
|
| 37 |
+
elif kind == "chain":
|
| 38 |
+
self.d = n-1
|
| 39 |
+
self.A = np.eye(n)[:,1:]
|
| 40 |
+
self.B = np.diff(np.eye(n),axis=0)[:,1:]*n
|
| 41 |
+
self.mass = np.eye(n-1)/n
|
| 42 |
+
else: raise ValueError(kind)
|
| 43 |
+
self.s = (np.arange(n)+.5)/n
|
| 44 |
+
|
| 45 |
+
def terms(self, z, c):
|
| 46 |
+
angle=self.A@z
|
| 47 |
+
ks=np.asarray(c.curvatures if c.curvatures is not None else [c.k1,c.k2,c.k3])
|
| 48 |
+
intrinsic=ks[np.minimum((np.arange(self.B.shape[0])+.5)*len(ks)/self.B.shape[0],len(ks)-1).astype(int)]
|
| 49 |
+
curvature=self.B@z-intrinsic
|
| 50 |
+
a=c.fx+2*c.h
|
| 51 |
+
f=a*np.cos(angle)+c.fy*np.sin(angle)
|
| 52 |
+
fp=-a*np.sin(angle)+c.fy*np.cos(angle)
|
| 53 |
+
g=4*c.h*c.delta
|
| 54 |
+
density=-f-.5*f*f+g*np.cos(angle)
|
| 55 |
+
energy=self.ds*(.5*c.chi*(curvature@curvature)+density.sum())-c.moment*z[-1]
|
| 56 |
+
local_g=-(1+f)*fp-g*np.sin(angle)
|
| 57 |
+
local_h=(1+f)*f-fp*fp-g*np.cos(angle)
|
| 58 |
+
grad=self.ds*(c.chi*self.B.T@curvature+self.A.T@local_g)
|
| 59 |
+
grad[-1]-=c.moment
|
| 60 |
+
tangent=self.ds*(c.chi*self.B.T@self.B+self.A.T@(local_h[:,None]*self.A))
|
| 61 |
+
return energy,grad,tangent
|
| 62 |
+
|
| 63 |
+
def solve(self,c,seed=None):
|
| 64 |
+
z=np.zeros(self.d) if seed is None else np.array(seed,dtype=float).copy()
|
| 65 |
+
it=0; trace=[]; fallback_nfev=0
|
| 66 |
+
for it in range(60):
|
| 67 |
+
_,r,K=self.terms(z,c)
|
| 68 |
+
trace.append(dict(iteration=it,residual_inf=float(np.max(np.abs(r)))))
|
| 69 |
+
if np.max(np.abs(r))<1e-10: break
|
| 70 |
+
try: dz=solve(K,-r,assume_a='sym')
|
| 71 |
+
except Exception: break
|
| 72 |
+
old=np.linalg.norm(r)
|
| 73 |
+
step=1.
|
| 74 |
+
for _ in range(16):
|
| 75 |
+
trial=z+step*dz
|
| 76 |
+
if np.linalg.norm(self.terms(trial,c)[1])<old: break
|
| 77 |
+
step*=.5
|
| 78 |
+
else: break
|
| 79 |
+
z=trial
|
| 80 |
+
trace[-1]['accepted_step_scale']=step
|
| 81 |
+
residual=float(np.max(np.abs(self.terms(z,c)[1])))
|
| 82 |
+
if residual>1e-9:
|
| 83 |
+
sol=root(lambda v:self.terms(v,c)[1],z,jac=lambda v:self.terms(v,c)[2],tol=1e-10)
|
| 84 |
+
z=sol.x; fallback_nfev=sol.nfev
|
| 85 |
+
energy,r,K=self.terms(z,c)
|
| 86 |
+
residual=float(np.max(np.abs(r)))
|
| 87 |
+
angle=self.A@z
|
| 88 |
+
stretch=1+(c.fx+2*c.h)*np.cos(angle)+c.fy*np.sin(angle)
|
| 89 |
+
admissible=bool(np.min(stretch)>(2*c.delta if c.h!=0 else 0))
|
| 90 |
+
good=bool(residual<1e-9 and admissible and np.all(np.isfinite(z)))
|
| 91 |
+
eig=float(eigh(K,self.mass,subset_by_index=[0,0],eigvals_only=True)[0]/c.chi) if good else None
|
| 92 |
+
stability='unknown' if eig is None else ('stable' if eig>1e-4 else 'unstable' if eig< -1e-4 else 'critical')
|
| 93 |
+
segments=self.ds*stretch[:,None]*np.column_stack([np.cos(angle),np.sin(angle)])
|
| 94 |
+
xy=np.vstack([np.zeros(2),np.cumsum(segments,axis=0)])
|
| 95 |
+
# Output resampled on reference coordinate, not current arc length.
|
| 96 |
+
center=np.column_stack([np.interp(np.linspace(0,1,65),np.linspace(0,1,self.n+1),xy[:,j]) for j in range(2)])
|
| 97 |
+
return dict(z=z,centerline=center,angle=angle,strain=stretch-1,energy=float(energy),residual=residual,
|
| 98 |
+
iterations=it+1,accepted=good,lambda_min_over_chi=eig,stability=stability,
|
| 99 |
+
tip=center[-1],min_stretch=float(stretch.min()),solver_trace=trace,fallback_nfev=fallback_nfev)
|
| 100 |
+
|
| 101 |
+
def mode_seed(self,amplitude):
|
| 102 |
+
s=np.arange(1,self.d+1)/self.n
|
| 103 |
+
return amplitude*np.sin(np.pi*s/2)
|
| 104 |
+
|
| 105 |
+
def serial(result):
|
| 106 |
+
return {k:(v.tolist() if isinstance(v,np.ndarray) else v) for k,v in result.items()}
|