{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Battery Cell 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 discharge capacity while preventing lithium plating by constraining the anode potential.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import ionworkspipeline as iwp\n", "from ionworkspipeline.data_fits.models.metrics import Maximum, Minimum, PointBased\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": [ "## Configure DFN Model and Optimization Parameters\n", "\n", " Import Libraries and Dependencies\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Setup model and baseline parameters\n", "model = pybamm.lithium_ion.DFN()\n", "\n", "# Configure symbolic mesh\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", "# Initialize parameters with thickness constraint\n", "params_baseline = pybamm.ParameterValues(\"Chen2020\")\n", "params_baseline.update({\"Cell thickness constraint [m]\": 3.045e-3})\n", "\n", "# Setup multi-layer cell geometry\n", "N = Parameter(\"Number of electrodes connected in parallel to make a cell\")\n", "\n", "# Configure current collectors, electrodes, and separators\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", "# Link porosity to active material fraction\n", "for electrode in [\"Positive\", \"Negative\"]:\n", " params_baseline[f\"{electrode} electrode porosity\"] = 1.0 - Parameter(\n", " f\"{electrode} electrode active material volume fraction\"\n", " )\n", "\n", "# Define optimization parameters\n", "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", " \"Negative electrode active material volume fraction\": iwp.Parameter(\n", " \"Negative electrode active material volume fraction\",\n", " initial_value=0.65,\n", " bounds=(0.5, 0.85),\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=10,\n", " bounds=(1, 50),\n", " ),\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\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Define experiment protocol\n", "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", " \"Maximize discharge capacity [A.h]\": Maximize(\n", " Maximum(\"Discharge capacity [A.h]\"), weight=1.0\n", " ),\n", "}\n", "\n", "# Constraints define bounds that must be satisfied (using GreaterThan/LessThan actions)\n", "action_constraints = {\n", " \"Anode potential minimum constraint [V]\": 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 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": [ "### Execute Battery Design Optimization\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Setup and run optimization\n", "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", " options={\"max_convergence_failures\": 30, \"max_error_test_failures\": 20}\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=action_constraints,\n", " parameters={\"Initial SOC [%]\": 100 * initial_soc},\n", ")\n", "\n", "optim = iwp.optimizers.Pints(\n", " method=\"PSO\",\n", " max_iterations=10, # Increase for better results\n", " max_unchanged_iterations=50,\n", " max_unchanged_iterations_threshold=1e-4,\n", " log_to_screen=True,\n", ")\n", "\n", "design_datafit = iwp.DataFit(\n", " objective, parameters=parameters, options={\"seed\": 0}, optimizer=optim\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": [ "### Run Validation Simulations\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Helper function to extract metrics from solution\n", "def extract_metrics(solution):\n", " sol_discharge = solution.sub_solutions[0]\n", " discharge_capacity = abs(sol_discharge[\"Discharge capacity [A.h]\"]()[-1])\n", "\n", " # Combine charge steps\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", " \"discharge_capacity\": discharge_capacity,\n", " \"min_anode_potential\": min_anode_potential,\n", " \"mean_anode_potential\": mean_anode_potential,\n", " \"solution\": solution,\n", " }\n", "\n", "\n", "# Setup baseline parameters for comparison\n", "params_optimal = params_baseline.copy()\n", "params_optimal.update(results.parameter_values.copy())\n", "\n", "params_baseline_original = params_baseline.copy()\n", "for name, param in parameters.items():\n", " params_baseline_original[name] = param.initial_value\n", "\n", "# Run simulations for comparison\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\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Performance comparison and analysis\n", "capacity_improvement = (\n", " (optimal_metrics[\"discharge_capacity\"] - baseline_metrics[\"discharge_capacity\"])\n", " / baseline_metrics[\"discharge_capacity\"]\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':<35} {'Baseline':<12} {'Optimal':<12} {'Improvement'}\")\n", "print(\"-\" * 70)\n", "print(\n", " f\"{'Discharge Capacity [A.h]':<35} {baseline_metrics['discharge_capacity']:<12.4f} {optimal_metrics['discharge_capacity']:<12.4f} {capacity_improvement:+.2f}%\"\n", ")\n", "print(\n", " f\"{'Min Anode Potential [V]':<35} {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]':<35} {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", "# Check thickness constraint\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": [ "### Create Comparative Visualizations" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Create comprehensive visualization\n", "fig, axes = plt.subplots(2, 3, figsize=(18, 10))\n", "\n", "# Voltage comparison\n", "t_baseline = baseline_metrics[\"solution\"].t / 3600\n", "t_optimal = optimal_metrics[\"solution\"].t / 3600\n", "\n", "axes[0, 0].plot(\n", " t_baseline,\n", " baseline_metrics[\"solution\"][\"Voltage [V]\"](baseline_metrics[\"solution\"].t),\n", " label=\"Baseline\",\n", " linewidth=2,\n", " alpha=0.8,\n", ")\n", "axes[0, 0].plot(\n", " t_optimal,\n", " optimal_metrics[\"solution\"][\"Voltage [V]\"](optimal_metrics[\"solution\"].t),\n", " label=\"Optimal\",\n", " linewidth=2,\n", " alpha=0.8,\n", " linestyle=\"--\",\n", ")\n", "axes[0, 0].set_xlabel(\"Time [h]\")\n", "axes[0, 0].set_ylabel(\"Voltage [V]\")\n", "axes[0, 0].set_title(\"Cell Voltage: Baseline vs Optimal\")\n", "axes[0, 0].legend()\n", "axes[0, 0].grid(True, alpha=0.3)\n", "\n", "# Discharge capacity\n", "sol_discharge_baseline = baseline_metrics[\"solution\"].sub_solutions[0]\n", "sol_discharge_optimal = optimal_metrics[\"solution\"].sub_solutions[0]\n", "\n", "axes[0, 1].plot(\n", " sol_discharge_baseline.t / 3600,\n", " abs(sol_discharge_baseline[\"Discharge capacity [A.h]\"](sol_discharge_baseline.t)),\n", " label=\"Baseline\",\n", " linewidth=2,\n", " alpha=0.8,\n", ")\n", "axes[0, 1].plot(\n", " sol_discharge_optimal.t / 3600,\n", " abs(sol_discharge_optimal[\"Discharge capacity [A.h]\"](sol_discharge_optimal.t)),\n", " label=\"Optimal\",\n", " linewidth=2,\n", " alpha=0.8,\n", " linestyle=\"--\",\n", ")\n", "axes[0, 1].set_xlabel(\"Time [h]\")\n", "axes[0, 1].set_ylabel(\"Discharge Capacity [A.h]\")\n", "axes[0, 1].set_title(\"Discharge Capacity\")\n", "axes[0, 1].legend()\n", "axes[0, 1].grid(True, alpha=0.3)\n", "\n", "# Anode potential during charge\n", "charge_solutions_baseline = baseline_metrics[\"solution\"].sub_solutions[2:]\n", "charge_solutions_optimal = optimal_metrics[\"solution\"].sub_solutions[2:]\n", "\n", "sol_charge_baseline = charge_solutions_baseline[0]\n", "sol_charge_optimal = charge_solutions_optimal[0]\n", "for charge_sol in charge_solutions_baseline[1:]:\n", " sol_charge_baseline = sol_charge_baseline + charge_sol\n", "for charge_sol in charge_solutions_optimal[1:]:\n", " sol_charge_optimal = sol_charge_optimal + charge_sol\n", "\n", "t_charge_baseline = (sol_charge_baseline.t / 3600) - (sol_charge_baseline.t[0] / 3600)\n", "t_charge_optimal = (sol_charge_optimal.t / 3600) - (sol_charge_optimal.t[0] / 3600)\n", "\n", "anode_baseline = sol_charge_baseline[\n", " \"Negative electrode surface potential difference at separator interface [V]\"\n", "](sol_charge_baseline.t)\n", "anode_optimal = sol_charge_optimal[\n", " \"Negative electrode surface potential difference at separator interface [V]\"\n", "](sol_charge_optimal.t)\n", "\n", "axes[1, 0].plot(\n", " t_charge_baseline, anode_baseline, label=\"Baseline\", linewidth=2, alpha=0.8\n", ")\n", "axes[1, 0].plot(\n", " t_charge_optimal,\n", " anode_optimal,\n", " label=\"Optimal\",\n", " linewidth=2,\n", " alpha=0.8,\n", " linestyle=\"--\",\n", ")\n", "axes[1, 0].axhline(y=0.0, color=\"r\", linestyle=\":\", alpha=0.7, label=\"Safety limit\")\n", "axes[1, 0].set_xlabel(\"Charge Time [h]\")\n", "axes[1, 0].set_ylabel(\"Anode - Electrolyte Potential [V]\")\n", "axes[1, 0].set_title(\"Anode Potential During Charge\")\n", "axes[1, 0].legend()\n", "axes[1, 0].grid(True, alpha=0.3)\n", "\n", "# Performance metrics bar chart\n", "metrics_names = [\n", " \"Discharge\\nCapacity [A.h]\",\n", " \"Min Anode\\nPotential [V]\",\n", " \"Mean Anode\\nPotential [V]\",\n", "]\n", "baseline_values = [\n", " baseline_metrics[\"discharge_capacity\"],\n", " baseline_metrics[\"min_anode_potential\"],\n", " baseline_metrics[\"mean_anode_potential\"],\n", "]\n", "optimal_values = [\n", " optimal_metrics[\"discharge_capacity\"],\n", " optimal_metrics[\"min_anode_potential\"],\n", " optimal_metrics[\"mean_anode_potential\"],\n", "]\n", "\n", "x = np.arange(len(metrics_names))\n", "width = 0.35\n", "\n", "axes[1, 1].bar(\n", " x - width / 2,\n", " baseline_values,\n", " width,\n", " label=\"Baseline\",\n", " alpha=0.8,\n", " color=\"lightblue\",\n", ")\n", "axes[1, 1].bar(\n", " x + width / 2, optimal_values, width, label=\"Optimal\", alpha=0.8, color=\"orange\"\n", ")\n", "\n", "for i, (baseline, optimal) in enumerate(\n", " zip(baseline_values, optimal_values, strict=False)\n", "):\n", " y_offset = baseline * 0.02 if baseline > 0 else 0.005\n", " axes[1, 1].text(\n", " i - width / 2,\n", " baseline + y_offset,\n", " f\"{baseline:.3f}\",\n", " ha=\"center\",\n", " va=\"bottom\",\n", " fontsize=9,\n", " )\n", " axes[1, 1].text(\n", " i + width / 2,\n", " optimal + y_offset,\n", " f\"{optimal:.3f}\",\n", " ha=\"center\",\n", " va=\"bottom\",\n", " fontsize=9,\n", " )\n", "\n", "\n", "axes[1, 1].set_xlabel(\"Performance Metrics\")\n", "axes[1, 1].set_ylabel(\"Value\")\n", "axes[1, 1].set_title(\"Performance Metrics Comparison\")\n", "axes[1, 1].set_xticks(x)\n", "axes[1, 1].set_xticklabels(metrics_names)\n", "axes[1, 1].legend()\n", "axes[1, 1].grid(True, alpha=0.3)\n", "\n", "# Parameter comparison bar chart\n", "param_names = list(parameters.keys())\n", "param_labels = [\n", " \"Pos. Active\\nMaterial [-]\",\n", " \"Neg. Active\\nMaterial [-]\",\n", " \"Pos. Thickness\\n[um]\",\n", " \"Neg. Thickness\\n[um]\",\n", " \"Number of\\nElectrodes [-]\",\n", "]\n", "\n", "baseline_param_values = [param.initial_value for param in parameters.values()]\n", "optimal_param_values = [results.parameter_values[name] for name in param_names]\n", "\n", "# Convert thickness values to micrometers for better readability\n", "baseline_param_values[2] *= 1e6 # Positive thickness to um\n", "baseline_param_values[3] *= 1e6 # Negative thickness to um\n", "optimal_param_values[2] *= 1e6 # Positive thickness to um\n", "optimal_param_values[3] *= 1e6 # Negative thickness to um\n", "\n", "x_params = np.arange(len(param_names))\n", "width_params = 0.35\n", "\n", "axes[1, 2].bar(\n", " x_params - width_params / 2,\n", " baseline_param_values,\n", " width_params,\n", " label=\"Baseline\",\n", " alpha=0.8,\n", " color=\"lightblue\",\n", ")\n", "axes[1, 2].bar(\n", " x_params + width_params / 2,\n", " optimal_param_values,\n", " width_params,\n", " label=\"Optimal\",\n", " alpha=0.8,\n", " color=\"orange\",\n", ")\n", "\n", "# Add value labels on bars\n", "for i, (baseline, optimal) in enumerate(\n", " zip(baseline_param_values, optimal_param_values, strict=False)\n", "):\n", " y_offset_baseline = baseline * 0.02 if baseline > 0 else 0.005\n", " y_offset_optimal = optimal * 0.02 if optimal > 0 else 0.005\n", "\n", " # Format based on parameter type\n", " if i < 2: # Volume fractions\n", " baseline_text = f\"{baseline:.3f}\"\n", " optimal_text = f\"{optimal:.3f}\"\n", " elif i < 4: # Thicknesses (now in um)\n", " baseline_text = f\"{baseline:.1f}\"\n", " optimal_text = f\"{optimal:.1f}\"\n", " else: # Number of electrodes\n", " baseline_text = f\"{baseline:.0f}\"\n", " optimal_text = f\"{optimal:.0f}\"\n", "\n", " axes[1, 2].text(\n", " i - width_params / 2,\n", " baseline + y_offset_baseline,\n", " baseline_text,\n", " ha=\"center\",\n", " va=\"bottom\",\n", " fontsize=8,\n", " )\n", " axes[1, 2].text(\n", " i + width_params / 2,\n", " optimal + y_offset_optimal,\n", " optimal_text,\n", " ha=\"center\",\n", " va=\"bottom\",\n", " fontsize=8,\n", " )\n", "\n", "axes[1, 2].set_xlabel(\"Optimization Parameters\")\n", "axes[1, 2].set_ylabel(\"Parameter Value\")\n", "axes[1, 2].set_title(\"Parameter Values: Baseline vs Optimal\")\n", "axes[1, 2].set_xticks(x_params)\n", "axes[1, 2].set_ylim(top=85)\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", "# Add upper right subplot for summary statistics\n", "param_changes = []\n", "for _i, (name, param) in enumerate(parameters.items()):\n", " baseline_val = param.initial_value\n", " optimal_val = results.parameter_values[name]\n", " percent_change = ((optimal_val - baseline_val) / baseline_val) * 100\n", " param_changes.append(percent_change)\n", "\n", "axes[0, 2].barh(\n", " range(len(param_names)),\n", " param_changes,\n", " color=[\"orange\" if x > 0 else \"lightblue\" for x in param_changes],\n", " alpha=0.7,\n", ")\n", "axes[0, 2].set_yticks(range(len(param_names)))\n", "axes[0, 2].set_yticklabels(param_labels, fontsize=9)\n", "axes[0, 2].set_xlabel(\"Percentage Change [%]\")\n", "axes[0, 2].set_title(\"Parameter Changes from Baseline\")\n", "axes[0, 2].set_xlim(-50, 110)\n", "axes[0, 2].axvline(x=0, color=\"black\", linestyle=\"-\", alpha=0.3)\n", "axes[0, 2].grid(True, alpha=0.3)\n", "\n", "# Add percentage labels\n", "for i, change in enumerate(param_changes):\n", " axes[0, 2].text(\n", " change + (1 if change >= 0 else -1),\n", " i,\n", " f\"{change:+.1f}%\",\n", " va=\"center\",\n", " ha=\"left\" if change >= 0 else \"right\",\n", " fontsize=8,\n", " )\n", "\n", "plt.tight_layout()\n", "plt.show()\n", "\n", "print(\"Optimization analysis completed!\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "ionworkspipeline", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.11.11" } }, "nbformat": 4, "nbformat_minor": 2 }