metafold 0.14.dev0__tar.gz → 0.15.dev1__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.14.dev0 → metafold-0.15.dev1}/PKG-INFO +2 -2
  2. {metafold-0.14.dev0 → metafold-0.15.dev1}/metafold/materials.py +105 -0
  3. {metafold-0.14.dev0 → metafold-0.15.dev1}/metafold/simulation/compression_simulation.py +15 -1
  4. {metafold-0.14.dev0 → metafold-0.15.dev1}/metafold/simulation/run_experiment.py +43 -1
  5. {metafold-0.14.dev0 → metafold-0.15.dev1}/metafold.egg-info/PKG-INFO +2 -2
  6. {metafold-0.14.dev0 → metafold-0.15.dev1}/metafold.egg-info/requires.txt +1 -1
  7. {metafold-0.14.dev0 → metafold-0.15.dev1}/pyproject.toml +2 -2
  8. {metafold-0.14.dev0 → metafold-0.15.dev1}/tests/test_materials.py +110 -0
  9. {metafold-0.14.dev0 → metafold-0.15.dev1}/tests/test_run_experiment.py +59 -0
  10. {metafold-0.14.dev0 → metafold-0.15.dev1}/LICENSE +0 -0
  11. {metafold-0.14.dev0 → metafold-0.15.dev1}/README.md +0 -0
  12. {metafold-0.14.dev0 → metafold-0.15.dev1}/metafold/__init__.py +0 -0
  13. {metafold-0.14.dev0 → metafold-0.15.dev1}/metafold/api.py +0 -0
  14. {metafold-0.14.dev0 → metafold-0.15.dev1}/metafold/assets.py +0 -0
  15. {metafold-0.14.dev0 → metafold-0.15.dev1}/metafold/auth.py +0 -0
  16. {metafold-0.14.dev0 → metafold-0.15.dev1}/metafold/client.py +0 -0
  17. {metafold-0.14.dev0 → metafold-0.15.dev1}/metafold/exceptions.py +0 -0
  18. {metafold-0.14.dev0 → metafold-0.15.dev1}/metafold/jobs.py +0 -0
  19. {metafold-0.14.dev0 → metafold-0.15.dev1}/metafold/projects.py +0 -0
  20. {metafold-0.14.dev0 → metafold-0.15.dev1}/metafold/simulation/__init__.py +0 -0
  21. {metafold-0.14.dev0 → metafold-0.15.dev1}/metafold/simulation/compression_experiment.py +0 -0
  22. {metafold-0.14.dev0 → metafold-0.15.dev1}/metafold/utils.py +0 -0
  23. {metafold-0.14.dev0 → metafold-0.15.dev1}/metafold/workflows.py +0 -0
  24. {metafold-0.14.dev0 → metafold-0.15.dev1}/metafold.egg-info/SOURCES.txt +0 -0
  25. {metafold-0.14.dev0 → metafold-0.15.dev1}/metafold.egg-info/dependency_links.txt +0 -0
  26. {metafold-0.14.dev0 → metafold-0.15.dev1}/metafold.egg-info/top_level.txt +0 -0
  27. {metafold-0.14.dev0 → metafold-0.15.dev1}/setup.cfg +0 -0
  28. {metafold-0.14.dev0 → metafold-0.15.dev1}/tests/test_assets.py +0 -0
  29. {metafold-0.14.dev0 → metafold-0.15.dev1}/tests/test_compession_experiment.py +0 -0
  30. {metafold-0.14.dev0 → metafold-0.15.dev1}/tests/test_compression_simulation.py +0 -0
  31. {metafold-0.14.dev0 → metafold-0.15.dev1}/tests/test_jobs.py +0 -0
  32. {metafold-0.14.dev0 → metafold-0.15.dev1}/tests/test_projects.py +0 -0
  33. {metafold-0.14.dev0 → metafold-0.15.dev1}/tests/test_shear_simulation.py +0 -0
  34. {metafold-0.14.dev0 → metafold-0.15.dev1}/tests/test_utils.py +0 -0
  35. {metafold-0.14.dev0 → metafold-0.15.dev1}/tests/test_workflows.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: metafold
