{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Silicon Design Optimization\n", "\n", "This example demonstrates how to optimize battery cell design parameters using ionworkspipeline. We optimize active material volume fractions and electrode thicknesses to maximize gravimetric energy density while preventing lithium plating by constraining the anode potential.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import copy\n", "from functools import reduce\n", "\n", "import ionworkspipeline as iwp\n", "from ionworkspipeline.data_fits.models.metrics import Minimum, PointBased, Voltage\n", "from ionworkspipeline.data_fits.models.metrics.actions import (\n", " GreaterThan,\n", " LessThan,\n", " Maximize,\n", ")\n", "import matplotlib.pyplot as plt\n", "import numpy as np\n", "import pybamm\n", "from pybamm import Parameter" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Volume Change Functions\n", "\n", "Define volume change functions for LiCoO2, graphite, and silicon electrodes based on Ai2020 model." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def lico2_volume_change_Ai2020(sto):\n", " omega = pybamm.Parameter(\"Positive electrode partial molar volume [m3.mol-1]\")\n", " c_s_max = pybamm.Parameter(\"Maximum concentration in positive electrode [mol.m-3]\")\n", " t_change = omega * c_s_max * sto\n", " return t_change\n", "\n", "\n", "def graphite_volume_change_Ai2020(sto):\n", " p1 = 145.907\n", " p2 = -681.229\n", " p3 = 1334.442\n", " p4 = -1415.710\n", " p5 = 873.906\n", " p6 = -312.528\n", " p7 = 60.641\n", " p8 = -5.706\n", " p9 = 0.386\n", " p10 = -4.966e-05\n", " t_change = (\n", " p1 * sto**9\n", " + p2 * sto**8\n", " + p3 * sto**7\n", " + p4 * sto**6\n", " + p5 * sto**5\n", " + p6 * sto**4\n", " + p7 * sto**3\n", " + p8 * sto**2\n", " + p9 * sto\n", " + p10\n", " )\n", " return t_change\n", "\n", "\n", "def silicon_volume_change(sto):\n", " return graphite_volume_change_Ai2020(sto) * 30.0" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Model Configuration\n", "\n", "Configure DFN model with particle mechanics, set up parameters, and add custom variables." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Define DFN model with composite electrode and swelling mechanics\n", "model = pybamm.lithium_ion.DFN(\n", " options={\n", " \"particle phases\": (\"2\", \"1\"),\n", " \"open-circuit potential\": ((\"single\", \"current sigmoid\"), \"single\"),\n", " \"particle mechanics\": \"swelling only\",\n", " }\n", ")\n", "\n", "submesh_types = model.default_submesh_types\n", "for domain in [\n", " \"negative electrode\",\n", " \"separator\",\n", " \"positive electrode\",\n", " \"positive particle\",\n", " \"negative particle\",\n", "]:\n", " submesh_types[domain] = pybamm.SymbolicUniform1DSubMesh\n", "\n", "params_baseline = pybamm.ParameterValues(\"Chen2020_composite\")\n", "\n", "params_baseline.update(\n", " {\n", " \"Positive electrode active material density [kg.m-3]\": 5010,\n", " \"Positive electrode carbon-binder density [kg.m-3]\": 2100,\n", " \"Primary: Negative electrode active material density [kg.m-3]\": 2170,\n", " \"Secondary: Negative electrode active material density [kg.m-3]\": 2260,\n", " \"Primary: Negative electrode carbon-binder density [kg.m-3]\": 2100,\n", " \"Secondary: Negative electrode carbon-binder density [kg.m-3]\": 2100,\n", " \"Positive current collector density [kg.m-3]\": 2700,\n", " \"Negative current collector density [kg.m-3]\": 8960,\n", " \"Electrolyte density [kg.m-3]\": 1200,\n", " \"Separator density [kg.m-3]\": 397,\n", " \"Cell thickness constraint [m]\": 3.045e-3,\n", " \"Upper voltage cut-off [V]\": 4.5,\n", " \"Lower voltage cut-off [V]\": 2.5,\n", " \"Primary: Maximum concentration in negative electrode [mol.m-3]\": 28700,\n", " \"Primary: Initial concentration in negative electrode [mol.m-3]\": 23000,\n", " \"Primary: Negative particle diffusivity [m2.s-1]\": 5.5e-14,\n", " \"Secondary: Negative particle diffusivity [m2.s-1]\": 1.67e-14,\n", " \"Secondary: Initial concentration in negative electrode [mol.m-3]\": 277000,\n", " \"Secondary: Maximum concentration in negative electrode [mol.m-3]\": 278000,\n", " \"Secondary: Initial hysteresis state in negative electrode\": 1,\n", " \"Positive electrode volume change\": lico2_volume_change_Ai2020,\n", " \"Positive electrode partial molar volume [m3.mol-1]\": -7.28e-07,\n", " \"Positive electrode Young's modulus [Pa]\": 375000000000.0,\n", " \"Positive electrode Poisson's ratio\": 0.2,\n", " \"Positive electrode critical stress [Pa]\": 375000000.0,\n", " \"Positive electrode LAM constant exponential term\": 2.0,\n", " \"Positive electrode reference concentration for free of deformation [mol.m-3]\": 0.0,\n", " \"Primary: Negative electrode volume change\": graphite_volume_change_Ai2020,\n", " \"Primary: Negative electrode reference concentration for free of deformation [mol.m-3]\": 0.0,\n", " \"Primary: Negative electrode partial molar volume [m3.mol-1]\": 3.1e-06,\n", " \"Primary: Negative electrode Young's modulus [Pa]\": 15000000000.0,\n", " \"Primary: Negative electrode Poisson's ratio\": 0.3,\n", " \"Secondary: Negative electrode volume change\": silicon_volume_change,\n", " \"Secondary: Negative electrode partial molar volume [m3.mol-1]\": 3.1e-06,\n", " \"Secondary: Negative electrode Young's modulus [Pa]\": 15000000000.0,\n", " \"Secondary: Negative electrode Poisson's ratio\": 0.3,\n", " \"Secondary: Negative electrode reference concentration for free of deformation [mol.m-3]\"\n", " \"\": 0.0,\n", " }\n", ")\n", "\n", "N = Parameter(\"Number of electrodes connected in parallel to make a cell\")\n", "\n", "params_baseline.update(\n", " {\n", " \"Full cell positive current collector thickness [m]\": Parameter(\n", " \"Positive current collector thickness [m]\"\n", " )\n", " * (N + 1),\n", " \"Full cell negative current collector thickness [m]\": Parameter(\n", " \"Negative current collector thickness [m]\"\n", " )\n", " * (N + 1),\n", " \"Full cell negative electrode thickness [m]\": Parameter(\n", " \"Negative electrode thickness [m]\"\n", " )\n", " * N,\n", " \"Full cell positive electrode thickness [m]\": Parameter(\n", " \"Positive electrode thickness [m]\"\n", " )\n", " * N,\n", " \"Cell thickness [m]\": (\n", " Parameter(\"Full cell negative electrode thickness [m]\")\n", " + Parameter(\"Full cell positive electrode thickness [m]\")\n", " + Parameter(\"Full cell negative current collector thickness [m]\")\n", " + Parameter(\"Full cell positive current collector thickness [m]\")\n", " + Parameter(\"Separator thickness [m]\") * N\n", " ),\n", " }\n", ")\n", "\n", "params_baseline[\"Positive electrode porosity\"] = 1.0 - Parameter(\n", " \"Positive electrode active material volume fraction\"\n", ")\n", "\n", "params_baseline[\"Negative electrode porosity\"] = (\n", " 1.0\n", " - Parameter(\"Primary: Negative electrode active material volume fraction\")\n", " - Parameter(\"Secondary: Negative electrode active material volume fraction\")\n", ")\n", "\n", "energy_density_var = iwp.models.variables.GravimetricEnergyDensity(\n", " \"Gravimetric energy density [W.h.kg-1]\", model, params_baseline\n", ")\n", "energy_density_var.add()\n", "\n", "model.variables[\"Cell thickness expansion ratio\"] = model.variables[\n", " \"Cell thickness change [m]\"\n", "] / pybamm.Parameter(\"Cell thickness [m]\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Optimization Setup\n", "\n", "Define parameters to optimize, experiment protocol, and constraints." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "parameters = {\n", " \"Positive electrode active material volume fraction\": iwp.Parameter(\n", " \"Positive electrode active material volume fraction\",\n", " initial_value=0.65,\n", " bounds=(0.5, 0.9),\n", " ),\n", " \"Primary: Negative electrode active material volume fraction\": iwp.Parameter(\n", " \"Primary: Negative electrode active material volume fraction\",\n", " initial_value=0.65,\n", " bounds=(0.5, 0.85),\n", " ),\n", " \"Secondary: Negative electrode active material volume fraction\": iwp.Parameter(\n", " \"Secondary: Negative electrode active material volume fraction\",\n", " initial_value=0.02,\n", " bounds=(1e-4, 0.1),\n", " ),\n", " \"Positive electrode thickness [m]\": iwp.Parameter(\n", " \"Positive electrode thickness [m]\", initial_value=68e-6, bounds=(25e-6, 100e-6)\n", " ),\n", " \"Negative electrode thickness [m]\": iwp.Parameter(\n", " \"Negative electrode thickness [m]\", initial_value=75e-6, bounds=(30e-6, 105e-6)\n", " ),\n", " \"Number of electrodes connected in parallel to make a cell\": iwp.Parameter(\n", " \"Number of electrodes connected in parallel to make a cell\",\n", " initial_value=15,\n", " bounds=(1, 50),\n", " ),\n", "}\n", "model.variables[\"Cell thickness [m]\"] = pybamm.Parameter(\"Cell thickness [m]\")\n", "\n", "print(f\"Model configured with {len(parameters)} optimization parameters\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Define Experiment Protocol and Optimization Metrics" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "experiment = pybamm.Experiment(\n", " [\n", " \"Discharge at 60A until 3.0 V\",\n", " \"Rest for 1 hours\",\n", " \"Charge at 60A until 4.05 V\",\n", " \"Charge at 30A until 4.20 V\",\n", " \"Hold at 4.20 V until C/20\",\n", " ]\n", ")\n", "\n", "# Define optimization actions\n", "actions = {\n", " \"Gravimetric energy density\": Maximize(\n", " Voltage(variable=\"Gravimetric energy density [W.h.kg-1]\", value=3.001)\n", " ),\n", "}\n", "\n", "# Constraints define bounds that must be satisfied (using GreaterThan/LessThan actions)\n", "constraints = {\n", " \"Negative surface potential\": GreaterThan(\n", " Minimum(\n", " \"Negative electrode surface potential difference at separator interface [V]\"\n", " ),\n", " value=0.0,\n", " penalty=150.0,\n", " ),\n", " \"Cell thickness expansion ratio\": GreaterThan(\n", " Minimum(\"Cell thickness expansion ratio\"),\n", " value=-0.04,\n", " penalty=1e4,\n", " ),\n", " \"Cell thickness constraint\": LessThan(\n", " PointBased(\"Cell thickness [m]\"),\n", " value=3.045e-3,\n", " ),\n", "}\n", "\n", "print(\"Experiment, metrics, and constraints configured\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Run Optimization" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "initial_soc = 1.0\n", "params_for_pipeline = {k: v for k, v in params_baseline.items() if k not in parameters}\n", "\n", "solver = pybamm.IDAKLUSolver(\n", " on_failure=\"ignore\",\n", " options={\n", " \"t_no_progress\": 0.99,\n", " \"num_steps_no_progress\": 1000,\n", " },\n", ")\n", "objective_options = {\n", " \"model\": model,\n", " \"simulation_kwargs\": {\n", " \"experiment\": experiment,\n", " \"submesh_types\": submesh_types,\n", " \"solver\": solver,\n", " },\n", "}\n", "\n", "objective = iwp.objectives.DesignObjective(\n", " options=objective_options,\n", " actions=actions,\n", " constraints=constraints,\n", " parameters={\"Initial SOC [%]\": 100 * initial_soc},\n", ")\n", "\n", "optim = iwp.optimizers.Pints(\n", " method=\"CMAES\",\n", " max_iterations=10, # Increase for better results\n", " max_unchanged_iterations=100,\n", " max_unchanged_iterations_threshold=1e-5,\n", " # population_size=32, # Uncomment to allow larger exploration per iteration\n", " log_to_screen=True,\n", ")\n", "\n", "design_datafit = iwp.DataFit(\n", " objective,\n", " parameters=parameters,\n", " options={\"seed\": 0},\n", " optimizer=optim,\n", " # parallel=True, # Disabled for CI\n", ")\n", "\n", "print(\"Running optimization...\")\n", "results = design_datafit.run(params_for_pipeline)\n", "\n", "print(f\"\\nOptimization completed! Final cost: {results.costs:.6f}\")\n", "print(\"Optimal parameters:\")\n", "for name in parameters:\n", " print(f\" {name}: {results.parameter_values[name]:.6f}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Validation and Analysis" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def extract_metrics(solution):\n", " sol_discharge = solution.sub_solutions[0]\n", " gravimetric_energy_density = abs(\n", " sol_discharge[\"Gravimetric energy density [W.h.kg-1]\"]()[-1]\n", " )\n", "\n", " charge_solutions = solution.sub_solutions[2:]\n", " sol_charge = charge_solutions[0]\n", " for charge_sol in charge_solutions[1:]:\n", " sol_charge = sol_charge + charge_sol\n", "\n", " anode_potential_series = sol_charge[\n", " \"Negative electrode surface potential difference at separator interface [V]\"\n", " ]()\n", " min_anode_potential = min(anode_potential_series)\n", " mean_anode_potential = anode_potential_series.mean()\n", "\n", " return {\n", " \"gravimetric_energy_density\": gravimetric_energy_density,\n", " \"min_anode_potential\": min_anode_potential,\n", " \"mean_anode_potential\": mean_anode_potential,\n", " \"solution\": solution,\n", " }\n", "\n", "\n", "params_optimal = params_baseline.copy()\n", "params_optimal.update(results.parameter_values.copy())\n", "\n", "params_baseline_original = copy.deepcopy(params_baseline)\n", "for name, param in parameters.items():\n", " params_baseline_original[name] = param.initial_value\n", "\n", "sim_baseline = iwp.Simulation(\n", " model=model, parameter_values=params_baseline_original, experiment=experiment\n", ")\n", "sim_optimal = iwp.Simulation(\n", " model=model, parameter_values=params_optimal, experiment=experiment\n", ")\n", "\n", "sol_baseline = sim_baseline.solve(initial_soc=initial_soc - 0.01)\n", "sol_optimal = sim_optimal.solve(initial_soc=initial_soc - 0.01)\n", "\n", "baseline_metrics = extract_metrics(sol_baseline)\n", "optimal_metrics = extract_metrics(sol_optimal)\n", "\n", "print(\"Validation simulations completed\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Analyze Performance Metrics and Parameter Changes" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "energy_density_improvement = (\n", " (\n", " optimal_metrics[\"gravimetric_energy_density\"]\n", " - baseline_metrics[\"gravimetric_energy_density\"]\n", " )\n", " / baseline_metrics[\"gravimetric_energy_density\"]\n", ") * 100\n", "min_anode_improvement = (\n", " optimal_metrics[\"min_anode_potential\"] - baseline_metrics[\"min_anode_potential\"]\n", ")\n", "\n", "print(\"Performance Comparison:\")\n", "print(\"=\" * 50)\n", "print(f\"{'Metric':<45} {'Baseline':<12} {'Optimal':<12} {'Improvement'}\")\n", "print(\"-\" * 80)\n", "print(\n", " f\"{'Gravimetric Energy Density [W.h.kg-1]':<45} {baseline_metrics['gravimetric_energy_density']:<12.4f} {optimal_metrics['gravimetric_energy_density']:<12.4f} {energy_density_improvement:+.2f}%\"\n", ")\n", "print(\n", " f\"{'Min Anode Potential [V]':<45} {baseline_metrics['min_anode_potential']:<12.4f} {optimal_metrics['min_anode_potential']:<12.4f} {min_anode_improvement:+.4f}\"\n", ")\n", "print(\n", " f\"{'Mean Anode Potential [V]':<45} {baseline_metrics['mean_anode_potential']:<12.4f} {optimal_metrics['mean_anode_potential']:<12.4f}\"\n", ")\n", "print(\"=\" * 50)\n", "\n", "print(\"\\nParameter Changes:\")\n", "for name, param in parameters.items():\n", " baseline_val = param.initial_value\n", " optimal_val = results.parameter_values[name]\n", " change = optimal_val - baseline_val\n", " percent_change = (change / baseline_val) * 100\n", " print(f\"{name}: {baseline_val:.6f} → {optimal_val:.6f} ({percent_change:+.2f}%)\")\n", "\n", "baseline_thickness = (\n", " params_baseline_original.evaluate(params_baseline_original[\"Cell thickness [m]\"])\n", " if \"Cell thickness [m]\" in params_baseline_original\n", " else \"N/A\"\n", ")\n", "optimal_thickness = params_optimal.evaluate(params_optimal[\"Cell thickness [m]\"])\n", "print(\n", " f\"\\nCell Thickness: {baseline_thickness} → {optimal_thickness:.6f} m (limit: {params_baseline['Cell thickness constraint [m]']:.6f} m)\"\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Visualization" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def plot_comparison(ax, t_base, y_base, t_opt, y_opt, xlabel, ylabel, title):\n", " ax.plot(t_base, y_base, label=\"Baseline\", linewidth=2, alpha=0.8)\n", " ax.plot(t_opt, y_opt, label=\"Optimal\", linewidth=2, alpha=0.8, linestyle=\"--\")\n", " ax.set(xlabel=xlabel, ylabel=ylabel, title=title)\n", " ax.legend()\n", " ax.grid(True, alpha=0.3)\n", "\n", "\n", "def add_bar_labels(ax, x_pos, values, fmt, offset=0.02):\n", " for i, val in enumerate(values):\n", " y_off = val * offset if val > 0 else 0.005\n", " ax.text(\n", " x_pos[i], val + y_off, fmt.format(val), ha=\"center\", va=\"bottom\", fontsize=8\n", " )\n", "\n", "\n", "fig, axes = plt.subplots(2, 3, figsize=(18, 10))\n", "\n", "# Voltage\n", "sol_base, sol_opt = baseline_metrics[\"solution\"], optimal_metrics[\"solution\"]\n", "plot_comparison(\n", " axes[0, 0],\n", " sol_base.t / 3600,\n", " sol_base[\"Voltage [V]\"](sol_base.t),\n", " sol_opt.t / 3600,\n", " sol_opt[\"Voltage [V]\"](sol_opt.t),\n", " \"Time [h]\",\n", " \"Voltage [V]\",\n", " \"Cell Voltage: Baseline vs Optimal\",\n", ")\n", "\n", "# Energy density\n", "discharge_base, discharge_opt = sol_base.sub_solutions[0], sol_opt.sub_solutions[0]\n", "plot_comparison(\n", " axes[0, 1],\n", " discharge_base.t / 3600,\n", " abs(discharge_base[\"Gravimetric energy density [W.h.kg-1]\"](discharge_base.t)),\n", " discharge_opt.t / 3600,\n", " abs(discharge_opt[\"Gravimetric energy density [W.h.kg-1]\"](discharge_opt.t)),\n", " \"Time [h]\",\n", " \"Gravimetric Energy Density [W.h.kg-1]\",\n", " \"Gravimetric Energy Density\",\n", ")\n", "\n", "# Anode potential\n", "charge_base = reduce(lambda a, b: a + b, sol_base.sub_solutions[2:])\n", "charge_opt = reduce(lambda a, b: a + b, sol_opt.sub_solutions[2:])\n", "anode_var = \"Negative electrode surface potential difference at separator interface [V]\"\n", "t_charge_base = (charge_base.t - charge_base.t[0]) / 3600\n", "t_charge_opt = (charge_opt.t - charge_opt.t[0]) / 3600\n", "plot_comparison(\n", " axes[1, 0],\n", " t_charge_base,\n", " charge_base[anode_var](charge_base.t),\n", " t_charge_opt,\n", " charge_opt[anode_var](charge_opt.t),\n", " \"Charge Time [h]\",\n", " \"Anode - Electrolyte Potential [V]\",\n", " \"Anode Potential During Charge\",\n", ")\n", "axes[1, 0].axhline(0, color=\"r\", linestyle=\":\", alpha=0.7, label=\"Safety limit\")\n", "\n", "# Performance metrics\n", "metrics_data = [\n", " (\n", " [\n", " \"Gravimetric Energy\\nDensity [W.h.kg-1]\",\n", " \"Min Anode\\nPotential [V]\",\n", " \"Mean Anode\\nPotential [V]\",\n", " ],\n", " [\n", " baseline_metrics[\"gravimetric_energy_density\"],\n", " baseline_metrics[\"min_anode_potential\"],\n", " baseline_metrics[\"mean_anode_potential\"],\n", " ],\n", " [\n", " optimal_metrics[\"gravimetric_energy_density\"],\n", " optimal_metrics[\"min_anode_potential\"],\n", " optimal_metrics[\"mean_anode_potential\"],\n", " ],\n", " )\n", "]\n", "x = np.arange(len(metrics_data[0][0]))\n", "width = 0.35\n", "axes[1, 1].bar(\n", " x - width / 2,\n", " metrics_data[0][1],\n", " width,\n", " label=\"Baseline\",\n", " alpha=0.8,\n", " color=\"lightblue\",\n", ")\n", "axes[1, 1].bar(\n", " x + width / 2, metrics_data[0][2], width, label=\"Optimal\", alpha=0.8, color=\"orange\"\n", ")\n", "add_bar_labels(axes[1, 1], x - width / 2, metrics_data[0][1], \"{:.3f}\")\n", "add_bar_labels(axes[1, 1], x + width / 2, metrics_data[0][2], \"{:.3f}\")\n", "axes[1, 1].set(\n", " xlabel=\"Performance Metrics\",\n", " ylabel=\"Value\",\n", " title=\"Performance Metrics Comparison\",\n", " xticks=x,\n", ")\n", "axes[1, 1].set_xticklabels(metrics_data[0][0])\n", "axes[1, 1].legend()\n", "axes[1, 1].grid(True, alpha=0.3)\n", "\n", "# Parameter values\n", "param_labels = [\n", " \"Pos. Active\\nMaterial [-]\",\n", " \"Primary: Neg. Active\\nMaterial [-]\",\n", " \"Secondary: Neg. Active\\nMaterial [-]\",\n", " \"Pos. Thickness\\n[um]\",\n", " \"Neg. Thickness\\n[um]\",\n", " \"Number of\\nElectrodes [-]\",\n", "]\n", "base_vals = [p.initial_value for p in parameters.values()]\n", "opt_vals = [results.parameter_values[n] for n in parameters.keys()]\n", "for i in [3, 4]:\n", " base_vals[i] *= 1e6\n", " opt_vals[i] *= 1e6\n", "\n", "x_params = np.arange(len(param_labels))\n", "axes[1, 2].bar(\n", " x_params - width / 2,\n", " base_vals,\n", " width,\n", " label=\"Baseline\",\n", " alpha=0.8,\n", " color=\"lightblue\",\n", ")\n", "axes[1, 2].bar(\n", " x_params + width / 2, opt_vals, width, label=\"Optimal\", alpha=0.8, color=\"orange\"\n", ")\n", "for i, (b, o) in enumerate(zip(base_vals, opt_vals, strict=False)):\n", " fmt = \"{:.3f}\" if i < 2 else \"{:.1f}\" if i < 4 else \"{:.0f}\"\n", " add_bar_labels(axes[1, 2], [x_params[i] - width / 2], [b], fmt)\n", " add_bar_labels(axes[1, 2], [x_params[i] + width / 2], [o], fmt)\n", "axes[1, 2].set(\n", " xlabel=\"Optimization Parameters\",\n", " ylabel=\"Parameter Value\",\n", " title=\"Parameter Values: Baseline vs Optimal\",\n", " xticks=x_params,\n", " ylim=(None, 85),\n", ")\n", "axes[1, 2].set_xticklabels(param_labels, fontsize=9)\n", "axes[1, 2].legend()\n", "axes[1, 2].grid(True, alpha=0.3)\n", "\n", "# Parameter changes\n", "changes = [\n", " 100 * (results.parameter_values[n] - p.initial_value) / p.initial_value\n", " for n, p in parameters.items()\n", "]\n", "axes[0, 2].barh(\n", " range(len(changes)),\n", " changes,\n", " alpha=0.7,\n", " color=[\"orange\" if c > 0 else \"lightblue\" for c in changes],\n", ")\n", "axes[0, 2].set(\n", " xlabel=\"Percentage Change [%]\",\n", " title=\"Parameter Changes from Baseline\",\n", " yticks=range(len(param_labels)),\n", " xlim=(-100, 110),\n", ")\n", "axes[0, 2].set_yticklabels(param_labels, fontsize=9)\n", "axes[0, 2].axvline(0, color=\"black\", linestyle=\"-\", alpha=0.3)\n", "axes[0, 2].grid(True, alpha=0.3)\n", "for i, c in enumerate(changes):\n", " axes[0, 2].text(\n", " c + (1 if c >= 0 else -1),\n", " i,\n", " f\"{c:+.1f}%\",\n", " va=\"center\",\n", " ha=\"left\" if c >= 0 else \"right\",\n", " fontsize=8,\n", " )\n", "\n", "plt.tight_layout()\n", "plt.show()\n", "print(\"Optimization analysis completed!\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Verify expansion ratio" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "expansion_ratio = np.ptp(\n", " sol_optimal[\"Cell thickness change [m]\"].data\n", ") / params_optimal.evaluate(params_optimal[\"Cell thickness [m]\"])\n", "print(f\"Cell thickness expansion percentage: {expansion_ratio * 100.0:.4f}%\")" ] } ], "metadata": { "kernelspec": { "display_name": ".venv", "language": "python", "name": "python3" }, "language_info": { "name": "python", "version": "3.12.11" } }, "nbformat": 4, "nbformat_minor": 2 }