metafold 0.12.dev10__tar.gz → 0.12.1__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.dev10 → metafold-0.12.1}/PKG-INFO +3 -3
  2. {metafold-0.12.dev10 → metafold-0.12.1}/metafold/simulation/compression_simulation.py +58 -41
  3. {metafold-0.12.dev10 → metafold-0.12.1}/metafold/simulation/run_experiment.py +6 -3
  4. {metafold-0.12.dev10 → metafold-0.12.1}/metafold.egg-info/PKG-INFO +3 -3
  5. {metafold-0.12.dev10 → metafold-0.12.1}/metafold.egg-info/requires.txt +1 -1
  6. {metafold-0.12.dev10 → metafold-0.12.1}/pyproject.toml +5 -5
  7. {metafold-0.12.dev10 → metafold-0.12.1}/tests/test_compression_simulation.py +37 -29
  8. {metafold-0.12.dev10 → metafold-0.12.1}/LICENSE +0 -0
  9. {metafold-0.12.dev10 → metafold-0.12.1}/README.md +0 -0
  10. {metafold-0.12.dev10 → metafold-0.12.1}/metafold/__init__.py +0 -0
  11. {metafold-0.12.dev10 → metafold-0.12.1}/metafold/api.py +0 -0
  12. {metafold-0.12.dev10 → metafold-0.12.1}/metafold/assets.py +0 -0
  13. {metafold-0.12.dev10 → metafold-0.12.1}/metafold/auth.py +0 -0
  14. {metafold-0.12.dev10 → metafold-0.12.1}/metafold/client.py +0 -0
  15. {metafold-0.12.dev10 → metafold-0.12.1}/metafold/exceptions.py +0 -0
  16. {metafold-0.12.dev10 → metafold-0.12.1}/metafold/jobs.py +0 -0
  17. {metafold-0.12.dev10 → metafold-0.12.1}/metafold/materials.py +0 -0
  18. {metafold-0.12.dev10 → metafold-0.12.1}/metafold/projects.py +0 -0
  19. {metafold-0.12.dev10 → metafold-0.12.1}/metafold/simulation/__init__.py +0 -0
  20. {metafold-0.12.dev10 → metafold-0.12.1}/metafold/simulation/compression_experiment.py +0 -0
  21. {metafold-0.12.dev10 → metafold-0.12.1}/metafold/utils.py +0 -0
  22. {metafold-0.12.dev10 → metafold-0.12.1}/metafold/workflows.py +0 -0
  23. {metafold-0.12.dev10 → metafold-0.12.1}/metafold.egg-info/SOURCES.txt +0 -0
  24. {metafold-0.12.dev10 → metafold-0.12.1}/metafold.egg-info/dependency_links.txt +0 -0
  25. {metafold-0.12.dev10 → metafold-0.12.1}/metafold.egg-info/top_level.txt +0 -0
  26. {metafold-0.12.dev10 → metafold-0.12.1}/setup.cfg +0 -0
  27. {metafold-0.12.dev10 → metafold-0.12.1}/tests/test_assets.py +0 -0
  28. {metafold-0.12.dev10 → metafold-0.12.1}/tests/test_compession_experiment.py +0 -0
  29. {metafold-0.12.dev10 → metafold-0.12.1}/tests/test_jobs.py +0 -0
  30. {metafold-0.12.dev10 → metafold-0.12.1}/tests/test_materials.py +0 -0
  31. {metafold-0.12.dev10 → metafold-0.12.1}/tests/test_projects.py +0 -0
  32. {metafold-0.12.dev10 → metafold-0.12.1}/tests/test_run_experiment.py +0 -0
  33. {metafold-0.12.dev10 → metafold-0.12.1}/tests/test_shear_simulation.py +0 -0
  34. {metafold-0.12.dev10 → metafold-0.12.1}/tests/test_utils.py +0 -0
  35. {metafold-0.12.dev10 → metafold-0.12.1}/tests/test_workflows.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: metafold
3
- Version: 0.12.dev10
3
+ Version: 0.12.1
4
4
  Summary: Metafold SDK for Python
5
5
  Author-email: Metafold 3D <info@metafold3d.com>
6
6
  License: Copyright 2024 Metafold 3D
