metafold 0.12.dev8__tar.gz → 0.12.dev10__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.
- {metafold-0.12.dev8 → metafold-0.12.dev10}/PKG-INFO +1 -1
- {metafold-0.12.dev8 → metafold-0.12.dev10}/metafold/simulation/compression_experiment.py +4 -0
- {metafold-0.12.dev8 → metafold-0.12.dev10}/metafold/simulation/compression_simulation.py +202 -115
- {metafold-0.12.dev8 → metafold-0.12.dev10}/metafold/simulation/run_experiment.py +4 -6
- {metafold-0.12.dev8 → metafold-0.12.dev10}/metafold.egg-info/PKG-INFO +1 -1
- {metafold-0.12.dev8 → metafold-0.12.dev10}/pyproject.toml +1 -1
- {metafold-0.12.dev8 → metafold-0.12.dev10}/tests/test_compression_simulation.py +209 -67
- {metafold-0.12.dev8 → metafold-0.12.dev10}/tests/test_run_experiment.py +2 -11
- {metafold-0.12.dev8 → metafold-0.12.dev10}/tests/test_shear_simulation.py +2 -2
- {metafold-0.12.dev8 → metafold-0.12.dev10}/LICENSE +0 -0
- {metafold-0.12.dev8 → metafold-0.12.dev10}/README.md +0 -0
- {metafold-0.12.dev8 → metafold-0.12.dev10}/metafold/__init__.py +0 -0
- {metafold-0.12.dev8 → metafold-0.12.dev10}/metafold/api.py +0 -0
- {metafold-0.12.dev8 → metafold-0.12.dev10}/metafold/assets.py +0 -0
- {metafold-0.12.dev8 → metafold-0.12.dev10}/metafold/auth.py +0 -0
- {metafold-0.12.dev8 → metafold-0.12.dev10}/metafold/client.py +0 -0
- {metafold-0.12.dev8 → metafold-0.12.dev10}/metafold/exceptions.py +0 -0
- {metafold-0.12.dev8 → metafold-0.12.dev10}/metafold/jobs.py +0 -0
- {metafold-0.12.dev8 → metafold-0.12.dev10}/metafold/materials.py +0 -0
- {metafold-0.12.dev8 → metafold-0.12.dev10}/metafold/projects.py +0 -0
- {metafold-0.12.dev8 → metafold-0.12.dev10}/metafold/simulation/__init__.py +0 -0
- {metafold-0.12.dev8 → metafold-0.12.dev10}/metafold/utils.py +0 -0
- {metafold-0.12.dev8 → metafold-0.12.dev10}/metafold/workflows.py +0 -0
- {metafold-0.12.dev8 → metafold-0.12.dev10}/metafold.egg-info/SOURCES.txt +0 -0
- {metafold-0.12.dev8 → metafold-0.12.dev10}/metafold.egg-info/dependency_links.txt +0 -0
- {metafold-0.12.dev8 → metafold-0.12.dev10}/metafold.egg-info/requires.txt +0 -0
- {metafold-0.12.dev8 → metafold-0.12.dev10}/metafold.egg-info/top_level.txt +0 -0
- {metafold-0.12.dev8 → metafold-0.12.dev10}/setup.cfg +0 -0
- {metafold-0.12.dev8 → metafold-0.12.dev10}/tests/test_assets.py +0 -0
- {metafold-0.12.dev8 → metafold-0.12.dev10}/tests/test_compession_experiment.py +0 -0
- {metafold-0.12.dev8 → metafold-0.12.dev10}/tests/test_jobs.py +0 -0
- {metafold-0.12.dev8 → metafold-0.12.dev10}/tests/test_materials.py +0 -0
- {metafold-0.12.dev8 → metafold-0.12.dev10}/tests/test_projects.py +0 -0
- {metafold-0.12.dev8 → metafold-0.12.dev10}/tests/test_utils.py +0 -0
- {metafold-0.12.dev8 → metafold-0.12.dev10}/tests/test_workflows.py +0 -0
|
@@ -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
|
|
420
|
-
# longest_axis / (max_resolution - 1). Cached so
|
|
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,17 +535,6 @@ 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")
|
|
551
|
-
|
|
552
538
|
def setup_results(self, output_path):
|
|
553
539
|
self.out_dir = Path(output_path)
|
|
554
540
|
self.out_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -735,7 +721,12 @@ class CompressionSimulation:
|
|
|
735
721
|
|
|
736
722
|
for info in part_infos:
|
|
737
723
|
if info.file_path and info.asset is None:
|
|
738
|
-
|
|
724
|
+
# Quote the filename so the search parser keeps it as one term,
|
|
725
|
+
# and match it exactly (the search is a case-insensitive ILIKE).
|
|
726
|
+
existing = [
|
|
727
|
+
a for a in self.client.assets.list(q=f'filename:"{info.part.filename}"')
|
|
728
|
+
if a.filename == info.part.filename
|
|
729
|
+
]
|
|
739
730
|
asset = None
|
|
740
731
|
if existing:
|
|
741
732
|
asset = existing[0]
|
|
@@ -906,38 +897,65 @@ class CompressionSimulation:
|
|
|
906
897
|
mx = np.array(bounds["max"], dtype=np.float64)
|
|
907
898
|
return float(np.max(mx - mn))
|
|
908
899
|
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
),
|
|
925
|
-
|
|
926
|
-
|
|
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
|
-
)
|
|
900
|
+
@staticmethod
|
|
901
|
+
def _part_bounds_mm(info):
|
|
902
|
+
"""(min, max) axis-aligned bounds of a part in mm, or None. Mesh parts
|
|
903
|
+
report bounds via the pass-1 preprocess job (mm); primitives report
|
|
904
|
+
them analytically via get_bounds() (metres)."""
|
|
905
|
+
part = info.part
|
|
906
|
+
if isinstance(part, ExperimentPrimitive):
|
|
907
|
+
pmin, pmax = part.get_bounds()
|
|
908
|
+
return (
|
|
909
|
+
np.array(pmin, dtype=np.float64) * 1000.0,
|
|
910
|
+
np.array(pmax, dtype=np.float64) * 1000.0,
|
|
911
|
+
)
|
|
912
|
+
if info.bounds:
|
|
913
|
+
return (
|
|
914
|
+
np.array(info.bounds["min"], dtype=np.float64),
|
|
915
|
+
np.array(info.bounds["max"], dtype=np.float64),
|
|
916
|
+
)
|
|
917
|
+
return None
|
|
931
918
|
|
|
919
|
+
def _union_bounds_mm(self, part_infos: list):
|
|
920
|
+
"""Union ("total box") of every part's bounds in mm, or None."""
|
|
921
|
+
mins, maxs = [], []
|
|
932
922
|
for info in part_infos:
|
|
923
|
+
b = self._part_bounds_mm(info)
|
|
924
|
+
if b is not None:
|
|
925
|
+
mins.append(b[0])
|
|
926
|
+
maxs.append(b[1])
|
|
927
|
+
if not mins:
|
|
928
|
+
return None
|
|
929
|
+
return np.min(mins, axis=0), np.max(maxs, axis=0)
|
|
930
|
+
|
|
931
|
+
def _ensure_sample_spacing(self, part_infos: list):
|
|
932
|
+
"""Set sample_spacing (mm) from the total box of all parts, if unset.
|
|
933
|
+
Anchoring to the union — not one 'representative' part — makes
|
|
934
|
+
max_resolution the resolution of the whole simulation's longest axis."""
|
|
935
|
+
if self.sample_spacing is not None:
|
|
936
|
+
return
|
|
937
|
+
max_resolution = max(2, int(self.simulation_parameters.max_resolution))
|
|
938
|
+
union = self._union_bounds_mm(part_infos)
|
|
939
|
+
if union is not None:
|
|
940
|
+
longest = float(np.max(union[1] - union[0]))
|
|
941
|
+
if longest > 0:
|
|
942
|
+
self.sample_spacing = longest / (max_resolution - 1)
|
|
943
|
+
|
|
944
|
+
def _assign_sample_resolutions(self, all_infos: list, mesh_infos: list):
|
|
945
|
+
"""Anchor sample spacing to the total box (union of all part bounds),
|
|
946
|
+
then give each mesh a resolution scaled to its own size so every part
|
|
947
|
+
samples at the same spatial density."""
|
|
948
|
+
max_resolution = max(2, int(self.simulation_parameters.max_resolution))
|
|
949
|
+
self._ensure_sample_spacing(all_infos)
|
|
950
|
+
|
|
951
|
+
for info in mesh_infos:
|
|
933
952
|
if self.sample_spacing and info.bounds:
|
|
934
953
|
longest = self._bounds_longest_axis(info.bounds)
|
|
935
954
|
info.sample_resolution = max(
|
|
936
955
|
2, int(round(longest / self.sample_spacing)) + 1
|
|
937
956
|
)
|
|
938
957
|
else:
|
|
939
|
-
#
|
|
940
|
-
# its own bounds (density then depends on the part's size).
|
|
958
|
+
# Fallback: sample at max_resolution over the part's own bounds.
|
|
941
959
|
info.sample_resolution = max_resolution
|
|
942
960
|
|
|
943
961
|
def sample_assets(self, part_infos=None):
|
|
@@ -958,10 +976,9 @@ class CompressionSimulation:
|
|
|
958
976
|
)
|
|
959
977
|
self._collect_preprocess_outputs(mesh_parts, preprocess_workflows)
|
|
960
978
|
|
|
961
|
-
# Anchor sample spacing to the
|
|
962
|
-
#
|
|
963
|
-
|
|
964
|
-
self._assign_sample_resolutions(mesh_parts)
|
|
979
|
+
# Anchor sample spacing to the total box (union of all parts, primitives
|
|
980
|
+
# included) so every part samples at the same spatial density.
|
|
981
|
+
self._assign_sample_resolutions(part_infos, mesh_parts)
|
|
965
982
|
|
|
966
983
|
# Pass 2: sample each mesh at its density-matched resolution, reusing
|
|
967
984
|
# the preprocessed/BVH assets from pass 1.
|
|
@@ -1016,11 +1033,11 @@ class CompressionSimulation:
|
|
|
1016
1033
|
|
|
1017
1034
|
def create_sim_config(self, name_suffix=""):
|
|
1018
1035
|
"""
|
|
1019
|
-
|
|
1036
|
+
The grid spans the total box (union of every part's bounds) and is
|
|
1037
|
+
sized by the shared sample spacing — no representative part.
|
|
1020
1038
|
Piston velocity is [0,0,0] in the UPS — actual motion driven by velocity.txt.
|
|
1021
1039
|
"""
|
|
1022
1040
|
name = self.simulation_name
|
|
1023
|
-
grid_patch = self.get_part_info(self.representative_part).patch
|
|
1024
1041
|
|
|
1025
1042
|
sim = Simulation(
|
|
1026
1043
|
name,
|
|
@@ -1031,13 +1048,11 @@ class CompressionSimulation:
|
|
|
1031
1048
|
timestep_multiplier=self.simulation_parameters.timestep_multiplier,
|
|
1032
1049
|
)
|
|
1033
1050
|
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
# part). Primitives report bounds via get_bounds() in metres, mesh
|
|
1040
|
-
# parts via their sampled patch (offset/size in mm)
|
|
1051
|
+
# Total box: union of every part's bounding box so nothing is clipped.
|
|
1052
|
+
# Primitives report bounds via get_bounds() in metres, mesh parts via
|
|
1053
|
+
# their sampled patch (offset/size in mm).
|
|
1054
|
+
grid_min = np.array([np.inf, np.inf, np.inf], dtype=np.float32)
|
|
1055
|
+
grid_max = np.array([-np.inf, -np.inf, -np.inf], dtype=np.float32)
|
|
1041
1056
|
for info in self.part_infos:
|
|
1042
1057
|
part = info.part
|
|
1043
1058
|
if isinstance(part, ExperimentPrimitive):
|
|
@@ -1057,11 +1072,10 @@ class CompressionSimulation:
|
|
|
1057
1072
|
grid_max[2] += self.simulation_parameters.margin_z
|
|
1058
1073
|
|
|
1059
1074
|
grid_size = grid_max - grid_min
|
|
1060
|
-
spacing
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
)
|
|
1075
|
+
# Every part is sampled at this uniform (isotropic) spacing, so the grid
|
|
1076
|
+
# cells match particle spacing. mm -> m.
|
|
1077
|
+
self._ensure_sample_spacing(self.part_infos)
|
|
1078
|
+
spacing = (self.sample_spacing or 1.0) * 1e-3
|
|
1065
1079
|
grid_resolution = np.ceil(
|
|
1066
1080
|
grid_size / (self.simulation_parameters.points_per_cell * spacing)
|
|
1067
1081
|
).astype(np.int32)
|
|
@@ -1371,20 +1385,36 @@ class CompressionSimulation:
|
|
|
1371
1385
|
)
|
|
1372
1386
|
|
|
1373
1387
|
def _add_to_workflow_stress_strain(self, part_infos: list[PartInfo]):
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
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]
|
|
1388
|
+
# One stress-strain job per deformable part, each normalizing the shared
|
|
1389
|
+
# (global) force-displacement curve by that part's own cross-section
|
|
1390
|
+
# (X*Y) and height (Z). Named stress-strain-<part> like metrics-<part>.
|
|
1391
|
+
fd_job = next(
|
|
1392
|
+
(p.jobs["force-displacement"] for p in part_infos
|
|
1393
|
+
if "force-displacement" in p.jobs),
|
|
1394
|
+
None,
|
|
1387
1395
|
)
|
|
1396
|
+
if fd_job is None:
|
|
1397
|
+
return
|
|
1398
|
+
for part_info in part_infos:
|
|
1399
|
+
if not self._is_analysis_target(part_info.part):
|
|
1400
|
+
continue
|
|
1401
|
+
if part_info.disabled or not part_info.patch:
|
|
1402
|
+
continue
|
|
1403
|
+
job_name = f"stress-strain-{part_info.part_unique_name}"
|
|
1404
|
+
self.workflow_jobs[job_name] = {
|
|
1405
|
+
"type": "sim/postprocess/stress-strain",
|
|
1406
|
+
"needs": [fd_job],
|
|
1407
|
+
"assets": {"data": fd_job},
|
|
1408
|
+
}
|
|
1409
|
+
self.workflow_params[f"{job_name}.keys"] = json.dumps(
|
|
1410
|
+
["/force_displacement"]
|
|
1411
|
+
)
|
|
1412
|
+
size = part_info.patch["size"]
|
|
1413
|
+
self.workflow_params[f"{job_name}.compression_area"] = str(
|
|
1414
|
+
size[0] * size[1]
|
|
1415
|
+
)
|
|
1416
|
+
self.workflow_params[f"{job_name}.initial_length"] = str(size[2])
|
|
1417
|
+
part_info.jobs["stress-strain"] = job_name
|
|
1388
1418
|
|
|
1389
1419
|
def _add_to_workflow_particle_displacement(self, part_infos: list[PartInfo]):
|
|
1390
1420
|
self._add_to_workflow_postprocess(
|
|
@@ -1973,16 +2003,25 @@ class CompressionSimulation:
|
|
|
1973
2003
|
loading.to_csv(f)
|
|
1974
2004
|
data["loadingForceDisplacement"] = filename
|
|
1975
2005
|
|
|
1976
|
-
# Stress-strain
|
|
2006
|
+
# Stress-strain: one curve per deformable part.
|
|
1977
2007
|
if self._contains_step(WorkflowStepType.STRESS_STRAIN):
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
2008
|
+
for part_info in self.part_infos:
|
|
2009
|
+
if not self._is_analysis_target(part_info.part):
|
|
2010
|
+
continue
|
|
2011
|
+
if part_info.disabled:
|
|
2012
|
+
continue
|
|
2013
|
+
i = part_info.material_index
|
|
2014
|
+
ss_asset = w.get_asset(
|
|
2015
|
+
f"stress-strain-{part_info.part_unique_name}.output"
|
|
2016
|
+
)
|
|
2017
|
+
if ss_asset is None:
|
|
2018
|
+
continue
|
|
2019
|
+
self.client.assets.download_file(ss_asset.id, hdf_filename)
|
|
2020
|
+
with pd.HDFStore(hdf_filename) as store:
|
|
2021
|
+
filename = f"{name}/stressStrain{i}.csv"
|
|
2022
|
+
with zf.open(filename, "w") as f:
|
|
2023
|
+
store["/stress_strain"].to_csv(f)
|
|
2024
|
+
data[f"stressStrain{i}"] = filename
|
|
1986
2025
|
|
|
1987
2026
|
# ----------------------------------------------------------
|
|
1988
2027
|
# Write PLY files from the combined all-material dataframe.
|
|
@@ -2218,18 +2257,26 @@ class CompressionSimulation:
|
|
|
2218
2257
|
if raw_ue is not None:
|
|
2219
2258
|
unloading_energy = float(raw_ue)
|
|
2220
2259
|
|
|
2221
|
-
# Stress-strain: ship whole.
|
|
2260
|
+
# Stress-strain: ship one whole file per deformable part.
|
|
2222
2261
|
if self._contains_step(WorkflowStepType.STRESS_STRAIN):
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
"
|
|
2231
|
-
|
|
2232
|
-
|
|
2262
|
+
for part_info in self.part_infos:
|
|
2263
|
+
if not self._is_analysis_target(part_info.part):
|
|
2264
|
+
continue
|
|
2265
|
+
if part_info.disabled:
|
|
2266
|
+
continue
|
|
2267
|
+
i = part_info.material_index
|
|
2268
|
+
ss_asset = w.get_asset(
|
|
2269
|
+
f"stress-strain-{part_info.part_unique_name}.output"
|
|
2270
|
+
)
|
|
2271
|
+
if ss_asset is not None:
|
|
2272
|
+
ss_hdf = tempdir_path / f"stress_strain_{i}.h5"
|
|
2273
|
+
self.client.assets.download_file(ss_asset.id, ss_hdf)
|
|
2274
|
+
ss_zip_path = f"{name}/stress_strain_{i}.h5"
|
|
2275
|
+
zf.write(ss_hdf, arcname=ss_zip_path)
|
|
2276
|
+
data[f"stressStrain{i}"] = {
|
|
2277
|
+
"name": ss_zip_path,
|
|
2278
|
+
"path": "/stress_strain",
|
|
2279
|
+
}
|
|
2233
2280
|
|
|
2234
2281
|
# Sum interior volumes across analysis-target parts.
|
|
2235
2282
|
total_volume = 0.0
|
|
@@ -2375,11 +2422,19 @@ class CompressionSimulation:
|
|
|
2375
2422
|
}
|
|
2376
2423
|
|
|
2377
2424
|
if self._contains_step(WorkflowStepType.STRESS_STRAIN):
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2425
|
+
for part_info in self.part_infos:
|
|
2426
|
+
if not self._is_analysis_target(part_info.part):
|
|
2427
|
+
continue
|
|
2428
|
+
if part_info.disabled:
|
|
2429
|
+
continue
|
|
2430
|
+
i = part_info.material_index
|
|
2431
|
+
server_data[f"stressStrain{i}"] = {
|
|
2432
|
+
"jobId": job_id_lookup.get(
|
|
2433
|
+
f"stress-strain-{part_info.part_unique_name}", ""
|
|
2434
|
+
),
|
|
2435
|
+
"assetName": "output",
|
|
2436
|
+
"path": "/stress_strain",
|
|
2437
|
+
}
|
|
2383
2438
|
|
|
2384
2439
|
return server_data
|
|
2385
2440
|
|
|
@@ -2604,21 +2659,53 @@ class CompressionSimulation:
|
|
|
2604
2659
|
},
|
|
2605
2660
|
]
|
|
2606
2661
|
|
|
2662
|
+
# One stress-strain card per deformable part (keyed stressStrain{i}),
|
|
2663
|
+
# under force-displacement: same data, per-part normalizations.
|
|
2664
|
+
if self._contains_step(WorkflowStepType.STRESS_STRAIN):
|
|
2665
|
+
for part_info in self.part_infos:
|
|
2666
|
+
if not self._is_analysis_target(part_info.part):
|
|
2667
|
+
continue
|
|
2668
|
+
if part_info.disabled:
|
|
2669
|
+
continue
|
|
2670
|
+
i = part_info.material_index
|
|
2671
|
+
self.manifest["cardsConfig"]["A"].append(
|
|
2672
|
+
{
|
|
2673
|
+
"id": f"stressStrain{i}",
|
|
2674
|
+
"title": f"{part_info.part_unique_name} Stress-Strain",
|
|
2675
|
+
"component": "MultiExperimentLineChart",
|
|
2676
|
+
"props": {
|
|
2677
|
+
"xColumn": "strain",
|
|
2678
|
+
"yColumn": "stress",
|
|
2679
|
+
"yMin": 0.0,
|
|
2680
|
+
"xAxisLabel": "Strain",
|
|
2681
|
+
"yAxisLabel": "Stress (MPa)",
|
|
2682
|
+
"dataSource": f"stressStrain{i}",
|
|
2683
|
+
},
|
|
2684
|
+
}
|
|
2685
|
+
)
|
|
2686
|
+
|
|
2687
|
+
# One von Mises histogram card per deformable part (keyed
|
|
2688
|
+
# vonMisesStressHistogram{i}).
|
|
2607
2689
|
self.manifest["cardsConfig"]["B"] = []
|
|
2608
2690
|
if self._contains_step(WorkflowStepType.VON_MISES_STRESS):
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
"
|
|
2618
|
-
"
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2691
|
+
for part_info in self.part_infos:
|
|
2692
|
+
if not self._is_analysis_target(part_info.part):
|
|
2693
|
+
continue
|
|
2694
|
+
if part_info.disabled:
|
|
2695
|
+
continue
|
|
2696
|
+
i = part_info.material_index
|
|
2697
|
+
self.manifest["cardsConfig"]["B"].append(
|
|
2698
|
+
{
|
|
2699
|
+
"id": f"vonMisesStress{i}",
|
|
2700
|
+
"title": f"{part_info.part_unique_name} von Mises Stress",
|
|
2701
|
+
"component": "SingleExperimentHistogram",
|
|
2702
|
+
"props": {
|
|
2703
|
+
"dataSource": f"vonMisesStressHistogram{i}",
|
|
2704
|
+
"xAxisLabel": "von Mises Stress",
|
|
2705
|
+
"yAxisLabel": "Frequency",
|
|
2706
|
+
},
|
|
2707
|
+
}
|
|
2708
|
+
)
|
|
2622
2709
|
|
|
2623
2710
|
if self._results_have_energy_metrics(results):
|
|
2624
2711
|
self.manifest["resultsListConfig"].append(
|
|
@@ -2645,7 +2732,7 @@ class CompressionSimulation:
|
|
|
2645
2732
|
"filtering": False,
|
|
2646
2733
|
}
|
|
2647
2734
|
)
|
|
2648
|
-
self.manifest["cardsConfig"]["C"]
|
|
2735
|
+
self.manifest["cardsConfig"]["C"].append(
|
|
2649
2736
|
{
|
|
2650
2737
|
"id": "energyVolume",
|
|
2651
2738
|
"title": "Energy Absorbed-Interior Volume",
|
|
@@ -2657,7 +2744,7 @@ class CompressionSimulation:
|
|
|
2657
2744
|
"yAxisLabel": "Energy Absorbed (J)",
|
|
2658
2745
|
"colorScaleMeasurement": "energyAbsorbed",
|
|
2659
2746
|
},
|
|
2660
|
-
}
|
|
2661
|
-
|
|
2747
|
+
}
|
|
2748
|
+
)
|
|
2662
2749
|
|
|
2663
2750
|
|
|
@@ -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
|
|
65
|
-
#
|
|
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
|
|
@@ -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"
|
|
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"
|
|
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,
|
|
252
|
-
|
|
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
|
|
271
|
-
#
|
|
272
|
-
#
|
|
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
|
|
419
|
-
|
|
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,
|
|
434
|
-
|
|
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=
|
|
419
|
+
parts=parts,
|
|
437
420
|
simulation_name="t",
|
|
438
|
-
stl_folder_path=str(
|
|
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-
|
|
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
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
def test_representative_part_gets_max_resolution(self, sampled_sim):
|
|
464
|
-
assert sampled_sim.get_part_info("midsole").sample_resolution == 31
|
|
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)
|
|
465
448
|
|
|
466
|
-
def
|
|
467
|
-
|
|
468
|
-
assert sampled_sim.get_part_info("upper_foam").sample_resolution == 16
|
|
449
|
+
def test_largest_part_gets_max_resolution(self, sampled_sim):
|
|
450
|
+
assert sampled_sim.get_part_info("outsole").sample_resolution == 31
|
|
469
451
|
|
|
470
|
-
def
|
|
471
|
-
|
|
472
|
-
assert sampled_sim.get_part_info("
|
|
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-
|
|
478
|
-
"sample-mesh-
|
|
479
|
-
"sample-mesh-
|
|
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
|
-
#
|
|
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 ==
|
|
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
|
-
|
|
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=[])
|
|
@@ -678,7 +754,6 @@ class TestMakeManifestV2:
|
|
|
678
754
|
def test_card_c_present_when_energy_metrics_step_present(self, sim_with_full_steps):
|
|
679
755
|
sim_with_full_steps.make_manifest_v2(results=[])
|
|
680
756
|
card_c = sim_with_full_steps.manifest["cardsConfig"]["C"]
|
|
681
|
-
assert len(card_c) == 1
|
|
682
757
|
assert card_c[0]["id"] == "energyVolume"
|
|
683
758
|
|
|
684
759
|
def test_card_c_absent_when_no_energy_metrics(self, ply_folder, basic_parts, tmp_path):
|
|
@@ -771,6 +846,73 @@ class TestMakeManifestV2:
|
|
|
771
846
|
sim.make_manifest_v2()
|
|
772
847
|
assert sim.manifest["cardsConfig"]["A"] == []
|
|
773
848
|
|
|
849
|
+
def test_von_mises_cards_one_per_deformable_part(self, sim_with_full_steps):
|
|
850
|
+
sim_with_full_steps.make_manifest_v2()
|
|
851
|
+
cards = sim_with_full_steps.manifest["cardsConfig"]["B"]
|
|
852
|
+
ids = [c["id"] for c in cards]
|
|
853
|
+
assert ids == ["vonMisesStress1", "vonMisesStress2", "vonMisesStress3"]
|
|
854
|
+
for card in cards:
|
|
855
|
+
assert card["component"] == "SingleExperimentHistogram"
|
|
856
|
+
assert card["props"]["dataSource"] == card["id"].replace(
|
|
857
|
+
"vonMisesStress", "vonMisesStressHistogram"
|
|
858
|
+
)
|
|
859
|
+
|
|
860
|
+
def test_von_mises_cards_absent_when_step_missing(self, ply_folder, basic_parts, tmp_path):
|
|
861
|
+
sim = CompressionSimulation(
|
|
862
|
+
parts=basic_parts,
|
|
863
|
+
simulation_name="t",
|
|
864
|
+
stl_folder_path=str(ply_folder),
|
|
865
|
+
output_path=str(tmp_path / "out"),
|
|
866
|
+
client=MagicMock(),
|
|
867
|
+
workflow_steps=[WorkflowStep(WorkflowStepType.COMPRESS)],
|
|
868
|
+
)
|
|
869
|
+
sim.make_manifest_v2()
|
|
870
|
+
assert sim.manifest["cardsConfig"]["B"] == []
|
|
871
|
+
|
|
872
|
+
def test_stress_strain_cards_in_pane_a_after_force_displacement(self, sim_with_full_steps):
|
|
873
|
+
sim_with_full_steps.make_manifest_v2()
|
|
874
|
+
ids = [c["id"] for c in sim_with_full_steps.manifest["cardsConfig"]["A"]]
|
|
875
|
+
assert ids == [
|
|
876
|
+
"forceDisplacement",
|
|
877
|
+
"stressStrain1",
|
|
878
|
+
"stressStrain2",
|
|
879
|
+
"stressStrain3",
|
|
880
|
+
]
|
|
881
|
+
# Piston (material_index 0) gets no stress-strain card.
|
|
882
|
+
assert "stressStrain0" not in ids
|
|
883
|
+
|
|
884
|
+
def test_stress_strain_card_props(self, sim_with_full_steps):
|
|
885
|
+
sim_with_full_steps.make_manifest_v2()
|
|
886
|
+
cards = [
|
|
887
|
+
c
|
|
888
|
+
for c in sim_with_full_steps.manifest["cardsConfig"]["A"]
|
|
889
|
+
if c["id"].startswith("stressStrain")
|
|
890
|
+
]
|
|
891
|
+
assert cards
|
|
892
|
+
for card in cards:
|
|
893
|
+
assert card["component"] == "MultiExperimentLineChart"
|
|
894
|
+
assert card["props"]["xColumn"] == "strain"
|
|
895
|
+
assert card["props"]["yColumn"] == "stress"
|
|
896
|
+
assert card["props"]["dataSource"] == card["id"]
|
|
897
|
+
|
|
898
|
+
def test_stress_strain_cards_absent_when_step_missing(self, ply_folder, basic_parts, tmp_path):
|
|
899
|
+
sim = CompressionSimulation(
|
|
900
|
+
parts=basic_parts,
|
|
901
|
+
simulation_name="t",
|
|
902
|
+
stl_folder_path=str(ply_folder),
|
|
903
|
+
output_path=str(tmp_path / "out"),
|
|
904
|
+
client=MagicMock(),
|
|
905
|
+
workflow_steps=[WorkflowStep(WorkflowStepType.COMPRESS)],
|
|
906
|
+
)
|
|
907
|
+
sim.make_manifest_v2()
|
|
908
|
+
ids = [c["id"] for c in sim.manifest["cardsConfig"]["A"]]
|
|
909
|
+
assert not any(i.startswith("stressStrain") for i in ids)
|
|
910
|
+
|
|
911
|
+
def test_pane_c_is_energy_volume_only(self, sim_with_full_steps):
|
|
912
|
+
sim_with_full_steps.make_manifest_v2(results=[])
|
|
913
|
+
ids = [c["id"] for c in sim_with_full_steps.manifest["cardsConfig"]["C"]]
|
|
914
|
+
assert ids == ["energyVolume"]
|
|
915
|
+
|
|
774
916
|
|
|
775
917
|
class TestResultsHaveEnergyMetrics:
|
|
776
918
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|