ionworks-api 0.1.0__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 +3 -0
- ionworks/cell_instance.py +201 -0
- ionworks/cell_measurement.py +339 -0
- ionworks/cell_specification.py +108 -0
- ionworks/client.py +126 -0
- ionworks/errors.py +35 -0
- ionworks/job.py +111 -0
- ionworks/models.py +185 -0
- ionworks/pipeline.py +256 -0
- ionworks/simulation.py +371 -0
- ionworks/validators.py +171 -0
- ionworks_api-0.1.0.dist-info/METADATA +318 -0
- ionworks_api-0.1.0.dist-info/RECORD +14 -0
- ionworks_api-0.1.0.dist-info/WHEEL +4 -0
ionworks/validators.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Reusable validator functions and composable pipelines for inbound/outbound value
|
|
3
|
+
normalization (e.g., converting between pandas DataFrames and dictionaries).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import math
|
|
7
|
+
import pathlib
|
|
8
|
+
from typing import Any, Callable, Iterable
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
import pandas as pd
|
|
12
|
+
import polars as pl
|
|
13
|
+
import pybamm
|
|
14
|
+
from pybamm.expression_tree.operations.serialise import convert_symbol_to_json
|
|
15
|
+
|
|
16
|
+
# --- Atomic validators ------------------------------------------------------ #
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def df_to_dict_validator(v: Any) -> Any:
|
|
20
|
+
"""Convert DataFrame to dict with orient='list' for serialization."""
|
|
21
|
+
if isinstance(v, pd.DataFrame):
|
|
22
|
+
# Replace NaN with None for JSON compatibility
|
|
23
|
+
return v.replace(np.nan, None).to_dict(orient="list")
|
|
24
|
+
elif isinstance(v, pl.DataFrame):
|
|
25
|
+
# Replace NaN with None for JSON compatibility, then convert to dict
|
|
26
|
+
return v.fill_nan(None).to_dict(as_series=False)
|
|
27
|
+
return v
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def dict_to_df_validator(v: Any) -> Any:
|
|
31
|
+
"""Convert dict to DataFrame for data processing."""
|
|
32
|
+
if isinstance(v, dict):
|
|
33
|
+
try:
|
|
34
|
+
return pd.DataFrame(v)
|
|
35
|
+
except ValueError as e:
|
|
36
|
+
if "If using all scalar values, you must pass an index" in str(e):
|
|
37
|
+
# Handle case where all values are scalars by providing an index
|
|
38
|
+
return pd.DataFrame(v, index=[0])
|
|
39
|
+
else:
|
|
40
|
+
raise
|
|
41
|
+
return v
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def parameter_validator(v: Any) -> Any:
|
|
45
|
+
"""Convert pybamm.Symbol values to JSON-serializable form."""
|
|
46
|
+
if isinstance(v, pybamm.Symbol):
|
|
47
|
+
return convert_symbol_to_json(v)
|
|
48
|
+
return v
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def float_sanitizer(v: Any) -> Any:
|
|
52
|
+
"""Sanitize float values to JSON-compatible forms. Currently removes NaN and
|
|
53
|
+
infinity values."""
|
|
54
|
+
if isinstance(v, float):
|
|
55
|
+
if math.isinf(v):
|
|
56
|
+
return "Infinity" if v > 0 else "-Infinity"
|
|
57
|
+
elif np.isnan(v):
|
|
58
|
+
return None
|
|
59
|
+
elif isinstance(v, np.floating):
|
|
60
|
+
if np.isinf(v):
|
|
61
|
+
return "Infinity" if v > 0 else "-Infinity"
|
|
62
|
+
elif np.isnan(v):
|
|
63
|
+
return None
|
|
64
|
+
return v
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def bounds_tuple_validator(v: Any) -> Any:
|
|
68
|
+
"""Convert bounds 2-tuple to list for JSON serialization.
|
|
69
|
+
|
|
70
|
+
Converts tuples with exactly 2 elements to lists. This is useful for
|
|
71
|
+
bounds parameters that may be provided as tuples (lower, upper) but
|
|
72
|
+
need to be serialized as lists.
|
|
73
|
+
|
|
74
|
+
Parameters
|
|
75
|
+
----------
|
|
76
|
+
v : Any
|
|
77
|
+
Value to validate. If it's a tuple with 2 elements, converts to list.
|
|
78
|
+
|
|
79
|
+
Returns
|
|
80
|
+
-------
|
|
81
|
+
Any
|
|
82
|
+
List if input was a 2-tuple, otherwise unchanged.
|
|
83
|
+
"""
|
|
84
|
+
if isinstance(v, tuple) and len(v) == 2:
|
|
85
|
+
return list(v)
|
|
86
|
+
return v
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def file_scheme_validator(v: Any) -> Any:
|
|
90
|
+
"""
|
|
91
|
+
Convert file:// and folder:// scheme paths to serialized dicts.
|
|
92
|
+
|
|
93
|
+
Handles:
|
|
94
|
+
- "file:" prefixed paths: loads CSV file as dict (serialized)
|
|
95
|
+
- "folder:" prefixed paths: loads time_series.csv and steps.csv as dict
|
|
96
|
+
- All other values: returned unchanged
|
|
97
|
+
|
|
98
|
+
Raises
|
|
99
|
+
------
|
|
100
|
+
FileNotFoundError
|
|
101
|
+
If the file or folder path doesn't exist
|
|
102
|
+
Exception
|
|
103
|
+
If reading the CSV file fails for any other reason
|
|
104
|
+
"""
|
|
105
|
+
if isinstance(v, str) and v.startswith("file:"):
|
|
106
|
+
path = pathlib.Path(v.split(":")[1]).expanduser().resolve()
|
|
107
|
+
if not path.exists() or not path.is_file():
|
|
108
|
+
raise FileNotFoundError(f"CSV file not found: {v}")
|
|
109
|
+
return df_to_dict_validator(pd.read_csv(path))
|
|
110
|
+
elif isinstance(v, str) and v.startswith("folder:"):
|
|
111
|
+
path = pathlib.Path(v.split(":")[1]).expanduser().resolve()
|
|
112
|
+
if not path.exists() or not path.is_dir():
|
|
113
|
+
raise FileNotFoundError(f"Folder not found: {v}")
|
|
114
|
+
return {
|
|
115
|
+
"time_series": df_to_dict_validator(pd.read_csv(path / "time_series.csv")),
|
|
116
|
+
"steps": df_to_dict_validator(pd.read_csv(path / "steps.csv")),
|
|
117
|
+
}
|
|
118
|
+
return v
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# --- Pipeline composition helpers ------------------------------------------ #
|
|
122
|
+
|
|
123
|
+
Validator = Callable[[Any], Any]
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _apply_pipeline(value: Any, validators: Iterable[Validator]) -> Any:
|
|
127
|
+
transformed = value
|
|
128
|
+
for validator in validators:
|
|
129
|
+
transformed = validator(transformed)
|
|
130
|
+
return transformed
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _apply_recursive(value: Any, validators: Iterable[Validator]) -> Any:
|
|
134
|
+
if isinstance(value, dict):
|
|
135
|
+
return {key: _apply_recursive(val, validators) for key, val in value.items()}
|
|
136
|
+
if isinstance(value, tuple):
|
|
137
|
+
# Apply validators to tuple first (e.g., to convert bounds tuples to lists)
|
|
138
|
+
transformed = _apply_pipeline(value, validators)
|
|
139
|
+
# If validator converted tuple to list, process recursively
|
|
140
|
+
if isinstance(transformed, list):
|
|
141
|
+
return [_apply_recursive(item, validators) for item in transformed]
|
|
142
|
+
# Otherwise, process tuple items recursively
|
|
143
|
+
return [_apply_recursive(item, validators) for item in transformed]
|
|
144
|
+
if isinstance(value, list):
|
|
145
|
+
return [_apply_recursive(item, validators) for item in value]
|
|
146
|
+
return _apply_pipeline(value, validators)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# --- Public pipelines ------------------------------------------------------- #
|
|
150
|
+
|
|
151
|
+
validators_outbound: list[Validator] = [
|
|
152
|
+
float_sanitizer,
|
|
153
|
+
bounds_tuple_validator,
|
|
154
|
+
file_scheme_validator,
|
|
155
|
+
df_to_dict_validator,
|
|
156
|
+
parameter_validator,
|
|
157
|
+
]
|
|
158
|
+
|
|
159
|
+
validators_inbound: list[Validator] = [
|
|
160
|
+
dict_to_df_validator,
|
|
161
|
+
]
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def run_validators_outbound(v: Any) -> Any:
|
|
165
|
+
"""Recursively apply outbound validators to values and nested containers."""
|
|
166
|
+
return _apply_recursive(v, validators_outbound)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def run_validators_inbound(v: Any) -> Any:
|
|
170
|
+
"""Recursively apply inbound validators to values and nested containers."""
|
|
171
|
+
return _apply_recursive(v, validators_inbound)
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ionworks-api
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client for interacting with the Ionworks API
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: black
|
|
7
|
+
Requires-Dist: iwutil
|
|
8
|
+
Requires-Dist: numpy
|
|
9
|
+
Requires-Dist: pandas
|
|
10
|
+
Requires-Dist: polars
|
|
11
|
+
Requires-Dist: pyarrow
|
|
12
|
+
Requires-Dist: pybamm>=25.10.0
|
|
13
|
+
Requires-Dist: pydantic>=2.6.0
|
|
14
|
+
Requires-Dist: python-dotenv==1.2.1
|
|
15
|
+
Requires-Dist: requests==2.32.5
|
|
16
|
+
Requires-Dist: supabase
|
|
17
|
+
Requires-Dist: types-requests>=2.31.0
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest>=7.0.0; extra == 'dev'
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# Ionworks API Client
|
|
23
|
+
|
|
24
|
+
⚠️ **Warning**: This client is under active development and the API may change without notice.
|
|
25
|
+
|
|
26
|
+
A Python client for interacting with the Ionworks API.
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
1. Clone this repository
|
|
31
|
+
2. Install the package in editable mode:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install -e .
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
3. Get your API key from the [Ionworks account settings](https://app.ionworks.com/dashboard/account) and set it as the `IONWORKS_API_KEY` environment variable (or in a `.env` file).
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
Basic usage example:
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from ionworks import Ionworks
|
|
45
|
+
|
|
46
|
+
# Initialize client (uses IONWORKS_API_KEY from environment/.env file)
|
|
47
|
+
client = Ionworks()
|
|
48
|
+
|
|
49
|
+
# or provide credentials directly
|
|
50
|
+
client = Ionworks(api_key="your_key")
|
|
51
|
+
|
|
52
|
+
# Check API health
|
|
53
|
+
health = client.health_check()
|
|
54
|
+
print(health)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Uploading data to the Ionworks app
|
|
58
|
+
|
|
59
|
+
Uploading data to the Ionworks app follows a three-step process:
|
|
60
|
+
|
|
61
|
+
1. Create a cell spec (or get an existing one)
|
|
62
|
+
2. Create a cell instance with the spec id
|
|
63
|
+
3. Upload measurement(s) with time series data using the instance id
|
|
64
|
+
|
|
65
|
+
Given time series data, the data can be uploaded as follows:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from ionworks import Ionworks
|
|
69
|
+
import pandas as pd
|
|
70
|
+
|
|
71
|
+
client = Ionworks()
|
|
72
|
+
|
|
73
|
+
# Step 1: Create or get a cell specification
|
|
74
|
+
cell_spec = client.cell_spec.create_or_get(
|
|
75
|
+
{
|
|
76
|
+
"name": "NCM622/Graphite Coin Cell",
|
|
77
|
+
"form_factor": "R2032",
|
|
78
|
+
"manufacturer": "Custom Cells",
|
|
79
|
+
"ratings": {
|
|
80
|
+
"capacity": {"value": 0.002, "unit": "A*h"},
|
|
81
|
+
"voltage_min": {"value": 2.5, "unit": "V"},
|
|
82
|
+
"voltage_max": {"value": 4.2, "unit": "V"},
|
|
83
|
+
},
|
|
84
|
+
"cathode": {
|
|
85
|
+
"properties": {"loading": {"value": 12.3, "unit": "mg/cm**2"}},
|
|
86
|
+
"material": {"name": "NCM622", "manufacturer": "BASF"},
|
|
87
|
+
},
|
|
88
|
+
"anode": {
|
|
89
|
+
"properties": {"loading": {"value": 6.5, "unit": "mg/cm**2"}},
|
|
90
|
+
"material": {"name": "Graphite", "manufacturer": "Customcells"},
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Step 2: Create or get a cell instance
|
|
96
|
+
cell_instance = client.cell_instance.create_or_get(
|
|
97
|
+
cell_spec.id,
|
|
98
|
+
{
|
|
99
|
+
"name": "NCM622-GR-001",
|
|
100
|
+
"batch": "BATCH-2024-001",
|
|
101
|
+
"date_manufactured": "2024-01-20",
|
|
102
|
+
"measured_properties": {
|
|
103
|
+
"cathode": {"loading": {"value": 12.1, "unit": "mg/cm**2"}},
|
|
104
|
+
"anode": {"loading": {"value": 6.4, "unit": "mg/cm**2"}},
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Step 3: Upload measurement with time series data
|
|
110
|
+
time_series = pd.DataFrame(
|
|
111
|
+
{
|
|
112
|
+
"Time [s]": [0, 1, 2, 3, 4, 5],
|
|
113
|
+
"Voltage [V]": [3.0, 3.2, 3.5, 3.8, 4.0, 4.2],
|
|
114
|
+
"Current [A]": [0.002, 0.002, 0.002, 0.002, 0.002, 0.002],
|
|
115
|
+
"Step count": [0, 0, 0, 1, 1, 1],
|
|
116
|
+
"Cycle count": [0, 0, 0, 0, 0, 0],
|
|
117
|
+
"Step from cycler": [1, 1, 1, 2, 2, 2],
|
|
118
|
+
"Cycle from cycler": [0, 0, 0, 0, 0, 0],
|
|
119
|
+
}
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
measurement_data = {
|
|
123
|
+
"measurement": {
|
|
124
|
+
"name": "Formation Cycle 1",
|
|
125
|
+
"protocol": {
|
|
126
|
+
"name": "CC-CV charge at C/10 to 4.2V",
|
|
127
|
+
"ambient_temperature_degc": 25,
|
|
128
|
+
},
|
|
129
|
+
"test_setup": {
|
|
130
|
+
"cycler": "Biologic VMP3",
|
|
131
|
+
"operator": "Jane Smith",
|
|
132
|
+
},
|
|
133
|
+
"notes": "Formation cycle - first charge",
|
|
134
|
+
},
|
|
135
|
+
"time_series": time_series,
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
measurement_bundle = client.cell_measurement.create(
|
|
139
|
+
cell_instance.id, measurement_data
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
print(f"Created measurement: {measurement_bundle.measurement.name}")
|
|
143
|
+
print(f"Steps created: {measurement_bundle.steps_created}")
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Reading cell data
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
from ionworks import Ionworks
|
|
150
|
+
|
|
151
|
+
client = Ionworks()
|
|
152
|
+
|
|
153
|
+
# List all cell specifications
|
|
154
|
+
specs = client.cell_spec.list()
|
|
155
|
+
for spec in specs[:5]:
|
|
156
|
+
print(f" - {spec.name} (form_factor: {spec.form_factor})")
|
|
157
|
+
|
|
158
|
+
# Get a specific cell spec with full nested data
|
|
159
|
+
full_spec = client.cell_spec.get(spec_id)
|
|
160
|
+
print(f"Capacity: {full_spec.ratings['capacity']['value']} "
|
|
161
|
+
f"{full_spec.ratings['capacity']['unit']}")
|
|
162
|
+
|
|
163
|
+
# Get cell instance by slug
|
|
164
|
+
instance = client.cell_instance.get_by_slug("ncm622-gr-001")
|
|
165
|
+
|
|
166
|
+
# List measurements for an instance
|
|
167
|
+
measurements = client.cell_measurement.list(instance.id)
|
|
168
|
+
|
|
169
|
+
# Get measurement detail with time series
|
|
170
|
+
measurement_detail = client.cell_measurement.detail(measurement_id)
|
|
171
|
+
print(f"Time series shape: {measurement_detail.time_series.shape}")
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Running pipelines
|
|
175
|
+
|
|
176
|
+
Pipelines allow you to run complex workflows combining data fitting, calculations, and validations. A pipeline consists of named elements, where each element has an `element_type` and configuration specific to that type.
|
|
177
|
+
|
|
178
|
+
**Recommended workflow:** First upload your experimental data using the cell measurement API (see "Uploading data to the Ionworks app" above), then reference it in your pipeline using the `db:<measurement_id>` format. This ensures your data is stored and versioned in the database.
|
|
179
|
+
|
|
180
|
+
Available element types:
|
|
181
|
+
|
|
182
|
+
- `entry`: Provide initial parameter values
|
|
183
|
+
- `data_fit`: Fit model parameters to experimental data
|
|
184
|
+
- `calculation`: Run calculations (e.g., OCP fitting)
|
|
185
|
+
- `validation`: Validate model against data
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
import time
|
|
189
|
+
from ionworks import Ionworks
|
|
190
|
+
|
|
191
|
+
client = Ionworks()
|
|
192
|
+
|
|
193
|
+
# First, upload your data (see "Uploading data to the Ionworks app" section)
|
|
194
|
+
# Then get the measurement ID to reference in the pipeline
|
|
195
|
+
measurements = client.cell_measurement.list(cell_instance_id)
|
|
196
|
+
measurement_id = measurements[0].id # or find the specific measurement you need
|
|
197
|
+
|
|
198
|
+
# Define entry configuration with initial parameter values
|
|
199
|
+
entry_config = {
|
|
200
|
+
"values": {
|
|
201
|
+
"Negative particle diffusivity [m2.s-1]": 3.3e-14,
|
|
202
|
+
"Positive particle diffusivity [m2.s-1]": 4e-15,
|
|
203
|
+
# ... other parameters
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
# Define datafit configuration - reference uploaded data with db:<measurement_id>
|
|
208
|
+
datafit_config = {
|
|
209
|
+
"objectives": {
|
|
210
|
+
"test_1C": {
|
|
211
|
+
"objective": "CurrentDriven",
|
|
212
|
+
"model": {"type": "SPMe"},
|
|
213
|
+
"data": f"db:{measurement_id}", # Reference data from database
|
|
214
|
+
"custom_parameters": {"Ambient temperature [K]": "initial_temperature"},
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
"parameters": {
|
|
218
|
+
"Negative particle diffusivity [m2.s-1]": {
|
|
219
|
+
"bounds": [1e-14, 1e-13],
|
|
220
|
+
"initial_value": 2e-14,
|
|
221
|
+
},
|
|
222
|
+
"Positive particle diffusivity [m2.s-1]": {
|
|
223
|
+
"bounds": [1e-15, 1e-14],
|
|
224
|
+
"initial_value": 2e-15,
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
"cost": {"type": "RMSE"},
|
|
228
|
+
"optimizer": {"type": "ScipyDifferentialEvolution"},
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
# Create pipeline config with named elements
|
|
232
|
+
pipeline_config = {
|
|
233
|
+
"elements": {
|
|
234
|
+
"entry": {**entry_config, "element_type": "entry"},
|
|
235
|
+
"fit data": {**datafit_config, "element_type": "data_fit"},
|
|
236
|
+
},
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
# Submit pipeline
|
|
240
|
+
pipeline = client.pipeline.create(pipeline_config)
|
|
241
|
+
print(f"Pipeline submitted: {pipeline.id}")
|
|
242
|
+
|
|
243
|
+
# Poll for completion
|
|
244
|
+
while True:
|
|
245
|
+
pipeline = client.pipeline.get(pipeline.id)
|
|
246
|
+
print(f"Status: {pipeline.status}")
|
|
247
|
+
if pipeline.status == "completed":
|
|
248
|
+
result = client.pipeline.result(pipeline.id)
|
|
249
|
+
print("Fitted parameters:", result.element_results["fit data"])
|
|
250
|
+
break
|
|
251
|
+
elif pipeline.status == "failed":
|
|
252
|
+
print("Pipeline failed:", pipeline.error)
|
|
253
|
+
break
|
|
254
|
+
time.sleep(2)
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Running simulations
|
|
258
|
+
|
|
259
|
+
The client supports running battery simulations using the Universal Cycler Protocol (UCP) format:
|
|
260
|
+
|
|
261
|
+
```python
|
|
262
|
+
from ionworks import Ionworks
|
|
263
|
+
|
|
264
|
+
client = Ionworks()
|
|
265
|
+
|
|
266
|
+
# Define a charge/discharge protocol in YAML format
|
|
267
|
+
protocol_yaml = """global:
|
|
268
|
+
initial_state_type: soc_percentage
|
|
269
|
+
initial_state_value: 50
|
|
270
|
+
initial_temperature: 25.0
|
|
271
|
+
steps:
|
|
272
|
+
- Charge:
|
|
273
|
+
mode: C-rate
|
|
274
|
+
value: "0.6"
|
|
275
|
+
ends:
|
|
276
|
+
- Voltage > 4.2
|
|
277
|
+
- Rest:
|
|
278
|
+
duration: 3600
|
|
279
|
+
- Discharge:
|
|
280
|
+
mode: C-rate
|
|
281
|
+
value: "0.5"
|
|
282
|
+
ends:
|
|
283
|
+
- Voltage < 2.5
|
|
284
|
+
"""
|
|
285
|
+
|
|
286
|
+
# Create simulation with quick model
|
|
287
|
+
config = {
|
|
288
|
+
"parameterized_model": {
|
|
289
|
+
"quick_model": {"capacity": 1.0, "chemistry": "NMC/Graphite"}
|
|
290
|
+
},
|
|
291
|
+
"protocol_experiment": {
|
|
292
|
+
"protocol": protocol_yaml,
|
|
293
|
+
"name": "NMC Charge Discharge Protocol",
|
|
294
|
+
},
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
result = client.simulation.protocol(config)
|
|
298
|
+
print(f"Simulation ID: {result.simulation_id}")
|
|
299
|
+
|
|
300
|
+
# Wait for completion
|
|
301
|
+
simulation = client.simulation.wait_for_completion(
|
|
302
|
+
result.simulation_id, timeout=60, poll_interval=2
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
# Get results
|
|
306
|
+
simulation_data = client.simulation.get_result(result.simulation_id)
|
|
307
|
+
time_series = simulation_data.get("time_series", {})
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
## Error Handling
|
|
311
|
+
|
|
312
|
+
The client will raise exceptions in the following cases:
|
|
313
|
+
|
|
314
|
+
- Missing API credentials
|
|
315
|
+
- Invalid API credentials
|
|
316
|
+
- API request errors (will raise `IonworksError` with details)
|
|
317
|
+
|
|
318
|
+
Make sure to handle these exceptions appropriately in your code.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
ionworks/__init__.py,sha256=OCNLH4VyRyH8SVjrNf-XmeltpvDBcAqwSWVJ2y2YMcY,85
|
|
2
|
+
ionworks/cell_instance.py,sha256=BGssFTos_U4EBVmbTRr-WjXmL5ZGu57c0FweEzM_QB4,7649
|
|
3
|
+
ionworks/cell_measurement.py,sha256=DfcgQMMs6K9Rc0SFjXgC3jWo-nQ-Wff_Bk_1Qys2lAo,11918
|
|
4
|
+
ionworks/cell_specification.py,sha256=mr_H3ppFWoEB4bwQb6SaX-OgZdACw2JEJvrfIKV_d8o,3944
|
|
5
|
+
ionworks/client.py,sha256=PPX2TGrKC64phNKglZJvJPFJl8oEB5qrmsre8h27ltI,4842
|
|
6
|
+
ionworks/errors.py,sha256=9aTLnXgusWnRaegkWMsfXj4EDnqQ8vbvv0C_QAzSWKU,972
|
|
7
|
+
ionworks/job.py,sha256=0ZokfwjhX4kseM3dl3CPKB5jr6-MzYuNJU4DUn7bck8,3505
|
|
8
|
+
ionworks/models.py,sha256=DIm69Q_9GuR-XVMvhyvqhrZQuWVB6puHb_Tr1R7a7D8,4581
|
|
9
|
+
ionworks/pipeline.py,sha256=_nDHQRIrc_g-k_OnrkFnIyOVNlwlMZgrppB-9tiLPO0,8403
|
|
10
|
+
ionworks/simulation.py,sha256=ZHbIAz4UVTloUismjNSzAqzzKOaF_QijZtAKuWc0lIg,13860
|
|
11
|
+
ionworks/validators.py,sha256=84hIefvXYvndkezEgnnxxmFeyLvCSr3d0wcbX9wRAWM,5728
|
|
12
|
+
ionworks_api-0.1.0.dist-info/METADATA,sha256=hNFyvwrWXLf9nnYdbgeoRy1k6lPR75sEathLS7NNqCU,9235
|
|
13
|
+
ionworks_api-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
14
|
+
ionworks_api-0.1.0.dist-info/RECORD,,
|