metafold 0.13.dev4__tar.gz → 0.13.1.dev0__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.13.dev4 → metafold-0.13.1.dev0}/PKG-INFO +1 -1
  2. {metafold-0.13.dev4 → metafold-0.13.1.dev0}/metafold/jobs.py +0 -76
  3. {metafold-0.13.dev4 → metafold-0.13.1.dev0}/metafold/simulation/compression_simulation.py +32 -0
  4. {metafold-0.13.dev4 → metafold-0.13.1.dev0}/metafold.egg-info/PKG-INFO +1 -1
  5. {metafold-0.13.dev4 → metafold-0.13.1.dev0}/pyproject.toml +1 -1
  6. {metafold-0.13.dev4 → metafold-0.13.1.dev0}/tests/test_compression_simulation.py +78 -0
  7. {metafold-0.13.dev4 → metafold-0.13.1.dev0}/tests/test_jobs.py +0 -137
  8. {metafold-0.13.dev4 → metafold-0.13.1.dev0}/LICENSE +0 -0
  9. {metafold-0.13.dev4 → metafold-0.13.1.dev0}/README.md +0 -0
  10. {metafold-0.13.dev4 → metafold-0.13.1.dev0}/metafold/__init__.py +0 -0
  11. {metafold-0.13.dev4 → metafold-0.13.1.dev0}/metafold/api.py +0 -0
  12. {metafold-0.13.dev4 → metafold-0.13.1.dev0}/metafold/assets.py +0 -0
  13. {metafold-0.13.dev4 → metafold-0.13.1.dev0}/metafold/auth.py +0 -0
  14. {metafold-0.13.dev4 → metafold-0.13.1.dev0}/metafold/client.py +0 -0
  15. {metafold-0.13.dev4 → metafold-0.13.1.dev0}/metafold/exceptions.py +0 -0
  16. {metafold-0.13.dev4 → metafold-0.13.1.dev0}/metafold/materials.py +0 -0
  17. {metafold-0.13.dev4 → metafold-0.13.1.dev0}/metafold/projects.py +0 -0
  18. {metafold-0.13.dev4 → metafold-0.13.1.dev0}/metafold/simulation/__init__.py +0 -0
  19. {metafold-0.13.dev4 → metafold-0.13.1.dev0}/metafold/simulation/compression_experiment.py +0 -0
  20. {metafold-0.13.dev4 → metafold-0.13.1.dev0}/metafold/simulation/run_experiment.py +0 -0
  21. {metafold-0.13.dev4 → metafold-0.13.1.dev0}/metafold/utils.py +0 -0
  22. {metafold-0.13.dev4 → metafold-0.13.1.dev0}/metafold/workflows.py +0 -0
  23. {metafold-0.13.dev4 → metafold-0.13.1.dev0}/metafold.egg-info/SOURCES.txt +0 -0
  24. {metafold-0.13.dev4 → metafold-0.13.1.dev0}/metafold.egg-info/dependency_links.txt +0 -0
  25. {metafold-0.13.dev4 → metafold-0.13.1.dev0}/metafold.egg-info/requires.txt +0 -0
  26. {metafold-0.13.dev4 → metafold-0.13.1.dev0}/metafold.egg-info/top_level.txt +0 -0
  27. {metafold-0.13.dev4 → metafold-0.13.1.dev0}/setup.cfg +0 -0
  28. {metafold-0.13.dev4 → metafold-0.13.1.dev0}/tests/test_assets.py +0 -0
  29. {metafold-0.13.dev4 → metafold-0.13.1.dev0}/tests/test_compession_experiment.py +0 -0
  30. {metafold-0.13.dev4 → metafold-0.13.1.dev0}/tests/test_materials.py +0 -0
  31. {metafold-0.13.dev4 → metafold-0.13.1.dev0}/tests/test_projects.py +0 -0
  32. {metafold-0.13.dev4 → metafold-0.13.1.dev0}/tests/test_run_experiment.py +0 -0
  33. {metafold-0.13.dev4 → metafold-0.13.1.dev0}/tests/test_shear_simulation.py +0 -0
  34. {metafold-0.13.dev4 → metafold-0.13.1.dev0}/tests/test_utils.py +0 -0
  35. {metafold-0.13.dev4 → metafold-0.13.1.dev0}/tests/test_workflows.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: metafold
