metafold 0.15.dev0__tar.gz → 0.15.dev2__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.dev0 → metafold-0.15.dev2}/PKG-INFO +2 -2
  2. {metafold-0.15.dev0 → metafold-0.15.dev2}/metafold/materials.py +120 -1
  3. {metafold-0.15.dev0 → metafold-0.15.dev2}/metafold/simulation/run_experiment.py +34 -1
  4. {metafold-0.15.dev0 → metafold-0.15.dev2}/metafold.egg-info/PKG-INFO +2 -2
  5. {metafold-0.15.dev0 → metafold-0.15.dev2}/metafold.egg-info/requires.txt +1 -1
  6. {metafold-0.15.dev0 → metafold-0.15.dev2}/pyproject.toml +2 -2
  7. {metafold-0.15.dev0 → metafold-0.15.dev2}/tests/test_materials.py +155 -0
  8. {metafold-0.15.dev0 → metafold-0.15.dev2}/tests/test_run_experiment.py +59 -0
  9. {metafold-0.15.dev0 → metafold-0.15.dev2}/LICENSE +0 -0
  10. {metafold-0.15.dev0 → metafold-0.15.dev2}/README.md +0 -0
  11. {metafold-0.15.dev0 → metafold-0.15.dev2}/metafold/__init__.py +0 -0
  12. {metafold-0.15.dev0 → metafold-0.15.dev2}/metafold/api.py +0 -0
  13. {metafold-0.15.dev0 → metafold-0.15.dev2}/metafold/assets.py +0 -0
  14. {metafold-0.15.dev0 → metafold-0.15.dev2}/metafold/auth.py +0 -0
  15. {metafold-0.15.dev0 → metafold-0.15.dev2}/metafold/client.py +0 -0
  16. {metafold-0.15.dev0 → metafold-0.15.dev2}/metafold/exceptions.py +0 -0
  17. {metafold-0.15.dev0 → metafold-0.15.dev2}/metafold/jobs.py +0 -0
  18. {metafold-0.15.dev0 → metafold-0.15.dev2}/metafold/projects.py +0 -0
  19. {metafold-0.15.dev0 → metafold-0.15.dev2}/metafold/simulation/__init__.py +0 -0
  20. {metafold-0.15.dev0 → metafold-0.15.dev2}/metafold/simulation/compression_experiment.py +0 -0
  21. {metafold-0.15.dev0 → metafold-0.15.dev2}/metafold/simulation/compression_simulation.py +0 -0
  22. {metafold-0.15.dev0 → metafold-0.15.dev2}/metafold/utils.py +0 -0
  23. {metafold-0.15.dev0 → metafold-0.15.dev2}/metafold/workflows.py +0 -0
  24. {metafold-0.15.dev0 → metafold-0.15.dev2}/metafold.egg-info/SOURCES.txt +0 -0
  25. {metafold-0.15.dev0 → metafold-0.15.dev2}/metafold.egg-info/dependency_links.txt +0 -0
  26. {metafold-0.15.dev0 → metafold-0.15.dev2}/metafold.egg-info/top_level.txt +0 -0
  27. {metafold-0.15.dev0 → metafold-0.15.dev2}/setup.cfg +0 -0
  28. {metafold-0.15.dev0 → metafold-0.15.dev2}/tests/test_assets.py +0 -0
  29. {metafold-0.15.dev0 → metafold-0.15.dev2}/tests/test_compession_experiment.py +0 -0
  30. {metafold-0.15.dev0 → metafold-0.15.dev2}/tests/test_compression_simulation.py +0 -0
  31. {metafold-0.15.dev0 → metafold-0.15.dev2}/tests/test_jobs.py +0 -0
  32. {metafold-0.15.dev0 → metafold-0.15.dev2}/tests/test_projects.py +0 -0
  33. {metafold-0.15.dev0 → metafold-0.15.dev2}/tests/test_shear_simulation.py +0 -0
  34. {metafold-0.15.dev0 → metafold-0.15.dev2}/tests/test_utils.py +0 -0
  35. {metafold-0.15.dev0 → metafold-0.15.dev2}/tests/test_workflows.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: metafold
3
- Version: 0.15.dev0
3
+ Version: 0.15.dev2
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.2.*; extra == "simulation"
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
- 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}.")
358
477
  return cls(
359
478
  density=d["density"],
360
479
  thermal_conductivity=d["thermal_conductivity"],
@@ -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
- validate_simulation_parts(_build_parts(config.get("parts", [])))
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]:
@@ -379,6 +410,8 @@ def run_experiment(
379
410
  varying = _build_varying(config.get("varying", []))
380
411
  sim_params = _build_simulation_parameters(config.get("simulation", {}))
381
412
 
413
+ _validate_experimental_materials(config, parts, varying)
414
+
382
415
  use_experimental_solver = config.get("use_experimental_solver", False)
383
416
 
384
417
  sim_kwargs: dict = dict(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: metafold
3
- Version: 0.15.dev0
3
+ Version: 0.15.dev2
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.2.*; extra == "simulation"
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
 
@@ -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.2.*
12
+ simulation-configurator==0.3.*
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.15.dev0"
7
+ version = "0.15.dev2"
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.2.*",
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