metafold 0.12.dev5__tar.gz → 0.12.dev7__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.12.dev5 → metafold-0.12.dev7}/PKG-INFO +2 -2
  2. {metafold-0.12.dev5 → metafold-0.12.dev7}/metafold/materials.py +94 -0
  3. {metafold-0.12.dev5 → metafold-0.12.dev7}/metafold/simulation/compression_simulation.py +5 -4
  4. {metafold-0.12.dev5 → metafold-0.12.dev7}/metafold/simulation/run_experiment.py +9 -1
  5. {metafold-0.12.dev5 → metafold-0.12.dev7}/metafold.egg-info/PKG-INFO +2 -2
  6. {metafold-0.12.dev5 → metafold-0.12.dev7}/metafold.egg-info/SOURCES.txt +1 -0
  7. {metafold-0.12.dev5 → metafold-0.12.dev7}/metafold.egg-info/requires.txt +1 -1
  8. {metafold-0.12.dev5 → metafold-0.12.dev7}/pyproject.toml +2 -2
  9. {metafold-0.12.dev5 → metafold-0.12.dev7}/tests/test_compression_simulation.py +37 -13
  10. metafold-0.12.dev7/tests/test_materials.py +281 -0
  11. {metafold-0.12.dev5 → metafold-0.12.dev7}/tests/test_run_experiment.py +33 -0
  12. {metafold-0.12.dev5 → metafold-0.12.dev7}/LICENSE +0 -0
  13. {metafold-0.12.dev5 → metafold-0.12.dev7}/README.md +0 -0
  14. {metafold-0.12.dev5 → metafold-0.12.dev7}/metafold/__init__.py +0 -0
  15. {metafold-0.12.dev5 → metafold-0.12.dev7}/metafold/api.py +0 -0
  16. {metafold-0.12.dev5 → metafold-0.12.dev7}/metafold/assets.py +0 -0
  17. {metafold-0.12.dev5 → metafold-0.12.dev7}/metafold/auth.py +0 -0
  18. {metafold-0.12.dev5 → metafold-0.12.dev7}/metafold/client.py +0 -0
  19. {metafold-0.12.dev5 → metafold-0.12.dev7}/metafold/exceptions.py +0 -0
  20. {metafold-0.12.dev5 → metafold-0.12.dev7}/metafold/jobs.py +0 -0
  21. {metafold-0.12.dev5 → metafold-0.12.dev7}/metafold/projects.py +0 -0
  22. {metafold-0.12.dev5 → metafold-0.12.dev7}/metafold/simulation/__init__.py +0 -0
  23. {metafold-0.12.dev5 → metafold-0.12.dev7}/metafold/simulation/compression_experiment.py +0 -0
  24. {metafold-0.12.dev5 → metafold-0.12.dev7}/metafold/utils.py +0 -0
  25. {metafold-0.12.dev5 → metafold-0.12.dev7}/metafold/workflows.py +0 -0
  26. {metafold-0.12.dev5 → metafold-0.12.dev7}/metafold.egg-info/dependency_links.txt +0 -0
  27. {metafold-0.12.dev5 → metafold-0.12.dev7}/metafold.egg-info/top_level.txt +0 -0
  28. {metafold-0.12.dev5 → metafold-0.12.dev7}/setup.cfg +0 -0
  29. {metafold-0.12.dev5 → metafold-0.12.dev7}/tests/test_assets.py +0 -0
  30. {metafold-0.12.dev5 → metafold-0.12.dev7}/tests/test_compession_experiment.py +0 -0
  31. {metafold-0.12.dev5 → metafold-0.12.dev7}/tests/test_jobs.py +0 -0
  32. {metafold-0.12.dev5 → metafold-0.12.dev7}/tests/test_projects.py +0 -0
  33. {metafold-0.12.dev5 → metafold-0.12.dev7}/tests/test_shear_simulation.py +0 -0
  34. {metafold-0.12.dev5 → metafold-0.12.dev7}/tests/test_utils.py +0 -0
  35. {metafold-0.12.dev5 → metafold-0.12.dev7}/tests/test_workflows.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: metafold
