metafold 0.12.dev8__tar.gz → 0.12.dev9__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.dev8 → metafold-0.12.dev9}/PKG-INFO +1 -1
  2. {metafold-0.12.dev8 → metafold-0.12.dev9}/metafold/simulation/compression_experiment.py +4 -0
  3. {metafold-0.12.dev8 → metafold-0.12.dev9}/metafold/simulation/compression_simulation.py +194 -100
  4. {metafold-0.12.dev8 → metafold-0.12.dev9}/metafold/simulation/run_experiment.py +4 -6
  5. {metafold-0.12.dev8 → metafold-0.12.dev9}/metafold.egg-info/PKG-INFO +1 -1
  6. {metafold-0.12.dev8 → metafold-0.12.dev9}/pyproject.toml +1 -1
  7. {metafold-0.12.dev8 → metafold-0.12.dev9}/tests/test_compression_simulation.py +178 -66
  8. {metafold-0.12.dev8 → metafold-0.12.dev9}/tests/test_run_experiment.py +2 -11
  9. {metafold-0.12.dev8 → metafold-0.12.dev9}/tests/test_shear_simulation.py +2 -2
  10. {metafold-0.12.dev8 → metafold-0.12.dev9}/LICENSE +0 -0
  11. {metafold-0.12.dev8 → metafold-0.12.dev9}/README.md +0 -0
  12. {metafold-0.12.dev8 → metafold-0.12.dev9}/metafold/__init__.py +0 -0
  13. {metafold-0.12.dev8 → metafold-0.12.dev9}/metafold/api.py +0 -0
  14. {metafold-0.12.dev8 → metafold-0.12.dev9}/metafold/assets.py +0 -0
  15. {metafold-0.12.dev8 → metafold-0.12.dev9}/metafold/auth.py +0 -0
  16. {metafold-0.12.dev8 → metafold-0.12.dev9}/metafold/client.py +0 -0
  17. {metafold-0.12.dev8 → metafold-0.12.dev9}/metafold/exceptions.py +0 -0
  18. {metafold-0.12.dev8 → metafold-0.12.dev9}/metafold/jobs.py +0 -0
  19. {metafold-0.12.dev8 → metafold-0.12.dev9}/metafold/materials.py +0 -0
  20. {metafold-0.12.dev8 → metafold-0.12.dev9}/metafold/projects.py +0 -0
  21. {metafold-0.12.dev8 → metafold-0.12.dev9}/metafold/simulation/__init__.py +0 -0
  22. {metafold-0.12.dev8 → metafold-0.12.dev9}/metafold/utils.py +0 -0
  23. {metafold-0.12.dev8 → metafold-0.12.dev9}/metafold/workflows.py +0 -0
  24. {metafold-0.12.dev8 → metafold-0.12.dev9}/metafold.egg-info/SOURCES.txt +0 -0
  25. {metafold-0.12.dev8 → metafold-0.12.dev9}/metafold.egg-info/dependency_links.txt +0 -0
  26. {metafold-0.12.dev8 → metafold-0.12.dev9}/metafold.egg-info/requires.txt +0 -0
  27. {metafold-0.12.dev8 → metafold-0.12.dev9}/metafold.egg-info/top_level.txt +0 -0
  28. {metafold-0.12.dev8 → metafold-0.12.dev9}/setup.cfg +0 -0
  29. {metafold-0.12.dev8 → metafold-0.12.dev9}/tests/test_assets.py +0 -0
  30. {metafold-0.12.dev8 → metafold-0.12.dev9}/tests/test_compession_experiment.py +0 -0
  31. {metafold-0.12.dev8 → metafold-0.12.dev9}/tests/test_jobs.py +0 -0
  32. {metafold-0.12.dev8 → metafold-0.12.dev9}/tests/test_materials.py +0 -0
  33. {metafold-0.12.dev8 → metafold-0.12.dev9}/tests/test_projects.py +0 -0
  34. {metafold-0.12.dev8 → metafold-0.12.dev9}/tests/test_utils.py +0 -0
  35. {metafold-0.12.dev8 → metafold-0.12.dev9}/tests/test_workflows.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: metafold
3
- Version: 0.12.dev8
3
+ Version: 0.12.dev9
4
4
  Summary: Metafold SDK for Python
5
5
  Author-email: Metafold 3D <info@metafold3d.com>
6
6
  License: Copyright 2024 Metafold 3D
@@ -246,6 +246,10 @@ class CompressionExperiment:
246
246
  self.base_simulation.populate_assets(self.experiment_part_infos)
247
247
  self._log("Sampling assets...")
248
248
  self.base_simulation.sample_assets(self.experiment_part_infos)
249
+ # Clones were deep-copied before sampling, so copy over the spacing that
250
+ # was anchored during sampling — each clone's grid must match it.
251
+ for local_sim in self.sims:
252
+ local_sim.sample_spacing = self.base_simulation.sample_spacing
249
253
  self._log("Collecting sampled volumes...")
250
254
  self.base_simulation.collect_sampled_volumes(self.experiment_part_infos)
251
255
  self._log("Prepare complete.")
@@ -6,9 +6,9 @@ from typing import Any, Optional, Union
6
6
  from io import BytesIO
7
7
  import uuid
8
8
 
9
- from dotenv import load_dotenv
10
9
  from requests import HTTPError
11
10
  import yaml
11
+ from dotenv import load_dotenv
12
12
  from metafold import MetafoldClient
13
13
  from pathlib import Path
14
14
  from numpy.typing import DTypeLike
@@ -171,7 +171,6 @@ class ExperimentPart:
171
171
  @dataclass
172
172
  class ExperimentMesh(ExperimentPart):
173
173
  filename: str = ""
174
- representative_part: bool = False
175
174
 
176
175
 
177
176
  @dataclass
@@ -389,7 +388,6 @@ class CompressionSimulation:
389
388
  stl_folder: Optional[Path] = None
390
389
  out_dir: Optional[Path] = None
391
390
  results: list = []
392
- representative_part: str = ""
393
391
  simulation_name: str = ""
394
392
  simulation_parameters: SimulationParameters
395
393
  piston_velocity: list[list[float]]
@@ -416,9 +414,9 @@ class CompressionSimulation:
416
414
  prep_workflows: list[Workflow] = []
417
415
  prep_workflow_batch_size: int = 10
418
416
  write_ups: bool = True