3
- Version: 0.13.dev4
3
+ Version: 0.13.1.dev0
4
4
  Summary: Metafold SDK for Python
5
5
  Author-email: Metafold 3D <info@metafold3d.com>
6
6
  License: Copyright 2024 Metafold 3D
@@ -135,82 +135,6 @@ class JobsEndpoint:
135
135
  r: Response = self._client.get(url)
136
136
  return Job(**r.json())
137
137
 
138
- def run(
139
- self, type: str, params: dict[str, Any],
140
- name: str | None = None,
141
- timeout: int | float = 120,
142
- project_id: str | None = None,
143
- ) -> Job:
144
- """Dispatch a new job and wait for a result.
145
-
146
- See Metafold API docs for the full list of jobs.
147
-
148
- Args:
149
- type: Job type.
150
- params: Job parameters.
151
- name: Optional job name.
152
- timeout: Time in seconds to wait for a result.
153
- project_id: Job project ID.
154
-
155
- Returns:
156
- Completed job resource.
157
- """
158
- url = self.run_status(type, params, name=name, project_id=project_id)
159
- try:
160
- r: Response = self.poll(url, timeout)
161
- except PollTimeout as e:
162
- raise RuntimeError(
163
- f"Job '{name or type}' failed to complete within {timeout} seconds"
164
- ) from e
165
- return Job(**r.json())
166
-
167
- def run_status(
168
- self, type: str, params: dict[str, Any],
169
- name: str | None = None,
170
- project_id: str | None = None,
171
- ) -> str:
172
- """Dispatch a new job and return immediately without waiting for result.
173
-
174
- See Metafold API docs for the full list of jobs.
175
-
176
- Args:
177
- type: Job type.
178
- params: Job parameters.
179
- name: Optional job name.
180
- project_id: Job project ID.
181
-
182
- Returns:
183
- Job status url.
184
- """
185
- project_id = self._client.project_id(project_id)
186
- payload = asdict(type=type, parameters=params, name=name)
187
- r: Response = self._client.post(f"/projects/{project_id}/jobs", json=payload)
188
- return r.json()["link"]
189
-
190
- def poll(self, *args, **kwargs):
191
- return self._client.poll(*args, **kwargs)
192
-
193
- def update(
194
- self, job_id: str,
195
- name: str | None = None,
196
- project_id: str | None = None,
197
- ) -> Job:
198
- """Update a job.
199
-
200
- Args:
201
- job_id: ID of job to update.
202
- name: New job name. The existing name remains unchanged if None.
203
- project_id: Job project ID.
204
-
205
- Returns:
206
- Updated job resource.
207
- """
208
- project_id = self._client.project_id(project_id)
209
- url = f"/projects/{project_id}/jobs/{job_id}"
210
- payload = asdict(name=name)
211
- r: Response = self._client.patch(url, data=payload)
212
- return Job(**r.json())
213
-
214
138
  def delete(self, job_id: str, project_id: str | None = None):