3
- Version: 0.12.dev5
3
+ Version: 0.12.dev7
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.1.1; extra == "simulation"
41
+ Requires-Dist: simulation-configurator==0.1.2; extra == "simulation"
42
42
  Requires-Dist: tables>=3.11.1; extra == "simulation"
43
43
  Dynamic: license-file
44
44
 
@@ -32,6 +32,94 @@ class RigidParams(ParamsBase):
32
32
  return {"shear_modulus": self.shear_modulus, "bulk_modulus": self.bulk_modulus}
33
33
 
34
34
 
35
+ @dataclass
36
+ class HypoElasticParams(ParamsBase):
37
+ G: float
38
+ K: float
39
+
40
+ @classmethod
41
+ def get_type(self):
42
+ return "hypo_elastic"
43
+
44
+ def to_dict(self):
45
+ return {"G": self.G, "K": self.K}
46
+
47
+
48
+ @dataclass
49
+ class ViscoTransIsoHyperParams(ParamsBase):
50
+ bulk_modulus: float
51
+ c1: float
52
+ c2: float
53
+ c3: float
54
+ c4: float
55
+ c5: float
56
+ fiber_stretch: float
57
+ direction_of_symm: List[float]
58
+ failure_option: int
59
+ max_fiber_strain: float
60
+ max_matrix_strain: float
61
+ y1: float
62
+ y2: float
63
+ y3: float
64
+ y4: float
65
+ y5: float
66
+ y6: float
67
+ t1: float
68
+ t2: float
69
+ t3: float
70
+ t4: float
71
+ t5: float
72
+ t6: float
73
+
74
+ @classmethod
75
+ def get_type(cls):
76
+ return "visco_trans_iso_hyper"
77
+
78
+ @classmethod
79
+ def from_dict(cls, d: dict) -> "ViscoTransIsoHyperParams":
80
+ dos = d["direction_of_symm"]
81
+ direction = [float(x) for x in dos.split()] if isinstance(dos, str) else [float(x) for x in dos]
82
+ return cls(
83
+ bulk_modulus=d["bulk_modulus"],
84
+ c1=d["c1"], c2=d["c2"], c3=d["c3"], c4=d["c4"], c5=d["c5"],
85
+ fiber_stretch=d["fiber_stretch"],
86
+ direction_of_symm=direction,
87
+ failure_option=d["failure_option"],
88
+ max_fiber_strain=d["max_fiber_strain"],
89
+ max_matrix_strain=d["max_matrix_strain"],
90
+ y1=d["y1"], y2=d["y2"], y3=d["y3"], y4=d["y4"], y5=d["y5"], y6=d["y6"],
91
+ t1=d["t1"], t2=d["t2"], t3=d["t3"], t4=d["t4"], t5=d["t5"], t6=d["t6"],
92
+ )
93
+
94
+ def to_dict(self):
95
+ return {
96
+ "bulk_modulus": float(self.bulk_modulus),
97
+ "c1": float(self.c1),
98
+ "c2": float(self.c2),
99
+ "c3": float(self.c3),
100
+ "c4": float(self.c4),
101
+ "c5": float(self.c5),
102
+ "fiber_stretch": float(self.fiber_stretch),
103
+ # Emit as a numeric list so simulation_configurator serialises it correctly
104
+ "direction_of_symm": [float(x) for x in self.direction_of_symm],
105
+ "failure_option": int(self.failure_option),
106
+ "max_fiber_strain": float(self.max_fiber_strain),
107
+ "max_matrix_strain": float(self.max_matrix_strain),
108
+ "y1": float(self.y1),
109
+ "y2": float(self.y2),
110
+ "y3": float(self.y3),
111
+ "y4": float(self.y4),
112
+ "y5": float(self.y5),
113
+ "y6": float(self.y6),
114
+ "t1": float(self.t1),
115
+ "t2": float(self.t2),
116
+ "t3": float(self.t3),
117
+ "t4": float(self.t4),
118
+ "t5": float(self.t5),
119
+ "t6": float(self.t6),
120
+ }
121
+
122
+
35
123
  @dataclass
