ionworks-api 0.1.0__py3-none-any.whl → 0.1.3__py3-none-any.whl
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.
- ionworks/__init__.py +27 -2
- ionworks/cell_instance.py +54 -48
- ionworks/cell_measurement.py +95 -58
- ionworks/cell_specification.py +106 -19
- ionworks/client.py +37 -8
- ionworks/errors.py +22 -5
- ionworks/job.py +90 -36
- ionworks/models.py +29 -37
- ionworks/pipeline.py +113 -4
- ionworks/simulation.py +127 -74
- ionworks/validators.py +553 -32
- {ionworks_api-0.1.0.dist-info → ionworks_api-0.1.3.dist-info}/METADATA +36 -17
- ionworks_api-0.1.3.dist-info/RECORD +15 -0
- ionworks_api-0.1.3.dist-info/licenses/LICENSE.md +21 -0
- ionworks_api-0.1.0.dist-info/RECORD +0 -14
- {ionworks_api-0.1.0.dist-info → ionworks_api-0.1.3.dist-info}/WHEEL +0 -0
ionworks/pipeline.py
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pipeline client for running parameterization workflows.
|
|
3
|
+
|
|
4
|
+
This module provides the :class:`PipelineClient` for creating and managing
|
|
5
|
+
pipelines that combine data fitting, calculations, and validation steps
|
|
6
|
+
for battery model parameterization.
|
|
7
|
+
"""
|
|
8
|
+
|
|
1
9
|
import os
|
|
2
10
|
import re
|
|
11
|
+
import time
|
|
3
12
|
from typing import Any
|
|
4
13
|
|
|
5
14
|
from pydantic import (
|
|
@@ -20,6 +29,8 @@ def _prepare_payload(data: Any) -> Any:
|
|
|
20
29
|
|
|
21
30
|
|
|
22
31
|
class DataFitConfig(BaseModel):
|
|
32
|
+
"""Configuration for data fitting step in a pipeline."""
|
|
33
|
+
|
|
23
34
|
objectives: dict[str, Any]
|
|
24
35
|
parameters: dict[str, Any]
|
|
25
36
|
cost: dict[str, Any] | None = None
|
|
@@ -28,14 +39,20 @@ class DataFitConfig(BaseModel):
|
|
|
28
39
|
|
|
29
40
|
|
|
30
41
|
class EntryConfig(BaseModel):
|
|
42
|
+
"""Configuration for entry point in a pipeline."""
|
|
43
|
+
|
|
31
44
|
values: dict[str, Any]
|
|
32
45
|
|
|
33
46
|
|
|
34
47
|
class BuiltInEntryConfig(BaseModel):
|
|
48
|
+
"""Configuration for built-in entry point in a pipeline."""
|
|
49
|
+
|
|
35
50
|
name: str
|
|
36
51
|
|
|
37
52
|
|
|
38
53
|
class CalculationConfig(BaseModel):
|
|
54
|
+
"""Configuration for calculation step in a pipeline."""
|
|
55
|
+
|
|
39
56
|
calculation: str
|
|
40
57
|
electrode: str | None = None
|
|
41
58
|
method: str | None = None
|
|
@@ -43,12 +60,16 @@ class CalculationConfig(BaseModel):
|
|
|
43
60
|
|
|
44
61
|
|
|
45
62
|
class ValidationConfig(BaseModel):
|
|
63
|
+
"""Configuration for validation step in a pipeline."""
|
|
64
|
+
|
|
46
65
|
objectives: dict[str, Any]
|
|
47
66
|
summary_stats: list[Any]
|
|
48
67
|
existing_parameters: dict[str, Any] | None = None
|
|
49
68
|
|
|
50
69
|
|
|
51
70
|
class PipelineConfig(BaseModel):
|
|
71
|
+
"""Configuration for a complete pipeline workflow."""
|
|
72
|
+
|
|
52
73
|
project_id: str | None = Field(
|
|
53
74
|
default=None,
|
|
54
75
|
description="The project id to submit the pipeline to. "
|
|
@@ -108,23 +129,33 @@ class PipelineConfig(BaseModel):
|
|
|
108
129
|
|
|
109
130
|
|
|
110
131
|
class DataFitResponse(BaseModel):
|
|
132
|
+
"""Response from a data fitting step containing fitted parameters."""
|
|
133
|
+
|
|
111
134
|
parameter_values: dict[str, Any]
|
|
112
135
|
|
|
113
136
|
|
|
114
137
|
class CalculationResponse(BaseModel):
|
|
138
|
+
"""Response from a calculation step containing calculated parameters."""
|
|
139
|
+
|
|
115
140
|
parameter_values: dict[str, Any]
|
|
116
141
|
|
|
117
142
|
|
|
118
143
|
class ValidationResponse(BaseModel):
|
|
144
|
+
"""Response from a validation step containing validation results."""
|
|
145
|
+
|
|
119
146
|
validation_results: dict[str, Any]
|
|
120
147
|
summary_stats: dict[str, list[Any]]
|
|
121
148
|
|
|
122
149
|
|
|
123
150
|
class EntryResponse(BaseModel):
|
|
151
|
+
"""Response from an entry point containing parameter values."""
|
|
152
|
+
|
|
124
153
|
parameter_values: dict[str, Any]
|
|
125
154
|
|
|
126
155
|
|
|
127
156
|
class PipelineSubmissionResponse(BaseModel):
|
|
157
|
+
"""Response from submitting a pipeline to the API."""
|
|
158
|
+
|
|
128
159
|
id: str
|
|
129
160
|
name: str
|
|
130
161
|
description: str | None = None
|
|
@@ -133,12 +164,23 @@ class PipelineSubmissionResponse(BaseModel):
|
|
|
133
164
|
|
|
134
165
|
|
|
135
166
|
class PipelineResponse(BaseModel):
|
|
167
|
+
"""Complete response from retrieving pipeline results."""
|
|
168
|
+
|
|
136
169
|
result: dict[str, Any]
|
|
137
170
|
element_results: dict[str, Any]
|
|
138
171
|
|
|
139
172
|
|
|
140
173
|
class PipelineClient:
|
|
141
|
-
|
|
174
|
+
"""Client for creating and managing pipeline workflows."""
|
|
175
|
+
|
|
176
|
+
def __init__(self, client: Any) -> None:
|
|
177
|
+
"""Initialize the pipeline client.
|
|
178
|
+
|
|
179
|
+
Parameters
|
|
180
|
+
----------
|
|
181
|
+
client : Any
|
|
182
|
+
The HTTP client to use for API requests.
|
|
183
|
+
"""
|
|
142
184
|
self.client = client
|
|
143
185
|
|
|
144
186
|
def create(self, config: dict[str, Any]) -> PipelineSubmissionResponse:
|
|
@@ -189,19 +231,29 @@ class PipelineClient:
|
|
|
189
231
|
# Re-raise original error for other cases
|
|
190
232
|
raise
|
|
191
233
|
|
|
192
|
-
def list(
|
|
234
|
+
def list(
|
|
235
|
+
self, project_id: str | None = None, limit: int | None = None
|
|
236
|
+
) -> list[PipelineSubmissionResponse]:
|
|
193
237
|
"""List all pipelines.
|
|
194
238
|
|
|
195
239
|
Parameters
|
|
196
240
|
----------
|
|
197
|
-
project_id : str
|
|
198
|
-
The project id to filter pipelines. If not provided,
|
|
241
|
+
project_id : str | None
|
|
242
|
+
The project id to filter pipelines. If not provided, uses
|
|
199
243
|
PROJECT_ID environment variable.
|
|
244
|
+
limit : int | None
|
|
245
|
+
Maximum number of pipelines to return. If not provided, returns
|
|
246
|
+
all pipelines (up to the API's default limit).
|
|
200
247
|
|
|
201
248
|
Returns
|
|
202
249
|
-------
|
|
203
250
|
list[PipelineSubmissionResponse]
|
|
204
251
|
List of pipeline submission responses.
|
|
252
|
+
|
|
253
|
+
Raises
|
|
254
|
+
------
|
|
255
|
+
ValueError
|
|
256
|
+
If response data is not a list or project_id is missing.
|
|
205
257
|
"""
|
|
206
258
|
if project_id is None:
|
|
207
259
|
project_id = os.getenv("PROJECT_ID")
|
|
@@ -212,6 +264,8 @@ class PipelineClient:
|
|
|
212
264
|
)
|
|
213
265
|
|
|
214
266
|
endpoint = f"/pipelines?project_id={project_id}"
|
|
267
|
+
if limit is not None:
|
|
268
|
+
endpoint += f"&limit={limit}"
|
|
215
269
|
try:
|
|
216
270
|
response_data = self.client.get(endpoint)
|
|
217
271
|
if not isinstance(response_data, list):
|
|
@@ -254,3 +308,58 @@ class PipelineClient:
|
|
|
254
308
|
"""
|
|
255
309
|
response_data = self.client.get(f"/pipelines/{job_id}/result")
|
|
256
310
|
return PipelineResponse(**response_data)
|
|
311
|
+
|
|
312
|
+
def wait_for_completion(
|
|
313
|
+
self,
|
|
314
|
+
pipeline_id: str,
|
|
315
|
+
timeout: int = 600,
|
|
316
|
+
poll_interval: int = 2,
|
|
317
|
+
verbose: bool = True,
|
|
318
|
+
) -> PipelineSubmissionResponse:
|
|
319
|
+
"""Wait for a pipeline to complete by polling until done or timeout.
|
|
320
|
+
|
|
321
|
+
Parameters
|
|
322
|
+
----------
|
|
323
|
+
pipeline_id : str
|
|
324
|
+
The pipeline ID to wait for.
|
|
325
|
+
timeout : int, optional
|
|
326
|
+
Maximum time to wait in seconds (default: 600).
|
|
327
|
+
poll_interval : int, optional
|
|
328
|
+
Time between polls in seconds (default: 2).
|
|
329
|
+
verbose : bool, optional
|
|
330
|
+
Whether to print status updates (default: True).
|
|
331
|
+
|
|
332
|
+
Returns
|
|
333
|
+
-------
|
|
334
|
+
PipelineSubmissionResponse
|
|
335
|
+
The completed or failed pipeline response.
|
|
336
|
+
|
|
337
|
+
Raises
|
|
338
|
+
------
|
|
339
|
+
TimeoutError
|
|
340
|
+
If timeout is reached before the pipeline completes.
|
|
341
|
+
"""
|
|
342
|
+
deadline = time.time() + timeout
|
|
343
|
+
pipeline = self.get(pipeline_id)
|
|
344
|
+
|
|
345
|
+
if verbose:
|
|
346
|
+
print(f"Polling pipeline {pipeline_id} for completion...")
|
|
347
|
+
|
|
348
|
+
while pipeline.status not in ("completed", "failed"):
|
|
349
|
+
if time.time() >= deadline:
|
|
350
|
+
raise TimeoutError(
|
|
351
|
+
f"Pipeline {pipeline_id} did not complete within "
|
|
352
|
+
f"{timeout} seconds (status: {pipeline.status})"
|
|
353
|
+
)
|
|
354
|
+
time.sleep(poll_interval)
|
|
355
|
+
pipeline = self.get(pipeline_id)
|
|
356
|
+
if verbose:
|
|
357
|
+
elapsed = int(timeout - (deadline - time.time()))
|
|
358
|
+
print(f" Status: {pipeline.status} (elapsed: {elapsed}s)")
|
|
359
|
+
|
|
360
|
+
if verbose:
|
|
361
|
+
print(f"Pipeline finished with status: {pipeline.status}")
|
|
362
|
+
if pipeline.status == "failed" and pipeline.error:
|
|
363
|
+
print(f" Error: {pipeline.error}")
|
|
364
|
+
|
|
365
|
+
return pipeline
|
ionworks/simulation.py
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Simulation client for running battery simulations.
|
|
3
|
+
|
|
4
|
+
This module provides the :class:`SimulationClient` for running battery
|
|
5
|
+
simulations using the Universal Cycler Protocol (UCP) format. It supports
|
|
6
|
+
single simulations, batch simulations with design of experiments (DOE),
|
|
7
|
+
and PyBaMM-based modeling.
|
|
8
|
+
"""
|
|
9
|
+
|
|
1
10
|
from __future__ import annotations
|
|
2
11
|
|
|
3
12
|
from datetime import datetime, timedelta
|
|
4
13
|
import time
|
|
5
|
-
from typing import Any,
|
|
14
|
+
from typing import Any, cast
|
|
6
15
|
|
|
7
16
|
from pydantic import BaseModel, Field, ValidationError
|
|
17
|
+
import requests
|
|
18
|
+
|
|
19
|
+
from .errors import IonworksError
|
|
8
20
|
|
|
9
21
|
|
|
10
22
|
class QuickModelConfig(BaseModel):
|
|
@@ -129,7 +141,14 @@ class SimulationResponse(BaseModel):
|
|
|
129
141
|
class SimulationClient:
|
|
130
142
|
"""Client for running simulations."""
|
|
131
143
|
|
|
132
|
-
def __init__(self, client: Any):
|
|
144
|
+
def __init__(self, client: Any) -> None:
|
|
145
|
+
"""Initialize the SimulationClient.
|
|
146
|
+
|
|
147
|
+
Parameters
|
|
148
|
+
----------
|
|
149
|
+
client : Any
|
|
150
|
+
The HTTP client instance for making API requests.
|
|
151
|
+
"""
|
|
133
152
|
self.client = client
|
|
134
153
|
|
|
135
154
|
def protocol(self, config: dict[str, Any]) -> SimulationResponse:
|
|
@@ -139,16 +158,18 @@ class SimulationClient:
|
|
|
139
158
|
----------
|
|
140
159
|
config : dict[str, Any]
|
|
141
160
|
Configuration dictionary containing:
|
|
142
|
-
|
|
143
|
-
-
|
|
144
|
-
|
|
145
|
-
-
|
|
146
|
-
|
|
161
|
+
|
|
162
|
+
- parameterized_model: quick_model dict, full model dict,
|
|
163
|
+
or model ID string
|
|
164
|
+
- protocol_experiment: ProtocolExperimentConfig dict with
|
|
165
|
+
protocol and name fields
|
|
166
|
+
- experiment_parameters: Optional dict with initial_soc and
|
|
167
|
+
initial_temperature
|
|
147
168
|
- design_parameters: Optional dict[str, float]
|
|
148
169
|
- max_backward_jumps: Optional int
|
|
149
170
|
- study_id: Optional str
|
|
150
|
-
- extra_variables: Optional list[str]
|
|
151
|
-
in simulation output
|
|
171
|
+
- extra_variables: Optional list[str] — extra variables to
|
|
172
|
+
include in simulation output
|
|
152
173
|
|
|
153
174
|
Returns
|
|
154
175
|
-------
|
|
@@ -177,15 +198,17 @@ class SimulationClient:
|
|
|
177
198
|
----------
|
|
178
199
|
config : dict[str, Any]
|
|
179
200
|
Configuration dictionary containing:
|
|
180
|
-
|
|
181
|
-
-
|
|
182
|
-
|
|
201
|
+
|
|
202
|
+
- parameterized_model: quick_model dict, full model dict,
|
|
203
|
+
or model ID string
|
|
204
|
+
- protocol_experiment: ProtocolExperimentConfig dict with
|
|
205
|
+
protocol and name fields
|
|
183
206
|
- design_parameters_doe: DesignParametersDOE dict
|
|
184
|
-
- experiment_parameters: Optional
|
|
207
|
+
- experiment_parameters: Optional dict
|
|
185
208
|
- max_backward_jumps: Optional int
|
|
186
209
|
- study_id: Optional str
|
|
187
|
-
- extra_variables: Optional list[str]
|
|
188
|
-
in simulation output
|
|
210
|
+
- extra_variables: Optional list[str] — extra variables to
|
|
211
|
+
include in simulation output
|
|
189
212
|
|
|
190
213
|
Returns
|
|
191
214
|
-------
|
|
@@ -204,10 +227,11 @@ class SimulationClient:
|
|
|
204
227
|
endpoint, json_payload=validated_config.model_dump(exclude_none=True)
|
|
205
228
|
)
|
|
206
229
|
if not isinstance(response_data, list):
|
|
207
|
-
|
|
230
|
+
msg = (
|
|
208
231
|
f"Unexpected response format from {endpoint}: expected a "
|
|
209
232
|
f"list, got {type(response_data).__name__}"
|
|
210
233
|
)
|
|
234
|
+
raise ValueError(msg)
|
|
211
235
|
return [SimulationResponse(**item) for item in response_data]
|
|
212
236
|
except ValidationError as e:
|
|
213
237
|
raise ValueError(
|
|
@@ -225,10 +249,11 @@ class SimulationClient:
|
|
|
225
249
|
endpoint = "/simulations"
|
|
226
250
|
response_data = self.client.get(endpoint)
|
|
227
251
|
if not isinstance(response_data, list):
|
|
228
|
-
|
|
252
|
+
msg = (
|
|
229
253
|
f"Unexpected response format from {endpoint}: expected a list, "
|
|
230
254
|
f"got {type(response_data).__name__}"
|
|
231
255
|
)
|
|
256
|
+
raise ValueError(msg)
|
|
232
257
|
return response_data
|
|
233
258
|
|
|
234
259
|
def get(self, simulation_id: str) -> dict[str, Any]:
|
|
@@ -273,99 +298,127 @@ class SimulationClient:
|
|
|
273
298
|
response_data = self.client.get(endpoint)
|
|
274
299
|
return cast(dict[str, Any], response_data)
|
|
275
300
|
|
|
276
|
-
def
|
|
301
|
+
def _poll_simulations(
|
|
277
302
|
self,
|
|
278
|
-
|
|
279
|
-
timeout: int
|
|
280
|
-
poll_interval: int
|
|
281
|
-
verbose: bool
|
|
282
|
-
) -> dict[str,
|
|
283
|
-
"""
|
|
303
|
+
simulation_ids: list[str],
|
|
304
|
+
timeout: int,
|
|
305
|
+
poll_interval: int,
|
|
306
|
+
verbose: bool,
|
|
307
|
+
) -> dict[str, dict[str, Any]]:
|
|
308
|
+
"""Poll simulations until all complete or timeout is reached.
|
|
284
309
|
|
|
285
310
|
Parameters
|
|
286
311
|
----------
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
timeout : int
|
|
290
|
-
Maximum time to wait in seconds
|
|
291
|
-
poll_interval : int
|
|
292
|
-
Time between polls in seconds
|
|
293
|
-
verbose : bool
|
|
294
|
-
Whether to print status updates
|
|
312
|
+
simulation_ids : list[str]
|
|
313
|
+
List of simulation IDs to poll.
|
|
314
|
+
timeout : int
|
|
315
|
+
Maximum time to wait in seconds.
|
|
316
|
+
poll_interval : int
|
|
317
|
+
Time between polls in seconds.
|
|
318
|
+
verbose : bool
|
|
319
|
+
Whether to print status updates.
|
|
295
320
|
|
|
296
321
|
Returns
|
|
297
322
|
-------
|
|
298
|
-
dict[str,
|
|
299
|
-
|
|
300
|
-
list of dicts if list of IDs provided. Only returns completed
|
|
301
|
-
simulations if timeout is reached.
|
|
323
|
+
dict[str, dict[str, Any]]
|
|
324
|
+
Dict mapping simulation IDs to their completed result dicts.
|
|
302
325
|
|
|
303
326
|
Raises
|
|
304
327
|
------
|
|
305
328
|
TimeoutError
|
|
306
|
-
If
|
|
329
|
+
If no simulations complete within the timeout.
|
|
307
330
|
"""
|
|
308
|
-
is_single = isinstance(simulation_id, str)
|
|
309
|
-
if is_single:
|
|
310
|
-
simulation_ids: List[str] = [simulation_id] # type: ignore[list-item]
|
|
311
|
-
else:
|
|
312
|
-
simulation_ids = simulation_id # type: ignore[assignment]
|
|
313
331
|
timeout_delta = timedelta(seconds=timeout)
|
|
314
332
|
start_time = datetime.now()
|
|
315
|
-
|
|
333
|
+
completed: dict[str, dict[str, Any]] = {}
|
|
316
334
|
|
|
317
335
|
if verbose:
|
|
318
336
|
print(f"Polling for {len(simulation_ids)} simulation(s) completion...")
|
|
319
337
|
|
|
320
338
|
while datetime.now() - start_time < timeout_delta:
|
|
321
|
-
completed_count = len(completed_simulations)
|
|
322
339
|
for sim_id in simulation_ids:
|
|
323
|
-
if sim_id in
|
|
340
|
+
if sim_id in completed:
|
|
324
341
|
continue
|
|
325
342
|
try:
|
|
326
343
|
simulation = self.get(sim_id)
|
|
327
|
-
if
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
344
|
+
if (
|
|
345
|
+
simulation.get("storage_folder")
|
|
346
|
+
or simulation.get("simulation_data") # Legacy fallback
|
|
347
|
+
):
|
|
348
|
+
completed[sim_id] = simulation
|
|
349
|
+
except (IonworksError, requests.exceptions.RequestException):
|
|
350
|
+
pass # Continue polling on transient errors
|
|
333
351
|
|
|
334
352
|
elapsed = (datetime.now() - start_time).seconds
|
|
335
353
|
if verbose:
|
|
336
354
|
print(
|
|
337
|
-
f" Status: {
|
|
355
|
+
f" Status: {len(completed)}/{len(simulation_ids)} completed "
|
|
338
356
|
f"(elapsed: {elapsed}s)"
|
|
339
357
|
)
|
|
340
358
|
|
|
341
|
-
if len(
|
|
359
|
+
if len(completed) == len(simulation_ids):
|
|
342
360
|
if verbose:
|
|
343
361
|
print("All simulations completed!")
|
|
344
|
-
|
|
362
|
+
return completed
|
|
345
363
|
|
|
346
364
|
time.sleep(poll_interval)
|
|
347
|
-
else:
|
|
348
|
-
# Timeout reached
|
|
349
|
-
if verbose:
|
|
350
|
-
print(
|
|
351
|
-
f"Timeout: Only {len(completed_simulations)}/{len(simulation_ids)} "
|
|
352
|
-
f"simulations completed within {timeout} seconds"
|
|
353
|
-
)
|
|
354
|
-
if not completed_simulations:
|
|
355
|
-
raise TimeoutError(f"No simulations completed within {timeout} seconds")
|
|
356
365
|
|
|
357
|
-
#
|
|
366
|
+
# Timeout reached
|
|
367
|
+
if verbose:
|
|
368
|
+
print(
|
|
369
|
+
f"Timeout: Only {len(completed)}/{len(simulation_ids)} "
|
|
370
|
+
f"simulations completed within {timeout} seconds"
|
|
371
|
+
)
|
|
372
|
+
if not completed:
|
|
373
|
+
msg = f"No simulations completed within {timeout} seconds"
|
|
374
|
+
raise TimeoutError(msg)
|
|
375
|
+
return completed
|
|
376
|
+
|
|
377
|
+
def wait_for_completion(
|
|
378
|
+
self,
|
|
379
|
+
simulation_id: str | list[str],
|
|
380
|
+
timeout: int = 60,
|
|
381
|
+
poll_interval: int = 2,
|
|
382
|
+
verbose: bool = True,
|
|
383
|
+
) -> dict[str, Any] | list[dict[str, Any]]:
|
|
384
|
+
"""Wait for simulation(s) to complete by polling until done or timeout.
|
|
385
|
+
|
|
386
|
+
Parameters
|
|
387
|
+
----------
|
|
388
|
+
simulation_id : str | list[str]
|
|
389
|
+
Single simulation ID or list of simulation IDs to wait for.
|
|
390
|
+
timeout : int
|
|
391
|
+
Maximum time to wait in seconds (default: 60).
|
|
392
|
+
poll_interval : int
|
|
393
|
+
Time between polls in seconds (default: 2).
|
|
394
|
+
verbose : bool
|
|
395
|
+
Whether to print status updates (default: True).
|
|
396
|
+
|
|
397
|
+
Returns
|
|
398
|
+
-------
|
|
399
|
+
dict[str, Any] | list[dict[str, Any]]
|
|
400
|
+
Completed simulation(s). Returns single dict if single ID
|
|
401
|
+
provided, list of dicts if list of IDs provided. Only returns
|
|
402
|
+
completed simulations if timeout is reached.
|
|
403
|
+
|
|
404
|
+
Raises
|
|
405
|
+
------
|
|
406
|
+
TimeoutError
|
|
407
|
+
If timeout is reached before all simulations complete.
|
|
408
|
+
"""
|
|
409
|
+
is_single = isinstance(simulation_id, str)
|
|
410
|
+
simulation_ids = [simulation_id] if is_single else simulation_id # type: ignore[list-item]
|
|
411
|
+
|
|
412
|
+
completed = self._poll_simulations(
|
|
413
|
+
simulation_ids, timeout, poll_interval, verbose
|
|
414
|
+
)
|
|
415
|
+
|
|
358
416
|
if is_single:
|
|
359
|
-
if simulation_ids[0] not in
|
|
360
|
-
|
|
417
|
+
if simulation_ids[0] not in completed:
|
|
418
|
+
msg = (
|
|
361
419
|
f"Simulation {simulation_ids[0]} did not complete within "
|
|
362
420
|
f"{timeout} seconds"
|
|
363
421
|
)
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
return [
|
|
368
|
-
completed_simulations[sim_id]
|
|
369
|
-
for sim_id in simulation_ids
|
|
370
|
-
if sim_id in completed_simulations
|
|
371
|
-
]
|
|
422
|
+
raise TimeoutError(msg)
|
|
423
|
+
return completed[simulation_ids[0]]
|
|
424
|
+
return [completed[sim_id] for sim_id in simulation_ids if sim_id in completed]
|