From bc603ae1884000aa74279b4b8c0b37f776c1e2b5 Mon Sep 17 00:00:00 2001 From: fderuiter <127706008+fderuiter@users.noreply.github.com> Date: Mon, 9 Mar 2026 12:31:05 +0000 Subject: [PATCH] feat: add battery lifetime estimator tool - Refactored `BatteryDegradationTab` to use the `BatteryDegradationTool` trait (Strategy pattern) to manage sub-tools. - Moved existing Capacity Fade logic into `CapacityFadeTool`. - Implemented `LifetimeEstimatorTool` using `PowerLawModel::cycles_to_capacity` and typed values (`Capacity`, `DepthOfDischarge`) to calculate and display expected battery lifetime. - Checked off "Lifetime Estimator" in `todo_gui.md`. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- .../capacity_fade.rs} | 12 +- .../battery_degradation/lifetime_estimator.rs | 106 ++++++++++++++++++ .../src/tabs/battery_degradation/mod.rs | 68 +++++++++++ todo_gui.md | 2 +- 4 files changed, 181 insertions(+), 7 deletions(-) rename math_explorer_gui/src/tabs/{battery_degradation.rs => battery_degradation/capacity_fade.rs} (93%) create mode 100644 math_explorer_gui/src/tabs/battery_degradation/lifetime_estimator.rs create mode 100644 math_explorer_gui/src/tabs/battery_degradation/mod.rs diff --git a/math_explorer_gui/src/tabs/battery_degradation.rs b/math_explorer_gui/src/tabs/battery_degradation/capacity_fade.rs similarity index 93% rename from math_explorer_gui/src/tabs/battery_degradation.rs rename to math_explorer_gui/src/tabs/battery_degradation/capacity_fade.rs index aa38b916..4d37ee60 100644 --- a/math_explorer_gui/src/tabs/battery_degradation.rs +++ b/math_explorer_gui/src/tabs/battery_degradation/capacity_fade.rs @@ -1,15 +1,15 @@ -use crate::tabs::ExplorerTab; +use super::BatteryDegradationTool; use eframe::egui; use egui_plot::{HLine, Line, Plot, PlotPoints}; use math_explorer::applied::battery_degradation::{Cycles, DepthOfDischarge, PowerLawModel}; -pub struct BatteryDegradationTab { +pub struct CapacityFadeTool { dod: f64, temperature: f64, cycles_to_simulate: f64, } -impl Default for BatteryDegradationTab { +impl Default for CapacityFadeTool { fn default() -> Self { Self { dod: 80.0, @@ -19,12 +19,12 @@ impl Default for BatteryDegradationTab { } } -impl ExplorerTab for BatteryDegradationTab { +impl BatteryDegradationTool for CapacityFadeTool { fn name(&self) -> &'static str { - "Battery Degradation" + "Capacity Fade" } - fn show(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + fn show(&mut self, ctx: &egui::Context) { // Enforce valid ranges for internal state before using it self.dod = self.dod.clamp(0.0, 100.0); self.cycles_to_simulate = self.cycles_to_simulate.max(100.0); diff --git a/math_explorer_gui/src/tabs/battery_degradation/lifetime_estimator.rs b/math_explorer_gui/src/tabs/battery_degradation/lifetime_estimator.rs new file mode 100644 index 00000000..0583a6da --- /dev/null +++ b/math_explorer_gui/src/tabs/battery_degradation/lifetime_estimator.rs @@ -0,0 +1,106 @@ +use super::BatteryDegradationTool; +use eframe::egui; +use math_explorer::applied::battery_degradation::{ + Capacity, DepthOfDischarge, PowerLawModel, +}; + +pub struct LifetimeEstimatorTool { + target_capacity: f64, + dod: f64, + cycles_per_day: f64, +} + +impl Default for LifetimeEstimatorTool { + fn default() -> Self { + Self { + target_capacity: 80.0, + dod: 50.0, + cycles_per_day: 1.0, + } + } +} + +impl BatteryDegradationTool for LifetimeEstimatorTool { + fn name(&self) -> &'static str { + "Lifetime Estimator" + } + + fn show(&mut self, ctx: &egui::Context) { + // Enforce valid ranges + self.target_capacity = self.target_capacity.clamp(0.1, 99.9); + self.dod = self.dod.clamp(0.1, 100.0); + self.cycles_per_day = self.cycles_per_day.max(0.1); + + egui::CentralPanel::default().show(ctx, |ui| { + ui.heading("Battery Lifetime Estimator"); + ui.label("Calculate the expected battery life based on specific usage profiles."); + ui.add_space(10.0); + + ui.group(|ui| { + ui.heading("Usage Profile"); + egui::Grid::new("lifetime_params_grid").show(ui, |ui| { + ui.label("Depth of Discharge (DoD):"); + ui.add(egui::Slider::new(&mut self.dod, 0.1..=100.0).text("%")) + .on_hover_text("Percentage of battery capacity used per cycle."); + ui.end_row(); + + ui.label("Target Capacity (End of Life):"); + ui.add(egui::Slider::new(&mut self.target_capacity, 10.0..=99.0).text("%")) + .on_hover_text("The threshold capacity to be considered 'end of life'."); + ui.end_row(); + + ui.label("Cycles per Day:"); + ui.add(egui::Slider::new(&mut self.cycles_per_day, 0.1..=10.0).text("cycles")) + .on_hover_text("How many charge/discharge cycles happen per day."); + ui.end_row(); + }); + }); + + ui.add_space(20.0); + ui.separator(); + ui.add_space(10.0); + + // Calculation + let model = PowerLawModel::standard(); + let dod_val = DepthOfDischarge::new(self.dod); + let capacity_val = Capacity::new(self.target_capacity / 100.0); + + let total_cycles = model.cycles_to_capacity(capacity_val, dod_val).as_f64(); + let lifetime_days = total_cycles / self.cycles_per_day; + let lifetime_years = lifetime_days / 365.25; + + // Results Display + ui.horizontal(|ui| { + ui.label( + egui::RichText::new("Estimated Total Cycles:") + .size(18.0) + .strong(), + ); + ui.label( + egui::RichText::new(format!("{:.0}", total_cycles)) + .size(24.0) + .color(egui::Color32::GREEN) + .strong(), + ); + }); + + ui.add_space(5.0); + + ui.horizontal(|ui| { + ui.label( + egui::RichText::new("Estimated Lifetime:") + .size(18.0) + .strong(), + ); + ui.label( + egui::RichText::new(format!("{:.1} years", lifetime_years)) + .size(24.0) + .color(egui::Color32::GREEN) + .strong(), + ); + }); + + ui.label(format!("({:.0} days)", lifetime_days)); + }); + } +} diff --git a/math_explorer_gui/src/tabs/battery_degradation/mod.rs b/math_explorer_gui/src/tabs/battery_degradation/mod.rs new file mode 100644 index 00000000..23b7d9de --- /dev/null +++ b/math_explorer_gui/src/tabs/battery_degradation/mod.rs @@ -0,0 +1,68 @@ +use crate::tabs::ExplorerTab; +use eframe::egui; + +pub mod capacity_fade; +pub mod lifetime_estimator; + +use capacity_fade::CapacityFadeTool; +use lifetime_estimator::LifetimeEstimatorTool; + +/// A trait for sub-tools within the Battery Degradation tab. +pub trait BatteryDegradationTool { + /// Returns the name of the tool. + fn name(&self) -> &'static str; + + /// Renders the tool's UI. + fn show(&mut self, ctx: &egui::Context); +} + +pub struct BatteryDegradationTab { + tools: Vec>, + selected_tool_index: usize, +} + +impl Default for BatteryDegradationTab { + fn default() -> Self { + Self { + tools: vec![ + Box::new(CapacityFadeTool::default()), + Box::new(LifetimeEstimatorTool::default()), + ], + selected_tool_index: 0, + } + } +} + +impl ExplorerTab for BatteryDegradationTab { + fn name(&self) -> &'static str { + "Battery Degradation" + } + + fn show(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + // Render Top Menu for Tool Selection + egui::TopBottomPanel::top("battery_degradation_tool_selector").show(ctx, |ui| { + ui.horizontal(|ui| { + ui.label("Tool:"); + for (i, tool) in self.tools.iter().enumerate() { + if ui + .selectable_label(self.selected_tool_index == i, tool.name()) + .clicked() + { + self.selected_tool_index = i; + } + } + }); + }); + + // Delegate to active tool + if let Some(tool) = self.tools.get_mut(self.selected_tool_index) { + tool.show(ctx); + } else { + egui::CentralPanel::default().show(ctx, |ui| { + ui.centered_and_justified(|ui| { + ui.label("No tool selected"); + }); + }); + } + } +} diff --git a/todo_gui.md b/todo_gui.md index 4a0fff5a..f448c8ec 100644 --- a/todo_gui.md +++ b/todo_gui.md @@ -107,7 +107,7 @@ This document outlines the roadmap for integrating the various modules of the `m * **Module:** `applied::battery_degradation` * **Features:** * [x] **Capacity Fade:** Plot capacity vs. cycle number based on depth of discharge (DoD) and temperature. - * [ ] **Lifetime Estimator:** Calculator for expected battery life under specific usage profiles. + * [x] **Lifetime Estimator:** Calculator for expected battery life under specific usage profiles. ### 4.2 Clinical Trials * **Module:** `applied::clinical_trials`