419
- # Sample spacing (mm) anchored to the representative part's longest axis:
420
- # longest_axis / (max_resolution - 1). Cached so experiment variants
421
- # sampled later match the base simulation's density.
417
+ # Sample spacing (mm) anchored to the union ("total box") of every part's
418
+ # bounds: longest_axis(total_box) / (max_resolution - 1). Cached so
419
+ # experiment variants sampled later match the base simulation's density.
422
420
  sample_spacing: Optional[float] = None
423
421
 
424
422
  def __init__(
@@ -478,7 +476,6 @@ class CompressionSimulation:
478
476
  else:
479
477
  self.part_infos.append(inner_part)
480
478
 
481
- self.validate_parts()
482
479
  self.setup_ply_files(stl_folder_path)
483
480
  self.setup_results(output_path)
484
481
  if client is None:
@@ -538,16 +535,19 @@ class CompressionSimulation:
538
535
  def download_results(self):
539
536
  self.write_results()
540
537
 
541
- def validate_parts(self):
542
- for p in self.part_infos:
543
- if hasattr(p.part, "representative_part") and p.part.representative_part:
544
- if self.representative_part:
545
- raise ValueError(
546
- f"At most one part can have representative_part set to True"
547
- )
548
- self.representative_part = p.part.name
549
- if not self.representative_part:
550
- raise ValueError(f"One part must have representative_part set to True")
538
+ def _get_primary_deformable_info(self):
539
+ """The specimen part for stress/strain and the featured histogram: the
540
+ first part that isn't a rigid piston/support (pistons are inserted at
541
+ index 0, so this is the first deformable body in the stack)."""
542
+ return next(
543
+ (
544
+ p for p in self.part_infos
545
+ if not isinstance(
546
+ p.part, (ExperimentPistonBase, ExperimentSupportBase)
547
+ )
548
+ ),
549
+ None,
550
+ )
551
551
 
552
552
  def setup_results(self, output_path):
553
553
  self.out_dir = Path(output_path)
@@ -735,7 +735,12 @@ class CompressionSimulation:
735
735
 
736
736
  for info in part_infos:
737
737
  if info.file_path and info.asset is None:
738
- existing = self.client.assets.list(q=f"filename:{info.part.filename}")
738
+ # Quote the filename so the search parser keeps it as one term,
739
+ # and match it exactly (the search is a case-insensitive ILIKE).
740
+ existing = [
741
+ a for a in self.client.assets.list(q=f'filename:"{info.part.filename}"')
742
+ if a.filename == info.part.filename
743
+ ]
739
744
  asset = None
740
745
  if existing:
741
746
  asset = existing[0]
@@ -906,38 +911,65 @@ class CompressionSimulation:
906
911
  mx = np.array(bounds["max"], dtype=np.float64)
907
912
  return float(np.max(mx - mn))
908
913
 
909
- def _assign_sample_resolutions(self, part_infos: list):
910
- """Assign each part a sampling resolution so all parts share the
911
- representative part's sample spacing. Parts smaller than the
912
- representative get proportionally lower resolutions (and larger ones
913
- higher), keeping particle density uniform across the simulation."""
914
- max_resolution = max(2, int(self.simulation_parameters.max_resolution))
915
-
916
- if self.sample_spacing is None:
917
- # Prefer a representative-named info from the batch being sampled:
918
- # experiments fork varied parts, so the base sim's own info may
919
- # not carry the pass-1 bounds.
920
- rep_info = next(
921
- (
922
- i for i in part_infos
923
- if i.part.name == self.representative_part and i.bounds
924
- ),
925
- None,
926
- ) or self.get_part_info(self.representative_part)
927
- if rep_info is not None and rep_info.bounds:
928
- self.sample_spacing = self._bounds_longest_axis(rep_info.bounds) / (
929
- max_resolution - 1
930
- )
914
+ @staticmethod
915
+ def _part_bounds_mm(info):
916
+ """(min, max) axis-aligned bounds of a part in mm, or None. Mesh parts
917
+ report bounds via the pass-1 preprocess job (mm); primitives report
918
+ them analytically via get_bounds() (metres)."""
919
+ part = info.part
920
+ if isinstance(part, ExperimentPrimitive):
921
+ pmin, pmax = part.get_bounds()
922
+ return (
923
+ np.array(pmin, dtype=np.float64) * 1000.0,
924
+ np.array(pmax, dtype=np.float64) * 1000.0,
925
+ )
926
+ if info.bounds:
927
+ return (
928
+ np.array(info.bounds["min"], dtype=np.float64),
929
+ np.array(info.bounds["max"], dtype=np.float64),
930
+ )
931
+ return None
931
932
 
933
+ def _union_bounds_mm(self, part_infos: list):
934
+ """Union ("total box") of every part's bounds in mm, or None."""
935
+ mins, maxs = [], []
932
936
  for info in part_infos:
937
+ b = self._part_bounds_mm(info)
938
+ if b is not None:
939
+ mins.append(b[0])
940
+ maxs.append(b[1])
941
+ if not mins:
942
+ return None
943
+ return np.min(mins, axis=0), np.max(maxs, axis=0)
944
+
945
+ def _ensure_sample_spacing(self, part_infos: list):
946
+ """Set sample_spacing (mm) from the total box of all parts, if unset.
947
+ Anchoring to the union — not one 'representative' part — makes
948
+ max_resolution the resolution of the whole simulation's longest axis."""
949
+ if self.sample_spacing is not None:
950
+ return
951
+ max_resolution = max(2, int(self.simulation_parameters.max_resolution))
952
+ union = self._union_bounds_mm(part_infos)
953
+ if union is not None:
954
+ longest = float(np.max(union[1] - union[0]))
955
+ if longest > 0:
956
+ self.sample_spacing = longest / (max_resolution - 1)
957
+
958
+ def _assign_sample_resolutions(self, all_infos: list, mesh_infos: list):
959
+ """Anchor sample spacing to the total box (union of all part bounds),
960
+ then give each mesh a resolution scaled to its own size so every part
961
+ samples at the same spatial density."""
962
+ max_resolution = max(2, int(self.simulation_parameters.max_resolution))
963
+ self._ensure_sample_spacing(all_infos)
964
+
965
+ for info in mesh_infos:
933
966
  if self.sample_spacing and info.bounds:
934
967
  longest = self._bounds_longest_axis(info.bounds)
935
968
  info.sample_resolution = max(
936
969
  2, int(round(longest / self.sample_spacing)) + 1
937
970
  )
938
971
  else:
939
- # Legacy fallback: this part samples at max_resolution over
940
- # its own bounds (density then depends on the part's size).
972
+ # Fallback: sample at max_resolution over the part's own bounds.
941
973
  info.sample_resolution = max_resolution
942
974
 
943
975
  def sample_assets(self, part_infos=None):
@@ -958,10 +990,9 @@ class CompressionSimulation:
958
990
  )
