ionworks-api 0.1.0__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.
@@ -0,0 +1,10 @@
1
+ .mypy_cache
2
+ .pytest_cache
3
+ .venv
4
+ .env*
5
+ .ruff_cache
6
+ .vscode
7
+ __pycache__
8
+ .DS_Store
9
+ .idea
10
+ .local
@@ -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,297 @@
1
+ # Ionworks API Client
2
+
3
+ ⚠️ **Warning**: This client is under active development and the API may change without notice.
4
+
5
+ A Python client for interacting with the Ionworks API.
6
+
7
+ ## Installation
8
+
9
+ 1. Clone this repository
10
+ 2. Install the package in editable mode:
11
+
12
+ ```bash
13
+ pip install -e .
14
+ ```
15
+
16
+ 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).
17
+
18
+ ## Usage
19
+
20
+ Basic usage example:
21
+
22
+ ```python
23
+ from ionworks import Ionworks
24
+
25
+ # Initialize client (uses IONWORKS_API_KEY from environment/.env file)
26
+ client = Ionworks()
27
+
28
+ # or provide credentials directly
29
+ client = Ionworks(api_key="your_key")
30
+
31
+ # Check API health
32
+ health = client.health_check()
33
+ print(health)
34
+ ```
35
+
36
+ ### Uploading data to the Ionworks app
37
+
38
+ Uploading data to the Ionworks app follows a three-step process:
39
+
40
+ 1. Create a cell spec (or get an existing one)
41
+ 2. Create a cell instance with the spec id
42
+ 3. Upload measurement(s) with time series data using the instance id
43
+
44
+ Given time series data, the data can be uploaded as follows:
45
+
46
+ ```python
47
+ from ionworks import Ionworks
48
+ import pandas as pd
49
+
50
+ client = Ionworks()
51
+
52
+ # Step 1: Create or get a cell specification
53
+ cell_spec = client.cell_spec.create_or_get(
54
+ {
55
+ "name": "NCM622/Graphite Coin Cell",
56
+ "form_factor": "R2032",
57
+ "manufacturer": "Custom Cells",
58
+ "ratings": {
59
+ "capacity": {"value": 0.002, "unit": "A*h"},
60
+ "voltage_min": {"value": 2.5, "unit": "V"},
61
+ "voltage_max": {"value": 4.2, "unit": "V"},
62
+ },
63
+ "cathode": {
64
+ "properties": {"loading": {"value": 12.3, "unit": "mg/cm**2"}},
65
+ "material": {"name": "NCM622", "manufacturer": "BASF"},
66
+ },
67
+ "anode": {
68
+ "properties": {"loading": {"value": 6.5, "unit": "mg/cm**2"}},
69
+ "material": {"name": "Graphite", "manufacturer": "Customcells"},
70
+ },
71
+ }
72
+ )
73
+
74
+ # Step 2: Create or get a cell instance
75
+ cell_instance = client.cell_instance.create_or_get(
76
+ cell_spec.id,
77
+ {
78
+ "name": "NCM622-GR-001",
79
+ "batch": "BATCH-2024-001",
80
+ "date_manufactured": "2024-01-20",
81
+ "measured_properties": {
82
+ "cathode": {"loading": {"value": 12.1, "unit": "mg/cm**2"}},
83
+ "anode": {"loading": {"value": 6.4, "unit": "mg/cm**2"}},
84
+ },
85
+ },
86
+ )
87
+
88
+ # Step 3: Upload measurement with time series data
89
+ time_series = pd.DataFrame(
90
+ {
91
+ "Time [s]": [0, 1, 2, 3, 4, 5],
92
+ "Voltage [V]": [3.0, 3.2, 3.5, 3.8, 4.0, 4.2],
93
+ "Current [A]": [0.002, 0.002, 0.002, 0.002, 0.002, 0.002],
94
+ "Step count": [0, 0, 0, 1, 1, 1],
95
+ "Cycle count": [0, 0, 0, 0, 0, 0],
96
+ "Step from cycler": [1, 1, 1, 2, 2, 2],
97
+ "Cycle from cycler": [0, 0, 0, 0, 0, 0],
98
+ }
99
+ )
100
+
101
+ measurement_data = {
102
+ "measurement": {
103
+ "name": "Formation Cycle 1",
104
+ "protocol": {
105
+ "name": "CC-CV charge at C/10 to 4.2V",
106
+ "ambient_temperature_degc": 25,
107
+ },
108
+ "test_setup": {
109
+ "cycler": "Biologic VMP3",
110
+ "operator": "Jane Smith",
111
+ },
112
+ "notes": "Formation cycle - first charge",
113
+ },
114
+ "time_series": time_series,
115
+ }
116
+
117
+ measurement_bundle = client.cell_measurement.create(
118
+ cell_instance.id, measurement_data
119
+ )
120
+
121
+ print(f"Created measurement: {measurement_bundle.measurement.name}")
122
+ print(f"Steps created: {measurement_bundle.steps_created}")
123
+ ```
124
+
125
+ ### Reading cell data
126
+
127
+ ```python
128
+ from ionworks import Ionworks
129
+
130
+ client = Ionworks()
131
+
132
+ # List all cell specifications
133
+ specs = client.cell_spec.list()
134
+ for spec in specs[:5]:
135
+ print(f" - {spec.name} (form_factor: {spec.form_factor})")
136
+
137
+ # Get a specific cell spec with full nested data
138
+ full_spec = client.cell_spec.get(spec_id)
139
+ print(f"Capacity: {full_spec.ratings['capacity']['value']} "
140
+ f"{full_spec.ratings['capacity']['unit']}")
141
+
142
+ # Get cell instance by slug
143
+ instance = client.cell_instance.get_by_slug("ncm622-gr-001")
144
+
145
+ # List measurements for an instance
146
+ measurements = client.cell_measurement.list(instance.id)
147
+
148
+ # Get measurement detail with time series
149
+ measurement_detail = client.cell_measurement.detail(measurement_id)
150
+ print(f"Time series shape: {measurement_detail.time_series.shape}")
151
+ ```
152
+
153
+ ### Running pipelines
154
+
155
+ 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.
156
+
157
+ **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.
158
+
159
+ Available element types:
160
+
161
+ - `entry`: Provide initial parameter values
162
+ - `data_fit`: Fit model parameters to experimental data
163
+ - `calculation`: Run calculations (e.g., OCP fitting)
164
+ - `validation`: Validate model against data
165
+
166
+ ```python
167
+ import time
168
+ from ionworks import Ionworks
169
+
170
+ client = Ionworks()
171
+
172
+ # First, upload your data (see "Uploading data to the Ionworks app" section)
173
+ # Then get the measurement ID to reference in the pipeline
174
+ measurements = client.cell_measurement.list(cell_instance_id)
175
+ measurement_id = measurements[0].id # or find the specific measurement you need
176
+
177
+ # Define entry configuration with initial parameter values
178
+ entry_config = {
179
+ "values": {
180
+ "Negative particle diffusivity [m2.s-1]": 3.3e-14,
181
+ "Positive particle diffusivity [m2.s-1]": 4e-15,
182
+ # ... other parameters
183
+ }
184
+ }
185
+
186
+ # Define datafit configuration - reference uploaded data with db:<measurement_id>
187
+ datafit_config = {
188
+ "objectives": {
189
+ "test_1C": {
190
+ "objective": "CurrentDriven",
191
+ "model": {"type": "SPMe"},
192
+ "data": f"db:{measurement_id}", # Reference data from database
193
+ "custom_parameters": {"Ambient temperature [K]": "initial_temperature"},
194
+ },
195
+ },
196
+ "parameters": {
197
+ "Negative particle diffusivity [m2.s-1]": {
198
+ "bounds": [1e-14, 1e-13],
199
+ "initial_value": 2e-14,
200
+ },
201
+ "Positive particle diffusivity [m2.s-1]": {
202
+ "bounds": [1e-15, 1e-14],
203
+ "initial_value": 2e-15,
204
+ },
205
+ },
206
+ "cost": {"type": "RMSE"},
207
+ "optimizer": {"type": "ScipyDifferentialEvolution"},
208
+ }
209
+
210
+ # Create pipeline config with named elements
211
+ pipeline_config = {
212
+ "elements": {
213
+ "entry": {**entry_config, "element_type": "entry"},
214
+ "fit data": {**datafit_config, "element_type": "data_fit"},
215
+ },
216
+ }
217
+
218
+ # Submit pipeline
219
+ pipeline = client.pipeline.create(pipeline_config)
220
+ print(f"Pipeline submitted: {pipeline.id}")
221
+
222
+ # Poll for completion
223
+ while True:
224
+ pipeline = client.pipeline.get(pipeline.id)
225
+ print(f"Status: {pipeline.status}")
226
+ if pipeline.status == "completed":
227
+ result = client.pipeline.result(pipeline.id)
228
+ print("Fitted parameters:", result.element_results["fit data"])
229
+ break
230
+ elif pipeline.status == "failed":
231
+ print("Pipeline failed:", pipeline.error)
232
+ break
233
+ time.sleep(2)
234
+ ```
235
+
236
+ ### Running simulations
237
+
238
+ The client supports running battery simulations using the Universal Cycler Protocol (UCP) format:
239
+
240
+ ```python
241
+ from ionworks import Ionworks
242
+
243
+ client = Ionworks()
244
+
245
+ # Define a charge/discharge protocol in YAML format
246
+ protocol_yaml = """global:
247
+ initial_state_type: soc_percentage
248
+ initial_state_value: 50
249
+ initial_temperature: 25.0
250
+ steps:
251
+ - Charge:
252
+ mode: C-rate
253
+ value: "0.6"
254
+ ends:
255
+ - Voltage > 4.2
256
+ - Rest:
257
+ duration: 3600
258
+ - Discharge:
259
+ mode: C-rate
260
+ value: "0.5"
261
+ ends:
262
+ - Voltage < 2.5
263
+ """
264
+
265
+ # Create simulation with quick model
266
+ config = {
267
+ "parameterized_model": {
268
+ "quick_model": {"capacity": 1.0, "chemistry": "NMC/Graphite"}
269
+ },
270
+ "protocol_experiment": {
271
+ "protocol": protocol_yaml,
272
+ "name": "NMC Charge Discharge Protocol",
273
+ },
274
+ }
275
+
276
+ result = client.simulation.protocol(config)
277
+ print(f"Simulation ID: {result.simulation_id}")
278
+
279
+ # Wait for completion
280
+ simulation = client.simulation.wait_for_completion(
281
+ result.simulation_id, timeout=60, poll_interval=2
282
+ )
283
+
284
+ # Get results
285
+ simulation_data = client.simulation.get_result(result.simulation_id)
286
+ time_series = simulation_data.get("time_series", {})
287
+ ```
288
+
289
+ ## Error Handling
290
+
291
+ The client will raise exceptions in the following cases:
292
+
293
+ - Missing API credentials
294
+ - Invalid API credentials
295
+ - API request errors (will raise `IonworksError` with details)
296
+
297
+ Make sure to handle these exceptions appropriately in your code.
@@ -0,0 +1,3 @@
1
+ from .client import Ionworks, IonworksError
2
+
3
+ __all__ = ["Ionworks", "IonworksError"]