metafold 0.14.dev0__tar.gz → 0.15.0__tar.gz
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.
- {metafold-0.14.dev0 → metafold-0.15.0}/PKG-INFO +2 -2
- {metafold-0.14.dev0 → metafold-0.15.0}/metafold/materials.py +120 -1
- {metafold-0.14.dev0 → metafold-0.15.0}/metafold/simulation/compression_simulation.py +15 -1
- {metafold-0.14.dev0 → metafold-0.15.0}/metafold/simulation/run_experiment.py +43 -1
- {metafold-0.14.dev0 → metafold-0.15.0}/metafold.egg-info/PKG-INFO +2 -2
- {metafold-0.14.dev0 → metafold-0.15.0}/metafold.egg-info/requires.txt +1 -1
- {metafold-0.14.dev0 → metafold-0.15.0}/pyproject.toml +2 -2
- {metafold-0.14.dev0 → metafold-0.15.0}/tests/test_materials.py +155 -0
- {metafold-0.14.dev0 → metafold-0.15.0}/tests/test_run_experiment.py +59 -0
- {metafold-0.14.dev0 → metafold-0.15.0}/LICENSE +0 -0
- {metafold-0.14.dev0 → metafold-0.15.0}/README.md +0 -0
- {metafold-0.14.dev0 → metafold-0.15.0}/metafold/__init__.py +0 -0
- {metafold-0.14.dev0 → metafold-0.15.0}/metafold/api.py +0 -0
- {metafold-0.14.dev0 → metafold-0.15.0}/metafold/assets.py +0 -0
- {metafold-0.14.dev0 → metafold-0.15.0}/metafold/auth.py +0 -0
- {metafold-0.14.dev0 → metafold-0.15.0}/metafold/client.py +0 -0
- {metafold-0.14.dev0 → metafold-0.15.0}/metafold/exceptions.py +0 -0
- {metafold-0.14.dev0 → metafold-0.15.0}/metafold/jobs.py +0 -0
- {metafold-0.14.dev0 → metafold-0.15.0}/metafold/projects.py +0 -0
- {metafold-0.14.dev0 → metafold-0.15.0}/metafold/simulation/__init__.py +0 -0
- {metafold-0.14.dev0 → metafold-0.15.0}/metafold/simulation/compression_experiment.py +0 -0
- {metafold-0.14.dev0 → metafold-0.15.0}/metafold/utils.py +0 -0
- {metafold-0.14.dev0 → metafold-0.15.0}/metafold/workflows.py +0 -0
- {metafold-0.14.dev0 → metafold-0.15.0}/metafold.egg-info/SOURCES.txt +0 -0
- {metafold-0.14.dev0 → metafold-0.15.0}/metafold.egg-info/dependency_links.txt +0 -0
- {metafold-0.14.dev0 → metafold-0.15.0}/metafold.egg-info/top_level.txt +0 -0
- {metafold-0.14.dev0 → metafold-0.15.0}/setup.cfg +0 -0
- {metafold-0.14.dev0 → metafold-0.15.0}/tests/test_assets.py +0 -0
- {metafold-0.14.dev0 → metafold-0.15.0}/tests/test_compession_experiment.py +0 -0
- {metafold-0.14.dev0 → metafold-0.15.0}/tests/test_compression_simulation.py +0 -0
- {metafold-0.14.dev0 → metafold-0.15.0}/tests/test_jobs.py +0 -0
- {metafold-0.14.dev0 → metafold-0.15.0}/tests/test_projects.py +0 -0
- {metafold-0.14.dev0 → metafold-0.15.0}/tests/test_shear_simulation.py +0 -0
- {metafold-0.14.dev0 → metafold-0.15.0}/tests/test_utils.py +0 -0
- {metafold-0.14.dev0 → metafold-0.15.0}/tests/test_workflows.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: metafold
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.15.0
|
|
4
4
|
Summary: Metafold SDK for Python
|
|
5
5
|
Author-email: Metafold 3D <info@metafold3d.com>
|
|
6
6
|
License: Copyright 2024 Metafold 3D
|
|
@@ -38,7 +38,7 @@ Requires-Dist: dotenv>=0.9.9; extra == "simulation"
|
|
|
38
38
|
Requires-Dist: pandas>=2.3.3; extra == "simulation"
|
|
39
39
|
Requires-Dist: plyfile>=1.1.3; extra == "simulation"
|
|
40
40
|
Requires-Dist: pyyaml>=6.0.3; extra == "simulation"
|
|
41
|
-
Requires-Dist: simulation-configurator==0.
|
|
41
|
+
Requires-Dist: simulation-configurator==0.3.*; extra == "simulation"
|
|
42
42
|
Requires-Dist: tables>=3.11.1; extra == "simulation"
|
|
43
43
|
Dynamic: license-file
|
|
44
44
|
|
|
@@ -211,6 +211,111 @@ class MaxwellWeichertParams(ParamsBase):
|
|
|
211
211
|
)
|
|
212
212
|
|
|
213
213
|
|
|
214
|
+
@dataclass
|
|
215
|
+
class HyperfoamTerm:
|
|
216
|
+
"""One Ogden term of a hyperfoam material (mpmgpu allows up to 3)."""
|
|
217
|
+
mu: float
|
|
218
|
+
alpha: float
|
|
219
|
+
poisson_ratio: float
|
|
220
|
+
|
|
221
|
+
def to_dict(self):
|
|
222
|
+
# "_element" tells the ups emitter to render this as a <term>.
|
|
223
|
+
return {
|
|
224
|
+
"_element": "term",
|
|
225
|
+
"mu": float(self.mu),
|
|
226
|
+
"alpha": float(self.alpha),
|
|
227
|
+
"poisson_ratio": float(self.poisson_ratio),
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
@dataclass
|
|
232
|
+
class HyperfoamParams(ParamsBase):
|
|
233
|
+
"""Ogden hyperfoam (mpmgpu only; no Uintah equivalent).
|
|
234
|
+
|
|
235
|
+
The Ogden hyperfoam model is defined entirely by its terms (bulk_modulus is
|
|
236
|
+
not used, so it is not part of this material; a legacy "bulk_modulus"/"K" is
|
|
237
|
+
ignored if present on from_dict).
|
|
238
|
+
|
|
239
|
+
An optional viscoelastic overlay (Prony series) may be supplied via
|
|
240
|
+
terminal_shear_modulus + viscoelastic_series, matching the MaxwellWeichert
|
|
241
|
+
mode structure. Omit both for a purely elastic hyperfoam.
|
|
242
|
+
"""
|
|
243
|
+
terms: List[HyperfoamTerm]
|
|
244
|
+
terminal_shear_modulus: Optional[float] = None
|
|
245
|
+
viscoelastic_series: Optional[List[ViscoelasticMode]] = None
|
|
246
|
+
|
|
247
|
+
@classmethod
|
|
248
|
+
def get_type(self):
|
|
249
|
+
return "hyperfoam"
|
|
250
|
+
|
|
251
|
+
def to_dict(self):
|
|
252
|
+
d = {
|
|
253
|
+
"terms": [t.to_dict() for t in self.terms],
|
|
254
|
+
}
|
|
255
|
+
if self.terminal_shear_modulus is not None:
|
|
256
|
+
d["terminal_shear_modulus"] = float(self.terminal_shear_modulus)
|
|
257
|
+
if self.viscoelastic_series:
|
|
258
|
+
d["viscoelastic_series"] = [m.to_dict() for m in self.viscoelastic_series]
|
|
259
|
+
return d
|
|
260
|
+
|
|
261
|
+
@classmethod
|
|
262
|
+
def from_dict(cls, d: dict) -> "HyperfoamParams":
|
|
263
|
+
# A legacy bulk_modulus/K is ignored (older scripts may still send it).
|
|
264
|
+
series = d.get("viscoelastic_series")
|
|
265
|
+
return cls(
|
|
266
|
+
terms=[
|
|
267
|
+
HyperfoamTerm(
|
|
268
|
+
mu=t["mu"], alpha=t["alpha"], poisson_ratio=t["poisson_ratio"])
|
|
269
|
+
for t in d["terms"]
|
|
270
|
+
],
|
|
271
|
+
terminal_shear_modulus=d.get("terminal_shear_modulus"),
|
|
272
|
+
viscoelastic_series=(
|
|
273
|
+
[ViscoelasticMode(**m) for m in series] if series else None),
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
@dataclass
|
|
278
|
+
class OrthotropicParams(ParamsBase):
|
|
279
|
+
"""Linear-elastic orthotropic (mpmgpu only; no Uintah equivalent)."""
|
|
280
|
+
E1: float
|
|
281
|
+
E2: float
|
|
282
|
+
E3: float
|
|
283
|
+
nu12: float
|
|
284
|
+
nu13: float
|
|
285
|
+
nu23: float
|
|
286
|
+
G12: float
|
|
287
|
+
G13: float
|
|
288
|
+
G23: float
|
|
289
|
+
direction_a: List[float]
|
|
290
|
+
direction_b: List[float]
|
|
291
|
+
|
|
292
|
+
@classmethod
|
|
293
|
+
def get_type(self):
|
|
294
|
+
return "orthotropic"
|
|
295
|
+
|
|
296
|
+
def to_dict(self):
|
|
297
|
+
return {
|
|
298
|
+
"E1": float(self.E1), "E2": float(self.E2), "E3": float(self.E3),
|
|
299
|
+
"nu12": float(self.nu12), "nu13": float(self.nu13), "nu23": float(self.nu23),
|
|
300
|
+
"G12": float(self.G12), "G13": float(self.G13), "G23": float(self.G23),
|
|
301
|
+
# Numeric lists so simulation_configurator emits them as vectors.
|
|
302
|
+
"direction_a": [float(x) for x in self.direction_a],
|
|
303
|
+
"direction_b": [float(x) for x in self.direction_b],
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
@classmethod
|
|
307
|
+
def from_dict(cls, d: dict) -> "OrthotropicParams":
|
|
308
|
+
def vec(v):
|
|
309
|
+
return [float(x) for x in (v.split() if isinstance(v, str) else v)]
|
|
310
|
+
return cls(
|
|
311
|
+
E1=d["E1"], E2=d["E2"], E3=d["E3"],
|
|
312
|
+
nu12=d["nu12"], nu13=d["nu13"], nu23=d["nu23"],
|
|
313
|
+
G12=d["G12"], G13=d["G13"], G23=d["G23"],
|
|
314
|
+
direction_a=vec(d["direction_a"]),
|
|
315
|
+
direction_b=vec(d["direction_b"]),
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
|
|
214
319
|
@dataclass
|
|
215
320
|
class CompNeoHookParams(ParamsBase):
|
|
216
321
|
bulk_modulus: float
|
|
@@ -351,10 +456,24 @@ class Material:
|
|
|
351
456
|
ElasticPlasticParams,
|
|
352
457
|
HypoElasticParams,
|
|
353
458
|
ViscoTransIsoHyperParams,
|
|
459
|
+
HyperfoamParams,
|
|
460
|
+
OrthotropicParams,
|
|
354
461
|
]
|
|
355
462
|
PARAMS_CLASSES = {c.get_type(): c for c in params_classes}
|
|
356
463
|
cm = d["constitutive_model"]
|
|
357
|
-
|
|
464
|
+
model_type = cm.get("type")
|
|
465
|
+
if model_type not in PARAMS_CLASSES:
|
|
466
|
+
# ValueError so API callers reject with a 400, not a 500. An
|
|
467
|
+
# unknown type usually means a client newer than the server.
|
|
468
|
+
raise ValueError(
|
|
469
|
+
f"Unknown constitutive model type {model_type!r}. "
|
|
470
|
+
f"Known types: {', '.join(sorted(PARAMS_CLASSES))}.")
|
|
471
|
+
try:
|
|
472
|
+
params = PARAMS_CLASSES[model_type].from_dict(cm["params"])
|
|
473
|
+
except KeyError as e:
|
|
474
|
+
# Missing param -> ValueError (400) rather than an uncaught 500.
|
|
475
|
+
raise ValueError(
|
|
476
|
+
f"{model_type} material is missing required param {e}.")
|
|
358
477
|
return cls(
|
|
359
478
|
density=d["density"],
|
|
360
479
|
thermal_conductivity=d["thermal_conductivity"],
|
|
@@ -474,6 +474,8 @@ class CompressionSimulation:
|
|
|
474
474
|
# experiment variants sampled later match the base simulation's density.
|
|
475
475
|
sample_spacing: Optional[float] = None
|
|
476
476
|
|
|
477
|
+
use_experimental_solver: bool = False
|
|
478
|
+
|
|
477
479
|
def __init__(
|
|
478
480
|
self,
|
|
479
481
|
parts: list[ExperimentPart],
|
|
@@ -503,6 +505,7 @@ class CompressionSimulation:
|
|
|
503
505
|
project_name: str = "",
|
|
504
506
|
use_legacy_results_format: bool = False,
|
|
505
507
|
write_ups: bool = True,
|
|
508
|
+
use_experimental_solver: bool = False,
|
|
506
509
|
):
|
|
507
510
|
if not output_path:
|
|
508
511
|
if project_name:
|
|
@@ -542,6 +545,8 @@ class CompressionSimulation:
|
|
|
542
545
|
else:
|
|
543
546
|
self.client = client
|
|
544
547
|
|
|
548
|
+
self.use_experimental_solver = use_experimental_solver
|
|
549
|
+
|
|
545
550
|
def _write_ups(self, content: str, filename: str) -> None:
|
|
546
551
|
if self.write_ups and self.out_dir is not None:
|
|
547
552
|
(self.out_dir / filename).write_text(content)
|
|
@@ -1370,8 +1375,12 @@ class CompressionSimulation:
|
|
|
1370
1375
|
# Only parts that have a volume go into the compress assets list
|
|
1371
1376
|
parts_with_volume = [p for p in part_infos if p.volume_filename]
|
|
1372
1377
|
|
|
1378
|
+
job_type = "sim/custom"
|
|
1379
|
+
if self.use_experimental_solver:
|
|
1380
|
+
job_type = "sim/custom-gpu"
|
|
1381
|
+
|
|
1373
1382
|
self.workflow_jobs[compress_job_name] = {
|
|
1374
|
-
"type":
|
|
1383
|
+
"type": job_type,
|
|
1375
1384
|
}
|
|
1376
1385
|
|
|
1377
1386
|
# compress.volume is a list of volume asset filenames in part order
|
|
@@ -1505,6 +1514,11 @@ class CompressionSimulation:
|
|
|
1505
1514
|
for row in piston_info.part.velocity
|
|
1506
1515
|
]
|
|
1507
1516
|
)
|
|
1517
|
+
if piston_info.part.force_limit:
|
|
1518
|
+
# Switch to using piston particle positions to determine displacement
|
|
1519
|
+
self.workflow_params[f"{job_name_base}.piston_position"] = (
|
|
1520
|
+
f"/material{piston_info.material_index}/position"
|
|
1521
|
+
)
|
|
1508
1522
|
|
|
1509
1523
|
def _add_to_workflow_stress_strain(self, part_infos: list[PartInfo]):
|
|
1510
1524
|
# One stress-strain job per deformable part, each normalizing the shared
|
|
@@ -251,6 +251,34 @@ def _build_parts(parts_config: list[dict]) -> list[ExperimentPart]:
|
|
|
251
251
|
return parts
|
|
252
252
|
|
|
253
253
|
|
|
254
|
+
# Constitutive models that only the experimental (mpmgpu GPU) solver supports;
|
|
255
|
+
# Uintah has no equivalent, so they require use_experimental_solver.
|
|
256
|
+
_EXPERIMENTAL_ONLY_MATERIALS = {
|
|
257
|
+
_materials_module.OrthotropicParams.get_type(),
|
|
258
|
+
_materials_module.HyperfoamParams.get_type(),
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _material_type(material: Material) -> str:
|
|
263
|
+
return material.constitutive_model.params.get_type()
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _validate_experimental_materials(config: dict, parts, varying) -> None:
|
|
267
|
+
"""Reject experimental-only materials unless use_experimental_solver is set."""
|
|
268
|
+
if config.get("use_experimental_solver", False):
|
|
269
|
+
return
|
|
270
|
+
materials = [p.material for p in parts]
|
|
271
|
+
for v in varying:
|
|
272
|
+
if isinstance(v, VaryMaterial):
|
|
273
|
+
materials.extend(v.materials)
|
|
274
|
+
used = {_material_type(m) for m in materials} & _EXPERIMENTAL_ONLY_MATERIALS
|
|
275
|
+
if used:
|
|
276
|
+
names = ", ".join(sorted(used))
|
|
277
|
+
raise ValueError(
|
|
278
|
+
f"Material(s) {names} are only supported with the experimental "
|
|
279
|
+
f"solver. Set use_experimental_solver to true to use them.")
|
|
280
|
+
|
|
281
|
+
|
|
254
282
|
def validate_experiment_config(config: dict) -> None:
|
|
255
283
|
"""Raise ValueError if a manifest can't produce a runnable experiment.
|
|
256
284
|
Mirrors the checks run_experiment performs so an API can reject bad
|
|
@@ -258,7 +286,10 @@ def validate_experiment_config(config: dict) -> None:
|
|
|
258
286
|
"""
|
|
259
287
|
if not config.get("project_name", ""):
|
|
260
288
|
raise ValueError("project_name is required")
|
|
261
|
-
|
|
289
|
+
parts = _build_parts(config.get("parts", []))
|
|
290
|
+
validate_simulation_parts(parts)
|
|
291
|
+
_validate_experimental_materials(
|
|
292
|
+
config, parts, _build_varying(config.get("varying", [])))
|
|
262
293
|
|
|
263
294
|
|
|
264
295
|
def _build_varying(varying_config: list[dict]) -> list[ExperimentVarying]:
|
|
@@ -329,6 +360,12 @@ def _build_simulation_parameters(sim_config: dict) -> SimulationParameters:
|
|
|
329
360
|
return params
|
|
330
361
|
|
|
331
362
|
|
|
363
|
+
def num_simulations(config: dict) -> int:
|
|
364
|
+
"""Count number of simulation variations to be generated by a given configuration.
|
|
365
|
+
"""
|
|
366
|
+
return len(_build_varying(config.get("varying", [])))
|
|
367
|
+
|
|
368
|
+
|
|
332
369
|
def run_experiment(
|
|
333
370
|
config: dict,
|
|
334
371
|
output_path: str = "",
|
|
@@ -373,6 +410,10 @@ def run_experiment(
|
|
|
373
410
|
varying = _build_varying(config.get("varying", []))
|
|
374
411
|
sim_params = _build_simulation_parameters(config.get("simulation", {}))
|
|
375
412
|
|
|
413
|
+
_validate_experimental_materials(config, parts, varying)
|
|
414
|
+
|
|
415
|
+
use_experimental_solver = config.get("use_experimental_solver", False)
|
|
416
|
+
|
|
376
417
|
sim_kwargs: dict = dict(
|
|
377
418
|
parts=parts,
|
|
378
419
|
simulation_name=project_name,
|
|
@@ -381,6 +422,7 @@ def run_experiment(
|
|
|
381
422
|
simulation_parameters=sim_params,
|
|
382
423
|
project_id=project_id,
|
|
383
424
|
create_project_if_needed=not bool(project_id),
|
|
425
|
+
use_experimental_solver=use_experimental_solver,
|
|
384
426
|
)
|
|
385
427
|
# Only pass project_name when we have to create a project. When a
|
|
386
428
|
# project_id is supplied the project already exists, and passing the name
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: metafold
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.15.0
|
|
4
4
|
Summary: Metafold SDK for Python
|
|
5
5
|
Author-email: Metafold 3D <info@metafold3d.com>
|
|
6
6
|
License: Copyright 2024 Metafold 3D
|
|
@@ -38,7 +38,7 @@ Requires-Dist: dotenv>=0.9.9; extra == "simulation"
|
|
|
38
38
|
Requires-Dist: pandas>=2.3.3; extra == "simulation"
|
|
39
39
|
Requires-Dist: plyfile>=1.1.3; extra == "simulation"
|
|
40
40
|
Requires-Dist: pyyaml>=6.0.3; extra == "simulation"
|
|
41
|
-
Requires-Dist: simulation-configurator==0.
|
|
41
|
+
Requires-Dist: simulation-configurator==0.3.*; extra == "simulation"
|
|
42
42
|
Requires-Dist: tables>=3.11.1; extra == "simulation"
|
|
43
43
|
Dynamic: license-file
|
|
44
44
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "metafold"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.15.0"
|
|
8
8
|
authors = [
|
|
9
9
|
{name = "Metafold 3D", email = "info@metafold3d.com"},
|
|
10
10
|
]
|
|
@@ -38,7 +38,7 @@ simulation = [
|
|
|
38
38
|
"pandas>=2.3.3",
|
|
39
39
|
"plyfile>=1.1.3",
|
|
40
40
|
"pyyaml>=6.0.3",
|
|
41
|
-
"simulation-configurator==0.
|
|
41
|
+
"simulation-configurator==0.3.*",
|
|
42
42
|
"tables>=3.11.1",
|
|
43
43
|
]
|
|
44
44
|
|
|
@@ -5,10 +5,13 @@ from metafold.materials import (
|
|
|
5
5
|
ConstitutiveModel,
|
|
6
6
|
ElasticPlasticParams,
|
|
7
7
|
FlowModel,
|
|
8
|
+
HyperfoamParams,
|
|
9
|
+
HyperfoamTerm,
|
|
8
10
|
HypoElasticParams,
|
|
9
11
|
JohnsonCookParams,
|
|
10
12
|
Material,
|
|
11
13
|
MaxwellWeichertParams,
|
|
14
|
+
OrthotropicParams,
|
|
12
15
|
RigidParams,
|
|
13
16
|
StabilityCheck,
|
|
14
17
|
UCNHParams,
|
|
@@ -279,3 +282,155 @@ class TestMaterialOptionalFields:
|
|
|
279
282
|
r = roundtrip(m)
|
|
280
283
|
assert r.room_temp == 300.0
|
|
281
284
|
assert r.melt_temp == 1500.0
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
class TestHyperfoamParams:
|
|
288
|
+
def _material(self):
|
|
289
|
+
return Material(
|
|
290
|
+
density=600.0,
|
|
291
|
+
thermal_conductivity=0.15,
|
|
292
|
+
specific_heat=1.36,
|
|
293
|
+
constitutive_model=ConstitutiveModel(
|
|
294
|
+
params=HyperfoamParams(
|
|
295
|
+
terms=[
|
|
296
|
+
HyperfoamTerm(mu=0.16e6, alpha=8.9, poisson_ratio=0.0),
|
|
297
|
+
HyperfoamTerm(mu=0.01e6, alpha=-2.0, poisson_ratio=0.0),
|
|
298
|
+
],
|
|
299
|
+
)
|
|
300
|
+
),
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
def test_roundtrip(self):
|
|
304
|
+
m = self._material()
|
|
305
|
+
assert roundtrip(m).to_dict() == m.to_dict()
|
|
306
|
+
|
|
307
|
+
def test_type_key(self):
|
|
308
|
+
d = ConstitutiveModel(
|
|
309
|
+
params=HyperfoamParams(terms=[])
|
|
310
|
+
).to_dict()
|
|
311
|
+
assert d["type"] == "hyperfoam"
|
|
312
|
+
|
|
313
|
+
def test_bulk_modulus_not_emitted(self):
|
|
314
|
+
# bulk_modulus is not part of the Ogden hyperfoam model.
|
|
315
|
+
d = self._material().constitutive_model.params.to_dict()
|
|
316
|
+
assert "bulk_modulus" not in d
|
|
317
|
+
assert "K" not in d
|
|
318
|
+
|
|
319
|
+
def test_legacy_bulk_modulus_ignored_on_from_dict(self):
|
|
320
|
+
# Older scripts / GH plugins may still send bulk_modulus or K — ignore it.
|
|
321
|
+
d = {
|
|
322
|
+
"type": "hyperfoam",
|
|
323
|
+
"params": {
|
|
324
|
+
"bulk_modulus": 1.0e6,
|
|
325
|
+
"K": 2.0e6,
|
|
326
|
+
"terms": [{"mu": 0.16e6, "alpha": 8.9, "poisson_ratio": 0.0}],
|
|
327
|
+
},
|
|
328
|
+
}
|
|
329
|
+
params = HyperfoamParams.from_dict(d["params"])
|
|
330
|
+
assert len(params.terms) == 1
|
|
331
|
+
assert "bulk_modulus" not in params.to_dict()
|
|
332
|
+
|
|
333
|
+
def test_terms_preserved(self):
|
|
334
|
+
d = self._material().constitutive_model.params.to_dict()
|
|
335
|
+
assert len(d["terms"]) == 2
|
|
336
|
+
# "_element" marks how the ups emitter renders each term.
|
|
337
|
+
assert d["terms"][0]["_element"] == "term"
|
|
338
|
+
assert d["terms"][0]["mu"] == 0.16e6
|
|
339
|
+
assert d["terms"][1]["alpha"] == -2.0
|
|
340
|
+
|
|
341
|
+
def test_pure_elastic_omits_viscoelastic_fields(self):
|
|
342
|
+
d = self._material().constitutive_model.params.to_dict()
|
|
343
|
+
assert "terminal_shear_modulus" not in d
|
|
344
|
+
assert "viscoelastic_series" not in d
|
|
345
|
+
|
|
346
|
+
def _viscoelastic_material(self):
|
|
347
|
+
return Material(
|
|
348
|
+
density=600.0,
|
|
349
|
+
thermal_conductivity=0.15,
|
|
350
|
+
specific_heat=1.36,
|
|
351
|
+
constitutive_model=ConstitutiveModel(
|
|
352
|
+
params=HyperfoamParams(
|
|
353
|
+
terms=[HyperfoamTerm(mu=0.16e6, alpha=8.9, poisson_ratio=0.0)],
|
|
354
|
+
terminal_shear_modulus=5.0e4,
|
|
355
|
+
viscoelastic_series=[
|
|
356
|
+
ViscoelasticMode("m1", 0.15, 25000),
|
|
357
|
+
ViscoelasticMode("m2", 0.20, 30000),
|
|
358
|
+
],
|
|
359
|
+
)
|
|
360
|
+
),
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
def test_viscoelastic_overlay_roundtrip(self):
|
|
364
|
+
m = self._viscoelastic_material()
|
|
365
|
+
assert roundtrip(m).to_dict() == m.to_dict()
|
|
366
|
+
|
|
367
|
+
def test_viscoelastic_overlay_emitted(self):
|
|
368
|
+
d = self._viscoelastic_material().constitutive_model.params.to_dict()
|
|
369
|
+
assert d["terminal_shear_modulus"] == 5.0e4
|
|
370
|
+
assert len(d["viscoelastic_series"]) == 2
|
|
371
|
+
assert d["viscoelastic_series"][0]["mode"] == "m1"
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
class TestOrthotropicParams:
|
|
375
|
+
def _material(self):
|
|
376
|
+
return Material(
|
|
377
|
+
density=1000.0,
|
|
378
|
+
thermal_conductivity=1.0,
|
|
379
|
+
specific_heat=1500,
|
|
380
|
+
constitutive_model=ConstitutiveModel(
|
|
381
|
+
params=OrthotropicParams(
|
|
382
|
+
E1=1e9, E2=2e9, E3=3e9,
|
|
383
|
+
nu12=0.3, nu13=0.3, nu23=0.3,
|
|
384
|
+
G12=0.5e9, G13=0.5e9, G23=0.5e9,
|
|
385
|
+
direction_a=[1.0, 0.0, 0.0],
|
|
386
|
+
direction_b=[0.0, 1.0, 0.0],
|
|
387
|
+
)
|
|
388
|
+
),
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
def test_roundtrip(self):
|
|
392
|
+
m = self._material()
|
|
393
|
+
assert roundtrip(m).to_dict() == m.to_dict()
|
|
394
|
+
|
|
395
|
+
def test_type_key(self):
|
|
396
|
+
d = ConstitutiveModel(params=self._material().constitutive_model.params).to_dict()
|
|
397
|
+
assert d["type"] == "orthotropic"
|
|
398
|
+
|
|
399
|
+
def test_directions_are_numeric_lists(self):
|
|
400
|
+
d = self._material().constitutive_model.params.to_dict()
|
|
401
|
+
assert d["direction_a"] == [1.0, 0.0, 0.0]
|
|
402
|
+
assert d["direction_b"] == [0.0, 1.0, 0.0]
|
|
403
|
+
|
|
404
|
+
def test_from_dict_accepts_string_direction(self):
|
|
405
|
+
# Directions may arrive as "x y z" strings (manifest form).
|
|
406
|
+
m = self._material()
|
|
407
|
+
d = m.to_dict()
|
|
408
|
+
d["constitutive_model"]["params"]["direction_a"] = "1 0 0"
|
|
409
|
+
assert OrthotropicParams.from_dict(d["constitutive_model"]["params"]).direction_a == [1.0, 0.0, 0.0]
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
class TestMaterialFromDictErrors:
|
|
413
|
+
# from_dict must raise ValueError (not KeyError) so API callers return 400,
|
|
414
|
+
# not 500. See lightcycle experiments blueprint.
|
|
415
|
+
def _material_dict(self, mtype, params):
|
|
416
|
+
return {
|
|
417
|
+
"density": 1000.0, "thermal_conductivity": 1.0, "specific_heat": 1500,
|
|
418
|
+
"constitutive_model": {"type": mtype, "params": params},
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
def test_unknown_type_raises_value_error(self):
|
|
422
|
+
# A client newer than the server sends a type we don't know.
|
|
423
|
+
d = self._material_dict("some_future_type", {})
|
|
424
|
+
with pytest.raises(ValueError, match="Unknown constitutive model type"):
|
|
425
|
+
Material.from_dict(d)
|
|
426
|
+
|
|
427
|
+
def test_missing_param_raises_value_error(self):
|
|
428
|
+
params = {
|
|
429
|
+
"E1": 1e9, "E2": 1e9, "E3": 1e9,
|
|
430
|
+
"nu12": 0.3, "nu13": 0.3, "nu23": 0.3,
|
|
431
|
+
"G12": 5e8, "G13": 5e8, "G23": 5e8,
|
|
432
|
+
"direction_a": [1.0, 0.0, 0.0], "direction_b": [0.0, 1.0, 0.0],
|
|
433
|
+
}
|
|
434
|
+
del params["E1"]
|
|
435
|
+
with pytest.raises(ValueError, match="missing required param"):
|
|
436
|
+
Material.from_dict(self._material_dict("orthotropic", params))
|
|
@@ -31,6 +31,7 @@ from metafold.simulation.run_experiment import (
|
|
|
31
31
|
_resolve_material,
|
|
32
32
|
run_experiment,
|
|
33
33
|
run_experiment_from_zip,
|
|
34
|
+
validate_experiment_config,
|
|
34
35
|
)
|
|
35
36
|
from metafold.simulation.compression_experiment import (
|
|
36
37
|
VaryMaterial,
|
|
@@ -657,3 +658,61 @@ class TestRunExperimentFromZip:
|
|
|
657
658
|
result = run_experiment_from_zip(zip_path, output_path=str(tmp_path / "out"))
|
|
658
659
|
|
|
659
660
|
assert result == "p"
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
class TestExperimentalMaterialGuard:
|
|
664
|
+
"""Orthotropic/hyperfoam are mpmgpu-only; rejected unless use_experimental_solver."""
|
|
665
|
+
|
|
666
|
+
HYPERFOAM = {
|
|
667
|
+
"density": 600.0,
|
|
668
|
+
"thermal_conductivity": 0.15,
|
|
669
|
+
"specific_heat": 1.36,
|
|
670
|
+
"constitutive_model": {
|
|
671
|
+
"type": "hyperfoam",
|
|
672
|
+
"params": {
|
|
673
|
+
"bulk_modulus": 1.0e6,
|
|
674
|
+
"terms": [{"mu": 1.6e5, "alpha": 8.9, "poisson_ratio": 0.0}],
|
|
675
|
+
},
|
|
676
|
+
},
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
def _config(self, **extra):
|
|
680
|
+
cfg = {
|
|
681
|
+
"project_name": "t",
|
|
682
|
+
"parts": [
|
|
683
|
+
{"type": "piston_cylinder"},
|
|
684
|
+
{"name": "foam", "material": self.HYPERFOAM, "file": "foam.ply"},
|
|
685
|
+
],
|
|
686
|
+
}
|
|
687
|
+
cfg.update(extra)
|
|
688
|
+
return cfg
|
|
689
|
+
|
|
690
|
+
def test_rejected_without_experimental_flag(self):
|
|
691
|
+
with pytest.raises(ValueError, match="experimental solver"):
|
|
692
|
+
validate_experiment_config(self._config())
|
|
693
|
+
|
|
694
|
+
def test_allowed_with_experimental_flag(self):
|
|
695
|
+
# Should not raise.
|
|
696
|
+
validate_experiment_config(self._config(use_experimental_solver=True))
|
|
697
|
+
|
|
698
|
+
def test_ordinary_material_unaffected(self):
|
|
699
|
+
cfg = {
|
|
700
|
+
"project_name": "t",
|
|
701
|
+
"parts": [
|
|
702
|
+
{"type": "piston_cylinder"},
|
|
703
|
+
{"name": "foam", "material": "default_midsole_nominal", "file": "foam.ply"},
|
|
704
|
+
],
|
|
705
|
+
}
|
|
706
|
+
validate_experiment_config(cfg) # no raise
|
|
707
|
+
|
|
708
|
+
def test_varied_experimental_material_rejected(self):
|
|
709
|
+
cfg = {
|
|
710
|
+
"project_name": "t",
|
|
711
|
+
"parts": [
|
|
712
|
+
{"type": "piston_cylinder"},
|
|
713
|
+
{"name": "foam", "material": "default_midsole_nominal", "file": "foam.ply"},
|
|
714
|
+
],
|
|
715
|
+
"varying": [{"part": "foam", "material": [self.HYPERFOAM]}],
|
|
716
|
+
}
|
|
717
|
+
with pytest.raises(ValueError, match="experimental solver"):
|
|
718
|
+
validate_experiment_config(cfg)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|