metafold 0.12.dev4__tar.gz → 0.12.dev6__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.dev4 → metafold-0.12.dev6}/PKG-INFO +2 -2
  2. {metafold-0.12.dev4 → metafold-0.12.dev6}/metafold/materials.py +94 -0
  3. {metafold-0.12.dev4 → metafold-0.12.dev6}/metafold/simulation/compression_simulation.py +5 -4
  4. {metafold-0.12.dev4 → metafold-0.12.dev6}/metafold/simulation/run_experiment.py +14 -0
  5. {metafold-0.12.dev4 → metafold-0.12.dev6}/metafold.egg-info/PKG-INFO +2 -2
  6. {metafold-0.12.dev4 → metafold-0.12.dev6}/metafold.egg-info/SOURCES.txt +1 -0
  7. {metafold-0.12.dev4 → metafold-0.12.dev6}/metafold.egg-info/requires.txt +1 -1
  8. {metafold-0.12.dev4 → metafold-0.12.dev6}/pyproject.toml +2 -2
  9. {metafold-0.12.dev4 → metafold-0.12.dev6}/tests/test_compression_simulation.py +37 -13
  10. metafold-0.12.dev6/tests/test_materials.py +281 -0
  11. {metafold-0.12.dev4 → metafold-0.12.dev6}/tests/test_run_experiment.py +12 -0
  12. {metafold-0.12.dev4 → metafold-0.12.dev6}/LICENSE +0 -0
  13. {metafold-0.12.dev4 → metafold-0.12.dev6}/README.md +0 -0
  14. {metafold-0.12.dev4 → metafold-0.12.dev6}/metafold/__init__.py +0 -0
  15. {metafold-0.12.dev4 → metafold-0.12.dev6}/metafold/api.py +0 -0
  16. {metafold-0.12.dev4 → metafold-0.12.dev6}/metafold/assets.py +0 -0
  17. {metafold-0.12.dev4 → metafold-0.12.dev6}/metafold/auth.py +0 -0
  18. {metafold-0.12.dev4 → metafold-0.12.dev6}/metafold/client.py +0 -0
  19. {metafold-0.12.dev4 → metafold-0.12.dev6}/metafold/exceptions.py +0 -0
  20. {metafold-0.12.dev4 → metafold-0.12.dev6}/metafold/jobs.py +0 -0
  21. {metafold-0.12.dev4 → metafold-0.12.dev6}/metafold/projects.py +0 -0
  22. {metafold-0.12.dev4 → metafold-0.12.dev6}/metafold/simulation/__init__.py +0 -0
  23. {metafold-0.12.dev4 → metafold-0.12.dev6}/metafold/simulation/compression_experiment.py +0 -0
  24. {metafold-0.12.dev4 → metafold-0.12.dev6}/metafold/utils.py +0 -0
  25. {metafold-0.12.dev4 → metafold-0.12.dev6}/metafold/workflows.py +0 -0
  26. {metafold-0.12.dev4 → metafold-0.12.dev6}/metafold.egg-info/dependency_links.txt +0 -0
  27. {metafold-0.12.dev4 → metafold-0.12.dev6}/metafold.egg-info/top_level.txt +0 -0
  28. {metafold-0.12.dev4 → metafold-0.12.dev6}/setup.cfg +0 -0
  29. {metafold-0.12.dev4 → metafold-0.12.dev6}/tests/test_assets.py +0 -0
  30. {metafold-0.12.dev4 → metafold-0.12.dev6}/tests/test_compession_experiment.py +0 -0
  31. {metafold-0.12.dev4 → metafold-0.12.dev6}/tests/test_jobs.py +0 -0
  32. {metafold-0.12.dev4 → metafold-0.12.dev6}/tests/test_projects.py +0 -0
  33. {metafold-0.12.dev4 → metafold-0.12.dev6}/tests/test_shear_simulation.py +0 -0
  34. {metafold-0.12.dev4 → metafold-0.12.dev6}/tests/test_utils.py +0 -0
  35. {metafold-0.12.dev4 → metafold-0.12.dev6}/tests/test_workflows.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: metafold
