Reading Cells#

examples/cells/02_read.py#
"""
Example: Reading cell specifications, instances, and measurements.

This example demonstrates various ways to retrieve cell data:
- Listing and filtering cell specifications
- Getting full nested data with components/materials
- Accessing cell instances and their measurements
- Working with time series and step data
"""

from ionworks import Ionworks

client = Ionworks()

# Configuration
cell_spec_name = "NCM622/Graphite Coin Cell"

# Resolve spec name -> spec id
specs = client.cell_spec.list()
spec = next((s for s in specs if s.name == cell_spec_name), None)
if spec is None:
    raise RuntimeError(f"Cell spec name not found: {cell_spec_name}")
cell_spec_id = spec.id

print("=== CELL SPECIFICATION OPERATIONS ===\n")

# 1a. List all cell specifications (metadata only)
print("1a. List all cell specifications (metadata only):")
all_specs = client.cell_spec.list()
for s in all_specs[:5]:  # Show first 5
    print(f"  - {s.name} (form_factor: {s.form_factor})")
print(f"  ... Total: {len(all_specs)} specifications")

# 1b. List with components (parallel get for each spec)
print("\n1b. List with components:")
specs_full = client.cell_spec.list(include_components=True)
for s in specs_full[:3]:
    components = []
    if getattr(s, "cathode", None):
        components.append(f"cathode={s.cathode['material']['name']}")
    if getattr(s, "anode", None):
        components.append(f"anode={s.anode['material']['name']}")
    print(f"  - {s.name}: {', '.join(components) or 'no components'}")
print()

# 2. Get full cell specification with nested components and materials
print("2. Get cell specification with components:")
full_spec = client.cell_spec.get(cell_spec_id)
print(f"  Name: {full_spec.name}")
print(f"  Form factor: {full_spec.form_factor}")
print(f"  Manufacturer: {full_spec.manufacturer}")

# Access ratings with units
print("\n  Ratings:")
cap = full_spec.ratings["capacity"]
print(f"    Capacity: {cap['value']} {cap['unit']}")
v_min = full_spec.ratings["voltage_min"]
v_max = full_spec.ratings["voltage_max"]
print(f"    Voltage range: {v_min['value']}-{v_max['value']} {v_max['unit']}")
if full_spec.ratings.get("nominal_voltage"):
    nom = full_spec.ratings["nominal_voltage"]
    print(f"    Nominal voltage: {nom['value']} {nom['unit']}")

# Access nested component data
if full_spec.cathode:
    print("\n  Cathode:")
    print(f"    Material: {full_spec.cathode['material']['name']}")
    if full_spec.cathode["material"].get("manufacturer"):
        print(f"    Manufacturer: {full_spec.cathode['material']['manufacturer']}")
    if full_spec.cathode.get("properties"):
        loading = full_spec.cathode["properties"].get("loading")
        if loading:
            print(f"    Loading: {loading['value']} {loading['unit']}")

if full_spec.anode:
    print("\n  Anode:")
    print(f"    Material: {full_spec.anode['material']['name']}")
    if full_spec.anode["material"].get("manufacturer"):
        print(f"    Manufacturer: {full_spec.anode['material']['manufacturer']}")

if full_spec.electrolyte:
    print("\n  Electrolyte:")
    print(f"    Material: {full_spec.electrolyte['material']['name']}")
    if full_spec.electrolyte["material"].get("definition"):
        defn = full_spec.electrolyte["material"]["definition"]
        if "salt" in defn:
            print(f"    Salt: {defn['salt']}")

# Access source/provenance
if full_spec.source:
    print("\n  Source:")
    for key, value in full_spec.source.items():
        print(f"    {key}: {value}")

print("\n" + "=" * 50)
print("=== SPEC-LEVEL DATA AGGREGATION ===\n")

# 3. List instances and measurements using flat endpoints
print("3. Instances and measurements (flat composition):")
instances = client.cell_instance.list(cell_spec_id)
print(f"  Number of instances: {len(instances)}")
all_measurements = []
for inst in instances:
    print(f"    - Instance: {inst.name} (batch: {inst.batch})")
    # Foreign key navigates to parent spec
    print(f"      cell_specification_id: {inst.cell_specification_id}")
    inst_measurements = client.cell_measurement.list(inst.id)
    all_measurements.extend(inst_measurements)
print(f"  Total measurements: {len(all_measurements)}")

print("\n" + "=" * 50)
print("=== INSTANCE-LEVEL OPERATIONS ===\n")

# 4. Get cell instance by ID
if not instances:
    raise RuntimeError("No instances found for spec")