36
124
  class CompMooneyRivlinParams(ParamsBase):
37
125
  he_constant_1: float
@@ -220,12 +308,16 @@ class ConstitutiveModel:
220
308
  UCNHParams,
221
309
  CompNeoHookParams,
222
310
  ElasticPlasticParams,
311
+ HypoElasticParams,
312
+ ViscoTransIsoHyperParams,
223
313
  ]
224
314
 
225
315
  def to_dict(self):
226
316
  return {"type": self.params.get_type(), "params": self.params.to_dict()}
227
317
 
228
318
 
319
+
320
+
229
321
  @dataclass
230
322
  class Material:
231
323
  density: float
@@ -257,6 +349,8 @@ class Material:
257
349
  MaxwellWeichertParams,
258
350
  CompNeoHookParams,
259
351
  ElasticPlasticParams,
352
+ HypoElasticParams,
353
+ ViscoTransIsoHyperParams,
260
354
  ]
261
355
  PARAMS_CLASSES = {c.get_type(): c for c in params_classes}
262
356
  cm = d["constitutive_model"]
@@ -625,10 +625,11 @@ class CompressionSimulation:
625
625
  auth_domain=auth_domain,
626
626
  base_url=base_url,
627
627
  )
628
- try:
629
- existing = client.projects.list(q=f"name:{project_name}")
630
- except HTTPError:
631
- existing = []
628
+ # Quote the project_name string for multi-word name searchess
629
+ existing = [
630
+ p for p in client.projects.list(q=f'name:"{project_name}"')
631
+ if p.name == project_name # ensure full match
632
+ ]
632
633
  if existing:
633
634
  project_id = existing[0].id
634
635
  if cancel_existing_workflows:
@@ -29,7 +29,9 @@ JSON manifest format
29
29
  ]
30
30
  },
31
31
  {"type": "piston_box", "shape_parameters": {"min": [...], "max": [...]}},
32
- {"type": "piston_mesh", "file": "piston.ply", "velocity": [...]},
32
+ # piston parts take an optional "material" (preset key or inline dict);
33
+ # omit to use DEFAULT_PISTON_MATERIAL
34
+ {"type": "piston_mesh", "file": "piston.ply", "velocity": [...], "material": "default_piston_material"},
33
35
  {
34
36
  "type": "mesh",
35
37
  "name": "midsole",
@@ -177,6 +179,8 @@ def _build_parts(parts_config: list[dict]) -> list[ExperimentPart]:
177
179
  kwargs["velocity"] = entry["velocity"]
178
180
  if "shape_parameters" in entry:
179
181
  kwargs["shape_parameters"] = entry["shape_parameters"]
182
+ if "material" in entry:
183
+ kwargs["material"] = _resolve_material(entry["material"])
180
184
  parts.append(ExperimentPistonCylinder(**kwargs))
181
185
 
182
186
  elif part_type == "piston_box":
@@ -185,12 +189,16 @@ def _build_parts(parts_config: list[dict]) -> list[ExperimentPart]:
185
189
  kwargs["velocity"] = entry["velocity"]
186
190
  if "shape_parameters" in entry:
187
191
  kwargs["shape_parameters"] = entry["shape_parameters"]
192
+ if "material" in entry:
193
+ kwargs["material"] = _resolve_material(entry["material"])
188
194
  parts.append(ExperimentPistonBox(**kwargs))
189
195
 
190
196
  elif part_type == "piston_mesh":
191
197
  kwargs = {"filename": entry["file"]}
192
198
  if "velocity" in entry:
193
199
  kwargs["velocity"] = entry["velocity"]
200
+ if "material" in entry:
201
+ kwargs["material"] = _resolve_material(entry["material"])
194
202
  parts.append(ExperimentPistonMesh(**kwargs))
195
203
 
196
204
  else:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: metafold
3
- Version: 0.12.dev5
3
+ Version: 0.12.dev7
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.1.1; extra == "simulation"
41
+ Requires-Dist: simulation-configurator==0.1.2; extra == "simulation"
42
42
  Requires-Dist: tables>=3.11.1; extra == "simulation"
43
43
  Dynamic: license-file
44
44
 
@@ -25,6 +25,7 @@ tests/test_assets.py
25
25
  tests/test_compession_experiment.py
26
26
  tests/test_compression_simulation.py
27
27
  tests/test_jobs.py
28
+ tests/test_materials.py
28
29
  tests/test_projects.py
29
30
  tests/test_run_experiment.py
30
31
  tests/test_shear_simulation.py
@@ -9,5 +9,5 @@ dotenv>=0.9.9
9
9
  pandas>=2.3.3
10
10
  plyfile>=1.1.3
11
11
  pyyaml>=6.0.3
12
- simulation-configurator==0.1.1
12
+ simulation-configurator==0.1.2
13
13
  tables>=3.11.1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "metafold"
7
- version = "0.12.dev5"
7
+ version = "0.12.dev7"
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.1.1",
41
+ "simulation-configurator==0.1.2",
42
42
  "tables>=3.11.1",
43
43
  ]
