Design Optimization

This example demonstrates how to use ionworkspipeline for battery design optimization. Unlike traditional parameter fitting, design optimization optimizes design parameters to achieve specific performance targets (e.g., maximizing energy density).

Key concepts demonstrated:

  • Setting up optimization parameters with bounds

  • Defining operating conditions (experiment protocol)

  • Creating design variables and metrics

  • Running design optimization

Complete Design Optimization Example
"""
Design Optimization Example

This example demonstrates how to use ionworkspipeline for battery design optimization.
Unlike traditional parameter fitting, design optimization optimizes design parameters
to achieve specific performance targets (e.g., maximizing energy density).

Key concepts demonstrated:
- Setting up optimization parameters with bounds
- Defining operating conditions (experiment protocol)
- Creating design variables and metrics
- Using Action classes (Maximize, Minimize) to define optimization goals
- Running design optimization
"""

import ionworkspipeline as iwp
from ionworkspipeline.data_fits.models.metrics import Minimum, PointBased
from ionworkspipeline.data_fits.models.metrics.actions import (
    GreaterThan,
    LessThan,
    Maximize,
)
import pybamm

# 1. Set up the model and base parameters
model = pybamm.lithium_ion.SPMe()
parameter_values = pybamm.ParameterValues("Chen2020")

# Add density parameters for energy density calculations
density_parameters = {
    "Positive electrode active material density [kg.m-3]": 5010,
    "Negative electrode active material density [kg.m-3]": 1657,
    "Positive electrode carbon-binder density [kg.m-3]": 1500,
    "Negative electrode carbon-binder density [kg.m-3]": 1200,
    "Positive current collector density [kg.m-3]": 2700,
    "Negative current collector density [kg.m-3]": 8960,
    "Electrolyte density [kg.m-3]": 1200,
    "Separator density [kg.m-3]": 397,
}
parameter_values.update(density_parameters)

# 2. Add a design variable
iwp.models.variables.GravimetricEnergyDensity(
    "Gravimetric energy density [W.h.kg-1]", model, parameter_values
).add()

# Add constraint variables via pybamm symbols
model.variables["Total active material fraction"] = pybamm.Parameter(
    "Positive electrode active material volume fraction"
) + pybamm.Parameter("Negative electrode active material volume fraction")

# 3. Define Operating Conditions (Experiment Protocol)
experiment = pybamm.Experiment(
    [
        "Discharge at 1C until 3.0V",  # Primary performance test
        "Rest for 10 minutes",  # Allow for relaxation
        "Charge at 0.5C until 4.2V",  # Realistic charging protocol
    ]
)

# 4. Define Optimization Parameters
parameters = {
    "Positive electrode active material volume fraction": iwp.Parameter(
        "pos_volume_frac",
        initial_value=0.55,  # Starting point for optimization
        bounds=(0.5, 0.9),  # Physical constraints
    ),
    "Negative electrode active material volume fraction": iwp.Parameter(
        "neg_volume_frac", initial_value=0.5, bounds=(0.5, 0.95)
    ),
}

# Link dependent parameters (porosity = 1 - active material fraction)
parameter_values.update(
    {
        "Negative electrode porosity": 1.0
        - pybamm.Parameter("Negative electrode active material volume fraction"),
        "Positive electrode porosity": 1.0
        - pybamm.Parameter("Positive electrode active material volume fraction"),
    }
)

# 5. Create an action to maximize energy density at end of discharge
# Use Maximize action to indicate this should be maximized
action = {
    "Energy density": Maximize(
        iwp.models.metrics.Voltage(
            variable="Gravimetric energy density [W.h.kg-1]",
            value=3.001,
        )
    )
}

constraints = {
    "Total active material fraction": LessThan(
        PointBased("Total active material fraction"),
        1.25,
    ),
    "Negative electrode potential": GreaterThan(
        Minimum(
            "Negative electrode surface potential difference at separator interface [V]"
        ),
        0.0,
    ),
}

# 6. Set up the design objective (combines experiment + parameters + actions)
objective_options = {
    "model": model,
    "simulation_kwargs": {"experiment": experiment},  # Operating conditions
}
objective = iwp.objectives.DesignObjective(
    actions=action,
    constraints=constraints,
    options=objective_options,
    validate_against_experiment_steps=False,
    save_at_cycles=[1],
)

# 7. Run the optimization
# Filter out optimization parameters from the base parameter set
params_for_pipeline = {k: v for k, v in parameter_values.items() if k not in parameters}

design_datafit = iwp.DataFit(
    objective,
    parameters=parameters,  # Optimization parameters
    options={"seed": 0},
    # parallel=True,  # Enable optimizer-level parallelism (parallel function evaluation)
    # num_workers=4,  # Use 4 CPU cores for parallel evaluation
)

results = design_datafit.run(params_for_pipeline)

# 8. Examine results
print("=== OPTIMIZATION RESULTS ===")
print(f"Optimized cost: {results.costs:.6f}")
print(f"Iterations: {results.optimizer_result}")
print("\nOptimized Parameters:")
for name, param in parameters.items():
    value = results.parameter_values[name]
    initial = param.initial_value
    print(f"  {name.split()[-3:]} : {initial:.6f}{value:.6f}")