cell_instance_id = instances[0].id
print("4. Get cell instance by ID:")
instance = client.cell_instance.get(cell_instance_id)
print(f"  Name: {instance.name}")
print(f"  Batch: {instance.batch}")
print(f"  Date manufactured: {instance.date_manufactured}")

# Access measured properties
if instance.measured_properties:
    print("  Measured properties:")
    for component, props in instance.measured_properties.items():
        if props:
            print(f"    {component}:")
            for prop_name, prop_value in props.items():
                if isinstance(prop_value, dict) and "value" in prop_value:
                    print(
                        f"      {prop_name}: {prop_value['value']} {prop_value['unit']}"
                    )
                else:
                    print(f"      {prop_name}: {prop_value}")

# 5. Get instance detail (composed from flat endpoints)
print("\n5. Cell instance detail (composed, parallel):")
instance_detail = client.cell_instance.detail(cell_instance_id)
print(f"  Specification ID: {instance_detail.specification_id}")
print(f"  Instance: {instance_detail.instance.name}")
print(f"  Number of measurements: {len(instance_detail.measurements)}")
for md in instance_detail.measurements:
    print(f"    - {md.measurement.name}")
    if md.steps is not None:
        print(f"      Steps shape: {md.steps.shape}")
    if md.cycles is not None:
        print(f"      Cycles shape: {md.cycles.shape}")
    if md.time_series is not None:
        print(f"      Time series shape: {md.time_series.shape}")

# 6. Instance detail with include flags (skip time series)
print("\n6. Instance detail (metadata only, no heavy data):")
light_detail = client.cell_instance.detail(
    cell_instance_id,
    include_steps=False,
    include_cycles=False,
    include_time_series=False,
)
print(f"  Measurements: {len(light_detail.measurements)}")
for md in light_detail.measurements:
    print(f"    - {md.measurement.name}")
    print(f"      steps=None: {md.steps is None}")
    print(f"      time_series=None: {md.time_series is None}")

print("\n" + "=" * 50)
print("=== MEASUREMENT-LEVEL OPERATIONS ===\n")

# 7. List measurements for an instance
print("7. List measurements for instance:")
measurements = client.cell_measurement.list(cell_instance_id)
print(f"  Found {len(measurements)} measurements")
for m in measurements:
    print(f"    - {m.name}")
    if m.protocol:
        print(f"      Protocol: {m.protocol.get('name')}")
        if m.protocol.get("ambient_temperature_degc"):
            print(f"      Temperature: {m.protocol['ambient_temperature_degc']} °C")
    if m.test_setup:
        if m.test_setup.get("cycler"):
            print(f"      Cycler: {m.test_setup['cycler']}")

# 8. Get measurement by ID
if not measurements:
    raise RuntimeError("No measurements found for instance")
cell_measurement_id = measurements[0].id
print("\n8. Get measurement by ID:")
measurement_by_id = client.cell_measurement.get(cell_measurement_id)
print(f"  Name: {measurement_by_id.name}")
print(f"  Protocol: {measurement_by_id.protocol}")
# Foreign key navigates to parent instance
print(f"  cell_instance_id: {measurement_by_id.cell_instance_id}")

# 9. Get full measurement detail (3 parallel requests + download)
# Uses flat endpoints: metadata, steps_and_cycles, signed URL
print("\n9. Single measurement detail:")
measurement_detail = client.cell_measurement.detail(cell_measurement_id)
print(f"  Measurement: {measurement_detail.measurement.name}")
print(f"  Instance ID: {measurement_detail.instance_id}")
print(f"  Steps shape: {measurement_detail.steps.shape}")
print(f"  Cycles shape: {measurement_detail.cycles.shape}")
print(f"  Time series shape: {measurement_detail.time_series.shape}")
print(f"  Time series columns: {list(measurement_detail.time_series.columns)}")

# Show sample of time series data
print("\n  Sample time series data (first 3 rows):")
print(measurement_detail.time_series.head(3))

# Show step summary
print("\n  Step summary:")
print(measurement_detail.steps[["Step count", "Step type", "Duration [s]"]])

# 10. Individual data access (steps, cycles, time series separately)
print("\n10. Individual data access:")
steps_df = client.cell_measurement.steps(cell_measurement_id)
print(f"  Steps shape: {steps_df.shape}")

cycles_df = client.cell_measurement.cycles(cell_measurement_id)
print(f"  Cycles shape: {cycles_df.shape}")

# steps_and_cycles is more efficient when you need both
sc = client.cell_measurement.steps_and_cycles(cell_measurement_id)
print(f"  Steps+Cycles (one call): {sc.steps.shape}, {sc.cycles.shape}")

ts_df = client.cell_measurement.time_series(cell_measurement_id)
print(f"  Time series shape: {ts_df.shape}")