metafold 0.15.dev1__tar.gz → 0.16.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.15.dev1 → metafold-0.16.0}/PKG-INFO +1 -1
- {metafold-0.15.dev1 → metafold-0.16.0}/metafold/materials.py +18 -4
- {metafold-0.15.dev1 → metafold-0.16.0}/metafold/simulation/compression_simulation.py +7 -0
- {metafold-0.15.dev1 → metafold-0.16.0}/metafold/simulation/run_experiment.py +2 -0
- {metafold-0.15.dev1 → metafold-0.16.0}/metafold.egg-info/PKG-INFO +1 -1
- {metafold-0.15.dev1 → metafold-0.16.0}/pyproject.toml +1 -1
- {metafold-0.15.dev1 → metafold-0.16.0}/tests/test_compression_simulation.py +14 -0
- {metafold-0.15.dev1 → metafold-0.16.0}/tests/test_materials.py +48 -3
- {metafold-0.15.dev1 → metafold-0.16.0}/LICENSE +0 -0
- {metafold-0.15.dev1 → metafold-0.16.0}/README.md +0 -0
- {metafold-0.15.dev1 → metafold-0.16.0}/metafold/__init__.py +0 -0
- {metafold-0.15.dev1 → metafold-0.16.0}/metafold/api.py +0 -0
- {metafold-0.15.dev1 → metafold-0.16.0}/metafold/assets.py +0 -0
- {metafold-0.15.dev1 → metafold-0.16.0}/metafold/auth.py +0 -0
- {metafold-0.15.dev1 → metafold-0.16.0}/metafold/client.py +0 -0
- {metafold-0.15.dev1 → metafold-0.16.0}/metafold/exceptions.py +0 -0
- {metafold-0.15.dev1 → metafold-0.16.0}/metafold/jobs.py +0 -0
- {metafold-0.15.dev1 → metafold-0.16.0}/metafold/projects.py +0 -0
- {metafold-0.15.dev1 → metafold-0.16.0}/metafold/simulation/__init__.py +0 -0
- {metafold-0.15.dev1 → metafold-0.16.0}/metafold/simulation/compression_experiment.py +0 -0
- {metafold-0.15.dev1 → metafold-0.16.0}/metafold/utils.py +0 -0
- {metafold-0.15.dev1 → metafold-0.16.0}/metafold/workflows.py +0 -0
- {metafold-0.15.dev1 → metafold-0.16.0}/metafold.egg-info/SOURCES.txt +0 -0
- {metafold-0.15.dev1 → metafold-0.16.0}/metafold.egg-info/dependency_links.txt +0 -0
- {metafold-0.15.dev1 → metafold-0.16.0}/metafold.egg-info/requires.txt +0 -0
- {metafold-0.15.dev1 → metafold-0.16.0}/metafold.egg-info/top_level.txt +0 -0
- {metafold-0.15.dev1 → metafold-0.16.0}/setup.cfg +0 -0
- {metafold-0.15.dev1 → metafold-0.16.0}/tests/test_assets.py +0 -0
- {metafold-0.15.dev1 → metafold-0.16.0}/tests/test_compession_experiment.py +0 -0
- {metafold-0.15.dev1 → metafold-0.16.0}/tests/test_jobs.py +0 -0
- {metafold-0.15.dev1 → metafold-0.16.0}/tests/test_projects.py +0 -0
- {metafold-0.15.dev1 → metafold-0.16.0}/tests/test_run_experiment.py +0 -0
- {metafold-0.15.dev1 → metafold-0.16.0}/tests/test_shear_simulation.py +0 -0
- {metafold-0.15.dev1 → metafold-0.16.0}/tests/test_utils.py +0 -0
- {metafold-0.15.dev1 → metafold-0.16.0}/tests/test_workflows.py +0 -0
|
@@ -232,11 +232,14 @@ class HyperfoamTerm:
|
|
|
232
232
|
class HyperfoamParams(ParamsBase):
|
|
233
233
|
"""Ogden hyperfoam (mpmgpu only; no Uintah equivalent).
|
|
234
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
|
+
|
|
235
239
|
An optional viscoelastic overlay (Prony series) may be supplied via
|
|
236
240
|
terminal_shear_modulus + viscoelastic_series, matching the MaxwellWeichert
|
|
237
241
|
mode structure. Omit both for a purely elastic hyperfoam.
|
|
238
242
|
"""
|
|
239
|
-
bulk_modulus: float
|
|
240
243
|
terms: List[HyperfoamTerm]
|
|
241
244
|
terminal_shear_modulus: Optional[float] = None
|
|
242
245
|
viscoelastic_series: Optional[List[ViscoelasticMode]] = None
|
|
@@ -247,7 +250,6 @@ class HyperfoamParams(ParamsBase):
|
|
|
247
250
|
|
|
248
251
|
def to_dict(self):
|
|
249
252
|
d = {
|
|
250
|
-
"bulk_modulus": float(self.bulk_modulus),
|
|
251
253
|
"terms": [t.to_dict() for t in self.terms],
|
|
252
254
|
}
|
|
253
255
|
if self.terminal_shear_modulus is not None:
|
|
@@ -258,9 +260,9 @@ class HyperfoamParams(ParamsBase):
|
|
|
258
260
|
|
|
259
261
|
@classmethod
|
|
260
262
|
def from_dict(cls, d: dict) -> "HyperfoamParams":
|
|
263
|
+
# A legacy bulk_modulus/K is ignored (older scripts may still send it).
|
|
261
264
|
series = d.get("viscoelastic_series")
|
|
262
265
|
return cls(
|
|
263
|
-
bulk_modulus=d["bulk_modulus"],
|
|
264
266
|
terms=[
|
|
265
267
|
HyperfoamTerm(
|
|
266
268
|
mu=t["mu"], alpha=t["alpha"], poisson_ratio=t["poisson_ratio"])
|
|
@@ -459,7 +461,19 @@ class Material:
|
|
|
459
461
|
]
|
|
460
462
|
PARAMS_CLASSES = {c.get_type(): c for c in params_classes}
|
|
461
463
|
cm = d["constitutive_model"]
|
|
462
|
-
|
|
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}.")
|
|
463
477
|
return cls(
|
|
464
478
|
density=d["density"],
|
|
465
479
|
thermal_conductivity=d["thermal_conductivity"],
|
|
@@ -260,6 +260,9 @@ class ExperimentPistonBase:
|
|
|
260
260
|
force_limit: ForceLimit | None = None
|
|
261
261
|
"""Apply condition to jump to zero velocity when the given force is reached."""
|
|
262
262
|
|
|
263
|
+
releasable: bool = False
|
|
264
|
+
"""Only constrain contact in normal direction if releasable is True."""
|
|
265
|
+
|
|
263
266
|
|
|
264
267
|
@dataclass
|
|
265
268
|
class ExperimentPistonMesh(ExperimentMesh, ExperimentPistonBase):
|
|
@@ -1308,6 +1311,10 @@ class CompressionSimulation:
|
|
|
1308
1311
|
"conditionValue": info.part.force_limit.value,
|
|
1309
1312
|
"conditionAction": "GoToZeroVelocity",
|
|
1310
1313
|
})
|
|
1314
|
+
if isinstance(info.part, ExperimentPistonBase) and info.part.releasable:
|
|
1315
|
+
kwargs.update({
|
|
1316
|
+
"normal_only": True,
|
|
1317
|
+
})
|
|
1311
1318
|
rigid_contacts.append(
|
|
1312
1319
|
Contact(
|
|
1313
1320
|
type=info.part.contact_type,
|
|
@@ -205,6 +205,8 @@ def _build_parts(parts_config: list[dict]) -> list[ExperimentPart]:
|
|
|
205
205
|
kwargs["material"] = _resolve_material(entry["material"])
|
|
206
206
|
if "force_limit" in entry:
|
|
207
207
|
kwargs["force_limit"] = ForceLimit(**entry["force_limit"])
|
|
208
|
+
if "releasable" in entry:
|
|
209
|
+
kwargs["releasable"] = entry["releasable"]
|
|
208
210
|
parts.append(ExperimentPistonCylinder(**kwargs))
|
|
209
211
|
|
|
210
212
|
elif part_type == "piston_box":
|
|
@@ -748,6 +748,20 @@ class TestSimConfig:
|
|
|
748
748
|
assert contact.find(tag) is not None
|
|
749
749
|
assert contact.find(tag).text == value
|
|
750
750
|
|
|
751
|
+
def test_releasable_contact(self, ply_folder, basic_parts, tmp_path):
|
|
752
|
+
parts = deepcopy(basic_parts)
|
|
753
|
+
parts[0].releasable = True
|
|
754
|
+
sim = CompressionSimulation(
|
|
755
|
+
parts=parts,
|
|
756
|
+
simulation_name="test_sim",
|
|
757
|
+
stl_folder_path=str(ply_folder),
|
|
758
|
+
output_path=str(tmp_path / "out"),
|
|
759
|
+
client=MagicMock(),
|
|
760
|
+
)
|
|
761
|
+
sim.create_sim_config()
|
|
762
|
+
contact = sim.ups.findall("./MaterialProperties/MPM/contact")[0]
|
|
763
|
+
contact.find("normal_only").text == "true"
|
|
764
|
+
|
|
751
765
|
|
|
752
766
|
class TestStressStrainPerPart:
|
|
753
767
|
"""stress-strain runs once per deformable part (not one representative).
|
|
@@ -292,7 +292,6 @@ class TestHyperfoamParams:
|
|
|
292
292
|
specific_heat=1.36,
|
|
293
293
|
constitutive_model=ConstitutiveModel(
|
|
294
294
|
params=HyperfoamParams(
|
|
295
|
-
bulk_modulus=1.0e6,
|
|
296
295
|
terms=[
|
|
297
296
|
HyperfoamTerm(mu=0.16e6, alpha=8.9, poisson_ratio=0.0),
|
|
298
297
|
HyperfoamTerm(mu=0.01e6, alpha=-2.0, poisson_ratio=0.0),
|
|
@@ -307,10 +306,30 @@ class TestHyperfoamParams:
|
|
|
307
306
|
|
|
308
307
|
def test_type_key(self):
|
|
309
308
|
d = ConstitutiveModel(
|
|
310
|
-
params=HyperfoamParams(
|
|
309
|
+
params=HyperfoamParams(terms=[])
|
|
311
310
|
).to_dict()
|
|
312
311
|
assert d["type"] == "hyperfoam"
|
|
313
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
|
+
|
|
314
333
|
def test_terms_preserved(self):
|
|
315
334
|
d = self._material().constitutive_model.params.to_dict()
|
|
316
335
|
assert len(d["terms"]) == 2
|
|
@@ -331,7 +350,6 @@ class TestHyperfoamParams:
|
|
|
331
350
|
specific_heat=1.36,
|
|
332
351
|
constitutive_model=ConstitutiveModel(
|
|
333
352
|
params=HyperfoamParams(
|
|
334
|
-
bulk_modulus=1.0e6,
|
|
335
353
|
terms=[HyperfoamTerm(mu=0.16e6, alpha=8.9, poisson_ratio=0.0)],
|
|
336
354
|
terminal_shear_modulus=5.0e4,
|
|
337
355
|
viscoelastic_series=[
|
|
@@ -389,3 +407,30 @@ class TestOrthotropicParams:
|
|
|
389
407
|
d = m.to_dict()
|
|
390
408
|
d["constitutive_model"]["params"]["direction_a"] = "1 0 0"
|
|
391
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))
|
|
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
|
|
File without changes
|