@@ -25,7 +25,7 @@ Classifier: Programming Language :: Python :: 3.12
25
25
  Classifier: Programming Language :: Python :: 3.13
26
26
  Classifier: Topic :: Scientific/Engineering
27
27
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
28
- Requires-Python: >=3.12
28
+ Requires-Python: >=3.11
29
29
  Description-Content-Type: text/markdown
30
30
  License-File: LICENSE
31
31
  Requires-Dist: attrs~=23.2
@@ -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.2; extra == "simulation"
41
+ Requires-Dist: simulation-configurator==0.2.*; extra == "simulation"
42
42
  Requires-Dist: tables>=3.11.1; extra == "simulation"
43
43
  Dynamic: license-file
44
44
 
@@ -8,7 +8,7 @@ import uuid
8
8
 
9
9
  from requests import HTTPError
10
10
  import yaml
11
- from dotenv import load_dotenv
11
+ from dotenv import load_dotenv # type: ignore
12
12
  from metafold import MetafoldClient
13
13
  from pathlib import Path
14
14
  from numpy.typing import DTypeLike
@@ -353,6 +353,8 @@ class CompressionSimulation:
353
353
  bvh_filename: Optional[str] = None
354
354
  # Sampling resolution (longest axis) assigned for the pass-2 sample job.
355
355
  sample_resolution: Optional[int] = None
356
+ # From the prep metrics job; None until prep completes.
357
+ interior_volume: Optional[float] = None
356
358
  patch: dict = field(default_factory=dict)
357
359
  jobs: dict[str, Any] = field(default_factory=lambda: {})
358
360
  part_unique_name = ""
@@ -374,6 +376,7 @@ class CompressionSimulation:
374
376
  "is_piston": isinstance(self.part, ExperimentPistonBase),
375
377
  "part_name": self.part.name,
376
378
  "disabled": self.disabled,
379
+ "interior_volume": self.interior_volume,
377
380
  }
378
381
 
379
382
  def restore_from_state_dict(self, saved: dict):
@@ -382,6 +385,7 @@ class CompressionSimulation:
382
385
  self.material_name = saved["material_name"]
383
386
  self.jobs = saved["jobs"]
384
387
  self.disabled = saved.get("disabled", False)
388
+ self.interior_volume = saved.get("interior_volume")
385
389
 
386
390
  client: MetafoldClient
387
391
  project_id: str = ""
@@ -425,7 +429,7 @@ class CompressionSimulation:
425
429
  simulation_name: str,
426
430
  project_id: str = "",
427
431
  stl_folder_path: str = "PLY",
428
- env_source: str = ".env.dev",
432
+ env_source: str | None = ".env.dev",
429
433
  output_path: str = "",
430
434
  client: Optional[MetafoldClient] = None,
431
435
  simulation_parameters: SimulationParameters = SimulationParameters(),
@@ -437,7 +441,7 @@ class CompressionSimulation:
437
441
  WorkflowStep(WorkflowStepType.VON_MISES_STRESS),
438
442
  WorkflowStep(WorkflowStepType.EFFECTIVE_STRAIN),
439
443
  WorkflowStep(WorkflowStepType.FORCE_DISPLACEMENT),
440
- WorkflowStep(WorkflowStepType.STRESS_STRAIN),
444
+ # stress_strain is opt-in (not default): callers add it via workflow_steps.
441
445
  WorkflowStep(WorkflowStepType.PARTICLE_DISPLACEMENT),
442
446
  WorkflowStep(WorkflowStepType.ENERGY_METRICS),
443
447
  ],
@@ -642,7 +646,8 @@ class CompressionSimulation:
642
646
  return created.id
643
647
 
644
648
  def setup_client(self, env_source, project_id) -> MetafoldClient:
645
- load_dotenv(env_source)
649
+ if env_source is not None:
650
+ load_dotenv(env_source)
646
651
 
647
652
  if not project_id:
648
653
  self.project_id = os.environ.get("METAFOLD_PROJECT_ID", "")
@@ -824,6 +829,24 @@ class CompressionSimulation:
824
829
  params[f"{sample_mesh_job}.resolution"] = f"{resolution}"
825
830
  part_info.jobs["sample-mesh"] = sample_mesh_job
826
831
 