3
- Version: 0.12.dev4
3
+ Version: 0.12.dev6
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:
@@ -18,6 +18,8 @@ JSON manifest format
18
18
  "parts": [
19
19
  {
20
20
  "type": "piston_cylinder",
21
+ # optional geometry - omit to use default
22
+ "shape_parameters": {"top": [...], "bottom": [...], "radius": 0.02},
21
23
  "velocity": [ # optional — defaults to DEFAULT_PISTON_VELOCITY
22
24
  [0.0, 0, 0, 0.0],
23
25
  [0.002, 0, 0, -1.25],
@@ -26,6 +28,7 @@ JSON manifest format
26
28
  [0.04, 0, 0, 0.0]
27
29
  ]
28
30
  },
31
+ {"type": "piston_box", "shape_parameters": {"min": [...], "max": [...]}},
29
32
  {"type": "piston_mesh", "file": "piston.ply", "velocity": [...]},
30
33
  {
31
34
  "type": "mesh",
@@ -132,6 +135,7 @@ from metafold.simulation.compression_simulation import (
132
135
  CompressionSimulation,
133
136
  ExperimentMesh,
134
137
  ExperimentPart,
138
+ ExperimentPistonBox,
135
139
  ExperimentPistonCylinder,
136
140
  ExperimentPistonMesh,
137
141
  SimulationParameters,
@@ -171,8 +175,18 @@ def _build_parts(parts_config: list[dict]) -> list[ExperimentPart]:
171
175
  kwargs = {}
172
176
  if "velocity" in entry:
173
177
  kwargs["velocity"] = entry["velocity"]
178
+ if "shape_parameters" in entry:
179
+ kwargs["shape_parameters"] = entry["shape_parameters"]
174
180
  parts.append(ExperimentPistonCylinder(**kwargs))
175
181
 
182
+ elif part_type == "piston_box":
183
+ kwargs = {}
184
+ if "velocity" in entry:
185
+ kwargs["velocity"] = entry["velocity"]
186
+ if "shape_parameters" in entry:
187
+ kwargs["shape_parameters"] = entry["shape_parameters"]
188
+ parts.append(ExperimentPistonBox(**kwargs))
189
+
176
190
  elif part_type == "piston_mesh":
177
191
  kwargs = {"filename": entry["file"]}
178
192
  if "velocity" in entry:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: metafold
3
- Version: 0.12.dev4
3
+ Version: 0.12.dev6
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.dev4"
7
+ version = "0.12.dev6"
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
@@ -16,6 +16,7 @@ from metafold.materials import (
16
16
  )
17
17
  from metafold.simulation.compression_simulation import (
18
18
  ExperimentMesh,
19
+ ExperimentPistonBox,
19
20
  ExperimentPistonCylinder,
20
21
  ExperimentPistonMesh,
21
22
  SimulationParameters,
@@ -92,6 +93,17 @@ class TestBuildParts:
92
93
  parts = _build_parts([{"type": "piston_cylinder"}])
93
94
  assert parts[0].velocity == DEFAULT_PISTON_VELOCITY
94
95
 
96
+ def test_piston_cylinder_with_shape_parameters(self):
97
+ shape = {"top": [0, 0, 0.01], "bottom": [0, 0, 0.012], "radius": 0.015}
98
+ parts = _build_parts([{"type": "piston_cylinder", "shape_parameters": shape}])
99
+ assert parts[0].shape_parameters == shape
100
+
101
+ def test_piston_box(self):
102
+ shape = {"min": [0, 0, 0.01], "max": [0.02, 0.02, 0.012]}
103
+ parts = _build_parts([{"type": "piston_box", "shape_parameters": shape}])
104
+ assert isinstance(parts[0], ExperimentPistonBox)
105
+ assert parts[0].shape_parameters == shape
106
+
95
107
  def test_piston_mesh(self):
96
108
  parts = _build_parts([{"type": "piston_mesh", "file": "piston.ply"}])
97
109
  assert isinstance(parts[0], ExperimentPistonMesh)
File without changes
File without changes
File without changes