44
44
 
@@ -890,6 +890,7 @@ class TestSetupClient:
890
890
  # projects.list() finds an existing match.
891
891
  existing = MagicMock()
892
892
  existing.id = "existing-pid"
893
+ existing.name = "my_project"
893
894
  first_client = MagicMock()
894
895
  first_client.projects.list.return_value = [existing]
895
896
  patched_client_class.side_effect = [first_client, MagicMock()]
@@ -901,12 +902,34 @@ class TestSetupClient:
901
902
  )
902
903
 
903
904
  assert sim.project_id == "existing-pid"
904
- first_client.projects.list.assert_called_once_with(q="name:my_project")
905
+ first_client.projects.list.assert_called_once_with(q='name:"my_project"')
905
906
  first_client.projects.create.assert_not_called()
906
907
  # MetafoldClient called twice — once without project_id, once with
907
908
  assert patched_client_class.call_count == 2
908
909
  assert patched_client_class.call_args_list[1].kwargs["project_id"] == "existing-pid"
909
910
 
911
+ def test_multiword_name_is_quoted_and_reused(
912
+ self, ply_folder, basic_parts, tmp_path, patched_client_class
913
+ ):
914
+ # Names with spaces must be quoted in the search query, otherwise the
915
+ # server's parser errors and a duplicate project gets created.
916
+ existing = MagicMock()
917
+ existing.id = "existing-pid"
918
+ existing.name = "grasshopper test 3"
919
+ first_client = MagicMock()
920
+ first_client.projects.list.return_value = [existing]
921
+ patched_client_class.side_effect = [first_client, MagicMock()]
922
+
923
+ sim = self._build_sim(
924
+ ply_folder, basic_parts, tmp_path,
925
+ project_name="grasshopper test 3",
926
+ create_project_if_needed=True,
927
+ )
928
+
929
+ assert sim.project_id == "existing-pid"
930
+ first_client.projects.list.assert_called_once_with(q='name:"grasshopper test 3"')
931
+ first_client.projects.create.assert_not_called()
932
+
910
933
  def test_new_project_created_when_name_not_found(
911
934
  self, ply_folder, basic_parts, tmp_path, patched_client_class
912
935
  ):
@@ -949,25 +972,24 @@ class TestSetupClient:
949
972
  assert first_client.projects.create.call_args.args[0].startswith("experiment_")
950
973
  assert sim.project_id == "auto-pid"
951
974
 