959
991
  self._collect_preprocess_outputs(mesh_parts, preprocess_workflows)
960
992
 
961
- # Anchor sample spacing to the representative part so every part is
962
- # sampled at the same spatial density (the grid is sized from the
963
- # representative patch downstream).
964
- self._assign_sample_resolutions(mesh_parts)
993
+ # Anchor sample spacing to the total box (union of all parts, primitives
994
+ # included) so every part samples at the same spatial density.
995
+ self._assign_sample_resolutions(part_infos, mesh_parts)
965
996
 
966
997
  # Pass 2: sample each mesh at its density-matched resolution, reusing
967
998
  # the preprocessed/BVH assets from pass 1.
@@ -1016,11 +1047,11 @@ class CompressionSimulation:
1016
1047
 
1017
1048
  def create_sim_config(self, name_suffix=""):
1018
1049
  """
1019
- grid_patch: the midsole patch, used as representative for grid sizing.
1050
+ The grid spans the total box (union of every part's bounds) and is
1051
+ sized by the shared sample spacing — no representative part.
1020
1052
  Piston velocity is [0,0,0] in the UPS — actual motion driven by velocity.txt.
1021
1053
  """
1022
1054
  name = self.simulation_name
1023
- grid_patch = self.get_part_info(self.representative_part).patch
1024
1055
 
1025
1056
  sim = Simulation(
1026
1057
  name,
@@ -1031,13 +1062,11 @@ class CompressionSimulation:
1031
1062
  timestep_multiplier=self.simulation_parameters.timestep_multiplier,
1032
1063
  )
1033
1064
 
1034
- grid_min = np.array(grid_patch["offset"], dtype=np.float32) * 1e-3
1035
- grid_max = np.array(grid_patch["size"], dtype=np.float32) * 1e-3 + grid_min
1036
-
1037
- # Expand grid to contain every part's bounding box, so nothing is
1038
- # clipped (e.g. a support/outsole extending below the representative
1039
- # part). Primitives report bounds via get_bounds() in metres, mesh
1040
- # parts via their sampled patch (offset/size in mm)
1065
+ # Total box: union of every part's bounding box so nothing is clipped.
1066
+ # Primitives report bounds via get_bounds() in metres, mesh parts via
1067
+ # their sampled patch (offset/size in mm).
1068
+ grid_min = np.array([np.inf, np.inf, np.inf], dtype=np.float32)
1069
+ grid_max = np.array([-np.inf, -np.inf, -np.inf], dtype=np.float32)
1041
1070
  for info in self.part_infos:
1042
1071
  part = info.part
1043
1072
  if isinstance(part, ExperimentPrimitive):
@@ -1057,11 +1086,10 @@ class CompressionSimulation:
1057
1086
  grid_max[2] += self.simulation_parameters.margin_z
1058
1087
 
1059
1088
  grid_size = grid_max - grid_min
1060
- spacing = (
1061
- np.array(grid_patch["size"], dtype=np.float32)
1062
- / (np.array(grid_patch["resolution"]) - 1)
1063
- * 1e-3
1064
- )
1089
+ # Every part is sampled at this uniform (isotropic) spacing, so the grid
1090
+ # cells match particle spacing. mm -> m.
1091
+ self._ensure_sample_spacing(self.part_infos)
1092
+ spacing = (self.sample_spacing or 1.0) * 1e-3
1065
1093
  grid_resolution = np.ceil(
1066
1094
  grid_size / (self.simulation_parameters.points_per_cell * spacing)
1067
1095
  ).astype(np.int32)
@@ -1371,20 +1399,36 @@ class CompressionSimulation:
1371
1399
  )
1372
1400
 
1373
1401
  def _add_to_workflow_stress_strain(self, part_infos: list[PartInfo]):
