loadpath 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- loadpath/README.md +294 -0
- loadpath/SKILL.md +584 -0
- loadpath/__init__.py +17 -0
- loadpath/__main__.py +1202 -0
- loadpath/am.py +316 -0
- loadpath/amgfast.py +326 -0
- loadpath/assembly.py +985 -0
- loadpath/benchmarks/__init__.py +0 -0
- loadpath/benchmarks/beams.py +266 -0
- loadpath/benchmarks/cases/ge-bracket.json +380 -0
- loadpath/benchmarks/evaluate.py +172 -0
- loadpath/benchmarks/mechanisms.py +416 -0
- loadpath/benchmarks/problems.py +140 -0
- loadpath/benchmarks/reference/bridge-3d.top3d.npz +0 -0
- loadpath/benchmarks/reference/cantilever-3d.top3d.npz +0 -0
- loadpath/benchmarks/reference/lbracket-2d.top88-ft2.npz +0 -0
- loadpath/benchmarks/reference/mbb-2d.top88-ft1.npz +0 -0
- loadpath/benchmarks/reference/mbb-2d.top88-ft2.npz +0 -0
- loadpath/benchmarks/reference.py +138 -0
- loadpath/benchmarks/run.py +301 -0
- loadpath/brep.py +1470 -0
- loadpath/bucklingopt.py +300 -0
- loadpath/buildsim.py +1490 -0
- loadpath/cadimport.py +904 -0
- loadpath/calibration.py +885 -0
- loadpath/cavities.py +211 -0
- loadpath/checks/__init__.py +0 -0
- loadpath/checks/dfm.py +354 -0
- loadpath/checks/exchanger.py +7 -0
- loadpath/checks/fea.py +212 -0
- loadpath/checks/fit.py +475 -0
- loadpath/checks/networks.py +260 -0
- loadpath/checks/opening.py +67 -0
- loadpath/checks/sdf_kit.py +7 -0
- loadpath/conjugate.py +267 -0
- loadpath/constrained.py +329 -0
- loadpath/contact.py +691 -0
- loadpath/coopt.py +342 -0
- loadpath/cores.py +163 -0
- loadpath/crease.py +1138 -0
- loadpath/critic.py +212 -0
- loadpath/crosscheck.py +1107 -0
- loadpath/cutcell.py +510 -0
- loadpath/data/lattice_buckling.json +439 -0
- loadpath/data/lattice_tensors.json +27449 -0
- loadpath/data/published_experiments.json +71 -0
- loadpath/decimate.py +177 -0
- loadpath/delivery.py +190 -0
- loadpath/directions.py +262 -0
- loadpath/domain.py +630 -0
- loadpath/drains.py +615 -0
- loadpath/durability.py +413 -0
- loadpath/editor.html +3135 -0
- loadpath/editor.py +610 -0
- loadpath/escape.py +354 -0
- loadpath/evals/__init__.py +0 -0
- loadpath/evals/baselines/coarse.json +1543 -0
- loadpath/evals/baselines/full.json +1497 -0
- loadpath/evals/brief_kinds/__init__.py +1 -0
- loadpath/evals/brief_kinds/heatx.py +387 -0
- loadpath/evals/brief_kinds/lattice.py +571 -0
- loadpath/evals/brief_kinds/multimat.py +593 -0
- loadpath/evals/briefs/assets/bracket.step +1201 -0
- loadpath/evals/briefs/assets/desk_arm.urdf +22 -0
- loadpath/evals/briefs/assets/elbow-gearbox.stl +0 -0
- loadpath/evals/briefs/assets/motor-body.stl +0 -0
- loadpath/evals/briefs/assets/sketch.png +0 -0
- loadpath/evals/briefs/base-adapter-fdm.json +492 -0
- loadpath/evals/briefs/bell-crank-lpbf.json +180 -0
- loadpath/evals/briefs/belt-tensioner-fdm.json +151 -0
- loadpath/evals/briefs/camera-boom-mjf.json +339 -0
- loadpath/evals/briefs/camera-mount-pla.json +203 -0
- loadpath/evals/briefs/coolant-isolator-petg.json +100 -0
- loadpath/evals/briefs/elbow-bearing-bracket-sls.json +300 -0
- loadpath/evals/briefs/elbow-gearbox-forearm-mjf.json +558 -0
- loadpath/evals/briefs/elbow-link-fatigue-fdm.json +251 -0
- loadpath/evals/briefs/elbow-stop-pad-tpu.json +39 -0
- loadpath/evals/briefs/forearm-fdm.json +146 -0
- loadpath/evals/briefs/forearm-servo-link-fdm.json +327 -0
- loadpath/evals/briefs/grip-pad-finger-mm.json +354 -0
- loadpath/evals/briefs/gripper-bracket-creep-fdm.json +340 -0
- loadpath/evals/briefs/gripper-finger-fdm.json +155 -0
- loadpath/evals/briefs/gripper-jaw-fatigue-mjf.json +328 -0
- loadpath/evals/briefs/gripper-palm-fdm.json +715 -0
- loadpath/evals/briefs/hot-stepper-mount-asa-fdm.json +471 -0
- loadpath/evals/briefs/hpu-oil-cooler-lpbf.json +121 -0
- loadpath/evals/briefs/isolated-camera-mount-mm.json +675 -0
- loadpath/evals/briefs/kart-charge-cooler-lpbf.json +93 -0
- loadpath/evals/briefs/lighten-step-bracket.json +220 -0
- loadpath/evals/briefs/nema17-mount-stl.json +429 -0
- loadpath/evals/briefs/pillow-block-sls.json +220 -0
- loadpath/evals/briefs/polyjet-fingertip-mm.json +305 -0
- loadpath/evals/briefs/rod-hook-pla.json +154 -0
- loadpath/evals/briefs/sensor-arm-resin.json +150 -0
- loadpath/evals/briefs/shelf-bracket-pla.json +158 -0
- loadpath/evals/briefs/shoulder-riser-mjf.json +487 -0
- loadpath/evals/briefs/sketch-clevis.json +229 -0
- loadpath/evals/briefs/spring-bracket-pacf.json +294 -0
- loadpath/evals/briefs/tool-flange-ti64.json +280 -0
- loadpath/evals/briefs/tray-arm-lattice-fdm.json +260 -0
- loadpath/evals/briefs/turret-fdm.json +281 -0
- loadpath/evals/briefs/upper-arm-sls.json +179 -0
- loadpath/evals/briefs/urdf-forearm.json +233 -0
- loadpath/evals/briefs/wrist-link-mjf.json +278 -0
- loadpath/evals/briefs/wrist-yoke-mjf.json +245 -0
- loadpath/evals/briefs.py +393 -0
- loadpath/evals/cases/arm-link-cast.json +157 -0
- loadpath/evals/cases/arm-link-driven.json +171 -0
- loadpath/evals/cases/arm-link-sandwich.json +315 -0
- loadpath/evals/cases/arm-link.json +111 -0
- loadpath/evals/cases/bell-crank.json +116 -0
- loadpath/evals/cases/grip-turret.json +479 -0
- loadpath/evals/cases/grip-upper-arm.json +253 -0
- loadpath/evals/cases/grip-wrist-link-mjf.json +929 -0
- loadpath/evals/cases/grip-wrist-yoke.json +831 -0
- loadpath/evals/cases/lpbf-intercooler-core.json +59 -0
- loadpath/evals/cases/mbb-beam.json +61 -0
- loadpath/evals/cases/motor-body.stl +0 -0
- loadpath/evals/cases/motor-mount.json +265 -0
- loadpath/evals/cases/printed-hook.json +77 -0
- loadpath/evals/cases/wall-bracket-fasteners.json +125 -0
- loadpath/evals/cases/wall-bracket.json +126 -0
- loadpath/evals/corpus.py +179 -0
- loadpath/evals/pareto.py +90 -0
- loadpath/evals/run.py +860 -0
- loadpath/examples/arm-link.json +25 -0
- loadpath/examples/bumpered-bracket-compression.json +225 -0
- loadpath/examples/bumpered-bracket-lattice.json +204 -0
- loadpath/examples/bumpered-bracket.json +34 -0
- loadpath/examples/gripper-finger-mm.json +185 -0
- loadpath/examples/heat-exchanger-counterflow.json +21 -0
- loadpath/examples/heat-exchanger-crossflow-tuned.json +48 -0
- loadpath/examples/heat-exchanger-crossflow.json +20 -0
- loadpath/examples/pedal-lattice.json +23 -0
- loadpath/examples/pressure-plenum.json +12 -0
- loadpath/examples/shelf-bracket.json +27 -0
- loadpath/exchanger.py +831 -0
- loadpath/fasteners.py +199 -0
- loadpath/fem.py +668 -0
- loadpath/femops.py +70 -0
- loadpath/fieldjit.py +981 -0
- loadpath/fieldops.py +119 -0
- loadpath/fields.py +3697 -0
- loadpath/fieldtorch.py +464 -0
- loadpath/flow.py +984 -0
- loadpath/flowtopo.py +300 -0
- loadpath/gpu.py +521 -0
- loadpath/heatx.py +1067 -0
- loadpath/heatx_thermal.py +432 -0
- loadpath/homogenized.py +886 -0
- loadpath/ibrep.py +552 -0
- loadpath/impact.py +693 -0
- loadpath/infill.py +376 -0
- loadpath/interface_library.py +340 -0
- loadpath/interlock.py +186 -0
- loadpath/isolator.py +149 -0
- loadpath/knn.py +204 -0
- loadpath/lattice.py +1065 -0
- loadpath/lattice3mf.py +760 -0
- loadpath/lattice_buckling.py +245 -0
- loadpath/lattice_grade.py +325 -0
- loadpath/lattice_orient.py +595 -0
- loadpath/lattice_print.py +159 -0
- loadpath/lattice_tensors.py +558 -0
- loadpath/latticestep.py +1210 -0
- loadpath/manufacture.py +446 -0
- loadpath/material_library.py +489 -0
- loadpath/materials.py +144 -0
- loadpath/mcp_server.py +786 -0
- loadpath/mechanism.py +218 -0
- loadpath/mechanism_nl.py +811 -0
- loadpath/meshops.py +452 -0
- loadpath/mg.py +853 -0
- loadpath/mma.py +214 -0
- loadpath/modal.py +210 -0
- loadpath/multimat.py +1788 -0
- loadpath/narrowband.py +418 -0
- loadpath/nlkernel.py +241 -0
- loadpath/nonlinear.py +1095 -0
- loadpath/orient.py +334 -0
- loadpath/porous.py +994 -0
- loadpath/pressure.py +171 -0
- loadpath/preview.py +335 -0
- loadpath/printcheck.py +263 -0
- loadpath/printsetup.py +207 -0
- loadpath/processcheck.py +376 -0
- loadpath/quick.py +113 -0
- loadpath/recall.py +406 -0
- loadpath/reconstruct.py +797 -0
- loadpath/render.py +436 -0
- loadpath/resolved.py +189 -0
- loadpath/schema.py +1701 -0
- loadpath/sdf_kit.py +843 -0
- loadpath/selfx.py +292 -0
- loadpath/shapes.py +584 -0
- loadpath/sharp.py +717 -0
- loadpath/simp.py +1170 -0
- loadpath/sizing.py +312 -0
- loadpath/slicer.py +321 -0
- loadpath/spec.py +1072 -0
- loadpath/step.py +806 -0
- loadpath/struts.py +639 -0
- loadpath/study.py +1937 -0
- loadpath/tasks.py +822 -0
- loadpath/thermal.py +1691 -0
- loadpath/threemf.py +487 -0
- loadpath/tune.py +437 -0
- loadpath/validation.py +145 -0
- loadpath/verify.py +1215 -0
- loadpath/viewer.html +694 -0
- loadpath/viewer.py +446 -0
- loadpath/viscoelastic.py +106 -0
- loadpath/voxelprint.py +582 -0
- loadpath/voxels.py +402 -0
- loadpath-0.1.1.dist-info/METADATA +319 -0
- loadpath-0.1.1.dist-info/RECORD +220 -0
- loadpath-0.1.1.dist-info/WHEEL +5 -0
- loadpath-0.1.1.dist-info/entry_points.txt +2 -0
- loadpath-0.1.1.dist-info/licenses/LICENSE +21 -0
- loadpath-0.1.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "wrist-yoke-mjf",
|
|
3
|
+
"brief": "Wrist yoke for a robot arm, MJF PA12. It sits on a 25 mm diameter servo horn: the horn's top\nface is at z = 0, its axis is Z through the origin, and the yoke is screwed to it with four\nM2.5 screws on a 16 mm bolt circle (holes at (8,0), (-8,0), (0,8), (0,-8)). Give the yoke a\nsolid 4 mm thick disc over the horn (z = 0 to 4, 25 mm diameter) with those holes through it.\nTwo cheeks rise from the base and carry the gripper's pivot: an 8 mm shaft along X at z = 45,\nthrough 8.2 mm holes in both cheeks, with at least 3 mm of wall round each hole. The wrist servo\nsits between the cheeks, so x = -15 to 15, y = -12 to 12, z = 10 to 60 has to stay empty, and\nthe cheeks can't go past x = +/-24. Overall the part fits in x = -24 to 24, y = -14 to 14,\nz = 0 to 56. The gripper hanging off the pivot is 800 g on a fast arm, and the wrist servo\ntwists it with up to 1.5 N\u00b7m about Z. Safety factor 2.",
|
|
4
|
+
"attachments": [],
|
|
5
|
+
"tags": [
|
|
6
|
+
"arm",
|
|
7
|
+
"mjf",
|
|
8
|
+
"moment"
|
|
9
|
+
],
|
|
10
|
+
"gold": {
|
|
11
|
+
"designSpace": [
|
|
12
|
+
{
|
|
13
|
+
"type": "box",
|
|
14
|
+
"min": [
|
|
15
|
+
-24.0,
|
|
16
|
+
-14.0,
|
|
17
|
+
0.0
|
|
18
|
+
],
|
|
19
|
+
"max": [
|
|
20
|
+
24.0,
|
|
21
|
+
14.0,
|
|
22
|
+
56.0
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
"preserve": [
|
|
27
|
+
{
|
|
28
|
+
"id": "horn-disc",
|
|
29
|
+
"shape": {
|
|
30
|
+
"type": "cylinder",
|
|
31
|
+
"p0": [
|
|
32
|
+
0.0,
|
|
33
|
+
0.0,
|
|
34
|
+
0.0
|
|
35
|
+
],
|
|
36
|
+
"p1": [
|
|
37
|
+
0.0,
|
|
38
|
+
0.0,
|
|
39
|
+
4.0
|
|
40
|
+
],
|
|
41
|
+
"r": 12.5
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"id": "bosses",
|
|
46
|
+
"shape": {
|
|
47
|
+
"type": "union",
|
|
48
|
+
"shapes": [
|
|
49
|
+
{
|
|
50
|
+
"type": "cylinder",
|
|
51
|
+
"p0": [
|
|
52
|
+
18.0,
|
|
53
|
+
0.0,
|
|
54
|
+
45.0
|
|
55
|
+
],
|
|
56
|
+
"p1": [
|
|
57
|
+
21.0,
|
|
58
|
+
0.0,
|
|
59
|
+
45.0
|
|
60
|
+
],
|
|
61
|
+
"r": 7.1
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"type": "cylinder",
|
|
65
|
+
"p0": [
|
|
66
|
+
-21.0,
|
|
67
|
+
0.0,
|
|
68
|
+
45.0
|
|
69
|
+
],
|
|
70
|
+
"p1": [
|
|
71
|
+
-18.0,
|
|
72
|
+
0.0,
|
|
73
|
+
45.0
|
|
74
|
+
],
|
|
75
|
+
"r": 7.1
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
],
|
|
81
|
+
"keepOut": [
|
|
82
|
+
{
|
|
83
|
+
"id": "horn-screws",
|
|
84
|
+
"shape": {
|
|
85
|
+
"type": "union",
|
|
86
|
+
"shapes": [
|
|
87
|
+
{
|
|
88
|
+
"type": "cylinder",
|
|
89
|
+
"p0": [
|
|
90
|
+
8.0,
|
|
91
|
+
0.0,
|
|
92
|
+
-1.0
|
|
93
|
+
],
|
|
94
|
+
"p1": [
|
|
95
|
+
8.0,
|
|
96
|
+
0.0,
|
|
97
|
+
6.0
|
|
98
|
+
],
|
|
99
|
+
"r": 1.4
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"type": "cylinder",
|
|
103
|
+
"p0": [
|
|
104
|
+
-8.0,
|
|
105
|
+
0.0,
|
|
106
|
+
-1.0
|
|
107
|
+
],
|
|
108
|
+
"p1": [
|
|
109
|
+
-8.0,
|
|
110
|
+
0.0,
|
|
111
|
+
6.0
|
|
112
|
+
],
|
|
113
|
+
"r": 1.4
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"type": "cylinder",
|
|
117
|
+
"p0": [
|
|
118
|
+
0.0,
|
|
119
|
+
8.0,
|
|
120
|
+
-1.0
|
|
121
|
+
],
|
|
122
|
+
"p1": [
|
|
123
|
+
0.0,
|
|
124
|
+
8.0,
|
|
125
|
+
6.0
|
|
126
|
+
],
|
|
127
|
+
"r": 1.4
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"type": "cylinder",
|
|
131
|
+
"p0": [
|
|
132
|
+
0.0,
|
|
133
|
+
-8.0,
|
|
134
|
+
-1.0
|
|
135
|
+
],
|
|
136
|
+
"p1": [
|
|
137
|
+
0.0,
|
|
138
|
+
-8.0,
|
|
139
|
+
6.0
|
|
140
|
+
],
|
|
141
|
+
"r": 1.4
|
|
142
|
+
}
|
|
143
|
+
]
|
|
144
|
+
},
|
|
145
|
+
"bore": false
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
"id": "pivot",
|
|
149
|
+
"shape": {
|
|
150
|
+
"type": "cylinder",
|
|
151
|
+
"p0": [
|
|
152
|
+
-30.0,
|
|
153
|
+
0.0,
|
|
154
|
+
45.0
|
|
155
|
+
],
|
|
156
|
+
"p1": [
|
|
157
|
+
30.0,
|
|
158
|
+
0.0,
|
|
159
|
+
45.0
|
|
160
|
+
],
|
|
161
|
+
"r": 4.1
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"id": "servo",
|
|
166
|
+
"shape": {
|
|
167
|
+
"type": "box",
|
|
168
|
+
"min": [
|
|
169
|
+
-15.0,
|
|
170
|
+
-12.0,
|
|
171
|
+
10.0
|
|
172
|
+
],
|
|
173
|
+
"max": [
|
|
174
|
+
15.0,
|
|
175
|
+
12.0,
|
|
176
|
+
60.0
|
|
177
|
+
]
|
|
178
|
+
},
|
|
179
|
+
"bore": false
|
|
180
|
+
}
|
|
181
|
+
],
|
|
182
|
+
"supports": [
|
|
183
|
+
{
|
|
184
|
+
"region": "horn-screws"
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
"shape": {
|
|
188
|
+
"type": "cylinder",
|
|
189
|
+
"p0": [
|
|
190
|
+
0.0,
|
|
191
|
+
0.0,
|
|
192
|
+
-0.5
|
|
193
|
+
],
|
|
194
|
+
"p1": [
|
|
195
|
+
0.0,
|
|
196
|
+
0.0,
|
|
197
|
+
0.3
|
|
198
|
+
],
|
|
199
|
+
"r": 12.5
|
|
200
|
+
},
|
|
201
|
+
"fix": "z"
|
|
202
|
+
}
|
|
203
|
+
],
|
|
204
|
+
"loadCases": [
|
|
205
|
+
{
|
|
206
|
+
"name": "hang",
|
|
207
|
+
"loads": [
|
|
208
|
+
{
|
|
209
|
+
"region": "pivot",
|
|
210
|
+
"force": [
|
|
211
|
+
0,
|
|
212
|
+
0,
|
|
213
|
+
-11.772000000000002
|
|
214
|
+
]
|
|
215
|
+
}
|
|
216
|
+
]
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
"name": "twist",
|
|
220
|
+
"loads": [
|
|
221
|
+
{
|
|
222
|
+
"region": "pivot",
|
|
223
|
+
"moment": [
|
|
224
|
+
0,
|
|
225
|
+
0,
|
|
226
|
+
1500
|
|
227
|
+
]
|
|
228
|
+
}
|
|
229
|
+
]
|
|
230
|
+
}
|
|
231
|
+
],
|
|
232
|
+
"material": "pa12",
|
|
233
|
+
"process": "mjf",
|
|
234
|
+
"goal": {
|
|
235
|
+
"safetyFactor": 2
|
|
236
|
+
},
|
|
237
|
+
"name": "wrist-yoke-mjf",
|
|
238
|
+
"resolution": {
|
|
239
|
+
"maxElements": 60000
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
"pass": {
|
|
243
|
+
"safetyFactor": 2
|
|
244
|
+
}
|
|
245
|
+
}
|
loadpath/evals/briefs.py
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Brief evals: from a plain-language request to a printable part, with no
|
|
3
|
+
human in the loop.
|
|
4
|
+
|
|
5
|
+
Each brief in briefs/*.json is what a person would type to their agent (plus
|
|
6
|
+
attachments in briefs/assets/: an STL, a STEP, a URDF, a dimensioned sketch)
|
|
7
|
+
and a gold study: the minimum defensible reading of the brief (its
|
|
8
|
+
interfaces, envelope, supports, loads, material and process). The agent never
|
|
9
|
+
sees the gold study. It writes its own study, runs it and hands back an STL;
|
|
10
|
+
the scorer re-verifies that STL against the gold study (verify.verify_part,
|
|
11
|
+
the same analysis loadpath uses on its own outcomes), so a part only passes
|
|
12
|
+
if it is right for the brief, not for whatever problem the agent wrote down:
|
|
13
|
+
|
|
14
|
+
pass = one body, inside the envelope, every gold keep-out clear, every
|
|
15
|
+
gold interface present, gold safety factor >= SF_SLACK x target in
|
|
16
|
+
every gold load case, deflection where the brief asks, and the
|
|
17
|
+
brief's mass limit (or, without one, at most half the solid
|
|
18
|
+
envelope's mass: exporting the design space is not a design)
|
|
19
|
+
|
|
20
|
+
Run:
|
|
21
|
+
python -m loadpath.evals.briefs list
|
|
22
|
+
python -m loadpath.evals.briefs gold # lint + build every gold study
|
|
23
|
+
python -m loadpath.evals.briefs prompt <id> # what the agent is told
|
|
24
|
+
python -m loadpath.evals.briefs score <id> part.stl
|
|
25
|
+
python -m loadpath.evals.briefs run --out briefs-out/ [--jobs 4] [ids...]
|
|
26
|
+
drives a headless agent (`claude -p`, loadpath MCP server + CLI) per
|
|
27
|
+
brief in its own empty folder, then scores what it left behind;
|
|
28
|
+
writes briefs-out/scorecard.json and scorecard.md.
|
|
29
|
+
python -m loadpath.evals.briefs table briefs-out/ # re-score / re-tabulate
|
|
30
|
+
|
|
31
|
+
Briefs are kept out of the agent's reach by running it in a fresh folder with
|
|
32
|
+
only the attachments; the prompt also says the answer key is off limits.
|
|
33
|
+
"""
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import argparse
|
|
37
|
+
import glob
|
|
38
|
+
import json
|
|
39
|
+
import os
|
|
40
|
+
import shutil
|
|
41
|
+
import subprocess
|
|
42
|
+
import sys
|
|
43
|
+
import time
|
|
44
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
45
|
+
|
|
46
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
47
|
+
BRIEFS = os.environ.get("LOADPATH_BRIEFS") or os.path.join(HERE, "briefs")
|
|
48
|
+
ASSETS = os.path.join(BRIEFS, "assets")
|
|
49
|
+
|
|
50
|
+
SF_SLACK = 0.9 # gold SF must reach 90% of the target: the agent's grid and ours differ
|
|
51
|
+
DEFLECTION_SLACK = 1.1
|
|
52
|
+
MASS_SLACK = 1.05
|
|
53
|
+
MAX_INTRUSION = 30.0 # mm^3 of part inside a gold keep-out (a 0.05 mm bore undersize is ~10)
|
|
54
|
+
MAX_MISSING = 30.0 # mm^3 of gold interface missing
|
|
55
|
+
MAX_OUTSIDE = 100.0 # mm^3 of part outside the envelope
|
|
56
|
+
MAX_SOLID_FRACTION = 0.5 # without a stated mass, at most half the solid envelope (no "export the box")
|
|
57
|
+
VERIFY_CELLS = 40_000
|
|
58
|
+
|
|
59
|
+
PROMPT = """{brief}
|
|
60
|
+
|
|
61
|
+
{attachments}You have loadpath for this: its MCP tools (server `loadpath`) and the `loadpath`
|
|
62
|
+
command line. Work it through on your own; nobody will answer questions, so make reasonable
|
|
63
|
+
engineering assumptions and note them. When you are done, leave in the current folder:
|
|
64
|
+
part.stl the part to print (your final, verified outcome)
|
|
65
|
+
study.json the study that produced it
|
|
66
|
+
notes.md two or three lines: the outcome's mass and safety factor, and your assumptions
|
|
67
|
+
This machine is shared: keep resolution.maxElements at or under {max_elements} and run one
|
|
68
|
+
optimization at a time. Do not read loadpath/evals/briefs (that is the answer key).
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def load(name: str) -> dict:
|
|
73
|
+
path = name if name.endswith(".json") else os.path.join(BRIEFS, name + ".json")
|
|
74
|
+
with open(path) as fh:
|
|
75
|
+
return json.load(fh)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def all_ids() -> list[str]:
|
|
79
|
+
return sorted(os.path.splitext(os.path.basename(p))[0] for p in glob.glob(os.path.join(BRIEFS, "*.json")))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def gold_study(brief: dict):
|
|
83
|
+
from ..spec import study_from_dict
|
|
84
|
+
return study_from_dict(brief["gold"], base_dir=ASSETS)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def kind(brief: dict):
|
|
88
|
+
"""The brief's kind module, for briefs that are not a single structural
|
|
89
|
+
part: loadpath/evals/brief_kinds/<kind>.py with DELIVER (what the agent
|
|
90
|
+
leaves), score(brief, folder) and gold(brief) (the gold's own check).
|
|
91
|
+
None for the default "study" kind (part.stl under a gold study)."""
|
|
92
|
+
k = brief.get("kind", "study")
|
|
93
|
+
if k == "study":
|
|
94
|
+
return None
|
|
95
|
+
import importlib
|
|
96
|
+
return importlib.import_module(f"loadpath.evals.brief_kinds.{k}")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def prompt(brief: dict, max_elements: int = 20000) -> str:
|
|
100
|
+
att = ""
|
|
101
|
+
if brief.get("attachments"):
|
|
102
|
+
att = "Attached (in the current folder): " + ", ".join(os.path.basename(a) for a in brief["attachments"]) + "\n\n"
|
|
103
|
+
text = PROMPT.format(brief=brief["brief"], attachments=att, max_elements=max_elements)
|
|
104
|
+
mod = kind(brief)
|
|
105
|
+
if mod is not None:
|
|
106
|
+
head, _, rest = text.partition("When you are done, leave in the current folder:\n")
|
|
107
|
+
tail = rest[rest.index("This machine is shared"):]
|
|
108
|
+
text = head + "When you are done, leave in the current folder:\n" + mod.DELIVER.rstrip("\n") + "\n" + tail
|
|
109
|
+
return text
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def score_dir(brief: dict, folder: str, max_cells: int = VERIFY_CELLS) -> dict:
|
|
113
|
+
"""Score what an agent left in `folder`, by the brief's kind."""
|
|
114
|
+
mod = kind(brief)
|
|
115
|
+
if mod is None:
|
|
116
|
+
return score(brief, os.path.join(folder, "part.stl"), max_cells)
|
|
117
|
+
try:
|
|
118
|
+
row = mod.score(brief, folder)
|
|
119
|
+
except Exception as exc: # noqa: BLE001 - a broken delivery is a failed brief, not a crashed eval
|
|
120
|
+
msg = str(exc).strip().splitlines()[-1] if str(exc).strip() else type(exc).__name__
|
|
121
|
+
row = {"pass": False, "failures": [f"scoring failed: {msg[:200]}"]}
|
|
122
|
+
row.setdefault("failures", [])
|
|
123
|
+
row["id"] = brief["id"]
|
|
124
|
+
row["pass"] = bool(row.get("pass")) and not row["failures"]
|
|
125
|
+
return row
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def score(brief: dict, stl: str, max_cells: int = VERIFY_CELLS) -> dict:
|
|
129
|
+
"""Re-verify the delivered STL under the gold study. Returns
|
|
130
|
+
{pass, failures, mass, cases: [{name, safetyFactor, deflection}], ...}."""
|
|
131
|
+
import trimesh
|
|
132
|
+
|
|
133
|
+
from ..verify import verify_part
|
|
134
|
+
|
|
135
|
+
want = brief["pass"]
|
|
136
|
+
row = {"id": brief["id"], "pass": False, "failures": []}
|
|
137
|
+
if not stl or not os.path.exists(stl):
|
|
138
|
+
row["failures"].append("no part.stl")
|
|
139
|
+
return row
|
|
140
|
+
try:
|
|
141
|
+
mesh = trimesh.load(stl, force="mesh")
|
|
142
|
+
study = gold_study(brief)
|
|
143
|
+
rep = verify_part(study, mesh, max_cells=max_cells, refine=3)
|
|
144
|
+
except Exception as exc: # noqa: BLE001 - a broken part is a failed part, not a crashed eval
|
|
145
|
+
msg = str(exc).strip().splitlines()[-1] if str(exc).strip() else type(exc).__name__
|
|
146
|
+
row["failures"].append(f"verify failed: {msg[:200]}")
|
|
147
|
+
return row
|
|
148
|
+
f = row["failures"]
|
|
149
|
+
row["mass"] = round(rep["mass"], 2)
|
|
150
|
+
row["bodies"] = rep.get("bodies")
|
|
151
|
+
row["keepOutIntrusion"] = round(rep.get("keepOutIntrusion", 0.0), 1)
|
|
152
|
+
row["preserveMissing"] = round(rep.get("preserveMissing", 0.0), 1)
|
|
153
|
+
row["outsideDesignSpace"] = round(rep.get("outsideDesignSpace", 0.0), 1)
|
|
154
|
+
row["cases"] = [{"name": c["name"], "safetyFactor": round(c["safetyFactor"], 3),
|
|
155
|
+
"deflection": round(c["deflection"], 4) if c.get("deflection") is not None else None}
|
|
156
|
+
for c in rep["cases"]]
|
|
157
|
+
row["safetyFactor"] = round(rep["safetyFactor"], 3)
|
|
158
|
+
if rep.get("bodies") != 1:
|
|
159
|
+
f.append(f"{rep.get('bodies')} bodies")
|
|
160
|
+
if row["outsideDesignSpace"] > MAX_OUTSIDE:
|
|
161
|
+
f.append(f"{row['outsideDesignSpace']} mm^3 outside the envelope")
|
|
162
|
+
if row["keepOutIntrusion"] > MAX_INTRUSION:
|
|
163
|
+
f.append(f"{row['keepOutIntrusion']} mm^3 in keep-outs")
|
|
164
|
+
if row["preserveMissing"] > MAX_MISSING:
|
|
165
|
+
f.append(f"{row['preserveMissing']} mm^3 of interfaces missing")
|
|
166
|
+
sf = want.get("safetyFactor")
|
|
167
|
+
if sf:
|
|
168
|
+
for c in row["cases"]:
|
|
169
|
+
if c["safetyFactor"] < SF_SLACK * sf:
|
|
170
|
+
f.append(f"SF {c['safetyFactor']:.2f} < {sf} in '{c['name']}'")
|
|
171
|
+
dsf = want.get("durabilitySafetyFactor")
|
|
172
|
+
if dsf:
|
|
173
|
+
# fatigue / creep under the gold study's durability block
|
|
174
|
+
dur = rep.get("durability") if isinstance(rep.get("durability"), dict) else {}
|
|
175
|
+
row["durabilitySafetyFactor"] = dur.get("safetyFactor")
|
|
176
|
+
if dur.get("safetyFactor") is None:
|
|
177
|
+
f.append("no durability check under the gold study")
|
|
178
|
+
elif dur["safetyFactor"] < SF_SLACK * dsf:
|
|
179
|
+
f.append(f"durability SF {dur['safetyFactor']:.2f} < {dsf}")
|
|
180
|
+
dmax = want.get("maxDeflection")
|
|
181
|
+
if dmax:
|
|
182
|
+
for c in row["cases"]:
|
|
183
|
+
if c["deflection"] is not None and c["deflection"] > DEFLECTION_SLACK * dmax:
|
|
184
|
+
f.append(f"deflection {c['deflection']:.3f} > {dmax} mm in '{c['name']}'")
|
|
185
|
+
mmax = want.get("maxMass")
|
|
186
|
+
row["solidFraction"] = round(rep["mass"] / rep["designSpaceMass"], 3) if rep.get("designSpaceMass") else None
|
|
187
|
+
if mmax and rep["mass"] > MASS_SLACK * mmax:
|
|
188
|
+
f.append(f"{rep['mass']:.1f} g > {mmax} g")
|
|
189
|
+
elif not mmax and row["solidFraction"] and row["solidFraction"] > want.get("maxSolidFraction", MAX_SOLID_FRACTION):
|
|
190
|
+
f.append(f"{row['solidFraction']:.0%} of the solid envelope's mass")
|
|
191
|
+
row["pass"] = not f
|
|
192
|
+
return row
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# --- the agent harness -------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
def _mcp_config(path: str):
|
|
198
|
+
with open(path, "w") as fh:
|
|
199
|
+
json.dump({"mcpServers": {"loadpath": {"command": shutil.which("loadpath") or "loadpath", "args": ["mcp"]}}}, fh)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _tool_stats(transcript: str) -> dict:
|
|
203
|
+
"""Count tool calls by name and pull the final result line out of a
|
|
204
|
+
stream-json transcript."""
|
|
205
|
+
calls, result = {}, {}
|
|
206
|
+
try:
|
|
207
|
+
with open(transcript) as fh:
|
|
208
|
+
for line in fh:
|
|
209
|
+
try:
|
|
210
|
+
ev = json.loads(line)
|
|
211
|
+
except json.JSONDecodeError:
|
|
212
|
+
continue
|
|
213
|
+
if ev.get("type") == "assistant":
|
|
214
|
+
for c in ev.get("message", {}).get("content", []):
|
|
215
|
+
if c.get("type") == "tool_use":
|
|
216
|
+
n = c.get("name", "?")
|
|
217
|
+
calls[n] = calls.get(n, 0) + 1
|
|
218
|
+
elif ev.get("type") == "result":
|
|
219
|
+
result = ev
|
|
220
|
+
except OSError:
|
|
221
|
+
pass
|
|
222
|
+
return {"toolCalls": calls, "turns": result.get("num_turns"), "costUSD": result.get("total_cost_usd"),
|
|
223
|
+
"agentSeconds": (result.get("duration_ms") or 0) / 1000.0 or None, "agentError": result.get("is_error")}
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def prepare(brief: dict, out: str, max_elements: int = 20000) -> tuple[str, str]:
|
|
227
|
+
"""A fresh folder for one brief: its attachments and PROMPT.md. Any agent
|
|
228
|
+
can then be pointed at it; `table` scores what it leaves behind."""
|
|
229
|
+
work = os.path.join(out, brief["id"])
|
|
230
|
+
if os.path.exists(work):
|
|
231
|
+
shutil.rmtree(work)
|
|
232
|
+
os.makedirs(work)
|
|
233
|
+
for a in brief.get("attachments", []):
|
|
234
|
+
shutil.copy(os.path.join(BRIEFS, a), os.path.join(work, os.path.basename(a)))
|
|
235
|
+
text = prompt(brief, max_elements)
|
|
236
|
+
with open(os.path.join(work, "PROMPT.md"), "w") as fh:
|
|
237
|
+
fh.write(text)
|
|
238
|
+
return work, text
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# Headless agents get exactly the tools the task needs, not a blanket bypass.
|
|
242
|
+
ALLOWED_TOOLS = ["Read", "Write", "Edit", "Glob", "Grep", "mcp__loadpath", "Bash(loadpath:*)",
|
|
243
|
+
"Bash(python -m loadpath:*)", "Bash(ls:*)", "Bash(cat:*)", "Bash(head:*)", "Bash(tail:*)"]
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def run_agent(brief: dict, out: str, agent: str = "claude", model: str | None = None, timeout: float = 3600,
|
|
247
|
+
max_elements: int = 20000, threads: int = 2) -> dict:
|
|
248
|
+
work, text = prepare(brief, out, max_elements)
|
|
249
|
+
cfg = os.path.join(out, f".{brief['id']}.mcp.json")
|
|
250
|
+
_mcp_config(cfg)
|
|
251
|
+
env = dict(os.environ, LOADPATH_HOME=os.path.join(out, f".{brief['id']}.jobs"))
|
|
252
|
+
for k in ("OMP_NUM_THREADS", "OPENBLAS_NUM_THREADS", "MKL_NUM_THREADS", "NUMBA_NUM_THREADS"):
|
|
253
|
+
env[k] = str(threads)
|
|
254
|
+
cmd = [agent, "-p", text, "--output-format", "stream-json", "--verbose", "--mcp-config", cfg,
|
|
255
|
+
"--allowedTools", *ALLOWED_TOOLS, "--max-turns", "150"]
|
|
256
|
+
if model:
|
|
257
|
+
cmd += ["--model", model]
|
|
258
|
+
t0 = time.time()
|
|
259
|
+
status = "ok"
|
|
260
|
+
with open(os.path.join(work, ".transcript.jsonl"), "w") as tf, open(os.path.join(work, ".agent.err"), "w") as ef:
|
|
261
|
+
try:
|
|
262
|
+
subprocess.run(cmd, cwd=work, env=env, stdout=tf, stderr=ef, timeout=timeout)
|
|
263
|
+
except subprocess.TimeoutExpired:
|
|
264
|
+
status = "timeout"
|
|
265
|
+
seconds = time.time() - t0
|
|
266
|
+
row = score_dir(brief, work)
|
|
267
|
+
row.update(_tool_stats(os.path.join(work, ".transcript.jsonl")))
|
|
268
|
+
row.update({"seconds": round(seconds, 1), "agentStatus": status,
|
|
269
|
+
"studyWritten": os.path.exists(os.path.join(work, "study.json")), "tags": brief.get("tags", [])})
|
|
270
|
+
with open(os.path.join(work, "score.json"), "w") as fh:
|
|
271
|
+
json.dump(row, fh, indent=1)
|
|
272
|
+
return row
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def table(rows: list[dict]) -> str:
|
|
276
|
+
n = len(rows)
|
|
277
|
+
ok = sum(1 for r in rows if r["pass"])
|
|
278
|
+
lines = [f"**{ok}/{n} briefs pass** ({100.0 * ok / max(n, 1):.0f}%) with no human fixes.", "",
|
|
279
|
+
"| brief | pass | mass g | gold SF (worst case) | failures | turns | min |",
|
|
280
|
+
"| --- | --- | --- | --- | --- | --- | --- |"]
|
|
281
|
+
for r in sorted(rows, key=lambda r: r["id"]):
|
|
282
|
+
lines.append("| {} | {} | {} | {} | {} | {} | {} |".format(
|
|
283
|
+
r["id"], "yes" if r["pass"] else "no", r.get("mass", ""), r.get("safetyFactor", ""),
|
|
284
|
+
"; ".join(r["failures"]) or "", r.get("turns") or "", round(r["seconds"] / 60, 1) if r.get("seconds") else ""))
|
|
285
|
+
return "\n".join(lines) + "\n"
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def main(argv=None):
|
|
289
|
+
ap = argparse.ArgumentParser(prog="python -m loadpath.evals.briefs", description=__doc__.split("\n\n")[0])
|
|
290
|
+
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
291
|
+
sub.add_parser("list")
|
|
292
|
+
g = sub.add_parser("gold")
|
|
293
|
+
g.add_argument("ids", nargs="*")
|
|
294
|
+
p = sub.add_parser("prompt")
|
|
295
|
+
p.add_argument("id")
|
|
296
|
+
s = sub.add_parser("score")
|
|
297
|
+
s.add_argument("id")
|
|
298
|
+
s.add_argument("path", help="the agent's folder (any kind), or part.stl (a study brief)")
|
|
299
|
+
r = sub.add_parser("run")
|
|
300
|
+
r.add_argument("ids", nargs="*")
|
|
301
|
+
r.add_argument("--out", default="briefs-out")
|
|
302
|
+
r.add_argument("--jobs", type=int, default=4)
|
|
303
|
+
r.add_argument("--agent", default="claude")
|
|
304
|
+
r.add_argument("--model")
|
|
305
|
+
r.add_argument("--timeout", type=float, default=3600)
|
|
306
|
+
r.add_argument("--max-elements", type=int, default=20000)
|
|
307
|
+
r.add_argument("--threads", type=int, default=2)
|
|
308
|
+
pr = sub.add_parser("prepare")
|
|
309
|
+
pr.add_argument("ids", nargs="*")
|
|
310
|
+
pr.add_argument("--out", default="briefs-out")
|
|
311
|
+
pr.add_argument("--max-elements", type=int, default=20000)
|
|
312
|
+
t = sub.add_parser("table")
|
|
313
|
+
t.add_argument("out")
|
|
314
|
+
t.add_argument("--rescore", action="store_true")
|
|
315
|
+
a = ap.parse_args(argv)
|
|
316
|
+
|
|
317
|
+
if a.cmd == "list":
|
|
318
|
+
for i in all_ids():
|
|
319
|
+
b = load(i)
|
|
320
|
+
print(f"{i:28s} {','.join(b.get('tags', []))}")
|
|
321
|
+
return 0
|
|
322
|
+
if a.cmd == "gold":
|
|
323
|
+
from ..tasks import check_study
|
|
324
|
+
bad = 0
|
|
325
|
+
for i in a.ids or all_ids():
|
|
326
|
+
b = load(i)
|
|
327
|
+
mod = kind(b)
|
|
328
|
+
res = mod.gold(b) if mod is not None else check_study(b["gold"], base_dir=ASSETS)
|
|
329
|
+
bad += not res["ok"]
|
|
330
|
+
print(i, "ok" if res["ok"] else res["error"], res.get("cells", ""), "; ".join(res.get("warnings", []))[:300])
|
|
331
|
+
return bad
|
|
332
|
+
if a.cmd == "prompt":
|
|
333
|
+
print(prompt(load(a.id)))
|
|
334
|
+
return 0
|
|
335
|
+
if a.cmd == "score":
|
|
336
|
+
b = load(a.id)
|
|
337
|
+
if os.path.isdir(a.path):
|
|
338
|
+
row = score_dir(b, a.path)
|
|
339
|
+
else:
|
|
340
|
+
row = score(b, a.path)
|
|
341
|
+
print(json.dumps(row, indent=1))
|
|
342
|
+
return 0
|
|
343
|
+
if a.cmd == "run":
|
|
344
|
+
os.makedirs(a.out, exist_ok=True)
|
|
345
|
+
ids = a.ids or all_ids()
|
|
346
|
+
rows = []
|
|
347
|
+
|
|
348
|
+
def one(i):
|
|
349
|
+
row = run_agent(load(i), a.out, a.agent, a.model, a.timeout, a.max_elements, a.threads)
|
|
350
|
+
print(json.dumps({k: row.get(k) for k in ("id", "pass", "failures", "mass", "safetyFactor", "turns", "seconds")}),
|
|
351
|
+
flush=True)
|
|
352
|
+
return row
|
|
353
|
+
|
|
354
|
+
with ThreadPoolExecutor(max_workers=a.jobs) as ex:
|
|
355
|
+
rows = list(ex.map(one, ids))
|
|
356
|
+
_write(a.out, rows)
|
|
357
|
+
return sum(not r["pass"] for r in rows)
|
|
358
|
+
if a.cmd == "prepare":
|
|
359
|
+
for i in a.ids or all_ids():
|
|
360
|
+
print(prepare(load(i), a.out, a.max_elements)[0])
|
|
361
|
+
return 0
|
|
362
|
+
if a.cmd == "table":
|
|
363
|
+
rows = []
|
|
364
|
+
for d in sorted(glob.glob(os.path.join(a.out, "*", "PROMPT.md"))):
|
|
365
|
+
d = os.path.dirname(d)
|
|
366
|
+
f = os.path.join(d, "score.json")
|
|
367
|
+
old = json.load(open(f)) if os.path.exists(f) else None
|
|
368
|
+
row = old
|
|
369
|
+
stl = os.path.join(d, "part.stl")
|
|
370
|
+
stale = old is not None and os.path.exists(stl) and os.path.getmtime(stl) > os.path.getmtime(f)
|
|
371
|
+
if old is None or a.rescore or stale:
|
|
372
|
+
row = score_dir(load(os.path.basename(d)), d)
|
|
373
|
+
row["studyWritten"] = os.path.exists(os.path.join(d, "study.json"))
|
|
374
|
+
if old:
|
|
375
|
+
row.update({k: old[k] for k in old if k not in row})
|
|
376
|
+
with open(f, "w") as fh:
|
|
377
|
+
json.dump(row, fh, indent=1)
|
|
378
|
+
rows.append(row)
|
|
379
|
+
_write(a.out, rows)
|
|
380
|
+
return sum(not r["pass"] for r in rows)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _write(out, rows):
|
|
384
|
+
with open(os.path.join(out, "scorecard.json"), "w") as fh:
|
|
385
|
+
json.dump(rows, fh, indent=1)
|
|
386
|
+
md = table(rows)
|
|
387
|
+
with open(os.path.join(out, "scorecard.md"), "w") as fh:
|
|
388
|
+
fh.write(md)
|
|
389
|
+
print(md)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
if __name__ == "__main__":
|
|
393
|
+
sys.exit(main())
|