952
- def test_http_error_during_list_falls_through_to_create(
975
+ def test_http_error_during_list_propagates_and_does_not_create(
953
976
  self, ply_folder, basic_parts, tmp_path, patched_client_class
954
977
  ):
978
+ # A failed search must NOT silently fall through to creating a project
979
+ # (that path produced duplicate projects). The error should surface.
955
980
  from requests import HTTPError
956
- created = MagicMock()
957
- created.id = "fallback-pid"
958
981
  first_client = MagicMock()
959
- first_client.projects.list.side_effect = HTTPError("404")
960
- first_client.projects.create.return_value = created
982
+ first_client.projects.list.side_effect = HTTPError("500")
961
983
  patched_client_class.side_effect = [first_client, MagicMock()]
962
984
 
963
- sim = self._build_sim(
964
- ply_folder, basic_parts, tmp_path,
965
- project_name="xyz",
966
- create_project_if_needed=True,
967
- )
985
+ with pytest.raises(HTTPError):
986
+ self._build_sim(
987
+ ply_folder, basic_parts, tmp_path,
988
+ project_name="xyz",
989
+ create_project_if_needed=True,
990
+ )
968
991
 
969
- assert sim.project_id == "fallback-pid"
970
- first_client.projects.create.assert_called_once()
992
+ first_client.projects.create.assert_not_called()
971
993
 
972
994
 
973
995
  class TestFindOrCreateProjectCancel:
@@ -991,6 +1013,7 @@ class TestFindOrCreateProjectCancel:
991
1013
  client = patched_client_class.return_value
992
1014
  existing = MagicMock()
993
1015
  existing.id = "pid-1"
1016
+ existing.name = "existing"
994
1017
  client.projects.list.return_value = [existing]
995
1018
  # list(q="state:pending") -> [w1]; list(q="state:started") -> [w2]
996
1019
  client.workflows.list.side_effect = [
@@ -1011,6 +1034,7 @@ class TestFindOrCreateProjectCancel:
1011
1034
  client = patched_client_class.return_value
1012
1035
  existing = MagicMock()
1013
1036
  existing.id = "pid-1"
1037
+ existing.name = "existing"
1014
1038
  client.projects.list.return_value = [existing]
1015
1039
 
1016
1040
  CompressionSimulation.find_or_create_project(
@@ -0,0 +1,281 @@
1
+ import pytest
2
+ from metafold.materials import (
3
+ CompMooneyRivlinParams,
4
+ CompNeoHookParams,
5
+ ConstitutiveModel,
6
+ ElasticPlasticParams,
7
+ FlowModel,
8
+ HypoElasticParams,
9
+ JohnsonCookParams,
10
+ Material,
11
+ MaxwellWeichertParams,
12
+ RigidParams,
13
+ StabilityCheck,
14
+ UCNHParams,
15
+ ViscoelasticMode,
16
+ ViscoTransIsoHyperParams,
17
+ YieldCondition,
18
+ )
19
+
20
+
21
+ def roundtrip(material: Material) -> Material:
22
+ return Material.from_dict(material.to_dict())
23
+
24
+
25
+ class TestRigidParams:
26
+ def test_roundtrip(self):
27
+ m = Material(
28
+ density=1730.0,
29
+ thermal_conductivity=45.0,
30
+ specific_heat=4.8e-4,
31
+ constitutive_model=ConstitutiveModel(
32
+ params=RigidParams(shear_modulus=2667e6, bulk_modulus=8000e6)
33
+ ),
34
+ )
35
+ assert roundtrip(m).to_dict() == m.to_dict()
36
+
37
+ def test_type_key(self):
38
+ d = ConstitutiveModel(params=RigidParams(1.0, 2.0)).to_dict()
39
+ assert d["type"] == "rigid"
40
+
41
+
42
+ class TestCompMooneyRivlinParams:
43
+ def test_roundtrip(self):
44
+ m = Material(
45
+ density=150.0,
46
+ thermal_conductivity=45.0,
47
+ specific_heat=4.8e-4,
48
+ constitutive_model=ConstitutiveModel(
49
+ params=CompMooneyRivlinParams(he_constant_1=1.5e5, he_constant_2=1.75e5, he_PR=0.41)
50
+ ),
51
+ )
52
+ assert roundtrip(m).to_dict() == m.to_dict()
53
+
54
+ def test_type_key(self):
55
+ d = ConstitutiveModel(params=CompMooneyRivlinParams(1.0, 2.0, 0.3)).to_dict()
56
+ assert d["type"] == "comp_mooney_rivlin"
57
+
58
+
59
+ class TestCompNeoHookParams:
60
+ def test_roundtrip(self):
61
+ m = Material(
62
+ density=100.0,
63
+ thermal_conductivity=1.0,
64
+ specific_heat=1.0,
65
+ constitutive_model=ConstitutiveModel(
66
+ params=CompNeoHookParams(bulk_modulus=1e6, shear_modulus=5e5)
67
+ ),
68
+ )
69
+ assert roundtrip(m).to_dict() == m.to_dict()
70
+
71
+ def test_type_key(self):
72
+ d = ConstitutiveModel(params=CompNeoHookParams(1.0, 2.0)).to_dict()
73
+ assert d["type"] == "comp_neo_hook"
74
+
75
+
76
+ class TestUCNHParams:
77
+ def test_roundtrip_minimal(self):
78
+ m = Material(
79
+ density=500.0,
80
+ thermal_conductivity=1.0,
81
+ specific_heat=1.0,
82
+ constitutive_model=ConstitutiveModel(
83
+ params=UCNHParams(shear_modulus=1e6, bulk_modulus=2e6, useModifiedEOS=True)
84
+ ),
85
+ )
86
+ assert roundtrip(m).to_dict() == m.to_dict()
87
+
88
+ def test_roundtrip_with_plasticity(self):
89
+ m = Material(
90
+ density=500.0,
91
+ thermal_conductivity=1.0,
92
+ specific_heat=1.0,
93
+ constitutive_model=ConstitutiveModel(
94
+ params=UCNHParams(
95
+ shear_modulus=1e6,
96
+ bulk_modulus=2e6,
97
+ useModifiedEOS=False,
98
+ usePlasticity=True,
99
+ yield_stress=1e4,
100
+ hardening_modulus=1e3,
101
+ alpha=0.5,
102
+ )
103
+ ),
104
+ )
105
+ assert roundtrip(m).to_dict() == m.to_dict()
106
+
107
+ def test_bool_serialised_as_lowercase_string(self):
108
+ p = UCNHParams(shear_modulus=1.0, bulk_modulus=1.0, useModifiedEOS=True)
109
+ assert p.to_dict()["useModifiedEOS"] == "true"
110
+ p2 = UCNHParams(shear_modulus=1.0, bulk_modulus=1.0, useModifiedEOS=False)
111
+ assert p2.to_dict()["useModifiedEOS"] == "false"
112
+
113
+ def test_optional_fields_omitted_when_none(self):
114
+ p = UCNHParams(shear_modulus=1.0, bulk_modulus=1.0, useModifiedEOS=True)
115
+ d = p.to_dict()
116
+ assert "usePlasticity" not in d
117
+ assert "yield_stress" not in d
118
+
119
+ def test_type_key(self):
120
+ d = ConstitutiveModel(params=UCNHParams(1.0, 2.0, True)).to_dict()
121
+ assert d["type"] == "UCNH"
122
+
123
+
124
+ class TestMaxwellWeichertParams:
125
+ def test_roundtrip(self):
126
+ m = Material(
127
+ density=200.0,
128
+ thermal_conductivity=45.0,
129
+ specific_heat=4.8e-4,
130
+ constitutive_model=ConstitutiveModel(
131
+ params=MaxwellWeichertParams(
132
+ bulk_modulus=931250,
133
+ terminal_shear_modulus=43750,
134
+ viscoelastic_series=[
135
+ ViscoelasticMode("mode1", 0.15, 25000),
136
+ ViscoelasticMode("mode2", 0.20, 30000),
137
+ ],
138
+ )
139
+ ),
140
+ )
141
+ assert roundtrip(m).to_dict() == m.to_dict()
142
+
143
+ def test_viscoelastic_modes_preserved(self):
144
+ params = MaxwellWeichertParams(
145
+ bulk_modulus=1.0,
146
+ terminal_shear_modulus=1.0,
147
+ viscoelastic_series=[
148
+ ViscoelasticMode("mode1", 0.1, 100.0),
149
+ ViscoelasticMode("mode2", 0.2, 200.0),
150
+ ],
151
+ )
152
+ d = params.to_dict()
153
+ assert len(d["viscoelastic_series"]) == 2
154
+ assert d["viscoelastic_series"][0]["mode"] == "mode1"
155
+ assert d["viscoelastic_series"][1]["relaxation_time"] == 0.2
156
+
157
+ def test_type_key(self):
158
+ d = ConstitutiveModel(
159
+ params=MaxwellWeichertParams(1.0, 1.0, [])
160
+ ).to_dict()
161
+ assert d["type"] == "Maxwell_Weichert"
162
+
163
+
164
+ class TestElasticPlasticParams:
165
+ def test_roundtrip(self):
166
+ m = Material(
167
+ density=7850.0,
168
+ thermal_conductivity=45.0,
169
+ specific_heat=4.8e-4,
170
+ constitutive_model=ConstitutiveModel(
171
+ params=ElasticPlasticParams(
172
+ shear_modulus=8e10,
173
+ bulk_modulus=1.6e11,
174
+ yield_condition=YieldCondition(type="vonMises"),
175
+ stability_check=StabilityCheck(type="drucker"),
176
+ flow_model=FlowModel(
177
+ params=JohnsonCookParams(A=0.9e9, B=0.5e9, C=0.014, n=0.26, m=1.03)
178
+ ),
179
+ )
180
+ ),
181
+ )
182
+ assert roundtrip(m).to_dict() == m.to_dict()
183
+
184
+ def test_type_key(self):
185
+ d = ConstitutiveModel(
186
+ params=ElasticPlasticParams(
187
+ shear_modulus=1.0,
188
+ bulk_modulus=1.0,
189
+ yield_condition=YieldCondition("vonMises"),
190
+ stability_check=StabilityCheck("drucker"),
191
+ flow_model=FlowModel(params=JohnsonCookParams(1, 1, 1, 1, 1)),
192
+ )
193
+ ).to_dict()
194
+ assert d["type"] == "elastic_plastic"
195
+
196
+
197
+ class TestHypoElasticParams:
198
+ def test_roundtrip(self):
199
+ m = Material(
200
+ density=1000.0,
201
+ thermal_conductivity=1.0,
202
+ specific_heat=1.0,
203
+ constitutive_model=ConstitutiveModel(
204
+ params=HypoElasticParams(G=1.5e6, K=3.0e6)
205
+ ),
206
+ )
207
+ assert roundtrip(m).to_dict() == m.to_dict()
208
+
209
+ def test_type_key(self):
210
+ d = ConstitutiveModel(params=HypoElasticParams(G=1.0, K=2.0)).to_dict()
211
+ assert d["type"] == "hypo_elastic"
212
+
213
+
214
+ class TestViscoTransIsoHyperParams:
215
+ def _make_params(self, **overrides):
216
+ defaults = dict(
217
+ bulk_modulus=1.0e6,
218
+ c1=1.0,
219
+ c2=2.0,
220
+ c3=3.0,
221
+ c4=4.0,
222
+ c5=5.0,
223
+ fiber_stretch=1.1,
224
+ direction_of_symm=[0.0, 1.0, 0.0],
225
+ failure_option=0,
226
+ max_fiber_strain=0.1,
227
+ max_matrix_strain=0.2,
228
+ y1=0.1, y2=0.2, y3=0.3, y4=0.4, y5=0.5, y6=0.6,
229
+ t1=1.0, t2=2.0, t3=3.0, t4=4.0, t5=5.0, t6=6.0,
230
+ )
231
+ defaults.update(overrides)
232
+ return ViscoTransIsoHyperParams(**defaults)
233
+
234
+ def test_roundtrip(self):
235
+ m = Material(
236
+ density=1200.0,
237
+ thermal_conductivity=0.5,
238
+ specific_heat=1.0e-3,
239
+ constitutive_model=ConstitutiveModel(params=self._make_params()),
240
+ )
241
+ assert roundtrip(m).to_dict() == m.to_dict()
242
+
243
+ def test_type_key(self):
244
+ d = ConstitutiveModel(params=self._make_params()).to_dict()
245
+ assert d["type"] == "visco_trans_iso_hyper"
246
+
247
+ def test_int_inputs_serialized_as_float(self):
248
+ params = self._make_params(c3=0, c4=0, c5=0, direction_of_symm=[0, 1, 0])
249
+ d = params.to_dict()
250
+ assert isinstance(d["c3"], float)
251
+ assert isinstance(d["c4"], float)
252
+ assert isinstance(d["c5"], float)
253
+ assert d["direction_of_symm"] == [0.0, 1.0, 0.0]
254
+ assert all(isinstance(x, float) for x in d["direction_of_symm"])
255
+
256
+ def test_failure_option_serialized_as_int(self):
257
+ params = self._make_params(failure_option=1)
258
+ d = params.to_dict()
259
+ assert isinstance(d["failure_option"], int)
260
+
261
+
262
+ class TestMaterialOptionalFields:
263
+ def test_optional_fields_omitted_when_none(self):
264
+ m = Material(density=1.0, thermal_conductivity=1.0, specific_heat=1.0,
265
+ constitutive_model=ConstitutiveModel(params=RigidParams(1.0, 1.0)))
266
+ d = m.to_dict()
267
+ assert "room_temp" not in d
268
+ assert "melt_temp" not in d
269
+
270
+ def test_optional_fields_roundtrip(self):
271
+ m = Material(
272
+ density=1.0,
273
+ thermal_conductivity=1.0,
274
+ specific_heat=1.0,
275
+ constitutive_model=ConstitutiveModel(params=RigidParams(1.0, 1.0)),
276
+ room_temp=300.0,
277
+ melt_temp=1500.0,
278
+ )
279
+ r = roundtrip(m)
280
+ assert r.room_temp == 300.0
281
+ assert r.melt_temp == 1500.0
@@ -114,6 +114,39 @@ class TestBuildParts:
114
114
  parts = _build_parts([{"type": "piston_mesh", "file": "piston.ply", "velocity": velocity}])
115
115
  assert parts[0].velocity == velocity
116
116
 
117
+ def test_piston_mesh_default_material_when_omitted(self):
118
+ from metafold.materials import DEFAULT_PISTON_MATERIAL
119
+ parts = _build_parts([{"type": "piston_mesh", "file": "piston.ply"}])
120
+ assert parts[0].material is DEFAULT_PISTON_MATERIAL
121
+
122
+ def test_piston_mesh_with_preset_material(self):
123
+ parts = _build_parts([
124
+ {"type": "piston_mesh", "file": "piston.ply", "material": "material_aluminum"}
125
+ ])
126
+ assert parts[0].material is MATERIAL_ALUMINUM
127
+
128
+ def test_piston_mesh_with_inline_material(self):
129
+ parts = _build_parts([
130
+ {
131
+ "type": "piston_mesh",
132
+ "file": "piston.ply",
133
+ "material": {
134
+ "density": 1730.0,
135
+ "thermal_conductivity": 45,
136
+ "specific_heat": 4.8e-4,
137
+ "constitutive_model": {
138
+ "type": "rigid",
139
+ "params": {"shear_modulus": 2667.0e6, "bulk_modulus": 8000.0e6},
140
+ },
141
+ },
142
+ }
143
+ ])
144
+ assert parts[0].material.constitutive_model.params.get_type() == "rigid"
145
+
146
+ def test_piston_cylinder_with_material(self):
147
+ parts = _build_parts([{"type": "piston_cylinder", "material": "material_aluminum"}])
148
+ assert parts[0].material is MATERIAL_ALUMINUM
149
+
117
150
  def test_mesh_part_with_preset_material(self):
118
151
  parts = _build_parts([
119
152
  {
File without changes
File without changes
File without changes