1374
- job_name_base = "stress-strain"
1375
- self._add_to_workflow_postprocess(
1376
- part_infos, "force-displacement", job_name_base
1377
- )
1378
- self.workflow_params[f"{job_name_base}.keys"] = json.dumps(
1379
- ["/force_displacement"]
1380
- )
1381
- representative_patch = self.get_part_info(self.representative_part).patch
1382
- self.workflow_params[f"{job_name_base}.compression_area"] = str(
1383
- representative_patch["size"][0] * representative_patch["size"][1]
1384
- )
1385
- self.workflow_params[f"{job_name_base}.initial_length"] = str(
1386
- representative_patch["size"][2]
1402
+ # One stress-strain job per deformable part, each normalizing the shared
1403
+ # (global) force-displacement curve by that part's own cross-section
1404
+ # (X*Y) and height (Z). Named stress-strain-<part> like metrics-<part>.
1405
+ fd_job = next(
1406
+ (p.jobs["force-displacement"] for p in part_infos
1407
+ if "force-displacement" in p.jobs),
1408
+ None,
1387
1409
  )
1410
+ if fd_job is None:
1411
+ return
1412
+ for part_info in part_infos:
1413
+ if not self._is_analysis_target(part_info.part):
1414
+ continue
1415
+ if part_info.disabled or not part_info.patch:
1416
+ continue
1417
+ job_name = f"stress-strain-{part_info.part_unique_name}"
1418
+ self.workflow_jobs[job_name] = {
1419
+ "type": "sim/postprocess/stress-strain",
1420
+ "needs": [fd_job],
1421
+ "assets": {"data": fd_job},
1422
+ }
1423
+ self.workflow_params[f"{job_name}.keys"] = json.dumps(
1424
+ ["/force_displacement"]
1425
+ )
1426
+ size = part_info.patch["size"]
1427
+ self.workflow_params[f"{job_name}.compression_area"] = str(
1428
+ size[0] * size[1]
1429
+ )
1430
+ self.workflow_params[f"{job_name}.initial_length"] = str(size[2])
1431
+ part_info.jobs["stress-strain"] = job_name
1388
1432
 
1389
1433
  def _add_to_workflow_particle_displacement(self, part_infos: list[PartInfo]):
1390
1434
  self._add_to_workflow_postprocess(
@@ -1973,16 +2017,25 @@ class CompressionSimulation:
1973
2017
  loading.to_csv(f)
1974
2018
  data["loadingForceDisplacement"] = filename
1975
2019
 
1976
- # Stress-strain
2020
+ # Stress-strain: one curve per deformable part.
1977
2021
  if self._contains_step(WorkflowStepType.STRESS_STRAIN):
1978
- ss_asset = w.get_asset("stress-strain.output")
1979
- assert ss_asset
1980
- self.client.assets.download_file(ss_asset.id, hdf_filename)
1981
- with pd.HDFStore(hdf_filename) as store:
1982
- filename = f"{name}/stressStrain.csv"
1983
- with zf.open(filename, "w") as f:
1984
- store["/stress_strain"].to_csv(f)
1985
- data["stressStrain"] = filename
2022
+ for part_info in self.part_infos:
2023
+ if not self._is_analysis_target(part_info.part):
2024
+ continue
2025
+ if part_info.disabled:
2026
+ continue
2027
+ i = part_info.material_index
2028
+ ss_asset = w.get_asset(
2029
+ f"stress-strain-{part_info.part_unique_name}.output"
2030
+ )
2031
+ if ss_asset is None:
2032
+ continue
2033
+ self.client.assets.download_file(ss_asset.id, hdf_filename)
2034
+ with pd.HDFStore(hdf_filename) as store:
2035
+ filename = f"{name}/stressStrain{i}.csv"
2036
+ with zf.open(filename, "w") as f:
2037
+ store["/stress_strain"].to_csv(f)
2038
+ data[f"stressStrain{i}"] = filename
1986
2039
 
1987
2040
  # ----------------------------------------------------------
1988
2041
  # Write PLY files from the combined all-material dataframe.
@@ -2218,18 +2271,26 @@ class CompressionSimulation:
2218
2271
  if raw_ue is not None:
2219
2272
  unloading_energy = float(raw_ue)
2220
2273
 
2221
- # Stress-strain: ship whole.
2274
+ # Stress-strain: ship one whole file per deformable part.
2222
2275
  if self._contains_step(WorkflowStepType.STRESS_STRAIN):
2223
- ss_asset = w.get_asset("stress-strain.output")
2224
- if ss_asset is not None:
2225
- ss_hdf = tempdir_path / "stress_strain.h5"
2226
- self.client.assets.download_file(ss_asset.id, ss_hdf)
2227
- ss_zip_path = f"{name}/stress_strain.h5"
2228
- zf.write(ss_hdf, arcname=ss_zip_path)
2229
- data["stressStrain"] = {
2230
- "name": ss_zip_path,
2231
- "path": "/stress_strain",
2232
- }
2276
+ for part_info in self.part_infos:
2277
+ if not self._is_analysis_target(part_info.part):
2278
+ continue
2279
+ if part_info.disabled:
2280
+ continue
2281
+ i = part_info.material_index
2282
+ ss_asset = w.get_asset(
2283
+ f"stress-strain-{part_info.part_unique_name}.output"
2284
+ )
2285
+ if ss_asset is not None:
2286
+ ss_hdf = tempdir_path / f"stress_strain_{i}.h5"
2287
+ self.client.assets.download_file(ss_asset.id, ss_hdf)
2288
+ ss_zip_path = f"{name}/stress_strain_{i}.h5"
2289
+ zf.write(ss_hdf, arcname=ss_zip_path)
2290
+ data[f"stressStrain{i}"] = {
2291
+ "name": ss_zip_path,
2292
+ "path": "/stress_strain",
2293
+ }
2233
2294
 
2234
2295
  # Sum interior volumes across analysis-target parts.
2235
2296
  total_volume = 0.0
@@ -2375,11 +2436,19 @@ class CompressionSimulation:
2375
2436
  }
2376
2437
 
2377
2438
  if self._contains_step(WorkflowStepType.STRESS_STRAIN):
2378
- server_data["stressStrain"] = {
2379
- "jobId": job_id_lookup.get("stress-strain", ""),
2380
- "assetName": "output",
2381
- "path": "/stress_strain",
2382
- }
2439
+ for part_info in self.part_infos:
2440
+ if not self._is_analysis_target(part_info.part):
2441
+ continue
2442
+ if part_info.disabled:
2443
+ continue
2444
+ i = part_info.material_index
2445
+ server_data[f"stressStrain{i}"] = {
2446
+ "jobId": job_id_lookup.get(
2447
+ f"stress-strain-{part_info.part_unique_name}", ""
2448
+ ),
2449
+ "assetName": "output",
2450
+ "path": "/stress_strain",
2451
+ }
2383
2452
 
2384
2453
  return server_data
2385
2454
 
@@ -2605,8 +2674,9 @@ class CompressionSimulation:
2605
2674
  ]
2606
2675
 
2607
2676
  self.manifest["cardsConfig"]["B"] = []
2608
- if self._contains_step(WorkflowStepType.VON_MISES_STRESS):
2609
- rep_idx = self.get_part_info(self.representative_part).material_index
2677
+ specimen = self._get_primary_deformable_info()
2678
+ if self._contains_step(WorkflowStepType.VON_MISES_STRESS) and specimen is not None:
2679
+ rep_idx = specimen.material_index
2610
2680
  self.manifest["cardsConfig"]["B"].append(
2611
2681
  {
2612
2682
  "id": "vonMisesStress",
@@ -2620,6 +2690,30 @@ class CompressionSimulation:
2620
2690
  }
2621
2691
  )
2622
2692
 
2693
+ # One stress-strain card per deformable part (keyed stressStrain{i}).
2694
+ if self._contains_step(WorkflowStepType.STRESS_STRAIN):
2695
+ for part_info in self.part_infos:
2696
+ if not self._is_analysis_target(part_info.part):
2697
+ continue
2698
+ if part_info.disabled:
2699
+ continue
2700
+ i = part_info.material_index
2701
+ self.manifest["cardsConfig"]["B"].append(
2702
+ {
2703
+ "id": f"stressStrain{i}",
2704
+ "title": f"{part_info.part_unique_name} Stress-Strain",
2705
+ "component": "MultiExperimentLineChart",
2706
+ "props": {
2707
+ "xColumn": "strain",
2708
+ "yColumn": "stress",
2709
+ "yMin": 0.0,
2710
+ "xAxisLabel": "Strain",
2711
+ "yAxisLabel": "Stress (MPa)",
2712
+ "dataSource": f"stressStrain{i}",
2713
+ },
2714
+ }
2715
+ )
2716
+
2623
2717
  if self._results_have_energy_metrics(results):
