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.
Files changed (35) hide show
  1. {metafold-0.15.dev1 → metafold-0.16.0}/PKG-INFO +1 -1
  2. {metafold-0.15.dev1 → metafold-0.16.0}/metafold/materials.py +18 -4
  3. {metafold-0.15.dev1 → metafold-0.16.0}/metafold/simulation/compression_simulation.py +7 -0
  4. {metafold-0.15.dev1 → metafold-0.16.0}/metafold/simulation/run_experiment.py +2 -0
  5. {metafold-0.15.dev1 → metafold-0.16.0}/metafold.egg-info/PKG-INFO +1 -1
  6. {metafold-0.15.dev1 → metafold-0.16.0}/pyproject.toml +1 -1
  7. {metafold-0.15.dev1 → metafold-0.16.0}/tests/test_compression_simulation.py +14 -0
  8. {metafold-0.15.dev1 → metafold-0.16.0}/tests/test_materials.py +48 -3
  9. {metafold-0.15.dev1 → metafold-0.16.0}/LICENSE +0 -0
  10. {metafold-0.15.dev1 → metafold-0.16.0}/README.md +0 -0
  11. {metafold-0.15.dev1 → metafold-0.16.0}/metafold/__init__.py +0 -0
  12. {metafold-0.15.dev1 → metafold-0.16.0}/metafold/api.py +0 -0
  13. {metafold-0.15.dev1 → metafold-0.16.0}/metafold/assets.py +0 -0
  14. {metafold-0.15.dev1 → metafold-0.16.0}/metafold/auth.py +0 -0
  15. {metafold-0.15.dev1 → metafold-0.16.0}/metafold/client.py +0 -0
  16. {metafold-0.15.dev1 → metafold-0.16.0}/metafold/exceptions.py +0 -0
  17. {metafold-0.15.dev1 → metafold-0.16.0}/metafold/jobs.py +0 -0
  18. {metafold-0.15.dev1 → metafold-0.16.0}/metafold/projects.py +0 -0
  19. {metafold-0.15.dev1 → metafold-0.16.0}/metafold/simulation/__init__.py +0 -0
  20. {metafold-0.15.dev1 → metafold-0.16.0}/metafold/simulation/compression_experiment.py +0 -0
  21. {metafold-0.15.dev1 → metafold-0.16.0}/metafold/utils.py +0 -0
  22. {metafold-0.15.dev1 → metafold-0.16.0}/metafold/workflows.py +0 -0
  23. {metafold-0.15.dev1 → metafold-0.16.0}/metafold.egg-info/SOURCES.txt +0 -0
  24. {metafold-0.15.dev1 → metafold-0.16.0}/metafold.egg-info/dependency_links.txt +0 -0
  25. {metafold-0.15.dev1 → metafold-0.16.0}/metafold.egg-info/requires.txt +0 -0
  26. {metafold-0.15.dev1 → metafold-0.16.0}/metafold.egg-info/top_level.txt +0 -0
  27. {metafold-0.15.dev1 → metafold-0.16.0}/setup.cfg +0 -0
  28. {metafold-0.15.dev1 → metafold-0.16.0}/tests/test_assets.py +0 -0
  29. {metafold-0.15.dev1 → metafold-0.16.0}/tests/test_compession_experiment.py +0 -0
  30. {metafold-0.15.dev1 → metafold-0.16.0}/tests/test_jobs.py +0 -0
  31. {metafold-0.15.dev1 → metafold-0.16.0}/tests/test_projects.py +0 -0
  32. {metafold-0.15.dev1 → metafold-0.16.0}/tests/test_run_experiment.py +0 -0
  33. {metafold-0.15.dev1 → metafold-0.16.0}/tests/test_shear_simulation.py +0 -0
  34. {metafold-0.15.dev1 → metafold-0.16.0}/tests/test_utils.py +0 -0
  35. {metafold-0.15.dev1 → metafold-0.16.0}/tests/test_workflows.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: metafold
3
- Version: 0.15.dev1
3
+ Version: 0.16.0
4
4
  Summary: Metafold SDK for Python
5
5
  Author-email: Metafold 3D <info@metafold3d.com>
6
6
  License: Copyright 2024 Metafold 3D
@@ -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
- params = PARAMS_CLASSES[cm["type"]].from_dict(cm["params"])
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":
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: metafold
3
- Version: 0.15.dev1
3
+ Version: 0.16.0
4
4
  Summary: Metafold SDK for Python
5
5
  Author-email: Metafold 3D <info@metafold3d.com>
6
6
  License: Copyright 2024 Metafold 3D
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "metafold"
7
- version = "0.15.dev1"
7
+ version = "0.16.0"
8
8
  authors = [
9
9
  {name = "Metafold 3D", email = "info@metafold3d.com"},
10
10
  ]
@@ -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(bulk_modulus=1.0, terms=[])
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