832
+ # Metrics run in prep (not the main workflow) so interior volume
833
+ # is known before dispatch, when the server manifest is uploaded.
834
+ if self._is_analysis_target(part_info.part) and self._get_step(
835
+ WorkflowStepType.METRICS, part_info.part.name
836
+ ):
837
+ metrics_job = f"metrics-{unique_name}"
838
+ jobs[metrics_job] = {
839
+ "type": "implicit/metrics",
840
+ "needs": [sample_mesh_job],
841
+ "assets": {"volume": sample_mesh_job},
842
+ "parameters": {
843
+ "volume_size": {"job": sample_mesh_job, "parameter": "patch_size"},
844
+ "volume_offset": {"job": sample_mesh_job, "parameter": "patch_offset"},
845
+ "volume_resolution": {"job": sample_mesh_job, "parameter": "patch_resolution"},
846
+ },
847
+ }
848
+ part_info.jobs["metrics"] = metrics_job
849
+
827
850
  workflow_yaml = yaml.dump({"jobs": jobs}, default_flow_style=False)
828
851
  return workflow_yaml, params, assets
829
852
 
@@ -1025,6 +1048,30 @@ class CompressionSimulation:
1025
1048
  if volume_asset is not None:
1026
1049
  info.volume_filename = volume_asset.filename
1027
1050
 
1051
+ metrics_job = prep_workflow_jobs.get(info.jobs.get("metrics", ""))
1052
+ if metrics_job is not None:
1053
+ raw = metrics_job.outputs.params.get("interior_volume")
1054
+ if raw is not None:
1055
+ info.interior_volume = float(raw)
1056
+
1057
+ def _total_interior_volume(self, workflow: Optional[Workflow] = None) -> float:
1058
+ """Sum interior volumes across analysis-target parts. Falls back to
1059
+ the main-workflow metrics readback for experiments dispatched before
1060
+ metrics moved to the prep workflow."""
1061
+ total = 0.0
1062
+ for part_info in self.part_infos:
1063
+ if not self._is_analysis_target(part_info.part) or part_info.disabled:
1064
+ continue
1065
+ if part_info.interior_volume is not None:
1066
+ total += part_info.interior_volume
1067
+ continue
1068
+ metrics_job = part_info.jobs.get("metrics", "")
1069
+ if workflow is not None and metrics_job:
1070
+ raw = workflow.get_parameter(f"{metrics_job}.interior_volume")
1071
+ if raw is not None:
1072
+ total += float(raw)
1073
+ return total
1074
+
1028
1075
  def get_part_info(self, name):
1029
1076
  for info in self.part_infos:
1030
1077
  if info.part.name == name:
@@ -1218,6 +1265,9 @@ class CompressionSimulation:
1218
1265
  ElementTree.indent(self.ups)
1219
1266
 
1220
1267
  def _add_to_workflow_metrics(self, part_infos: list[PartInfo]):
1268
+ # Duplicates the prep metrics jobs (which feed the server manifest):
1269
+ # removing these changed main-workflow scheduling and left it pending,
1270
+ # so they stay until that's understood.
1221
1271
  for part_info in part_infos:
1222
1272
  part = part_info.part
1223
1273
  if not self._is_analysis_target(part):
@@ -1241,7 +1291,6 @@ class CompressionSimulation:
1241
1291
  self.workflow_params[f"{metrics_job_name}.volume_resolution"] = json.dumps(
1242
1292
  part_info.patch["resolution"]
1243
1293
  )
1244
- part_info.jobs["metrics"] = metrics_job_name
1245
1294
 
1246
1295
  def _add_to_workflow_compress(self, part_infos: list[PartInfo]):
1247
1296
  compress_job_name = "compress"
@@ -2046,16 +2095,7 @@ class CompressionSimulation:
2046
2095
  data["ply"] = f"{name}/ply/part.####.ply"
2047
2096
 
2048
2097
  # Sum interior volumes from all parts
2049
- total_volume = 0.0
2050
- for part_info in self.part_infos:
2051
- if self._is_analysis_target(part_info.part):
2052
- if part_info.disabled:
2053
- continue
2054
- metrics_job = part_info.jobs.get("metrics", "")
2055
- if metrics_job:
2056
- raw = w.get_parameter(f"{metrics_job}.interior_volume")
2057
- if raw is not None:
2058
- total_volume += float(raw)
2098
+ total_volume = self._total_interior_volume(w)
2059
2099
 