2624
2718
  self.manifest["resultsListConfig"].append(
2625
2719
  {"key": "volume", "heading": "Volume (mm³)", "filtering": False},
@@ -36,8 +36,7 @@ JSON manifest format
36
36
  "type": "mesh",
37
37
  "name": "midsole",
38
38
  "material": "default_midsole_nominal",
39
- "file": "mid.ply",
40
- "representative": true
39
+ "file": "mid.ply"
41
40
  }
42
41
  ],
43
42
 
@@ -61,9 +60,9 @@ JSON manifest format
61
60
 
62
61
  "simulation": {
63
62
  "max_time": 0.04,
64
- # Sampling resolution along the representative part's longest axis.
65
- # Other parts sample at resolutions scaled to their size so every
66
- # part shares the same spatial density.
63
+ # Sampling resolution along the longest axis of the total box (the
64
+ # union of every part's bounds). Each part samples at a resolution
65
+ # scaled to its own size so every part shares the same spatial density.
67
66
  "max_resolution": 512,
68
67
 
69
68
  # Force source for force-displacement: "boundary_force" (default) or
@@ -217,7 +216,6 @@ def _build_parts(parts_config: list[dict]) -> list[ExperimentPart]:
217
216
  name=entry["name"],
218
217
  material=_resolve_material(entry["material"]),
219
218
  filename=entry["file"],
220
- representative_part=entry.get("representative", False),
221
219
  )
222
220
  )
223
221
  return parts
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: metafold
3
- Version: 0.12.dev8
3
+ Version: 0.12.dev9
4
4
  Summary: Metafold SDK for Python
5
5
  Author-email: Metafold 3D <info@metafold3d.com>
6
6
  License: Copyright 2024 Metafold 3D
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "metafold"
7
- version = "0.12.dev8"
7
+ version = "0.12.dev9"
8
8
  authors = [
9
9
  {name = "Metafold 3D", email = "info@metafold3d.com"},
10
10
  ]