215
139
  """Delete a job.
216
140
 
@@ -72,6 +72,14 @@ ZERO_VELOCITY = [
72
72
  ]
73
73
 
74
74
 
75
+ def _has_motion(velocity: Optional[list]) -> bool:
76
+ """True if a velocity profile has any non-zero component.
77
+ [time, vx, vy, vz]."""
78
+ if not velocity:
79
+ return False
80
+ return any(any(c for c in row[1:4]) for row in velocity)
81
+
82
+
75
83
  class BoundaryCondition(Enum):
76
84
  SYMMETRIC = "symmetric" # symmetry plane (acts as frictionless wall)
77
85
  VELOCITY_DIRICHLET = "velocity_dirichlet" # velocity fixed to zero at the face
@@ -482,6 +490,30 @@ class CompressionSimulation:
482
490
  else:
483
491
  self.part_infos.append(inner_part)
484
492
 
493
+ # A rigid part drives the compression.
494
+ if parts:
495
+ has_piston = False
496
+ has_support = False
497
+ piston_moves = False
498
+ for p in parts:
499
+ if isinstance(p, ExperimentPistonBase):
500
+ has_piston = True
501
+ piston_moves = piston_moves or _has_motion(p.velocity)
502
+ elif isinstance(p, ExperimentSupportBase):
503
+ has_support = True
504
+ if not has_piston and not has_support:
505
+ raise ValueError(
506
+ "No piston part: a compression simulation needs "
507
+ "a rigid part to apply load. Add a part of type piston_mesh, "
508
+ "piston_cylinder, or piston_box."
509
+ )
510
+ # Supports are stationary by design, so only pistons must move.
511
+ if has_piston and not piston_moves:
512
+ raise ValueError(
513
+ "Piston velocity is zero. Set a velocity "
514
+ "profile of [time, vx, vy, vz] rows with a non-zero component."
515
+ )
516
+
485
517
  self.setup_ply_files(stl_folder_path)
486
518
  self.setup_results(output_path)
487
519
  if client is None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: metafold
3
- Version: 0.13.dev4
3
+ Version: 0.13.1.dev0
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.13.dev4"
7
+ version = "0.13.1.dev0"
8
8
  authors = [
9
9
  {name = "Metafold 3D", email = "info@metafold3d.com"},
10
10
  ]
@@ -15,6 +15,7 @@ from metafold.simulation.compression_simulation import (
15
15
  ExperimentPistonBase,
16
16
  ExperimentPistonCylinder,
17
17
  ExperimentPistonMesh,
18
+ ExperimentSupportMesh,
18
19
  SimulationParameters,
19
20
  WorkflowStep,
20
21
  WorkflowStepType,
@@ -84,6 +85,7 @@ class TestConstruction:
84
85
 
85
86
  def test_missing_ply_raises(self, ply_folder, tmp_path):
86
87
  parts = [
88
+ ExperimentPistonCylinder(),
87
89
  ExperimentMesh("midsole", DEFAULT_MIDSOLE_NOMINAL, "nonexistent.ply"),
88
90
  ]
89
91
  with pytest.raises(ValueError, match="not found"):
@@ -95,6 +97,82 @@ class TestConstruction:
95
97
  client=MagicMock(),
96
98
  )
97
99
 
100
+ def test_no_piston_raises(self, ply_folder, tmp_path):
101
+ # Without a rigid part nothing drives the compression
102
+ parts = [
103
+ ExperimentMesh("midsole", DEFAULT_MIDSOLE_NOMINAL, "mid.ply"),
104
+ ExperimentMesh("outsole", DEFAULT_OUTSOLE, "out.ply"),
105
+ ]
106
+ with pytest.raises(ValueError, match="piston"):
107
+ CompressionSimulation(
108
+ parts=parts,
109
+ simulation_name="t",
110
+ stl_folder_path=str(ply_folder),
111
+ output_path=str(tmp_path / "out"),
112
+ client=MagicMock(),
113
+ )
114
+
115
+ def test_zero_piston_velocity_raises(self, ply_folder, tmp_path):
116
+ # A piston that never moves applies no load
117
+ parts = [
118
+ ExperimentPistonCylinder(velocity=[[0.0, 0, 0, 0.0], [0.04, 0, 0, 0.0]]),
119
+ ExperimentMesh("midsole", DEFAULT_MIDSOLE_NOMINAL, "mid.ply"),
120
+ ]
121
+ with pytest.raises(ValueError, match="velocity is zero"):
122
+ CompressionSimulation(
123
+ parts=parts,
124
+ simulation_name="t",
125
+ stl_folder_path=str(ply_folder),
126
+ output_path=str(tmp_path / "out"),
127
+ client=MagicMock(),
128
+ )
129
+
130
+ def test_empty_piston_velocity_raises(self, ply_folder, tmp_path):
131
+ parts = [
132
+ ExperimentPistonCylinder(velocity=[]),
133
+ ExperimentMesh("midsole", DEFAULT_MIDSOLE_NOMINAL, "mid.ply"),
134
+ ]
135
+ with pytest.raises(ValueError, match="velocity is zero"):
136
+ CompressionSimulation(
137
+ parts=parts,
138
+ simulation_name="t",
139
+ stl_folder_path=str(ply_folder),
140
+ output_path=str(tmp_path / "out"),
141
+ client=MagicMock(),
142
+ )
143
+
144
+ def test_stationary_support_with_moving_piston_ok(self, ply_folder, tmp_path):
145
+ # Supports are stationary by design; only the piston must move.
146
+ parts = [
147
+ ExperimentPistonCylinder(), # default velocity moves
148
+ ExperimentSupportMesh(name="base", filename="top.ply"),
149
+ ExperimentMesh("midsole", DEFAULT_MIDSOLE_NOMINAL, "mid.ply"),
150
+ ]
151
+ sim = CompressionSimulation(
152
+ parts=parts,
153
+ simulation_name="t",
154
+ stl_folder_path=str(ply_folder),
155
+ output_path=str(tmp_path / "out"),
156
+ client=MagicMock(),
157
+ )
158
+ assert len(sim.part_infos) == 3
159
+
160
+ def test_support_part_satisfies_rigid_requirement(self, ply_folder, tmp_path):
161
+ # A support is also a rigid driver, so it's a valid alternative to a piston.
162
+ parts = [
163
+ ExperimentSupportMesh(name="base", filename="top.ply"),
164
+ ExperimentMesh("midsole", DEFAULT_MIDSOLE_NOMINAL, "mid.ply"),
165
+ ]
166
+ sim = CompressionSimulation(
167
+ parts=parts,
168
+ simulation_name="t",
169
+ stl_folder_path=str(ply_folder),
170
+ output_path=str(tmp_path / "out"),
171
+ client=MagicMock(),
172
+ )
173
+ assert len(sim.part_infos) == 2
174
+
175
+
98
176
  class TestWorkflowStepsInput:
99
177
  def test_accepts_workflow_step_instance(self):
100
178
  steps = CompressionSimulation._clean_workflow_steps_input([
@@ -102,38 +102,6 @@ job_list = [
102
102
  },
103
103
  ]
104
104
 
105
- new_job = {
106
- "id": "1",
107
- "name": "My Job",
108
- "type": "test_job",
109
- "created": "Mon, 01 Jan 2024 00:00:00 GMT",
110
- "started": "Mon, 01 Jan 2024 00:00:00 GMT",
111
- "finished": "Mon, 01 Jan 2024 00:00:00 GMT",
112
- "error": None,
113
- "inputs": {
114
- "params": {
115
- "foo": "1",
116
- "bar": "a",
117
- "baz": "[2, \"b\"]",
118
- },
119
- },
120
- "outputs": {
121
- "params": None,
122
- },
123
- "needs": [],
124
- "project_id": "1",
125
- "workflow_id": None,
126
- "assets": [],
127
- "parameters": {
128
- "foo": "1",
129
- "bar": "a",
130
- "baz": "[2, \"b\"]",
131
- },
132
- "meta": None,
133
- }
134
-
135
- poll_count: int = 0
136
-
137
105
 
138
106
  class MockRequestHandler(BaseHTTPRequestHandler):
139
107
  def do_GET(self):
@@ -156,51 +124,6 @@ class MockRequestHandler(BaseHTTPRequestHandler):
156
124
  payload = deepcopy(job_list[-1])
157
125
  payload["assets"] = [asset_json]
158
126
  self.wfile.write(json.dumps(payload).encode())
159
- elif u.path == "/projects/1/jobs/1/status":
160
- global poll_count
161
- poll_count += 1
162
- if poll_count < 3:
163
- self.send_response(HTTPStatus.ACCEPTED)
164
- payload = deepcopy(new_job)
165
- payload["state"] = "started"
166
- else:
167
- self.send_response(HTTPStatus.CREATED)
168
- payload = deepcopy(new_job)
169
- payload.update({
170
- "state": "success",
171
- "assets": [asset_json],
172
- })
173
- self.send_header("Content-Type", "application/json")
174
- self.end_headers()
175
- self.wfile.write(json.dumps(payload).encode())
176
- else:
177
- self.send_error(HTTPStatus.NOT_FOUND)
178
-
179
- def do_POST(self):
180
- if self.path == "/projects/1/jobs":
181
- self.send_response(HTTPStatus.ACCEPTED)
182
- self.send_header("Content-Type", "application/json")
183
- self.end_headers()
184
- payload = deepcopy(new_job)
185
- payload.update({
186
- "state": "pending",
187
- "link": "http://localhost:8000/projects/1/jobs/1/status",
188
- })
189
- self.wfile.write(json.dumps(payload).encode())
190
- else:
191
- self.send_error(HTTPStatus.NOT_FOUND)
192
-
193
- def do_PATCH(self):
194
- if self.path == "/projects/1/jobs/1":
195
- self.send_response(HTTPStatus.OK)
196
- self.send_header("Content-Type", "application/json")
197
- self.end_headers()
198
- payload = deepcopy(job_list[-1])
199
- payload.update({
200
- "name": "baz",
201
- "assets": [asset_json],
202
- })
203
- self.wfile.write(json.dumps(payload).encode())
204
127
  else:
205
128
  self.send_error(HTTPStatus.NOT_FOUND)
206
129
 
@@ -245,63 +168,3 @@ def test_get_job(client):
245
168
  parameters=default_params,
246
169
  meta=None,
247
170
  )
248
-
249
-
250
- def test_run_job(client):
251
- params = {
252
- "foo": "1",
253
- "bar": "a",
254
- "baz": "[2, \"b\"]",
255
- }
256
- j = client.jobs.run("test_job", params, name="My Job")
257
- assert j == Job(
258
- id="1",
259
- name="My Job",
260
- type="test_job",
261
- state="success",
262
- created=default_dt,
263
- started=default_dt,
264
- finished=default_dt,
265
- inputs=IO(params=params),
266
- outputs=IO(),
267
- needs=[],
268
- project_id="1",
269
- workflow_id=None,
270
- assets=[asset_obj],
271
- parameters=params,
272
- meta=None,
273
- )
274
-
275
-
276
- def test_poll_job(client):
277
- params = {
278
- "foo": "1",
279
- "bar": "a",
280
- "baz": "[2, \"b\"]",
281
- }
282
- url = client.jobs.run_status("test_job", params, name="My Job")
283
- assert url == "http://localhost:8000/projects/1/jobs/1/status"
284
-
285
- r = client.jobs.poll(url)
286
- assert Job(**r.json()) == Job(
287
- id="1",
288
- name="My Job",
289
- type="test_job",
290
- state="success",
291
- created=default_dt,
292
- started=default_dt,
293
- finished=default_dt,
294
- inputs=IO(params=params),
295
- outputs=IO(),
296
- needs=[],
297
- project_id="1",
298
- workflow_id=None,
299
- assets=[asset_obj],
300
- parameters=params,
301
- meta=None,
302
- )
303
-
304
-
305
- def test_update_job(client):
306
- j = client.jobs.update("1", name="baz")
307
- assert j.name == "baz"
File without changes
File without changes
File without changes