3
- Version: 0.14.dev0
3
+ Version: 0.15.dev1
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,109 @@ 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
+ An optional viscoelastic overlay (Prony series) may be supplied via
236
+ terminal_shear_modulus + viscoelastic_series, matching the MaxwellWeichert
237
+ mode structure. Omit both for a purely elastic hyperfoam.
238
+ """
239
+ bulk_modulus: float
240
+ terms: List[HyperfoamTerm]
241
+ terminal_shear_modulus: Optional[float] = None
242
+ viscoelastic_series: Optional[List[ViscoelasticMode]] = None
243
+
244
+ @classmethod
245
+ def get_type(self):
246
+ return "hyperfoam"
247
+
248
+ def to_dict(self):
249
+ d = {
250
+ "bulk_modulus": float(self.bulk_modulus),
251
+ "terms": [t.to_dict() for t in self.terms],
252
+ }
253
+ if self.terminal_shear_modulus is not None:
254
+ d["terminal_shear_modulus"] = float(self.terminal_shear_modulus)
255
+ if self.viscoelastic_series:
256
+ d["viscoelastic_series"] = [m.to_dict() for m in self.viscoelastic_series]
257
+ return d
258
+
259
+ @classmethod
260
+ def from_dict(cls, d: dict) -> "HyperfoamParams":
261
+ series = d.get("viscoelastic_series")
262
+ return cls(
263
+ bulk_modulus=d["bulk_modulus"],
264
+ terms=[
265
+ HyperfoamTerm(
266
+ mu=t["mu"], alpha=t["alpha"], poisson_ratio=t["poisson_ratio"])
267
+ for t in d["terms"]
268
+ ],
269
+ terminal_shear_modulus=d.get("terminal_shear_modulus"),
270
+ viscoelastic_series=(
271
+ [ViscoelasticMode(**m) for m in series] if series else None),
272
+ )
273
+
274
+
275
+ @dataclass
276
+ class OrthotropicParams(ParamsBase):
277
+ """Linear-elastic orthotropic (mpmgpu only; no Uintah equivalent)."""
278
+ E1: float
279
+ E2: float
280
+ E3: float
281
+ nu12: float
282
+ nu13: float
283
+ nu23: float
284
+ G12: float
285
+ G13: float
286
+ G23: float
287
+ direction_a: List[float]
288
+ direction_b: List[float]
289
+
290
+ @classmethod
291
+ def get_type(self):
292
+ return "orthotropic"
293
+
294
+ def to_dict(self):
295
+ return {
296
+ "E1": float(self.E1), "E2": float(self.E2), "E3": float(self.E3),
297
+ "nu12": float(self.nu12), "nu13": float(self.nu13), "nu23": float(self.nu23),
298
+ "G12": float(self.G12), "G13": float(self.G13), "G23": float(self.G23),
299
+ # Numeric lists so simulation_configurator emits them as vectors.
300
+ "direction_a": [float(x) for x in self.direction_a],
301
+ "direction_b": [float(x) for x in self.direction_b],
302
+ }
303
+
304
+ @classmethod
305
+ def from_dict(cls, d: dict) -> "OrthotropicParams":
306
+ def vec(v):
307
+ return [float(x) for x in (v.split() if isinstance(v, str) else v)]
308
+ return cls(
309
+ E1=d["E1"], E2=d["E2"], E3=d["E3"],
310
+ nu12=d["nu12"], nu13=d["nu13"], nu23=d["nu23"],
311
+ G12=d["G12"], G13=d["G13"], G23=d["G23"],
312
+ direction_a=vec(d["direction_a"]),
313
+ direction_b=vec(d["direction_b"]),
314
+ )
315
+
316
+
214
317
  @dataclass
215
318
  class CompNeoHookParams(ParamsBase):
216
319
  bulk_modulus: float
@@ -351,6 +454,8 @@ class Material:
351
454
  ElasticPlasticParams,
352
455
  HypoElasticParams,
353
456
  ViscoTransIsoHyperParams,
457
+ HyperfoamParams,
458
+ OrthotropicParams,
354
459
  ]
355
460
  PARAMS_CLASSES = {c.get_type(): c for c in params_classes}
356
461
  cm = d["constitutive_model"]
@@ -474,6 +474,8 @@ class CompressionSimulation:
474
474
  # experiment variants sampled later match the base simulation's density.
475
475
  sample_spacing: Optional[float] = None
476
476
 
477
+ use_experimental_solver: bool = False
478
+
477
479
  def __init__(
478
480
  self,
479
481
  parts: list[ExperimentPart],
@@ -503,6 +505,7 @@ class CompressionSimulation:
503
505
  project_name: str = "",
504
506
  use_legacy_results_format: bool = False,
505
507
  write_ups: bool = True,
508
+ use_experimental_solver: bool = False,
506
509
  ):
507
510
  if not output_path:
508
511
  if project_name:
@@ -542,6 +545,8 @@ class CompressionSimulation:
542
545
  else:
543
546
  self.client = client
544
547
 
548
+ self.use_experimental_solver = use_experimental_solver
549
+
545
550
  def _write_ups(self, content: str, filename: str) -> None:
546
551
  if self.write_ups and self.out_dir is not None:
547
552
  (self.out_dir / filename).write_text(content)
@@ -1370,8 +1375,12 @@ class CompressionSimulation:
1370
1375
  # Only parts that have a volume go into the compress assets list
1371
1376
  parts_with_volume = [p for p in part_infos if p.volume_filename]
1372
1377
 
1378
+ job_type = "sim/custom"
1379
+ if self.use_experimental_solver:
1380
+ job_type = "sim/custom-gpu"
1381
+
1373
1382
  self.workflow_jobs[compress_job_name] = {
1374
- "type": "sim/custom",
1383
+ "type": job_type,
1375
1384
  }
1376
1385
 
1377
1386
  # compress.volume is a list of volume asset filenames in part order
@@ -1505,6 +1514,11 @@ class CompressionSimulation:
1505
1514
  for row in piston_info.part.velocity
1506
1515
  ]
1507
1516
  )
1517
+ if piston_info.part.force_limit:
1518
+ # Switch to using piston particle positions to determine displacement
1519
+ self.workflow_params[f"{job_name_base}.piston_position"] = (
1520
+ f"/material{piston_info.material_index}/position"
1521
+ )
1508
1522
 
1509
1523
  def _add_to_workflow_stress_strain(self, part_infos: list[PartInfo]):
1510
1524
  # One stress-strain job per deformable part, each normalizing the shared
@@ -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]:
@@ -329,6 +360,12 @@ def _build_simulation_parameters(sim_config: dict) -> SimulationParameters:
329
360
  return params
330
361
 
331
362
 
363
+ def num_simulations(config: dict) -> int:
364
+ """Count number of simulation variations to be generated by a given configuration.
365
+ """
366
+ return len(_build_varying(config.get("varying", [])))
367
+
368
+
332
369
  def run_experiment(
333
370
  config: dict,
334
371
  output_path: str = "",
@@ -373,6 +410,10 @@ def run_experiment(
373
410
  varying = _build_varying(config.get("varying", []))
374
411
  sim_params = _build_simulation_parameters(config.get("simulation", {}))
375
412
 
413
+ _validate_experimental_materials(config, parts, varying)
414
+
415
+ use_experimental_solver = config.get("use_experimental_solver", False)
416
+
376
417
  sim_kwargs: dict = dict(
377
418
  parts=parts,
378
419
  simulation_name=project_name,
@@ -381,6 +422,7 @@ def run_experiment(
381
422
  simulation_parameters=sim_params,
382
423
  project_id=project_id,
383
424
  create_project_if_needed=not bool(project_id),
425
+ use_experimental_solver=use_experimental_solver,
384
426
  )
385
427
  # Only pass project_name when we have to create a project. When a
386
428
  # project_id is supplied the project already exists, and passing the name
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: metafold
3
- Version: 0.14.dev0
3
+ Version: 0.15.dev1
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.14.dev0"
7
+ version = "0.15.dev1"
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,110 @@ 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
+ bulk_modulus=1.0e6,
296
+ terms=[
297
+ HyperfoamTerm(mu=0.16e6, alpha=8.9, poisson_ratio=0.0),
298
+ HyperfoamTerm(mu=0.01e6, alpha=-2.0, poisson_ratio=0.0),
299
+ ],
300
+ )
301
+ ),
302
+ )
303
+
304
+ def test_roundtrip(self):
305
+ m = self._material()
306
+ assert roundtrip(m).to_dict() == m.to_dict()
307
+
308
+ def test_type_key(self):
309
+ d = ConstitutiveModel(
310
+ params=HyperfoamParams(bulk_modulus=1.0, terms=[])
311
+ ).to_dict()
312
+ assert d["type"] == "hyperfoam"
313
+
314
+ def test_terms_preserved(self):
315
+ d = self._material().constitutive_model.params.to_dict()
316
+ assert len(d["terms"]) == 2
317
+ # "_element" marks how the ups emitter renders each term.
318
+ assert d["terms"][0]["_element"] == "term"
319
+ assert d["terms"][0]["mu"] == 0.16e6
320
+ assert d["terms"][1]["alpha"] == -2.0
321
+
322
+ def test_pure_elastic_omits_viscoelastic_fields(self):
323
+ d = self._material().constitutive_model.params.to_dict()
324
+ assert "terminal_shear_modulus" not in d
325
+ assert "viscoelastic_series" not in d
326
+
327
+ def _viscoelastic_material(self):
328
+ return Material(
329
+ density=600.0,
330
+ thermal_conductivity=0.15,
331
+ specific_heat=1.36,
332
+ constitutive_model=ConstitutiveModel(
333
+ params=HyperfoamParams(
334
+ bulk_modulus=1.0e6,
335
+ terms=[HyperfoamTerm(mu=0.16e6, alpha=8.9, poisson_ratio=0.0)],
336
+ terminal_shear_modulus=5.0e4,
337
+ viscoelastic_series=[
338
+ ViscoelasticMode("m1", 0.15, 25000),
339
+ ViscoelasticMode("m2", 0.20, 30000),
340
+ ],
341
+ )
342
+ ),
343
+ )
344
+
345
+ def test_viscoelastic_overlay_roundtrip(self):
346
+ m = self._viscoelastic_material()
347
+ assert roundtrip(m).to_dict() == m.to_dict()
348
+
349
+ def test_viscoelastic_overlay_emitted(self):
350
+ d = self._viscoelastic_material().constitutive_model.params.to_dict()
351
+ assert d["terminal_shear_modulus"] == 5.0e4
352
+ assert len(d["viscoelastic_series"]) == 2
353
+ assert d["viscoelastic_series"][0]["mode"] == "m1"
354
+
355
+
356
+ class TestOrthotropicParams:
357
+ def _material(self):
358
+ return Material(
359
+ density=1000.0,
360
+ thermal_conductivity=1.0,
361
+ specific_heat=1500,
362
+ constitutive_model=ConstitutiveModel(
363
+ params=OrthotropicParams(
364
+ E1=1e9, E2=2e9, E3=3e9,
365
+ nu12=0.3, nu13=0.3, nu23=0.3,
366
+ G12=0.5e9, G13=0.5e9, G23=0.5e9,
367
+ direction_a=[1.0, 0.0, 0.0],
368
+ direction_b=[0.0, 1.0, 0.0],
369
+ )
370
+ ),
371
+ )
372
+
373
+ def test_roundtrip(self):
374
+ m = self._material()
375
+ assert roundtrip(m).to_dict() == m.to_dict()
376
+
377
+ def test_type_key(self):
378
+ d = ConstitutiveModel(params=self._material().constitutive_model.params).to_dict()
379
+ assert d["type"] == "orthotropic"
380
+
381
+ def test_directions_are_numeric_lists(self):
382
+ d = self._material().constitutive_model.params.to_dict()
383
+ assert d["direction_a"] == [1.0, 0.0, 0.0]
384
+ assert d["direction_b"] == [0.0, 1.0, 0.0]
385
+
386
+ def test_from_dict_accepts_string_direction(self):
387
+ # Directions may arrive as "x y z" strings (manifest form).
388
+ m = self._material()
389
+ d = m.to_dict()
390
+ d["constitutive_model"]["params"]["direction_a"] = "1 0 0"
391
+ assert OrthotropicParams.from_dict(d["constitutive_model"]["params"]).direction_a == [1.0, 0.0, 0.0]
@@ -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