@@ -14,6 +14,7 @@ from metafold.simulation.compression_simulation import (
14
14
  ExperimentMesh,
15
15
  ExperimentPistonBase,
16
16
  ExperimentPistonCylinder,
17
+ ExperimentPistonMesh,
17
18
  SimulationParameters,
18
19
  WorkflowStep,
19
20
  WorkflowStepType,
@@ -38,7 +39,7 @@ def basic_parts():
38
39
  return [
39
40
  ExperimentPistonCylinder(),
40
41
  ExperimentMesh("upper_foam", DEFAULT_UPPER_FOAM, "top.ply"),
41
- ExperimentMesh("midsole", DEFAULT_MIDSOLE_NOMINAL, "mid.ply", representative_part=True),
42
+ ExperimentMesh("midsole", DEFAULT_MIDSOLE_NOMINAL, "mid.ply"),
42
43
  ExperimentMesh("outsole", DEFAULT_OUTSOLE, "out.ply"),
43
44
  ]
44
45
 
@@ -83,7 +84,7 @@ class TestConstruction:
83
84
 
84
85
  def test_missing_ply_raises(self, ply_folder, tmp_path):
85
86
  parts = [
86
- ExperimentMesh("midsole", DEFAULT_MIDSOLE_NOMINAL, "nonexistent.ply", representative_part=True),
87
+ ExperimentMesh("midsole", DEFAULT_MIDSOLE_NOMINAL, "nonexistent.ply"),
87
88
  ]
88
89
  with pytest.raises(ValueError, match="not found"):
89
90
  CompressionSimulation(
@@ -94,36 +95,6 @@ class TestConstruction:
94
95
  client=MagicMock(),
95
96
  )
96
97
 
97
- class TestValidation:
98
- def test_requires_representative_part(self, ply_folder, tmp_path):
99
- parts = [
100
- ExperimentPistonCylinder(),
101
- ExperimentMesh("midsole", DEFAULT_MIDSOLE_NOMINAL, "mid.ply"), # no rep
102
- ]
103
- with pytest.raises(ValueError, match="representative_part"):
104
- CompressionSimulation(
105
- parts=parts,
106
- simulation_name="t",
107
- stl_folder_path=str(ply_folder),
108
- output_path=str(tmp_path / "out"),
109
- client=MagicMock(),
110
- )
111
-
112
- def test_only_one_representative_part_allowed(self, ply_folder, tmp_path):
113
- parts = [
114
- ExperimentMesh("midsole", DEFAULT_MIDSOLE_NOMINAL, "mid.ply", representative_part=True),
115
- ExperimentMesh("outsole", DEFAULT_OUTSOLE, "out.ply", representative_part=True),
116
- ]
117
- with pytest.raises(ValueError, match="At most one"):
118
- CompressionSimulation(
119
- parts=parts,
120
- simulation_name="t",
121
- stl_folder_path=str(ply_folder),
122
- output_path=str(tmp_path / "out"),
123
- client=MagicMock(),
124
- )
125
-
126
-
127
98
  class TestWorkflowStepsInput:
128
99
  def test_accepts_workflow_step_instance(self):
129
100
  steps = CompressionSimulation._clean_workflow_steps_input([
@@ -248,8 +219,8 @@ class TestBuildWorkflow:
248
219
 
249
220
 
250
221
  class TestGridBounds:
251
- """The grid must enclose every part, not just the representative one, so
252
- nothing (e.g. an outsole below the midsole) gets clipped."""
222
+ """The grid (total box) must enclose every part, so nothing (e.g. an
223
+ outsole below the midsole) gets clipped."""
253
224
 
254
225
  def _grid_bounds(self, sim):
255
226
  """Return (lower, upper) of the generated UPS grid box, in metres."""
@@ -267,9 +238,9 @@ class TestGridBounds:
267
238
  sim.create_sim_config()
268
239
 
269
240
  def test_low_mesh_part_expands_grid_downward(self, sim):
270
- # The regression: a mesh part (outsole) below the representative midsole
271
- # must still expand the grid down. Before the fix, mesh parts were
272
- # skipped and the outsole's bottom got clipped.
241
+ # The regression: a mesh part (outsole) below the midsole must still
242
+ # expand the grid down. Before the fix, mesh parts were skipped and the
243
+ # outsole's bottom got clipped.
273
244
  # Patch offsets/sizes are in mm; outsole bottom at -50 mm is below all
274
245
  # other parts (incl. the piston cylinder, whose lowest point is +25 mm).
275
246
  rep = {"size": [100.0, 100.0, 50.0], "offset": [0.0, 0.0, 0.0], "resolution": [32, 32, 16]}
@@ -415,8 +386,9 @@ class TestSampleAssets:
415
386
 
416
387
 
417
388
  class TestDensityMatchedSampling:
418
- """Sampling resolutions are scaled per part so every mesh shares the
419
- representative part's sample spacing (the piston-density regression)."""
389
+ """Sampling resolutions scale per part so every mesh shares one sample
390
+ spacing anchored to the total box (union of all parts) — the piston-density
391
+ regression, now decoupled from any 'representative' part."""
420
392
 
421
393
  def _make_job(self, name, longest_axis=None, asset_key=None, filename=None):
422
394
  params = {}
@@ -430,19 +402,32 @@ class TestDensityMatchedSampling:
430
402
  )
431
403
 
432
404
  @pytest.fixture
433
- def sampled_sim(self, ply_folder, basic_parts, tmp_path):
434
- # max_resolution=31 and rep (midsole) longest axis 300 → spacing 10
405
+ def sampled_sim(self, tmp_path):
406
+ folder = tmp_path / "ply"
407
+ folder.mkdir()
408
+ for n in ["pist.ply", "top.ply", "mid.ply", "out.ply"]:
409
+ (folder / n).write_bytes(b"ply\n")
410
+
411
+ # Total box longest axis = 600 (outsole); max_resolution=31 → spacing 20.
412
+ parts = [
413
+ ExperimentPistonMesh(filename="pist.ply"),
414
+ ExperimentMesh("upper_foam", DEFAULT_UPPER_FOAM, "top.ply"),
415
+ ExperimentMesh("midsole", DEFAULT_MIDSOLE_NOMINAL, "mid.ply"),
416
+ ExperimentMesh("outsole", DEFAULT_OUTSOLE, "out.ply"),
417
+ ]
435
418
  sim = CompressionSimulation(
436
- parts=basic_parts,
419
+ parts=parts,
437
420
  simulation_name="t",
438
- stl_folder_path=str(ply_folder),
421
+ stl_folder_path=str(folder),
439
422
  output_path=str(tmp_path / "out"),
440
423
  client=MagicMock(),
441
424
  simulation_parameters=SimulationParameters(max_resolution=31),
442
425
  )
443
426
 
444
427
  jobs = {
445
- "p-up": self._make_job("preprocess-mesh-upper_foam", 150.0, "mesh", "pre_up.ply"),
428
+ "p-pi": self._make_job("preprocess-mesh-piston", 100.0, "mesh", "pre_pi.ply"),
429
+ "b-pi": self._make_job("compute-bvh-piston", None, "bvh", "bvh_pi.bin"),
430
+ "p-up": self._make_job("preprocess-mesh-upper_foam", 160.0, "mesh", "pre_up.ply"),
446
431
  "b-up": self._make_job("compute-bvh-upper_foam", None, "bvh", "bvh_up.bin"),
447
432
  "p-mid": self._make_job("preprocess-mesh-midsole", 300.0, "mesh", "pre_mid.ply"),
448
433
  "b-mid": self._make_job("compute-bvh-midsole", None, "bvh", "bvh_mid.bin"),
@@ -457,28 +442,29 @@ class TestDensityMatchedSampling:
457
442
  sim.sample_assets()
458
443
  return sim
459
444
 
460
- def test_spacing_anchored_to_representative(self, sampled_sim):
461
- assert sampled_sim.sample_spacing == pytest.approx(10.0)
445
+ def test_spacing_anchored_to_total_box(self, sampled_sim):
446
+ # Union longest axis 600 / (31 - 1) = 20
447
+ assert sampled_sim.sample_spacing == pytest.approx(20.0)
462
448
 
463
- def test_representative_part_gets_max_resolution(self, sampled_sim):
464
- assert sampled_sim.get_part_info("midsole").sample_resolution == 31
449
+ def test_largest_part_gets_max_resolution(self, sampled_sim):
450
+ assert sampled_sim.get_part_info("outsole").sample_resolution == 31
465
451
 
466
- def test_smaller_part_gets_lower_resolution(self, sampled_sim):
467
- # 150 mm at 10 mm spacing → 15 cells → 16 samples
468
- assert sampled_sim.get_part_info("upper_foam").sample_resolution == 16
469
-
470
- def test_larger_part_gets_higher_resolution(self, sampled_sim):
471
- # 600 mm at 10 mm spacing → 60 cells → 61 samples (above max_resolution)
472
- assert sampled_sim.get_part_info("outsole").sample_resolution == 61
452
+ def test_parts_scale_to_shared_spacing(self, sampled_sim):
453
+ assert sampled_sim.get_part_info("piston").sample_resolution == 6 # 100/20
454
+ assert sampled_sim.get_part_info("upper_foam").sample_resolution == 9 # 160/20
455
+ assert sampled_sim.get_part_info("midsole").sample_resolution == 16 # 300/20
473
456
 
474
457
  def test_sampling_pass_binds_pass1_outputs(self, sampled_sim):
475
458
  args, kwargs = sampled_sim.client.workflows.run_async.call_args_list[1]
476
459
  assert kwargs["parameters"] == {
477
- "sample-mesh-upper_foam.resolution": "16",
478
- "sample-mesh-midsole.resolution": "31",
479
- "sample-mesh-outsole.resolution": "61",
460
+ "sample-mesh-piston.resolution": "6",
461
+ "sample-mesh-upper_foam.resolution": "9",
462
+ "sample-mesh-midsole.resolution": "16",
463
+ "sample-mesh-outsole.resolution": "31",
480
464
  }
481
465
  assert kwargs["assets"] == {
466
+ "sample-mesh-piston.mesh": "pre_pi.ply",
467
+ "sample-mesh-piston.bvh": "bvh_pi.bin",
482
468
  "sample-mesh-upper_foam.mesh": "pre_up.ply",
483
469
  "sample-mesh-upper_foam.bvh": "bvh_up.bin",
484
470
  "sample-mesh-midsole.mesh": "pre_mid.ply",
@@ -495,15 +481,38 @@ class TestDensityMatchedSampling:
495
481
  assert "mesh/preprocess" in yaml_arg
496
482
 
497
483
  def test_spacing_cached_for_later_variant_sampling(self, sampled_sim):
498
- # Experiment variants sampled in a later call reuse the cached spacing
499
- # even though the representative part isn't in that batch.
484
+ # A variant sampled in a later call reuses the cached spacing.
500
485
  variant = CompressionSimulation.PartInfo(
501
486
  ExperimentMesh("midsole_v1", None, "mid.ply")
502
487
  )
503
488
  variant.part_unique_name = "midsole_v1"
504
489
  variant.bounds = {"min": [0.0, 0.0, 0.0], "max": [300.0, 50.0, 20.0]}
505
- sampled_sim._assign_sample_resolutions([variant])
506
- assert variant.sample_resolution == 31
490
+ sampled_sim._assign_sample_resolutions([variant], [variant])
491
+ assert variant.sample_resolution == 16 # 300 / 20 spacing
492
+
493
+ def test_union_bounds_includes_primitive(self, tmp_path):
494
+ # A primitive's analytic bounds (metres) contribute to the total box.
495
+ folder = tmp_path / "ply"
496
+ folder.mkdir()
497
+ (folder / "mid.ply").write_bytes(b"ply\n")
498
+ sim = CompressionSimulation(
499
+ parts=[
500
+ ExperimentPistonCylinder(),
501
+ ExperimentMesh("midsole", DEFAULT_MIDSOLE_NOMINAL, "mid.ply"),
502
+ ],
503
+ simulation_name="t",
504
+ stl_folder_path=str(folder),
505
+ output_path=str(tmp_path / "out"),
506
+ client=MagicMock(),
507
+ )
508
+ sim.get_part_info("midsole").bounds = {
509
+ "min": [0.0, 0.0, 0.0], "max": [10.0, 10.0, 10.0]
510
+ }
511
+ umin, umax = sim._union_bounds_mm(sim.part_infos)
512
+ pmin, pmax = sim._part_bounds_mm(sim.get_part_info("piston"))
513
+ # Union contains both the mesh box and the (mm-converted) primitive box
514
+ assert (umin <= pmin).all() and (umax >= pmax).all()
515
+ assert umin[0] <= 0.0 and umax[0] >= 10.0
507
516
 
508
517
 
509
518
 
@@ -556,7 +565,19 @@ jobs:
556
565
  - compress
557
566
  assets:
558
567
  data: compress
559
- stress-strain:
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:
560
581
  type: sim/postprocess/stress-strain
561
582
  needs:
562
583
  - force-displacement
@@ -617,8 +638,12 @@ jobs:
617
638
  "metrics-upper_foam.volume_resolution": "[32, 32, 16]",
618
639
  "metrics-upper_foam.volume_size": "[0.1, 0.1, 0.05]",
619
640
  "particle-displacement.keys": "[\"/material0/position\", \"/material1/position\", \"/material2/position\", \"/material3/position\"]",
620
- "stress-strain.initial_length": "0.05",
621
- "stress-strain.keys": "[\"/force_displacement\"]",
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\"]",
622
647
  "von-mises-stress.keys": "[\"/material0/cauchy_stress\", \"/material1/cauchy_stress\", \"/material2/cauchy_stress\", \"/material3/cauchy_stress\"]"
623
648
  }
624
649
  """)
@@ -627,6 +652,52 @@ jobs:
627
652
  assert workflow_params[key] == value, f"mismatch for {key}: {workflow_params[key]!r} != {value!r}"
628
653
 
629
654
 
655
+ class TestStressStrainPerPart:
656
+ """stress-strain runs once per deformable part (not one representative)."""
657
+
658
+ def _built_sim(self, ply_folder, basic_parts, tmp_path):
659
+ sim = CompressionSimulation(
660
+ parts=basic_parts,
661
+ simulation_name="t",
662
+ stl_folder_path=str(ply_folder),
663
+ output_path=str(tmp_path / "out"),
664
+ client=MagicMock(),
665
+ )
666
+ # Distinct X/Y so compression_area = X*Y is unambiguous.
667
+ fake_patch = {"size": [0.1, 0.2, 0.05], "offset": [0.0, 0.0, 0.0], "resolution": [32, 32, 16]}
668
+ for info in sim.part_infos:
669
+ info.patch = fake_patch
670
+ if hasattr(info.part, "filename"):
671
+ info.volume_filename = f"{info.part_unique_name}_volume.bin"
672
+ sim.create_sim_config()
673
+ sim.build_workflow()
674
+ return sim
675
+
676
+ def test_one_job_per_deformable_part(self, ply_folder, basic_parts, tmp_path):
677
+ sim = self._built_sim(ply_folder, basic_parts, tmp_path)
678
+ for name in ("upper_foam", "midsole", "outsole"):
679
+ assert f"stress-strain-{name}" in sim.workflow_jobs
680
+
681
+ def test_no_single_or_rigid_job(self, ply_folder, basic_parts, tmp_path):
682
+ sim = self._built_sim(ply_folder, basic_parts, tmp_path)
683
+ assert "stress-strain" not in sim.workflow_jobs
684
+ assert "stress-strain-piston" not in sim.workflow_jobs
685
+
686
+ def test_each_job_reads_the_shared_force_displacement(self, ply_folder, basic_parts, tmp_path):
687
+ sim = self._built_sim(ply_folder, basic_parts, tmp_path)
688
+ for name in ("upper_foam", "midsole", "outsole"):
689
+ job = sim.workflow_jobs[f"stress-strain-{name}"]
690
+ assert job["type"] == "sim/postprocess/stress-strain"
691
+ assert job["needs"] == ["force-displacement"]
692
+ assert job["assets"] == {"data": "force-displacement"}
693
+
694
+ def test_params_normalized_by_own_patch(self, ply_folder, basic_parts, tmp_path):
695
+ sim = self._built_sim(ply_folder, basic_parts, tmp_path)
696
+ p = sim.workflow_params
697
+ assert p["stress-strain-midsole.compression_area"] == str(0.1 * 0.2)
698
+ assert p["stress-strain-midsole.initial_length"] == "0.05"
699
+ assert p["stress-strain-midsole.keys"] == json.dumps(["/force_displacement"])
700
+
630
701
 
631
702
  class TestMeshDataKey:
632
703
  def test_simple_name(self):
@@ -644,13 +715,18 @@ class TestMakeManifestV2:
644
715
 
645
716
  @pytest.fixture
646
717
  def sim_with_full_steps(self, ply_folder, basic_parts, tmp_path):
647
- return CompressionSimulation(
718
+ sim = CompressionSimulation(
648
719
  parts=basic_parts,
649
720
  simulation_name="t",
650
721
  stl_folder_path=str(ply_folder),
651
722
  output_path=str(tmp_path / "out"),
652
723
  client=MagicMock(),
653
724
  )
725
+ # In the real flow create_sim_config() assigns material indices before
726
+ # make_manifest_v2() runs; mirror that so per-part cards key correctly.
727
+ for i, info in enumerate(sim.part_infos):
728
+ info.material_index = i
729
+ return sim
654
730
 
655
731
  def test_results_list_config_has_name_column_always(self, sim_with_full_steps):
656
732
  sim_with_full_steps.make_manifest_v2(results=[])
@@ -771,6 +847,42 @@ class TestMakeManifestV2:
771
847
  sim.make_manifest_v2()
772
848
  assert sim.manifest["cardsConfig"]["A"] == []
773
849
 
850
+ def test_stress_strain_cards_one_per_deformable_part(self, sim_with_full_steps):
851
+ sim_with_full_steps.make_manifest_v2()
852
+ ids = [c["id"] for c in sim_with_full_steps.manifest["cardsConfig"]["B"]]
853
+ assert "stressStrain1" in ids
854
+ assert "stressStrain2" in ids
855
+ assert "stressStrain3" in ids
856
+ # Piston (material_index 0) gets no stress-strain card.
857
+ assert "stressStrain0" not in ids
858
+
859
+ def test_stress_strain_card_props(self, sim_with_full_steps):
860
+ sim_with_full_steps.make_manifest_v2()
861
+ cards = [
862
+ c
863
+ for c in sim_with_full_steps.manifest["cardsConfig"]["B"]
864
+ if c["id"].startswith("stressStrain")
865
+ ]
866
+ assert cards
867
+ for card in cards:
868
+ assert card["component"] == "MultiExperimentLineChart"
869
+ assert card["props"]["xColumn"] == "strain"
870
+ assert card["props"]["yColumn"] == "stress"
871
+ assert card["props"]["dataSource"] == card["id"]
872
+
873
+ def test_stress_strain_cards_absent_when_step_missing(self, ply_folder, basic_parts, tmp_path):
874
+ sim = CompressionSimulation(
875
+ parts=basic_parts,
876
+ simulation_name="t",
877
+ stl_folder_path=str(ply_folder),
878
+ output_path=str(tmp_path / "out"),
879
+ client=MagicMock(),
880
+ workflow_steps=[WorkflowStep(WorkflowStepType.COMPRESS)],
881
+ )
882
+ sim.make_manifest_v2()
883
+ ids = [c["id"] for c in sim.manifest["cardsConfig"]["B"]]
884
+ assert not any(i.startswith("stressStrain") for i in ids)
885
+
774
886
 
775
887
  class TestResultsHaveEnergyMetrics:
776
888
  def test_returns_true_when_energy_metrics_step_present(self, ply_folder, basic_parts, tmp_path):
@@ -153,7 +153,6 @@ class TestBuildParts:
153
153
  "name": "midsole",
154
154
  "material": "default_midsole_nominal",
155
155
  "file": "mid.ply",
156
- "representative": True,
157
156
  }
158
157
  ])
159
158
  p = parts[0]
@@ -161,7 +160,6 @@ class TestBuildParts:
161
160
  assert p.name == "midsole"
162
161
  assert p.material is DEFAULT_MIDSOLE_NOMINAL
163
162
  assert p.filename == "mid.ply"
164
- assert p.representative_part is True
165
163
 
166
164
  def test_mesh_part_with_material_instance(self):
167
165
  parts = _build_parts([
@@ -169,12 +167,6 @@ class TestBuildParts:
169
167
  ])
170
168
  assert parts[0].material is DEFAULT_OUTSOLE
171
169
 
172
- def test_representative_defaults_to_false(self):
173
- parts = _build_parts([
174
- {"name": "upper_foam", "material": "default_upper_foam", "file": "top.ply"}
175
- ])
176
- assert parts[0].representative_part is False
177
-
178
170
  def test_missing_name_raises(self):
179
171
  with pytest.raises(ValueError, match="missing 'name'"):
180
172
  _build_parts([{"material": "default_outsole", "file": "out.ply"}])
@@ -191,7 +183,7 @@ class TestBuildParts:
191
183
  parts = _build_parts([
192
184
  {"type": "piston_cylinder"},
193
185
  {"name": "upper_foam", "material": "default_upper_foam", "file": "top.ply"},
194
- {"name": "midsole", "material": "default_midsole_nominal", "file": "mid.ply", "representative": True},
186
+ {"name": "midsole", "material": "default_midsole_nominal", "file": "mid.ply"},
195
187
  {"name": "outsole", "material": "default_outsole", "file": "out.ply"},
196
188
  ])
197
189
  assert len(parts) == 4
@@ -339,7 +331,7 @@ class TestRunExperiment:
339
331
  "ply_folder": str(ply_folder),
340
332
  "parts": [
341
333
  {"type": "piston_cylinder"},
342
- {"name": "midsole", "material": "default_midsole_nominal", "file": "mid.ply", "representative": True},
334
+ {"name": "midsole", "material": "default_midsole_nominal", "file": "mid.ply"},
343
335
  ],
344
336
  })
345
337
  assert result == "proj-abc"
@@ -604,7 +596,6 @@ class TestRunExperimentFromZip:
604
596
  "name": "midsole",
605
597
  "material": "default_midsole_nominal",
606
598
  "file": "mid.ply",
607
- "representative": True,
608
599
  }],
609
600
  }
610
601
  zip_path = self._make_zip(tmp_path, manifest, mesh_files=["mid.ply"])
@@ -31,7 +31,7 @@ def basic_parts():
31
31
  return [
32
32
  ExperimentPistonCylinder(),
33
33
  ExperimentMesh("upper_foam", DEFAULT_UPPER_FOAM, "top.ply"),
34
- ExperimentMesh("midsole", DEFAULT_MIDSOLE_NOMINAL, "mid.ply", representative_part=True),
34
+ ExperimentMesh("midsole", DEFAULT_MIDSOLE_NOMINAL, "mid.ply"),
35
35
  ExperimentMesh("outsole", DEFAULT_OUTSOLE, "out.ply"),
36
36
  ]
37
37
 
@@ -290,7 +290,7 @@ class TestShearUPS:
290
290
  material=piston_mat,
291
291
  velocity=self.SHEAR_VELOCITY,
292
292
  ),
293
- ExperimentMesh("puck", puck_mat, "puck.stl", representative_part=True),
293
+ ExperimentMesh("puck", puck_mat, "puck.stl"),
294
294
  ]
295
295
 
296
296
  sim_params = SimulationParameters(
File without changes
File without changes
File without changes