2060
2100
  result.update(
2061
2101
  {
@@ -2279,18 +2319,7 @@ class CompressionSimulation:
2279
2319
  }
2280
2320
 
2281
2321
  # Sum interior volumes across analysis-target parts.
2282
- total_volume = 0.0
2283
- for part_info in self.part_infos:
2284
- if not self._is_analysis_target(part_info.part):
2285
- continue
2286
- if part_info.disabled:
2287
- continue
2288
- metrics_job = part_info.jobs.get("metrics", "")
2289
- if not metrics_job:
2290
- continue
2291
- raw = w.get_parameter(f"{metrics_job}.interior_volume")
2292
- if raw is not None:
2293
- total_volume += float(raw)
2322
+ total_volume = self._total_interior_volume(w)
2294
2323
 
2295
2324
  result.update(
2296
2325
  {
@@ -2445,19 +2474,7 @@ class CompressionSimulation:
2445
2474
  Volume is inlined (from prep workflow parameters); energy values are
2446
2475
  refs that the server resolves from the energy-metrics job output."""
2447
2476
  scalars: dict[str, Any] = {}
2448
-
2449
- total_volume = 0.0
2450
- for part_info in self.part_infos:
2451
- if not self._is_analysis_target(part_info.part):
2452
- continue
2453
- if part_info.disabled:
2454
- continue
2455
- metrics_job = part_info.jobs.get("metrics", "")
2456
- if metrics_job:
2457
- raw = workflow.get_parameter(f"{metrics_job}.interior_volume")
2458
- if raw is not None:
2459
- total_volume += float(raw)
2460
- scalars["volume"] = total_volume
2477
+ scalars["volume"] = self._total_interior_volume(workflow)
2461
2478
 
2462
2479
  if self._contains_step(WorkflowStepType.ENERGY_METRICS):
2463
2480
  em_job_id = job_id_lookup.get("energy-metrics", "")
@@ -2717,7 +2734,7 @@ class CompressionSimulation:
2717
2734
  "heading": "Absorbed Energy (J)",
2718
2735
  "filtering": False,
2719
2736
  }
2720
- )
2737
+ )
2721
2738
  self.manifest["resultsListConfig"].append(
2722
2739
  {
2723
2740
  "key": "loadingEnergy",
@@ -98,12 +98,15 @@ Available material presets
98
98
  "material_pla", "material_stainless_steel",
99
99
  "material_ti64", "material_tpu"
100
100
 
101
- Available workflow steps (default: all)
102
- ----------------------------------------
101
+ Available workflow steps (default: all except stress_strain)
102
+ ------------------------------------------------------------
103
103
  "compute_bvh", "metrics", "compress", "von_mises_stress",
104
- "effective_strain", "force_displacement", "stress_strain",
104
+ "effective_strain", "force_displacement",
105
105
  "particle_displacement", "energy_metrics"
106
106
 
107
+ Opt-in (not run by default; add explicitly to workflow_steps):
108
+ "stress_strain"
109
+
107
110
  Zip format (for run_experiment_from_zip)
108
111
  -----------------------------------------
109
112
  A zip containing:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: metafold
3
- Version: 0.12.dev10
3
+ Version: 0.12.1
4
4
  Summary: Metafold SDK for Python
5
5
  Author-email: Metafold 3D <info@metafold3d.com>
6
6
  License: Copyright 2024 Metafold 3D
@@ -25,7 +25,7 @@ Classifier: Programming Language :: Python :: 3.12
25
25
  Classifier: Programming Language :: Python :: 3.13
26
26
  Classifier: Topic :: Scientific/Engineering
27
27
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
28
- Requires-Python: >=3.12
28
+ Requires-Python: >=3.11
29
29
  Description-Content-Type: text/markdown
30
30
  License-File: LICENSE
31
31
  Requires-Dist: attrs~=23.2
@@ -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.2; extra == "simulation"
41
+ Requires-Dist: simulation-configurator==0.2.*; 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.1.2
12
+ simulation-configurator==0.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.dev10"
7
+ version = "0.12.1"
8
8
  authors = [
9
9
  {name = "Metafold 3D", email = "info@metafold3d.com"},
10
10
  ]
@@ -18,7 +18,7 @@ dependencies = [
18
18
  readme = "README.md"
19
19
  license = {file = "LICENSE"}
20
20
  description = "Metafold SDK for Python"
21
- requires-python = ">=3.12"
21
+ requires-python = ">=3.11"
22
22
  keywords = ["metafold", "api", "sdk", "implicit geometry"]
23
23
  classifiers = [
24
24
  "Development Status :: 3 - Alpha",
@@ -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.2",
41
+ "simulation-configurator==0.2.*",
42
42
  "tables>=3.11.1",
43
43
  ]
44
44
 
@@ -68,7 +68,7 @@ packages = ["metafold", "metafold.simulation"]
68
68
  environments = ["sys_platform == 'linux'"]
69
69
 
70
70
  [tool.uv.sources]
71
- simulation-configurator = { index = "sim-config-idx" }
71
+ simulation-configurator = { index = "gitlab" }
72
72
 
73
73
  [[tool.mypy.overrides]]
74
74
  module = [
@@ -79,7 +79,7 @@ module = [
79
79
  ignore_missing_imports = true
80
80
 
81
81
  [[tool.uv.index]]
82
- name = "sim-config-idx"
82
+ name = "gitlab"
83
83
  url = "https://gitlab.com/api/v4/projects/75460563/packages/pypi/simple"
84
84
  explicit = true
85
85
  authenticate = "always"
@@ -167,13 +167,15 @@ class TestBuildWorkflow:
167
167
  assert isinstance(piston.part, ExperimentPistonBase)
168
168
  assert piston.material_index == 0
169
169
 
170
- def test_build_workflow_has_per_part_metrics_jobs(self, prepared_sim):
171
- yaml_str, _ = prepared_sim.build_workflow()
170
+ def test_prep_workflow_has_per_part_metrics_jobs(self, prepared_sim):
171
+ mesh_parts = [p for p in prepared_sim.part_infos if hasattr(p.part, "filename")]
172
+ yaml_str, _, _ = prepared_sim._build_sample_workflow_for_batch(mesh_parts)
172
173
  loaded = yaml.safe_load(yaml_str)
173
- # one metrics job per non-piston mesh part
174
+ # one metrics job per non-piston mesh part, chained to its sample job
174
175
  for info in prepared_sim.part_infos:
175
176
  if not isinstance(info.part, ExperimentPistonBase) and hasattr(info.part, "filename"):
176
- assert f"metrics-{info.part_unique_name}" in loaded["jobs"]
177
+ job = loaded["jobs"][f"metrics-{info.part_unique_name}"]
178
+ assert job["needs"] == [f"sample-mesh-{info.part_unique_name}"]
177
179
 
178
180
  def test_build_workflow_compress_has_velocity(self, prepared_sim):
179
181
  _, params = prepared_sim.build_workflow()
@@ -565,24 +567,6 @@ jobs:
565
567
  - compress
566
568
  assets:
567
569
  data: compress
568
- stress-strain-upper_foam:
569
- type: sim/postprocess/stress-strain
570
- needs:
571
- - force-displacement
572
- assets:
573
- data: force-displacement
574
- stress-strain-midsole:
575
- type: sim/postprocess/stress-strain
576
- needs:
577
- - force-displacement
578
- assets:
579
- data: force-displacement
580
- stress-strain-outsole:
581
- type: sim/postprocess/stress-strain
582
- needs:
583
- - force-displacement
584
- assets:
585
- data: force-displacement
586
570
  particle-displacement:
587
571
  type: sim/postprocess/particle-displacement
588
572
  needs:
@@ -638,12 +622,6 @@ jobs:
638
622
  "metrics-upper_foam.volume_resolution": "[32, 32, 16]",
639
623
  "metrics-upper_foam.volume_size": "[0.1, 0.1, 0.05]",
640
624
  "particle-displacement.keys": "[\"/material0/position\", \"/material1/position\", \"/material2/position\", \"/material3/position\"]",
641
- "stress-strain-midsole.initial_length": "0.05",
642
- "stress-strain-midsole.keys": "[\"/force_displacement\"]",
643
- "stress-strain-outsole.initial_length": "0.05",
644
- "stress-strain-outsole.keys": "[\"/force_displacement\"]",
645
- "stress-strain-upper_foam.initial_length": "0.05",
646
- "stress-strain-upper_foam.keys": "[\"/force_displacement\"]",
647
625
  "von-mises-stress.keys": "[\"/material0/cauchy_stress\", \"/material1/cauchy_stress\", \"/material2/cauchy_stress\", \"/material3/cauchy_stress\"]"
648
626
  }
649
627
  """)
@@ -653,7 +631,8 @@ jobs:
653
631
 
654
632
 
655
633
  class TestStressStrainPerPart:
656
- """stress-strain runs once per deformable part (not one representative)."""
634
+ """stress-strain runs once per deformable part (not one representative).
635
+ It is opt-in: callers must add the step to workflow_steps."""
657
636
 
658
637
  def _built_sim(self, ply_folder, basic_parts, tmp_path):
659
638
  sim = CompressionSimulation(
@@ -662,6 +641,12 @@ class TestStressStrainPerPart:
662
641
  stl_folder_path=str(ply_folder),
663
642
  output_path=str(tmp_path / "out"),
664
643
  client=MagicMock(),
644
+ # stress_strain is not default; opt in (needs compress + force-displacement).
645
+ workflow_steps=[
646
+ WorkflowStep(WorkflowStepType.COMPRESS),
647
+ WorkflowStep(WorkflowStepType.FORCE_DISPLACEMENT),
648
+ WorkflowStep(WorkflowStepType.STRESS_STRAIN),
649
+ ],
665
650
  )
666
651
  # Distinct X/Y so compression_area = X*Y is unambiguous.
667
652
  fake_patch = {"size": [0.1, 0.2, 0.05], "offset": [0.0, 0.0, 0.0], "resolution": [32, 32, 16]}
@@ -673,6 +658,16 @@ class TestStressStrainPerPart:
673
658
  sim.build_workflow()
674
659
  return sim
675
660
 
661
+ def test_not_run_by_default(self, ply_folder, basic_parts, tmp_path):
662
+ sim = CompressionSimulation(
663
+ parts=basic_parts,
664
+ simulation_name="t",
665
+ stl_folder_path=str(ply_folder),
666
+ output_path=str(tmp_path / "out"),
667
+ client=MagicMock(),
668
+ )
669
+ assert not sim._contains_step(WorkflowStepType.STRESS_STRAIN)
670
+
676
671
  def test_one_job_per_deformable_part(self, ply_folder, basic_parts, tmp_path):
677
672
  sim = self._built_sim(ply_folder, basic_parts, tmp_path)
678
673
  for name in ("upper_foam", "midsole", "outsole"):
@@ -721,6 +716,18 @@ class TestMakeManifestV2:
721
716
  stl_folder_path=str(ply_folder),
722
717
  output_path=str(tmp_path / "out"),
723
718
  client=MagicMock(),
719
+ # Every step, including opt-in stress_strain, so all cards render.
720
+ workflow_steps=[
721
+ WorkflowStep(WorkflowStepType.COMPUTE_BVH),
722
+ WorkflowStep(WorkflowStepType.METRICS),
723
+ WorkflowStep(WorkflowStepType.COMPRESS),
724
+ WorkflowStep(WorkflowStepType.VON_MISES_STRESS),
725
+ WorkflowStep(WorkflowStepType.EFFECTIVE_STRAIN),
726
+ WorkflowStep(WorkflowStepType.FORCE_DISPLACEMENT),
727
+ WorkflowStep(WorkflowStepType.STRESS_STRAIN),
728
+ WorkflowStep(WorkflowStepType.PARTICLE_DISPLACEMENT),
729
+ WorkflowStep(WorkflowStepType.ENERGY_METRICS),
730
+ ],
724
731
  )
725
732
  # In the real flow create_sim_config() assigns material indices before
726
733
  # make_manifest_v2() runs; mirror that so per-part cards key correctly.
@@ -1125,6 +1132,7 @@ class TestSetupClient:
1125
1132
  simulation_name="t",
1126
1133
  stl_folder_path=str(ply_folder),
1127
1134
  output_path=str(tmp_path / "out"),
1135
+ env_source=None, # Ensure local .env files do not pollute test environment
1128
1136
  **kwargs,
1129
1137
  )
1130
1138
 
File without changes
File without changes
File without changes
File without changes