From 4306071d1efb61c003d5b85e94004cb943432781 Mon Sep 17 00:00:00 2001 From: Stravinci Date: Tue, 11 Aug 2026 13:15:15 +0100 Subject: [PATCH 1/6] deprecate cleanup candidates for 0.6.0 --- breos/_deprecations.py | 57 +++++++++++ breos/battery.py | 5 + breos/io.py | 8 ++ breos/numba_kernels.py | 7 ++ breos/optimization.py | 3 + breos/plotting.py | 11 +++ breos/polysun_degradation.py | 7 ++ breos/solar.py | 3 + breos/utils.py | 4 + breos/weather.py | 5 + docs/api/appendix.md | 4 + docs/api/optimization.md | 3 + docs/api/weather.md | 3 + docs/deprecations.md | 71 ++++++++++++++ docs/getting-started/installation.md | 2 +- docs/getting-started/troubleshooting.md | 6 +- docs/index.md | 1 + tests/test_deprecations.py | 123 ++++++++++++++++++++++++ tests/test_numba_kernels.py | 2 + 19 files changed, 322 insertions(+), 3 deletions(-) create mode 100644 breos/_deprecations.py create mode 100644 docs/deprecations.md create mode 100644 tests/test_deprecations.py diff --git a/breos/_deprecations.py b/breos/_deprecations.py new file mode 100644 index 0000000..3ac2ffc --- /dev/null +++ b/breos/_deprecations.py @@ -0,0 +1,57 @@ +"""Internal helpers for BREOS's staged public API removals.""" + +from __future__ import annotations + +import warnings +from functools import wraps +from typing import Any, Callable, TypeVar, cast + +_F = TypeVar("_F", bound=Callable[..., Any]) + + +def deprecation_message(name: str, *, replacement: str | None = None) -> str: + """Build the standard message for APIs scheduled for BREOS 0.6.0.""" + + message = f"{name} is deprecated and will be removed in BREOS 0.6.0." + if replacement: + message += f" Use {replacement} instead." + return message + + +def deprecated(*, name: str, replacement: str | None = None): + """Warn when a deprecated function is called or class is instantiated.""" + + message = deprecation_message(name, replacement=replacement) + + def decorate(obj): + if isinstance(obj, type): + original_init = obj.__init__ + + @wraps(original_init) + def warned_init(self, *args, **kwargs): + warnings.warn(message, DeprecationWarning, stacklevel=2) + original_init(self, *args, **kwargs) + + obj.__init__ = warned_init + obj.__breos_deprecated_removal__ = "0.6.0" + return obj + + @wraps(obj) + def warned_call(*args, **kwargs): + warnings.warn(message, DeprecationWarning, stacklevel=2) + return obj(*args, **kwargs) + + warned_call.__breos_deprecated_removal__ = "0.6.0" + return cast(_F, warned_call) + + return decorate + + +def warn_deprecated(name: str, *, replacement: str | None = None, stacklevel: int = 2) -> None: + """Warn for deprecated surfaces that are not represented by a callable.""" + + warnings.warn( + deprecation_message(name, replacement=replacement), + DeprecationWarning, + stacklevel=stacklevel, + ) diff --git a/breos/battery.py b/breos/battery.py index 5af37fd..a44dab3 100644 --- a/breos/battery.py +++ b/breos/battery.py @@ -16,6 +16,7 @@ import pandas as pd import rainflow +from breos._deprecations import deprecated from breos.constants import ( A_Q, A_R, @@ -1678,18 +1679,21 @@ def detect_cycles_rainflow( return cycles +@deprecated(name="breos.battery.compute_halfcycle_energy_throughput") def compute_halfcycle_energy_throughput(hc: Dict, soc_series_absolute: pd.Series, nominal_energy_Wh: float) -> float: """Compute energy throughput (Wh) for a half-cycle.""" s = soc_series_absolute.iloc[hc["start_idx"] : hc["end_idx"] + 1].values return abs(s[-1] - s[0]) * nominal_energy_Wh +@deprecated(name="breos.battery.k_c_rate_Q") def k_c_rate_Q(C_rate: float) -> float: """Calculate C-rate factor for capacity fade (Naumann Eq. 8).""" kC = A_Q * C_rate + B_Q return max(0.0, kC) +@deprecated(name="breos.battery.k_doc_Q") def k_doc_Q(DOC_frac: float) -> float: """Calculate DOC factor for capacity fade (Naumann Eq. 10).""" kDOC = C_DOC_Q * ((DOC_frac - 0.6) ** 3) + D_DOC_Q @@ -2010,6 +2014,7 @@ def update_battery_soh_calendar( return soh_after, d_soh_fraction, t_new +@deprecated(name="breos.battery.update_battery_soc") def update_battery_soc( battery_energy_wh: float, nominal_energy_wh: float, soh_fraction: float, max_soc: float, min_soc: float ) -> Tuple[float, float]: diff --git a/breos/io.py b/breos/io.py index cd11065..91a6ea2 100644 --- a/breos/io.py +++ b/breos/io.py @@ -14,6 +14,8 @@ import numpy as np import pandas as pd +from breos._deprecations import deprecated + def export_results( results_df: pd.DataFrame, @@ -181,6 +183,10 @@ def _economics_summary_metrics(cost_projection_df: Optional[pd.DataFrame]) -> Di return metrics +@deprecated( + name="breos.io.save_simulation_report", + replacement="the focused export_results, export_summary, and export_cost_analysis functions", +) def save_simulation_report( results_df: pd.DataFrame, summary_df: pd.DataFrame, @@ -284,6 +290,7 @@ def load_results(filepath: str, parse_dates: Union[bool, List[str]] = True) -> p return df +@deprecated(name="breos.io.export_monthly_summary", replacement="pandas.DataFrame.resample") def export_monthly_summary(results_df: pd.DataFrame, results_directory: str, prefix: str = "", suffix: str = "") -> str: """ Export monthly aggregated summary to CSV. @@ -322,6 +329,7 @@ def export_monthly_summary(results_df: pd.DataFrame, results_directory: str, pre return filepath +@deprecated(name="breos.io.export_yearly_summary", replacement="pandas.DataFrame.resample") def export_yearly_summary(results_df: pd.DataFrame, results_directory: str, prefix: str = "", suffix: str = "") -> str: """ Export yearly aggregated summary to CSV. diff --git a/breos/numba_kernels.py b/breos/numba_kernels.py index bcf31cc..0714b50 100644 --- a/breos/numba_kernels.py +++ b/breos/numba_kernels.py @@ -23,6 +23,13 @@ import numpy as np from numba import jit, prange +from breos._deprecations import warn_deprecated + +warn_deprecated( + "breos.numba_kernels", + replacement="breos.battery.simulate_energy_balance", +) + ENERGY_BALANCE_CAPABILITIES = { "status": "approximate_screening_only", "production_caller": False, diff --git a/breos/optimization.py b/breos/optimization.py index 7230658..dd56189 100644 --- a/breos/optimization.py +++ b/breos/optimization.py @@ -13,6 +13,7 @@ import numpy as np import pandas as pd +from breos._deprecations import deprecated from breos.battery import BatteryConfig, simulate_energy_balance from breos.economics import calculate_costs, cost_params_from_config, system_ac_production_power from breos.solar import PVModuleParams, calculate_pv_production_dc, default_azimuth @@ -126,6 +127,7 @@ def optimize_tilt( ) +@deprecated(name="breos.optimization.optimize_tilt_brent", replacement="breos.optimization.optimize_tilt") def optimize_tilt_brent( weather_data: pd.DataFrame, location, @@ -284,6 +286,7 @@ def optimize_battery_size( ) +@deprecated(name="breos.optimization.size_for_zeb") def size_for_zeb(houseload: pd.DataFrame, ac_loss: pd.Series, current_n_modules: int) -> Dict[str, float]: """ Calculate PV system size needed for Zero Energy Building (ZEB). diff --git a/breos/plotting.py b/breos/plotting.py index 36dec4c..954af47 100644 --- a/breos/plotting.py +++ b/breos/plotting.py @@ -14,6 +14,8 @@ import numpy as np import pandas as pd +from breos._deprecations import deprecated + MONTH_LABELS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] # Plotting imports with backend handling @@ -2204,6 +2206,7 @@ def autolabel(rects, is_gray=False): plt.close() +@deprecated(name="breos.plotting.plot_smart_charging_sweep") def plot_smart_charging_sweep( results_df: pd.DataFrame, optimal_pct: float, results_directory: str, scenario_name: str = "" ) -> None: @@ -2248,6 +2251,7 @@ def plot_smart_charging_sweep( plt.close() +@deprecated(name="breos.plotting.plot_optimization_results_3d") def plot_optimization_results_3d(results_df: pd.DataFrame, results_directory: str, scenario_name: str = "") -> None: """ Create 3D scatter plot for 3-objective optimization results. @@ -2290,6 +2294,7 @@ def plot_optimization_results_3d(results_df: pd.DataFrame, results_directory: st plt.close() +@deprecated(name="breos.plotting.plot_optimization_results_2d") def plot_optimization_results_2d(results_df: pd.DataFrame, results_directory: str, scenario_name: str = "") -> None: """ Create 2D scatter plot for optimization results (Pareto Front). @@ -2867,6 +2872,7 @@ def _is_pareto_efficient(costs, independence): plt.close() +@deprecated(name="breos.plotting.plot_loo_cv_summary") def plot_loo_cv_summary( loo_data: dict, results_directory: str, @@ -2913,6 +2919,7 @@ def plot_loo_cv_summary( plt.close() +@deprecated(name="breos.plotting.plot_loo_param_stability") def plot_loo_param_stability( loo_data: dict, full_cal_params: dict, @@ -2973,6 +2980,7 @@ def plot_loo_param_stability( plt.close() +@deprecated(name="breos.plotting.plot_loo_predictions") def plot_loo_predictions( systems_predictions: list, results_directory: str, @@ -3324,6 +3332,7 @@ def plot_co2_savings( # ========================================================================= +@deprecated(name="breos.plotting.plot_degradation_methodology_comparison") def plot_degradation_methodology_comparison( breos_soh: "pd.DataFrame", polysun_df: "pd.DataFrame", @@ -3430,6 +3439,7 @@ def plot_degradation_methodology_comparison( plt.close(fig) +@deprecated(name="breos.plotting.plot_lifetime_prediction_comparison") def plot_lifetime_prediction_comparison( scenarios: dict, results_directory: str, @@ -3494,6 +3504,7 @@ def plot_lifetime_prediction_comparison( plt.close(fig) +@deprecated(name="breos.plotting.plot_temperature_sensitivity_comparison") def plot_temperature_sensitivity_comparison( locations: dict, results_directory: str, diff --git a/breos/polysun_degradation.py b/breos/polysun_degradation.py index 429213c..f9d479e 100644 --- a/breos/polysun_degradation.py +++ b/breos/polysun_degradation.py @@ -26,6 +26,7 @@ import numpy as np import pandas as pd +from breos._deprecations import deprecated from breos.constants import ( POLYSUN_CALENDAR_LIFE_LEAD, POLYSUN_CALENDAR_LIFE_LION, @@ -38,6 +39,7 @@ ) +@deprecated(name="breos.polysun_degradation.PolysunDegradationConfig") @dataclass class PolysunDegradationConfig: """Configuration for Polysun-style degradation model. @@ -61,6 +63,7 @@ class PolysunDegradationConfig: deep_cycle_threshold: float = 0.50 +@deprecated(name="breos.polysun_degradation.woehler_cycles_to_failure") def woehler_cycles_to_failure(dod: float, a: float, b: float) -> float: """Cycles to failure from Wöhler curve: N(DOD) = a * DOD^(-b). @@ -77,6 +80,7 @@ def woehler_cycles_to_failure(dod: float, a: float, b: float) -> float: return a * dod ** (-b) +@deprecated(name="breos.polysun_degradation.compute_dod_histogram") def compute_dod_histogram( soc_series: np.ndarray, n_bins: int = 20, @@ -153,6 +157,7 @@ def compute_dod_histogram( return bin_centers, cycle_counts, total_cycles, deep_cycles +@deprecated(name="breos.polysun_degradation.compute_miner_damage") def compute_miner_damage( cycle_counts: np.ndarray, bin_centers: np.ndarray, @@ -182,6 +187,7 @@ def compute_miner_damage( return damage +@deprecated(name="breos.polysun_degradation.predict_polysun_lifetime") def predict_polysun_lifetime( annual_damage: float, calendar_life_years: float, @@ -206,6 +212,7 @@ def predict_polysun_lifetime( return total_life, cycle_life, calendar_life_years +@deprecated(name="breos.polysun_degradation.simulate_polysun_degradation") def simulate_polysun_degradation( soc_series: np.ndarray, config: PolysunDegradationConfig, diff --git a/breos/solar.py b/breos/solar.py index 346b69b..3d52986 100644 --- a/breos/solar.py +++ b/breos/solar.py @@ -16,6 +16,7 @@ from pvlib.albedo import SURFACE_ALBEDOS from pvlib.location import Location +from breos._deprecations import deprecated from breos.cec_fit import fit_cec_params from breos.inverter import calculate_dc_ac_power from breos.pv.iam import calculate_front_effective_irradiance @@ -1062,6 +1063,7 @@ def dc_to_ac( return pd.Series(ac_power, index=dc_power.index, name="ac_power_W") +@deprecated(name="breos.solar.calculate_pv_production_tmy", replacement="breos.solar.calculate_pv_production_dc") def calculate_pv_production_tmy( tmy_data: pd.DataFrame, location: Location, @@ -1278,6 +1280,7 @@ def default_azimuth(latitude: float) -> float: return 180.0 if latitude >= 0 else 0.0 +@deprecated(name="breos.solar.zeb_sizer") def zeb_sizer(houseload: pd.DataFrame, ac_loss: pd.Series, current_n_modules: int, freq: str = "h") -> Dict[str, float]: """ Size a Zero Energy Building (ZEB) PV system. diff --git a/breos/utils.py b/breos/utils.py index e9cea01..f587c47 100644 --- a/breos/utils.py +++ b/breos/utils.py @@ -9,6 +9,8 @@ import numpy as np import pandas as pd +from breos._deprecations import deprecated + _SAFE_SLUG_RE = re.compile(r"^[a-z0-9][a-z0-9_-]{0,63}$") @@ -45,6 +47,7 @@ def is_leap_year(year: int) -> bool: return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0) +@deprecated(name="breos.utils.count_leap_years", replacement="breos.utils.is_leap_year") def count_leap_years(start_year: int, num_years: int) -> int: """ Count the number of leap years in a range. @@ -91,6 +94,7 @@ def remap_datetime_index_years(obj, year_offset: int): return out +@deprecated(name="breos.utils.number_of_cores", replacement="os.cpu_count") def number_of_cores() -> int: """ Get the number of available CPU cores for parallel processing. diff --git a/breos/weather.py b/breos/weather.py index 1a8b70c..1954fdf 100644 --- a/breos/weather.py +++ b/breos/weather.py @@ -20,6 +20,7 @@ from pvlib.location import Location from scipy.interpolate import Akima1DInterpolator +from breos._deprecations import deprecated from breos.utils import safe_path_slug logger = logging.getLogger(__name__) @@ -581,6 +582,7 @@ def resample_to_15min( return df_15min +@deprecated(name="breos.weather.resample_to_hourly", replacement="pandas.DataFrame.resample") def resample_to_hourly(df_15min: pd.DataFrame, agg_method: str = "mean") -> pd.DataFrame: """ Resample 15-minute DataFrame to hourly intervals. @@ -604,6 +606,7 @@ def resample_to_hourly(df_15min: pd.DataFrame, agg_method: str = "mean") -> pd.D raise ValueError(f"Unknown aggregation method: {agg_method}") +@deprecated(name="breos.weather.csv_15min_to_hourly", replacement="pandas.DataFrame.resample") def csv_15min_to_hourly( input_file_name: str, output_file_name: str, @@ -647,6 +650,7 @@ def csv_15min_to_hourly( return None +@deprecated(name="breos.weather.csv_hourly_to_15min", replacement="breos.weather.resample_to_15min") def csv_hourly_to_15min( input_file_name: str, output_file_name: str, @@ -821,6 +825,7 @@ def preload_weather_by_year( return result +@deprecated(name="breos.weather.fetch_tmy_nsrdb", replacement="breos.weather.fetch_tmy_weather_data") def fetch_tmy_nsrdb( latitude: float, longitude: float, diff --git a/docs/api/appendix.md b/docs/api/appendix.md index ac62759..44dcc38 100644 --- a/docs/api/appendix.md +++ b/docs/api/appendix.md @@ -4,6 +4,10 @@ Modules that are re-exported from the `breos` namespace but aren't part of the primary puzzle-piece surface — utilities, I/O helpers, model constants, and research-validation modules. +Some article-scoped and report helpers on these modules are scheduled for +removal in 0.6.0. See [Deprecations for 0.6.0](../deprecations.md) for the +complete inventory and migration guidance. + ```{eval-rst} .. autosummary:: :toctree: generated/ diff --git a/docs/api/optimization.md b/docs/api/optimization.md index e365efc..651dcd3 100644 --- a/docs/api/optimization.md +++ b/docs/api/optimization.md @@ -10,6 +10,9 @@ command documented in [Recipes](../getting-started/recipes.md#parameter-sweep). Install `breos[optimization]` to use pymoo-backed multi-objective sizing. The one-dimensional helpers use the core scientific stack. +The Brent tilt helper and both standalone ZEB sizing helpers are scheduled for +removal in 0.6.0. See [Deprecations for 0.6.0](../deprecations.md). + ZEB and financial production use usable AC system energy from the dispatch ledger, not raw PV DC, so inverter efficiency and clipping affect candidate scores. Physical size, inverter rating, and CAPEX use the selected module's diff --git a/docs/api/weather.md b/docs/api/weather.md index 01a63e2..d5a0d58 100644 --- a/docs/api/weather.md +++ b/docs/api/weather.md @@ -6,6 +6,9 @@ temperature time series. Local weather loading and PVGIS/NSRDB TMY helpers use the core install. Open-Meteo historical fetching requires `breos[weather]`. +The NSRDB helper and legacy downsampling/CSV converters are scheduled for +removal in 0.6.0. See [Deprecations for 0.6.0](../deprecations.md). + ## Loading from local files ```{eval-rst} diff --git a/docs/deprecations.md b/docs/deprecations.md new file mode 100644 index 0000000..19d5e58 --- /dev/null +++ b/docs/deprecations.md @@ -0,0 +1,71 @@ +# Deprecations for 0.6.0 + +BREOS 0.5.1 keeps the APIs below working but emits a `DeprecationWarning` when +they are used. They are +scheduled for removal in BREOS 0.6.0. Python hides `DeprecationWarning` by +default; run tests with `-W default` or `-W error::DeprecationWarning` to find +calls before upgrading. + +The {py:class}`~breos.App` facade and its configuration are unaffected. + +## Accelerated screening kernels + +`breos.numba_kernels` and the `breos[fast]` optional extra are deprecated. +These approximate standalone kernels are not called by `App` or by +{py:func}`breos.battery.simulate_energy_balance`, and their degradation and +dispatch behavior does not match the reference simulation. Use +{py:func}`breos.battery.simulate_energy_balance` for supported results. There +is no supported accelerated replacement in 0.5.x. + +## Polysun comparison baseline + +The article-scoped `breos.polysun_degradation` module and its three comparison +plots are deprecated without a package replacement: + +- `PolysunDegradationConfig`, `woehler_cycles_to_failure`, + `compute_dod_histogram`, `compute_miner_damage`, + `predict_polysun_lifetime`, and `simulate_polysun_degradation` +- `plot_degradation_methodology_comparison`, + `plot_lifetime_prediction_comparison`, and + `plot_temperature_sensitivity_comparison` + +Copy the comparison implementation into the research artifact that needs it +before moving to 0.6.0. BREOS's supported degradation models are documented in +[Degradation models](api/degradation-models.md). + +## Undocumented plotting helpers + +The following unverified plotting helpers are deprecated without a direct +replacement: + +- `plot_smart_charging_sweep` +- `plot_optimization_results_2d` and `plot_optimization_results_3d` +- `plot_loo_cv_summary`, `plot_loo_param_stability`, and + `plot_loo_predictions` + +The supported plotting surface remains in [Plotting](api/plotting.md). + +## Orphaned module helpers + +| Deprecated API | Migration | +|---|---| +| `breos.io.save_simulation_report` | Call `export_results`, `export_summary`, and `export_cost_analysis` for the artifacts needed by the application. | +| `breos.io.export_monthly_summary` | Aggregate numeric columns with `DataFrame.resample("ME").sum()`, then write the result with pandas. | +| `breos.io.export_yearly_summary` | Aggregate numeric columns with `DataFrame.resample("YE").sum()`, then write the result with pandas. | +| `breos.weather.resample_to_hourly` | Use `DataFrame.resample("h")` with the required aggregation. | +| `breos.weather.csv_15min_to_hourly` | Read and write the CSV with pandas and use `DataFrame.resample("h")`. | +| `breos.weather.csv_hourly_to_15min` | Read and write the CSV with pandas around `breos.weather.resample_to_15min`. | +| `breos.weather.fetch_tmy_nsrdb` | Use `breos.weather.fetch_tmy_weather_data` for the supported PVGIS TMY path. | +| `breos.solar.calculate_pv_production_tmy` | Call `breos.solar.calculate_pv_production_dc`; TMY data needs no special production wrapper. | +| `breos.solar.zeb_sizer` | Compute the annual usable-AC-production to load ratio in application code. | +| `breos.optimization.optimize_tilt_brent` | Use the supported `breos.optimization.optimize_tilt` grid search. | +| `breos.optimization.size_for_zeb` | Compute the annual usable-AC-production to load ratio in application code. | +| `breos.utils.count_leap_years` | Sum `breos.utils.is_leap_year(year)` over the required range. | +| `breos.utils.number_of_cores` | Use `os.cpu_count()` and apply the desired worker policy in application code. | +| `breos.battery.compute_halfcycle_energy_throughput` | Compute throughput directly from the cycle boundaries when maintaining a custom degradation model. | +| `breos.battery.k_c_rate_Q` | Keep this equation with the custom degradation model that uses it. | +| `breos.battery.k_doc_Q` | Keep this equation with the custom degradation model that uses it. | +| `breos.battery.update_battery_soc` | Derive SOC from the energy ledger in application code; BREOS's simulation handles this internally. | + +All deprecated functions retain their 0.5.0 signatures and return values +during 0.5.x. The warnings identify 0.6.0 as the earliest removal release. diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 6fddb1c..6c65453 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -40,7 +40,7 @@ workflows that need heavier optional packages: pip install "breos[plots]" # matplotlib plotting helpers pip install "breos[optimization]" # pymoo multi-objective sizing pip install "breos[weather]" # Open-Meteo historical weather fetching -pip install "breos[fast]" # Approximate screening kernels only +pip install "breos[fast]" # Deprecated; removed in BREOS 0.6.0 pip install "breos[location-tools]" # geocoding and timezone lookup helpers ``` diff --git a/docs/getting-started/troubleshooting.md b/docs/getting-started/troubleshooting.md index 9be6838..f5f905c 100644 --- a/docs/getting-started/troubleshooting.md +++ b/docs/getting-started/troubleshooting.md @@ -49,12 +49,14 @@ by the workflow: pip install "breos[plots]" # Matplotlib plotting helpers pip install "breos[optimization]" # pymoo optimization pip install "breos[weather]" # Open-Meteo historical weather -pip install "breos[fast]" # Approximate screening kernels only +pip install "breos[fast]" # Deprecated; removed in BREOS 0.6.0 ``` The current Numba kernels are not called by `breos.App`, Monte Carlo, or multi-objective optimization, so installing `fast` does not accelerate those -production paths. +production paths. The extra and its standalone kernels are deprecated in 0.5.1 +and scheduled for removal in 0.6.0; see +[Deprecations for 0.6.0](../deprecations.md). Core imports, help, option discovery, and configuration validation do not load Matplotlib. If an actual plotting command reports that its configuration diff --git a/docs/index.md b/docs/index.md index 6fcb4ba..e9b06d1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -175,4 +175,5 @@ api/index :caption: Project changelog +deprecations ``` diff --git a/tests/test_deprecations.py b/tests/test_deprecations.py new file mode 100644 index 0000000..8a73638 --- /dev/null +++ b/tests/test_deprecations.py @@ -0,0 +1,123 @@ +"""Compatibility tests for APIs scheduled for removal in BREOS 0.6.0.""" + +import inspect +import os +import subprocess +import sys +from pathlib import Path + +import pytest + +import breos +from breos import battery, io, optimization, plotting, polysun_degradation, solar, utils, weather + +DEPRECATED_CALLABLES = { + battery: {"compute_halfcycle_energy_throughput", "k_c_rate_Q", "k_doc_Q", "update_battery_soc"}, + polysun_degradation: { + "PolysunDegradationConfig", + "compute_dod_histogram", + "compute_miner_damage", + "predict_polysun_lifetime", + "simulate_polysun_degradation", + "woehler_cycles_to_failure", + }, + plotting: { + "plot_degradation_methodology_comparison", + "plot_lifetime_prediction_comparison", + "plot_loo_cv_summary", + "plot_loo_param_stability", + "plot_loo_predictions", + "plot_optimization_results_2d", + "plot_optimization_results_3d", + "plot_smart_charging_sweep", + "plot_temperature_sensitivity_comparison", + }, + io: {"export_monthly_summary", "export_yearly_summary", "save_simulation_report"}, + weather: {"csv_15min_to_hourly", "csv_hourly_to_15min", "fetch_tmy_nsrdb", "resample_to_hourly"}, + solar: {"calculate_pv_production_tmy", "zeb_sizer"}, + optimization: {"optimize_tilt_brent", "size_for_zeb"}, + utils: {"count_leap_years", "number_of_cores"}, +} + + +def _source_tree_env(tmp_path): + env = os.environ.copy() + env["MPLCONFIGDIR"] = str(tmp_path) + project_root = str(Path(__file__).resolve().parents[1]) + env["PYTHONPATH"] = os.pathsep.join(filter(None, [project_root, env.get("PYTHONPATH")])) + return env + + +def test_importing_core_package_does_not_emit_deprecation_warning(tmp_path): + code = "import warnings; warnings.simplefilter('error', DeprecationWarning); import breos" + env = _source_tree_env(tmp_path) + + result = subprocess.run( + [sys.executable, "-c", code], + cwd=tmp_path, + env=env, + text=True, + capture_output=True, + check=False, + ) + + assert result.returncode == 0, result.stderr + + +def test_every_scheduled_callable_records_its_removal_release(): + for module, names in DEPRECATED_CALLABLES.items(): + for name in names: + assert getattr(module, name).__breos_deprecated_removal__ == "0.6.0" + + +def test_function_warning_preserves_behavior_and_points_to_replacement(): + with pytest.warns( + DeprecationWarning, + match=r"breos\.utils\.count_leap_years.*BREOS 0\.6\.0.*breos\.utils\.is_leap_year", + ): + result = utils.count_leap_years(2024, 5) + + assert result == 2 + assert list(inspect.signature(utils.count_leap_years).parameters) == ["start_year", "num_years"] + + +def test_deprecated_dataclass_warns_only_when_instantiated(): + config_class = polysun_degradation.PolysunDegradationConfig + + with pytest.warns(DeprecationWarning, match=r"PolysunDegradationConfig.*BREOS 0\.6\.0"): + config = config_class(n_bins=12) + + assert config.n_bins == 12 + assert "n_bins" in inspect.signature(config_class).parameters + + +def test_deprecated_plot_warns_before_preserving_argument_validation(): + with pytest.warns(DeprecationWarning, match=r"plot_loo_cv_summary.*BREOS 0\.6\.0"): + with pytest.raises(TypeError): + plotting.plot_loo_cv_summary() + + +def test_numba_module_warns_on_direct_import(tmp_path): + code = "import warnings; warnings.simplefilter('always', DeprecationWarning); import breos.numba_kernels" + env = _source_tree_env(tmp_path) + + result = subprocess.run( + [sys.executable, "-c", code], + cwd=tmp_path, + env=env, + text=True, + capture_output=True, + check=False, + ) + + assert result.returncode == 0, result.stderr + assert "breos.numba_kernels is deprecated" in result.stderr + assert "removed in BREOS 0.6.0" in result.stderr + + +def test_top_level_compatibility_aliases_are_unchanged(): + assert breos.save_simulation_report is io.save_simulation_report + assert breos.fetch_tmy_nsrdb is weather.fetch_tmy_nsrdb + assert breos.optimize_tilt_brent is optimization.optimize_tilt_brent + assert breos.calculate_pv_production_tmy is solar.calculate_pv_production_tmy + assert breos.compute_dod_histogram is polysun_degradation.compute_dod_histogram diff --git a/tests/test_numba_kernels.py b/tests/test_numba_kernels.py index 113ce57..3479879 100644 --- a/tests/test_numba_kernels.py +++ b/tests/test_numba_kernels.py @@ -6,6 +6,8 @@ pytest.importorskip("numba") +pytestmark = pytest.mark.filterwarnings("ignore:breos.numba_kernels is deprecated:DeprecationWarning") + def test_numba_energy_balance_capability_contract_is_explicit(): from breos.numba_kernels import ENERGY_BALANCE_CAPABILITIES From eaa5444c2ddb55b00889beb0f0edd53142c6c149 Mon Sep 17 00:00:00 2001 From: Stravinci Date: Tue, 11 Aug 2026 14:24:41 +0100 Subject: [PATCH 2/6] Refine cleanup deprecation rollout --- breos/polysun_degradation.py | 15 +++- design/architecture/0.5x-cleanup-plan.md | 87 +++++++------------ design/architecture/0.5x-deprecation-audit.md | 54 ++++++++++++ design/architecture/README.md | 3 +- docs/api/battery.md | 4 + docs/api/optimization.md | 12 +-- docs/api/plotting.md | 5 ++ docs/deprecations.md | 21 +++-- tests/test_deprecations.py | 19 ++++ 9 files changed, 146 insertions(+), 74 deletions(-) create mode 100644 design/architecture/0.5x-deprecation-audit.md diff --git a/breos/polysun_degradation.py b/breos/polysun_degradation.py index f9d479e..331e765 100644 --- a/breos/polysun_degradation.py +++ b/breos/polysun_degradation.py @@ -182,7 +182,10 @@ def compute_miner_damage( for n_i, dod_i in zip(cycle_counts, bin_centers): if n_i <= 0 or dod_i <= 0: continue - n_fail = woehler_cycles_to_failure(dod_i, woehler_a, woehler_b) + # This public helper is deprecated, but an internal call is not another + # user invocation. Bypass the warning wrapper so callers of this + # higher-level entry point receive one warning at their own call site. + n_fail = woehler_cycles_to_failure.__wrapped__(dod_i, woehler_a, woehler_b) damage += n_i / n_fail return damage @@ -235,17 +238,21 @@ def simulate_polysun_degradation( Total_Cycles, Deep_Cycles. """ # Compute annual cycle histogram once (same profile every year) - bin_centers, cycle_counts, total_cycles, deep_cycles = compute_dod_histogram( + # Use the preserved implementations directly so one public call produces + # one user-facing warning instead of a cascade from library-internal calls. + bin_centers, cycle_counts, total_cycles, deep_cycles = compute_dod_histogram.__wrapped__( soc_series, n_bins=config.n_bins, min_doc=config.min_doc, ) # Annual Miner's damage - annual_damage = compute_miner_damage(cycle_counts, bin_centers, config.woehler_a, config.woehler_b) + annual_damage = compute_miner_damage.__wrapped__(cycle_counts, bin_centers, config.woehler_a, config.woehler_b) # Predicted lifetime - total_life, cycle_life, calendar_life = predict_polysun_lifetime(annual_damage, config.calendar_life_years) + total_life, cycle_life, calendar_life = predict_polysun_lifetime.__wrapped__( + annual_damage, config.calendar_life_years + ) rows = [] cumulative_damage = 0.0 diff --git a/design/architecture/0.5x-cleanup-plan.md b/design/architecture/0.5x-cleanup-plan.md index 8eef304..85d67af 100644 --- a/design/architecture/0.5x-cleanup-plan.md +++ b/design/architecture/0.5x-cleanup-plan.md @@ -52,9 +52,10 @@ uv run --extra docs sphinx-build -W -b html docs docs/_build/html ## Release boundary Phases C1-C4 touch advertised extras, exported symbols, or documented public -APIs. Their 0.5.x work is usage auditing, deprecation, and any non-breaking -preparation; confirmed removals land in 0.6.0. Phases C5-C8 are internal code, -tooling, or test work and may land in 0.5.x when they preserve behaviour. +APIs. Their usage audit and deprecation half was completed for 0.5.1; see the +[deprecation audit](0.5x-deprecation-audit.md). Confirmed removals land in +0.6.0. Phases C5-C8 are internal code, tooling, or test work and may land in +0.5.x when they preserve behaviour. ## Inventory @@ -65,7 +66,7 @@ name in `docs/api/*.md` does not count. | Item | Lines | Reachable | Tested | Documented | |---|---:|---|---|---| | `breos/numba_kernels.py` (whole module) | 770 | no | partial | no | -| `breos/polysun_degradation.py` + 2 plots + constants | ~550 | no | no | appendix only | +| `breos/polysun_degradation.py` + 3 plots + constants | ~550 | no | no | appendix only | | Uncalled `plotting.py` functions (excl. retained) | ~478 | no | no | no | | `io.py` report/summary exporters | 146 | no | no | no | | `weather.py` resample/CSV converters | 114 | no | no | partial | @@ -81,7 +82,7 @@ proposed for removal, but they are wholly unverified — see Phase C7. --- -## Phase C1 — Replace `numba_kernels.py` with a day-kernel seam +## Phase C1 — Replace `numba_kernels.py` with a day-kernel seam (deprecated in 0.5.1) ### Objective @@ -175,7 +176,7 @@ tests. --- -## Phase C2 — Remove the Polysun comparison baseline +## Phase C2 — Remove the Polysun comparison baseline (deprecated in 0.5.1) ### Objective @@ -225,7 +226,7 @@ article's own repository, not the shipped package. --- -## Phase C3 — Remove uncalled, undocumented plotting functions +## Phase C3 — Remove uncalled, undocumented plotting functions (deprecated in 0.5.1) ### Objective @@ -265,7 +266,7 @@ and with `tools/azitilt_optimizer.py`, which draws its own landscapes via --- -## Phase C4 — Remove orphaned module-level helpers +## Phase C4 — Remove orphaned module-level helpers (deprecated in 0.5.1) ### Objective @@ -322,47 +323,25 @@ revertible. --- -## Phase C5 — Reconcile the duplicated resistance-fade derate +## Phase C5 — Reconcile the duplicated resistance-fade derate (completed in 0.5.1) ### Objective -This one is a latent correctness problem, not just bloat. +PR #114 made `resistance_to_efficiency` reproduce the live mapping: base +efficiencies are returned for non-positive growth, otherwise each is divided +by `sqrt(1 + resistance_growth)`. Both the daily resistance update and initial +dispatch setup now call that helper, preserving charge/discharge asymmetry and +removing the helper's former artificial floor. -`breos/battery.py:1815` defines and exports `resistance_to_efficiency`. -Nothing calls it — not `battery.py` itself, not the tests. Meanwhile two live -paths implement a different mapping directly: the daily resistance update at -`breos/battery.py:790-792` and initial dispatch setup at -`breos/battery.py:1133-1136`: +Focused tests cover non-positive growth, asymmetric efficiencies, and extreme +growth. The live simulation already used this mapping, so the consolidation +does not move simulation results. -```python -if battery_config.enable_resistance_fade and resistance_growth > 0.0: - _rte_derate = math.sqrt(1.0 + resistance_growth) - eff_charge /= _rte_derate - eff_discharge /= _rte_derate -``` - -The two live paths divide each original one-way efficiency by the same -`sqrt(1 + resistance_growth)` derate, preserving any charge/discharge -asymmetry. The exported, documented helper (`docs/api/battery.md:71`) instead -computes one shared `sqrt(rte_new)`, caps it against each base efficiency, and -applies a `0.01` RTE floor. It has the same product in the common symmetric, -unclamped case but changes one-way efficiencies for asymmetric inputs and at -the clamp. `test_resistance_fade_derates_energy_loop_efficiency` -(`tests/test_battery.py:202`) exercises only the symmetric live path. - -The 0.5.0 dispatch-seam extraction moved this code but did not reconcile it — -the duplication survived the refactor, which is exactly why it is worth fixing -before any further work lands on that path. - -### Proposed shape +### Delivered shape -First make `resistance_to_efficiency` exactly reproduce the live mapping: return -the base efficiencies for non-positive growth; otherwise divide each base by -`sqrt(1 + resistance_growth)` with no additional floor or rebalance. Then route -both the daily-update and initial-dispatch sites through it. Alternatively, -delete the helper and stop documenting it. Prefer consolidation: it is the -shape a future day-kernel seam (Phase C1) needs, since the kernel must receive -already-derated efficiencies as scalars computed once at the day boundary. +The consolidated helper is the shape a future day-kernel seam (Phase C1) +needs, since the kernel can receive already-derated efficiencies as scalars +computed once at the day boundary. Also in this area, three trivially small helpers are exported and uncalled: `compute_halfcycle_energy_throughput` (`battery.py:1678`), `k_c_rate_Q` @@ -550,26 +529,22 @@ Do not remove these in a future sweep — they are uncalled today by design. ## Recommended execution order -During 0.5.x: +During 0.5.x (C1-C4 deprecations and C5 were completed for 0.5.1): 1. **C6** — `tools/` pruning. Zero public surface, low risk, immediate. -2. **C5** — resistance-fade reconciliation. Do this before any kernel work, - since the seam depends on it. -3. **C8** — test consolidation. Independent of the public-surface decisions. -4. **C7** — plotting smoke tests, establishing coverage before deciding which - exports can be retired. -5. **C1-C4 audits/deprecations** — check downstream use, record evidence, and - announce only those 0.6.0 removals the evidence supports. +2. **C8** — test consolidation. Independent of the public-surface decisions. +3. **C7** — plotting smoke tests for retained exports, establishing coverage + before any additional retirement decisions. During 0.6.0: -6. **C1 removal half** — remove `numba_kernels.py`, `_get_numba`, and the +4. **C1 removal half** — remove `numba_kernels.py`, `_get_numba`, and the `fast` extra if the deprecation decision stands. Largest single win at 770 lines. -7. **C2** — Polysun retirement, if confirmed. Self-contained, ~550 lines. -8. **C3** — confirmed uncalled undocumented plots, ~478 lines. -9. **C4** — confirmed orphaned helpers, one PR per module. -10. **C1 rebuild half** — day-kernel implementation, if wanted. Separate PR, +5. **C2** — Polysun retirement, if confirmed. Self-contained, ~550 lines. +6. **C3** — confirmed uncalled undocumented plots, ~478 lines. +7. **C4** — confirmed orphaned helpers, one PR per module. +8. **C1 rebuild half** — day-kernel implementation, if wanted. Separate PR, own parity tests, no other cleanup mixed in. The confirmed 0.6.0 removals target roughly 2 100 lines of source. C7 adds diff --git a/design/architecture/0.5x-deprecation-audit.md b/design/architecture/0.5x-deprecation-audit.md new file mode 100644 index 0000000..417d91b --- /dev/null +++ b/design/architecture/0.5x-deprecation-audit.md @@ -0,0 +1,54 @@ +# 0.5.x Cleanup Deprecation Audit + +## Decision + +The public cleanup candidates in phases C1-C4 of the +[0.5.x cleanup plan](0.5x-cleanup-plan.md), together with the four battery +helpers identified beside phase C5, are deprecated in 0.5.1 with 0.6.0 as the +earliest removal release. No function, class, module, top-level compatibility +alias, or optional dependency is removed during 0.5.x. + +## Downstream search + +The audit was run on 2026-08-11 using both repository-wide `rg` searches and +GitHub public code search. Each identifier was searched as an exact term with +`breos` and `language:Python`; the `breos[fast]` install spelling was searched +separately. Search results were inspected rather than treating same-name +functions in unrelated projects as BREOS callers. + +No qualified use outside `Str4vinci/breos` was found for the scheduled +symbols. A broad, unqualified search for `save_simulation_report` also found +`Str4vinci/phd/dev/pvbat`, but that code imports its repository-local `pvbat` +module rather than the released `breos` package, so it is historical precursor +code rather than a downstream BREOS caller. + +Public code search cannot establish that private or dynamically imported code +does not use these APIs. That limitation is why the implementations and public +signatures remain intact for the full 0.5.x deprecation window. + +## Audited inventory + +- Performance: `breos.numba_kernels` and the `breos[fast]` extra. The package's + production simulation does not call these approximate kernels. +- Polysun comparison: `PolysunDegradationConfig`, + `woehler_cycles_to_failure`, `compute_dod_histogram`, + `compute_miner_damage`, `predict_polysun_lifetime`, + `simulate_polysun_degradation`, and the three associated comparison plots. +- Undocumented plotting: `plot_smart_charging_sweep`, + `plot_optimization_results_2d`, `plot_optimization_results_3d`, and the three + `plot_loo_*` functions named in the cleanup plan. +- I/O: `save_simulation_report`, `export_monthly_summary`, and + `export_yearly_summary`. +- Weather: `resample_to_hourly`, `csv_15min_to_hourly`, + `csv_hourly_to_15min`, and `fetch_tmy_nsrdb`. +- Solar and optimization: `calculate_pv_production_tmy`, `zeb_sizer`, + `optimize_tilt_brent`, and `size_for_zeb`. +- Utilities: `count_leap_years` and `number_of_cores`. +- Battery helpers: `compute_halfcycle_energy_throughput`, `k_c_rate_Q`, + `k_doc_Q`, and `update_battery_soc`. + +Every callable above warns only when called (or, for the configuration class, +when instantiated). Importing `breos` remains warning-free. Directly importing +`breos.numba_kernels` warns because the module itself is the deprecated +surface. User-facing migration guidance lives in +[`docs/deprecations.md`](../../docs/deprecations.md). diff --git a/design/architecture/README.md b/design/architecture/README.md index 46c14c8..b0704f2 100644 --- a/design/architecture/README.md +++ b/design/architecture/README.md @@ -9,7 +9,8 @@ or implementation history. | [Third-party module wrapping](third-party-wrapping.md) | Proposed; tracked by GitHub issue #11 | | [String inverter sizing](string-inverter-sizing.md) | Proposed capability | | [0.4.x refactor and onboarding plan](0.4x-refactor-plan.md) | Historical delivery plan | -| [0.5.x dead code and bloat cleanup plan](0.5x-cleanup-plan.md) | Proposed delivery plan | +| [0.5.x dead code and bloat cleanup plan](0.5x-cleanup-plan.md) | Active; public deprecations delivered in 0.5.1 | +| [0.5.x cleanup deprecation audit](0.5x-deprecation-audit.md) | Downstream-search and removal record | | [Battery degradation policy](battery-degradation-policy.md) | Active maintainer policy | | [BLAST degradation engine](blast-degradation-engine.md) | Implementation record with deferred work | diff --git a/docs/api/battery.md b/docs/api/battery.md index 68328cf..8182797 100644 --- a/docs/api/battery.md +++ b/docs/api/battery.md @@ -45,6 +45,10 @@ and daily mean absolute SOC. ## Cycle detection +`compute_halfcycle_energy_throughput`, `k_c_rate_Q`, `k_doc_Q`, and +`update_battery_soc` are deprecated for removal in 0.6.0. See +[Deprecations for 0.6.0](../deprecations.md) for migration guidance. + ```{eval-rst} .. autosummary:: :toctree: generated/ diff --git a/docs/api/optimization.md b/docs/api/optimization.md index 651dcd3..a483126 100644 --- a/docs/api/optimization.md +++ b/docs/api/optimization.md @@ -1,11 +1,11 @@ # Optimization -Optimization helpers for system configuration. Brent's method handles smooth -one-dimensional problems (tilt); helper sweeps handle battery sizing and ZEB -sizing; [pymoo](https://pymoo.org/) powers public multi-objective PV/battery -sizing (PV count, battery, cost, grid independence, and ZEB ratio). For -end-to-end App runs over an explicit config grid, use the `breos sweep` CLI -command documented in [Recipes](../getting-started/recipes.md#parameter-sweep). +Optimization helpers for system configuration. The supported tilt grid search +and battery-sizing helper cover one-dimensional sizing; +[pymoo](https://pymoo.org/) powers public multi-objective PV/battery sizing (PV +count, battery, cost, grid independence, and ZEB ratio). For end-to-end App +runs over an explicit config grid, use the `breos sweep` CLI command documented +in [Recipes](../getting-started/recipes.md#parameter-sweep). Install `breos[optimization]` to use pymoo-backed multi-objective sizing. The one-dimensional helpers use the core scientific stack. diff --git a/docs/api/plotting.md b/docs/api/plotting.md index 2a39478..0149332 100644 --- a/docs/api/plotting.md +++ b/docs/api/plotting.md @@ -4,6 +4,11 @@ Publication-ready matplotlib figures grouped by what they visualize. All functions write a PNG to a results directory and accept optional styling overrides. +The article-scoped Polysun comparison plots and six undocumented helpers are +deprecated for removal in 0.6.0. This page still lists +`plot_temperature_sensitivity_comparison` during the compatibility window; see +[Deprecations for 0.6.0](../deprecations.md). + ## Time series ```{eval-rst} diff --git a/docs/deprecations.md b/docs/deprecations.md index 19d5e58..be98b31 100644 --- a/docs/deprecations.md +++ b/docs/deprecations.md @@ -1,10 +1,12 @@ # Deprecations for 0.6.0 -BREOS 0.5.1 keeps the APIs below working but emits a `DeprecationWarning` when -they are used. They are -scheduled for removal in BREOS 0.6.0. Python hides `DeprecationWarning` by -default; run tests with `-W default` or `-W error::DeprecationWarning` to find -calls before upgrading. +BREOS 0.5.1 keeps the APIs below working. Deprecated callables emit a +`DeprecationWarning` when called, `PolysunDegradationConfig` warns when +instantiated, and directly importing `breos.numba_kernels` warns. The +`breos[fast]` extra and comparison-only constants cannot warn on use and are +announced here and in the changelog. All are scheduled for removal in BREOS +0.6.0. Python hides `DeprecationWarning` by default; run tests with `-W default` +or `-W error::DeprecationWarning` to find calls before upgrading. The {py:class}`~breos.App` facade and its configuration are unaffected. @@ -19,8 +21,9 @@ is no supported accelerated replacement in 0.5.x. ## Polysun comparison baseline -The article-scoped `breos.polysun_degradation` module and its three comparison -plots are deprecated without a package replacement: +The article-scoped `breos.polysun_degradation` module, its comparison-only +constants in `breos.constants`, and its three comparison plots are deprecated +without a package replacement: - `PolysunDegradationConfig`, `woehler_cycles_to_failure`, `compute_dod_histogram`, `compute_miner_damage`, @@ -28,6 +31,10 @@ plots are deprecated without a package replacement: - `plot_degradation_methodology_comparison`, `plot_lifetime_prediction_comparison`, and `plot_temperature_sensitivity_comparison` +- `WOEHLER_LFP_CONSERVATIVE_A`, `WOEHLER_LFP_CONSERVATIVE_B`, + `WOEHLER_LFP_TYPICAL_A`, `WOEHLER_LFP_TYPICAL_B`, + `WOEHLER_LFP_OPTIMISTIC_A`, `WOEHLER_LFP_OPTIMISTIC_B`, + `POLYSUN_CALENDAR_LIFE_LION`, and `POLYSUN_CALENDAR_LIFE_LEAD` Copy the comparison implementation into the research artifact that needs it before moving to 0.6.0. BREOS's supported degradation models are documented in diff --git a/tests/test_deprecations.py b/tests/test_deprecations.py index 8a73638..309c276 100644 --- a/tests/test_deprecations.py +++ b/tests/test_deprecations.py @@ -4,8 +4,10 @@ import os import subprocess import sys +import warnings from pathlib import Path +import numpy as np import pytest import breos @@ -91,6 +93,23 @@ def test_deprecated_dataclass_warns_only_when_instantiated(): assert "n_bins" in inspect.signature(config_class).parameters +def test_polysun_entrypoint_emits_one_warning_at_the_user_call_site(): + with pytest.warns(DeprecationWarning): + config = polysun_degradation.PolysunDegradationConfig(n_bins=4) + + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always", DeprecationWarning) + result = polysun_degradation.simulate_polysun_degradation( + np.array([0.2, 0.8, 0.2]), + config, + n_years=1, + ) + + assert len(caught) == 1 + assert Path(caught[0].filename) == Path(__file__) + assert len(result) == 1 + + def test_deprecated_plot_warns_before_preserving_argument_validation(): with pytest.warns(DeprecationWarning, match=r"plot_loo_cv_summary.*BREOS 0\.6\.0"): with pytest.raises(TypeError): From 4043645d756d21febea325a7a459d70d64d9e4f0 Mon Sep 17 00:00:00 2001 From: Stravinci Date: Tue, 11 Aug 2026 14:25:40 +0100 Subject: [PATCH 3/6] Prepare 0.5.1 release --- CHANGELOG.md | 32 +++++++++++++- CITATION.cff | 4 +- ROADMAP.md | 53 ++++++++++-------------- pyproject.toml | 2 +- uv.lock | 2 +- validation/REPORT.md | 2 +- validation/baselines/breos_baseline.json | 4 +- 7 files changed, 58 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2cdaf0..74f2dac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,13 +4,41 @@ All notable changes to BREOS are documented here. Format follows [Keep a Changel ## [Unreleased] +## [0.5.1] - 2026-08-11 + +### Changed +- Centralized App configuration metadata in one declarative registry that now + derives defaults, allowed top-level keys, CLI options, and CLI override + handling. Historical ordering, aliases, normalization, config-file + precedence, validation messages, and simulation results are preserved. + +### Deprecated +- Deprecated the unused `breos.numba_kernels` module and `breos[fast]` extra; + the standalone approximate kernels are not used by `App` or the supported + simulation path and are scheduled for removal in 0.6.0. +- Deprecated the article-scoped Polysun comparison subsystem, its three plots, + and its comparison-only constants for removal in 0.6.0. +- Deprecated six uncalled, undocumented optimization/leave-one-out plots and + orphaned helpers across `battery`, `io`, `optimization`, `solar`, `utils`, + and `weather` for removal in 0.6.0. Functions keep their signatures and + behaviour throughout 0.5.x and emit `DeprecationWarning` only when called; + see the [deprecation guide](https://breos.readthedocs.io/en/latest/deprecations.html) + for the complete inventory and migration paths. + ### Fixed - Accept the optional `[sweep]` config section in `ALLOWED_CONFIG_KEYS`, so `breos validate-config configs/examples/sweep.toml` no longer rejects a shipped example that `breos sweep` runs successfully. The documented behaviour was already that `[sweep]` and `[montecarlo]` are recognised; only - `[montecarlo]` actually was. Every `configs/examples/*.toml` is now covered - by a `validate-config` regression test. + `[montecarlo]` actually was. `validate-config` now also rejects an empty or + malformed sweep grid, and every `configs/examples/*.toml` is covered by a + regression test. +- Made the public `resistance_to_efficiency()` helper match the live + resistance-fade path: both one-way efficiencies receive the same + `sqrt(1 + growth)` derating, preserving configured charge/discharge + asymmetry and removing the helper's former artificial floor. Initial and + daily simulation paths now call the helper; simulation results are + unchanged because they already used this mapping. ## [0.5.0] - 2026-08-05 diff --git a/CITATION.cff b/CITATION.cff index b43111e..94b638d 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -20,8 +20,8 @@ keywords: - solar - degradation license: BSD-3-Clause -version: 0.5.0 -date-released: "2026-08-05" +version: 0.5.1 +date-released: "2026-08-11" preferred-citation: type: article title: "A Modular, Open-Source Python Framework for Household PV-Battery Sizing: Validation, Multi-Objective Optimisation, and Uncertainty Analysis" diff --git a/ROADMAP.md b/ROADMAP.md index deb9f19..c706711 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -28,11 +28,14 @@ intentions, not commitments; reassess after each release. configuration: multi-array tracking now honours the top-level `gcr`, an out-of-range `gcr` is rejected instead of silently backtracking, and the `pvsyst-*` temperature presets model a realistic module efficiency. -- **0.5.x** — the declarative config schema (behavior-preserving, and - deliberately before TOU adds another cluster of config keys); - horizon-profile input; the cost-override seam (phase 0 of economic - scenario analysis, which TOU does not invalidate); and further internal - maintainability work if needed. +- **0.5.1** — completed maintenance release: fixed validation of the shipped + sweep example, centralized the mechanical App configuration contract, and + reconciled resistance-fade efficiency mapping without changing simulation + results. Deprecated the confirmed public dead-code surface ahead of its + planned removal in 0.6.0. +- **0.5.x** — horizon-profile input; the cost-override seam (phase 0 of + economic scenario analysis, which TOU does not invalidate); and further + internal maintainability work if needed. - **0.6.0** — the currency concept plus time-of-use tariff valuation and static presets; flat pricing preserved bit-for-bit. - **0.6.x / 0.7.0** — economic scenario and sensitivity analysis phases 1–3 @@ -130,33 +133,19 @@ must ship with a documented yield/self-consumption delta). ## Architecture -### Declarative config schema with strict validation - -The public `App` config surface is currently defined and checked in four -separate places: the `DEFAULTS` dict and imperative `validate_config` in -`breos.app_config`, plus the `argparse` flag definitions and the -`_add_override` calls in `breos.cli`. Adding one parameter means editing all -four, which is drift-prone, and the hand-rolled validation is hard to keep in -sync with the defaults. Replace it with a single declarative schema (a -dataclass with field metadata, or `pydantic`) so defaults, types, bounds, and -documentation live in one place. - -- **Full step (pending, targeted at a 0.5.x behavior-preserving release):** - collapse `DEFAULTS`, the validation rules, and the CLI flag definitions - into the schema so a new parameter is added once, not four times. This is - deliberately scheduled *before* the 0.6.0 TOU/currency work adds another - cluster of config keys, and deserves its own release slot rather than - riding along a feature release. -- **Coordination with the [function-level refactor plan](design/architecture/0.4x-refactor-plan.md):** earlier internal - validation cleanup should create reusable boundaries for the full schema, - not throwaway helpers that need another rewrite in 0.5.x. -- **The hard part is error-message parity**, not the schema itself: the - acceptance bar is the same exception types with equally actionable - "Unknown X. Available: ..." messages. Off-the-shelf pydantic messages do - not meet it, so plan for either a dataclass-with-field-metadata schema - with hand-rolled errors, or pydantic behind a message-translation layer. -- Keep all error messages actionable; preserve current behaviour for valid - configs (regression-test the example configs in `configs/examples/`). +### Declarative config registry with strict validation (completed in 0.5.1) + +One `AppConfigField` registry now derives public App defaults, allowed +top-level keys, CLI argument definitions, and CLI override handling. Adding a +mechanical configuration field no longer requires keeping four declarations in +sync, and registry invariants plus every shipped example guard against future +drift. + +Scientific and cross-field constraints deliberately remain in focused +validators. That boundary preserves the established validation order, +exception types, and actionable error messages while keeping the registry from +becoming a second scientific rules engine. The 0.6.0 TOU/currency work can add +its configuration cluster through the completed registry. ## Performance and portability diff --git a/pyproject.toml b/pyproject.toml index 7c2f074..17dcc81 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "breos" -version = "0.5.0" +version = "0.5.1" description = "Python library for PV and battery energy-system simulation and optimization" readme = "README.md" license = "BSD-3-Clause" diff --git a/uv.lock b/uv.lock index 3e41f52..e62bf17 100644 --- a/uv.lock +++ b/uv.lock @@ -101,7 +101,7 @@ wheels = [ [[package]] name = "breos" -version = "0.5.0" +version = "0.5.1" source = { editable = "." } dependencies = [ { name = "numpy" }, diff --git a/validation/REPORT.md b/validation/REPORT.md index 3004a04..3de0c98 100644 --- a/validation/REPORT.md +++ b/validation/REPORT.md @@ -1,6 +1,6 @@ # BREOS validation report -Generated by `validation/compare.py` on 2026-08-05 against BREOS 0.5.0. +Generated by `validation/compare.py` on 2026-08-11 against BREOS 0.5.1. System: 4.0 kWp (10× Generic_400W), DC/AC 1.2, inverter η 0.96, albedo 0.2, free-standing mount, BREOS default loss stack. diff --git a/validation/baselines/breos_baseline.json b/validation/baselines/breos_baseline.json index 2a35e6b..0d45e8c 100644 --- a/validation/baselines/breos_baseline.json +++ b/validation/baselines/breos_baseline.json @@ -1,6 +1,6 @@ { - "breos_version": "0.5.0", - "generated": "2026-08-05", + "breos_version": "0.5.1", + "generated": "2026-08-11", "system": { "module": "Generic_400W", "n_modules": 10, From edab955dd7fa443da4601a4e016c6d4c86151221 Mon Sep 17 00:00:00 2001 From: Stravinci Date: Tue, 11 Aug 2026 14:36:13 +0100 Subject: [PATCH 4/6] Clarify Polysun comparison provenance --- ATTRIBUTIONS.md | 5 +- CHANGELOG.md | 10 +++- ROADMAP.md | 13 +++--- breos/__init__.py | 2 +- breos/constants.py | 9 ++-- breos/plotting.py | 53 ++++++++++++++++------ breos/polysun_degradation.py | 58 +++++++++++++----------- design/architecture/0.5x-cleanup-plan.md | 11 +++-- docs/api/appendix.md | 4 ++ docs/api/plotting.md | 6 ++- docs/deprecations.md | 23 ++++++++-- tests/test_deprecations.py | 9 ++++ 12 files changed, 138 insertions(+), 65 deletions(-) diff --git a/ATTRIBUTIONS.md b/ATTRIBUTIONS.md index 63cf804..e3552ac 100644 --- a/ATTRIBUTIONS.md +++ b/ATTRIBUTIONS.md @@ -74,9 +74,8 @@ documentation where the relevant models affect results. | Battery cycle and calendar ageing | `breos/battery.py`, `breos/constants.py`, `breos/numba_kernels.py` | Naumann et al. (2020) parameterization and equations are used for cycle ageing and selected calendar/resistance ageing behavior. | | LFP calendar ageing calibration | `breos/constants.py`, `breos/battery.py` | Lam et al. (2025) LFP calendar ageing behavior informs the `naumann_lam*` calendar-model variants and field-calibrated defaults. | | BLAST-Lite battery ageing models | `breos/degradation/blast/` | BLAST-Lite model classes preserve DOI-cited empirical degradation models for LFP-Gr, NMC-Gr, NMC-GrSi, NMC-LTO, NCA-Gr, NCA-GrSi, and LMO-Gr cells. Primary source DOIs preserved from BLAST-Lite include `10.1016/j.est.2018.01.019`, `10.1016/j.jpowsour.2019.227666`, `10.1149/1945-7111/ac86a8`, `10.1109/EEEIC/ICPSEUROPE54979.2022.9854784`, `10.1016/j.est.2020.101695`, `10.1149/2.0411609jes`, `10.1149/1945-7111/abae37`, `10.1016/j.jpowsour.2022.232498`, `10.1016/j.jpowsour.2020.228566`, `10.1016/j.jpowsour.2014.02.012`, `10.1016/j.est.2023.109042`, and `10.1149/1945-7111/ac2ebd`. | -| Polysun-style degradation comparison | `breos/polysun_degradation.py`, `breos/plotting.py` | The comparison baseline follows Polysun / Vela Solaris battery-lifetime methodology: Woehler curve, Miner's linear damage accumulation, DOD histograms, fixed calendar lifetime, and no continuous SOH feedback. | -| PerMod comparison context | `breos/polysun_degradation.py` | Weniger et al., "Performance Model for PV-Battery Systems (PerMod)", HTW Berlin, 2023, is used as a comparison reference for PV-battery performance modelling. | -| Linear damage accumulation | `breos/polysun_degradation.py` | Palmgren-Miner linear damage accumulation is used for Polysun-style cycle damage aggregation. | +| Documentation-derived battery-lifetime comparison | `breos/polysun_degradation.py`, `breos/plotting.py` | This deprecated article baseline independently approximates selected concepts in Vela Solaris's public [Polysun battery-lifetime documentation](https://www.velasolaris.com/en/handbuch/polysun-designer/electric-components/batteries/battery-lifetime-estimation/): Wöhler cycle-life curves, Miner's linear damage accumulation, 20 DOD bins, and fixed default calendar lifetimes. It contains no Polysun or PerMod source code, uses BREOS-specific cycle counting and literature-derived parameters, and has not been validated as a reproduction of the Polysun product. BREOS is not affiliated with or endorsed by Vela Solaris AG. | +| Linear damage accumulation | `breos/polysun_degradation.py` | Palmgren-Miner linear damage accumulation is used by the independent comparison approximation. | ### Citing pvlib diff --git a/CHANGELOG.md b/CHANGELOG.md index 74f2dac..e963c87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,13 +11,19 @@ All notable changes to BREOS are documented here. Format follows [Keep a Changel derives defaults, allowed top-level keys, CLI options, and CLI override handling. Historical ordering, aliases, normalization, config-file precedence, validation messages, and simulation results are preserved. +- Clarified that the deprecated article lifetime baseline is an independent + approximation reconstructed from public documentation. Its generated plot + legends now say "documentation-derived baseline" instead of presenting the + series as Polysun output; calculations and compatibility APIs are unchanged. ### Deprecated - Deprecated the unused `breos.numba_kernels` module and `breos[fast]` extra; the standalone approximate kernels are not used by `App` or the supported simulation path and are scheduled for removal in 0.6.0. -- Deprecated the article-scoped Polysun comparison subsystem, its three plots, - and its comparison-only constants for removal in 0.6.0. +- Deprecated the article-scoped, documentation-derived Wöhler/Miner comparison + subsystem, its three plots, and its comparison-only constants for removal in + 0.6.0. Despite legacy API names, this is an independent BREOS approximation, + not Polysun or PerMod source code or a validated reproduction of Polysun. - Deprecated six uncalled, undocumented optimization/leave-one-out plots and orphaned helpers across `battery`, `io`, `optimization`, `solar`, `utils`, and `weather` for removal in 0.6.0. Functions keep their signatures and diff --git a/ROADMAP.md b/ROADMAP.md index c706711..8d55558 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -542,12 +542,13 @@ Phases: ### Additional Li-ion battery chemistries -The battery degradation model is calibrated for LFP only. Calendar aging uses the -Naumann 2020 LFP parameter sets (`naumann_lam_field_calibrated` default and -variants in `breos/constants.py`), and cycle aging uses LFP Wöhler curves -(`WOEHLER_LFP_CONSERVATIVE` / `_TYPICAL` / `_OPTIMISTIC`, consumed in -`breos/polysun_degradation.py`). This is the same "label without the physics" gap -as bifacial modules. In 0.3.3 the native `BatteryConfig.battery_type` selector +The native battery degradation model is calibrated for LFP only. Calendar and +cycle aging use the Naumann 2020 LFP parameterization, while the supported +BLAST models expose their own cell-specific calibrations. The deprecated +documentation-derived lifetime comparison and its example Wöhler constants are +not part of the supported degradation path and will be removed in 0.6.0. This +is the same "label without the physics" gap as bifacial modules. In 0.3.3 the +native `BatteryConfig.battery_type` selector was made honest: `LFP` normalizes to `lfp`, and unsupported values now raise instead of silently reusing LFP cycle-aging parameters. Add real per-chemistry aging so NMC / NCA packs degrade on their own parameters. diff --git a/breos/__init__.py b/breos/__init__.py index 5ea2d51..2a98728 100644 --- a/breos/__init__.py +++ b/breos/__init__.py @@ -165,7 +165,7 @@ size_for_zeb, ) -# Polysun Degradation (comparison baseline) +# Independent documentation-derived lifetime approximation (deprecated) from breos.polysun_degradation import ( PolysunDegradationConfig, compute_dod_histogram, diff --git a/breos/constants.py b/breos/constants.py index 6c0d892..6f97c26 100644 --- a/breos/constants.py +++ b/breos/constants.py @@ -103,9 +103,10 @@ DEFAULT_INDOOR_FLOOR_C = 15.0 # Min indoor temp — even unheated garage in mild climate DEFAULT_INDOOR_CEILING_C = 35.0 # Max indoor temp — summer heat buildup -# === Polysun Wöhler Curve Parameters (Cycle Life vs DOD) === -# Polysun models cycle life using a Wöhler (S-N) curve: N(DOD) = a * DOD^(-b) -# where N = cycles to failure, DOD = depth of discharge (0-1). +# === Deprecated comparison Wöhler parameters (Cycle Life vs DOD) === +# The public Polysun lifetime documentation describes a Wöhler (S-N) curve, +# N(DOD) = a * DOD^(-b), but these example values are BREOS choices derived +# from the literature below, not Polysun catalogue parameters. # # LFP parameters derived from published cycle life data: # - Wang et al. 2011: ~3000 cycles at 100% DOD, ~7500 at 50% DOD for LFP @@ -125,6 +126,6 @@ # Reference data points for validation (typical, a=5000, b=1.6): # DOD=1.0 → 5000, DOD=0.8 → 7440, DOD=0.5 → 15157, DOD=0.2 → 67860 -# Polysun default calendar lifetimes (fixed, no temperature dependence) +# Default calendar lifetimes reported in the public Polysun documentation. POLYSUN_CALENDAR_LIFE_LION = 20.0 # Li-ion (LFP) POLYSUN_CALENDAR_LIFE_LEAD = 10.0 # Lead-acid diff --git a/breos/plotting.py b/breos/plotting.py index 954af47..65d487e 100644 --- a/breos/plotting.py +++ b/breos/plotting.py @@ -3328,7 +3328,7 @@ def plot_co2_savings( # ========================================================================= -# Polysun vs BREOS degradation comparison plots +# Deprecated documentation-derived baseline vs BREOS comparison plots # ========================================================================= @@ -3341,16 +3341,17 @@ def plot_degradation_methodology_comparison( suffix: str = "", ) -> None: """ - Compare BREOS continuous SOH vs Polysun Miner's damage accumulation. + Compare BREOS continuous SOH with the documentation-derived baseline. Produces two separate figures: - 1. SOH over time: BREOS's declining SOH curve vs Polysun's equivalent SOH - 2. Polysun damage accumulation (D) with replacement threshold at D=1 + 1. BREOS's declining SOH curve vs the baseline's illustrative equivalent + 2. Baseline damage accumulation (D) with replacement threshold at D=1 Args: breos_soh: BREOS degradation DataFrame with 'SOH' column (%) indexed by year or containing a 'Year' column. - polysun_df: Output of simulate_polysun_degradation(). + polysun_df: Output of the deprecated ``simulate_polysun_degradation`` + compatibility function. The legacy parameter name is preserved. results_directory: Directory to save plots. scenario_label: Label for annotation (e.g., "Porto 5kWp/5kWh"). suffix: Filename suffix. @@ -3366,7 +3367,15 @@ def plot_degradation_methodology_comparison( # --- Figure 1: SOH comparison --- fig, ax = plt.subplots(figsize=(12, 6)) ax.plot(years_breos, soh_breos, "b-", linewidth=2.5, marker="o", markersize=3, label="BREOS (Naumann)") - ax.plot(years_polysun, soh_polysun, "r--", linewidth=2.5, marker="s", markersize=3, label="Polysun (Miner/Wöhler)") + ax.plot( + years_polysun, + soh_polysun, + "r--", + linewidth=2.5, + marker="s", + markersize=3, + label="Documentation-derived baseline (Miner/Wöhler)", + ) ax.axhline(80, color="grey", linestyle=":", linewidth=1.5, alpha=0.7, label="EOL threshold (80%)") # Mark replacements @@ -3399,7 +3408,7 @@ def plot_degradation_methodology_comparison( ) plt.close(fig) - # --- Figure 2: Polysun damage accumulation --- + # --- Figure 2: comparison-baseline damage accumulation --- fig, ax = plt.subplots(figsize=(12, 6)) ax.plot( years_polysun, @@ -3451,9 +3460,9 @@ def plot_lifetime_prediction_comparison( Args: scenarios: Dict mapping scenario label to dict with keys: 'breos_eol_year': Year BREOS hits 80% SOH (float or int). - 'polysun_total_life': Polysun predicted total life (years). - 'polysun_cycle_life': Polysun cycle life component (years). - 'polysun_calendar_life': Polysun calendar life component (years). + 'polysun_total_life': Baseline total life (years; legacy key). + 'polysun_cycle_life': Baseline cycle life (years; legacy key). + 'polysun_calendar_life': Baseline calendar life (years; legacy key). results_directory: Directory to save plot. suffix: Filename suffix. """ @@ -3469,7 +3478,14 @@ def plot_lifetime_prediction_comparison( fig, ax = plt.subplots(figsize=(10, 6)) bars1 = ax.bar(x - width / 2, breos_years, width, label="BREOS (Naumann)", color="#1976D2", alpha=0.85) - bars2 = ax.bar(x + width / 2, polysun_years, width, label="Polysun (Miner/Wöhler)", color="#D32F2F", alpha=0.85) + bars2 = ax.bar( + x + width / 2, + polysun_years, + width, + label="Documentation-derived baseline", + color="#D32F2F", + alpha=0.85, + ) # Annotate bar values for bar in bars1: @@ -3511,8 +3527,10 @@ def plot_temperature_sensitivity_comparison( suffix: str = "", ) -> None: """ - Show how BREOS lifetime varies across locations (temperature-dependent) - while Polysun predicts the same lifetime everywhere (temperature-blind). + Compare temperature-dependent BREOS results with the fixed-input baseline. + + The baseline's lack of temperature response is a limitation of this + approximation, not a claim about the current Polysun product. Args: locations: Dict mapping location names to ``breos_eol_year``, @@ -3540,7 +3558,14 @@ def plot_temperature_sensitivity_comparison( fig, ax = plt.subplots(figsize=(10, 6)) bars1 = ax.bar(x - width / 2, breos_years, width, label="BREOS (Naumann)", color="#1976D2", alpha=0.85) - bars2 = ax.bar(x + width / 2, polysun_years, width, label="Polysun (Miner/Wöhler)", color="#D32F2F", alpha=0.85) + bars2 = ax.bar( + x + width / 2, + polysun_years, + width, + label="Documentation-derived baseline", + color="#D32F2F", + alpha=0.85, + ) # Annotate with temperature for i, (bar, temp) in enumerate(zip(bars1, temps)): diff --git a/breos/polysun_degradation.py b/breos/polysun_degradation.py index 331e765..98e07c9 100644 --- a/breos/polysun_degradation.py +++ b/breos/polysun_degradation.py @@ -1,22 +1,24 @@ -""" -Polysun-style battery degradation model. +"""Independent approximation of a publicly documented lifetime method. + +This deprecated, article-scoped module reconstructs a simplified Wöhler/Miner +comparison baseline from Vela Solaris's public Polysun battery-lifetime +documentation. It is not Polysun source code, is not derived from PerMod, and +has not been validated as a reproduction of the current Polysun product. BREOS +is not affiliated with or endorsed by Vela Solaris AG. -Implements the Wöhler curve + Miner's linear damage accumulation methodology -used by Polysun (Vela Solaris) for battery lifetime estimation. This serves -as a comparison baseline against BREOS's Naumann-based continuous degradation. +The public documentation describes 20 equal-width depth-of-discharge bins, a +Wöhler cycle-life curve for lithium-ion batteries, linear damage accumulation, +fixed default calendar lifetimes, and selection of the shorter cycle or +calendar life. This module adds BREOS-specific choices that must not be +presented as results generated by Polysun: -Polysun methodology: - 1. Cycle counting: 20 DOD histogram bins (equal width) - 2. Cycle life: Wöhler curve N(DOD) = a * DOD^(-b) - 3. Damage: Miner's rule D = sum(n_i / N_i) - 4. Calendar life: Fixed (20 years for Li-ion) - 5. Total life: min(calendar_life, 1/D_annual) - 6. No temperature effects, no continuous SOH tracking +* local-extrema half-cycle counting; +* literature-derived example Wöhler parameters rather than catalogue data; +* repeated use of one annual state-of-charge profile; and +* an illustrative linear ``SOH_Equivalent`` series for legacy plots. References: - - Polysun User Manual, Section "Battery Lifetime" (Vela Solaris AG) - - Weniger et al., "Performance Model for PV-Battery Systems (PerMod)", - HTW Berlin, 2023 + - Vela Solaris AG, Polysun User Manual, "Battery Lifetime Estimation" - Palmgren-Miner linear damage hypothesis (Miner, 1945) """ @@ -42,7 +44,7 @@ @deprecated(name="breos.polysun_degradation.PolysunDegradationConfig") @dataclass class PolysunDegradationConfig: - """Configuration for Polysun-style degradation model. + """Configuration for the documentation-derived comparison approximation. Attributes: woehler_a: Scale parameter for Wöhler curve N(DOD) = a * DOD^(-b). @@ -50,7 +52,7 @@ class PolysunDegradationConfig: woehler_b: Shape parameter for Wöhler curve. Higher b means deeper cycles are disproportionately more damaging. calendar_life_years: Fixed calendar lifetime in years. - n_bins: Number of DOD histogram bins (Polysun uses 20). + n_bins: Number of DOD histogram bins (the public method uses 20). min_doc: Minimum DOD to count as a cycle (fraction, 0-1). deep_cycle_threshold: DOD above which a cycle is classified as "deep". """ @@ -88,12 +90,13 @@ def compute_dod_histogram( ) -> Tuple[np.ndarray, np.ndarray, int, int]: """Count cycles per DOD range from an SOC timeseries using peak detection. - Uses simple local-extrema-based half-cycle detection to match Polysun's - approach (not rainflow counting). Half-cycles are paired and binned by DOD. + This is a BREOS-specific local-extrema approximation, not a verified copy + of Polysun's cycle-counting implementation. Half-cycles are binned by DOD + and each contributes 0.5 full-cycle equivalents. Args: soc_series: SOC timeseries (0-1 range), typically one year of data. - n_bins: Number of equal-width DOD bins (Polysun uses 20). + n_bins: Number of equal-width DOD bins (the public method uses 20). min_doc: Minimum DOD to include a cycle (fraction, 0-1). Returns: @@ -195,7 +198,7 @@ def predict_polysun_lifetime( annual_damage: float, calendar_life_years: float, ) -> Tuple[float, float, float]: - """Predict battery lifetime following Polysun methodology. + """Apply the lifetime aggregation described in the public documentation. Total life = min(calendar_life, cycle_life) where cycle_life = 1 / annual_damage. @@ -221,11 +224,12 @@ def simulate_polysun_degradation( config: PolysunDegradationConfig, n_years: int = 20, ) -> pd.DataFrame: - """Run Polysun-style degradation over multiple years. + """Run the independent documentation-derived approximation. - Polysun assumes constant annual usage (same SOC profile each year) and - does not feed degradation back into the energy balance. Damage accumulates - linearly; when cumulative damage >= 1, the battery is replaced. + This implementation reuses the same annual SOC profile and does not feed + degradation back into the energy balance. Damage accumulates linearly; + when cumulative damage >= 1, the battery is replaced. These implementation + choices are not claims about the current Polysun product. Args: soc_series: One year of SOC data (0-1 range). Reused each year. @@ -283,8 +287,8 @@ def simulate_polysun_degradation( cumulative_damage = 0.0 last_replacement_year = year - # SOH equivalent: Polysun is binary, but for comparison we map - # damage to an equivalent SOH assuming linear capacity fade + # Legacy illustrative mapping: this approximation does not model + # continuous SOH, so map damage linearly for the old comparison plot. # SOH = 1 - (damage * 0.20) maps D=1 to SOH=80% (typical EOL) soh_equivalent = max(0, 1.0 - cumulative_damage * 0.20) * 100.0 diff --git a/design/architecture/0.5x-cleanup-plan.md b/design/architecture/0.5x-cleanup-plan.md index 85d67af..8331548 100644 --- a/design/architecture/0.5x-cleanup-plan.md +++ b/design/architecture/0.5x-cleanup-plan.md @@ -182,7 +182,10 @@ tests. `breos/polysun_degradation.py` (293 lines) implements a Wöhler/Miner comparison baseline built for an article. It has no in-repository caller and no -tests; downstream use still needs the audit required above. +supported production role; its focused tests now cover only compatibility and +the 0.6.0 deprecation contract. It is an independent approximation built from +public documentation, not Polysun or PerMod source code and not a validated +reproduction of the Polysun product. `tests/test_public_api.py:40` lists `PolysunDegradationConfig` under `intentionally_excluded` — it is exported from `breos/__init__.py:169` and simultaneously asserted to be outside the stable API. @@ -210,8 +213,10 @@ methodology is still cited elsewhere in the paper trail; otherwise drop them with the code. Note in `CHANGELOG.md` that the comparison baseline was article-scoped and has been retired. -If the comparison is worth preserving for reproducibility, its home is the -article's own repository, not the shipped package. +Preserve the exact 0.5.1 release tag and, if the comparison affects published +results, archive that tag or copy the implementation into the article's +versioned research artifact. Its long-term home is the article's repository, +not the shipped package. ### Acceptance criteria diff --git a/docs/api/appendix.md b/docs/api/appendix.md index 44dcc38..723305a 100644 --- a/docs/api/appendix.md +++ b/docs/api/appendix.md @@ -8,6 +8,10 @@ Some article-scoped and report helpers on these modules are scheduled for removal in 0.6.0. See [Deprecations for 0.6.0](../deprecations.md) for the complete inventory and migration guidance. +The deprecated `polysun_degradation` name denotes an independent, +documentation-derived comparison approximation. It is not Polysun source code +or a validated reproduction of the Polysun product. + ```{eval-rst} .. autosummary:: :toctree: generated/ diff --git a/docs/api/plotting.md b/docs/api/plotting.md index 0149332..0e1366d 100644 --- a/docs/api/plotting.md +++ b/docs/api/plotting.md @@ -4,8 +4,10 @@ Publication-ready matplotlib figures grouped by what they visualize. All functions write a PNG to a results directory and accept optional styling overrides. -The article-scoped Polysun comparison plots and six undocumented helpers are -deprecated for removal in 0.6.0. This page still lists +The article-scoped, documentation-derived lifetime comparison plots and six +undocumented helpers are deprecated for removal in 0.6.0. The comparison is +not Polysun output or a validated reproduction of the Polysun product. This +page still lists `plot_temperature_sensitivity_comparison` during the compatibility window; see [Deprecations for 0.6.0](../deprecations.md). diff --git a/docs/deprecations.md b/docs/deprecations.md index be98b31..a06d81f 100644 --- a/docs/deprecations.md +++ b/docs/deprecations.md @@ -23,7 +23,22 @@ is no supported accelerated replacement in 0.5.x. The article-scoped `breos.polysun_degradation` module, its comparison-only constants in `breos.constants`, and its three comparison plots are deprecated -without a package replacement: +without a package replacement. Despite their legacy names, these APIs are an +independent approximation of selected Wöhler/Miner concepts described in +Vela Solaris's public [Polysun battery-lifetime documentation](https://www.velasolaris.com/en/handbuch/polysun-designer/electric-components/batteries/battery-lifetime-estimation/). +They contain no +Polysun or PerMod source code and have not been validated as a reproduction of +the Polysun product. Their local-extrema cycle counting, literature-derived +parameters, repeated annual profile, and illustrative continuous-SOH mapping +are BREOS-specific choices. BREOS is not affiliated with or endorsed by Vela +Solaris AG. + +Do not describe results from these APIs as generated by Polysun or as BREOS +validation against Polysun. For an article, use wording such as "a simplified +Wöhler/Miner lifetime baseline reconstructed from public Polysun +documentation." + +The deprecated inventory is: - `PolysunDegradationConfig`, `woehler_cycles_to_failure`, `compute_dod_histogram`, `compute_miner_damage`, @@ -36,8 +51,10 @@ without a package replacement: `WOEHLER_LFP_OPTIMISTIC_A`, `WOEHLER_LFP_OPTIMISTIC_B`, `POLYSUN_CALENDAR_LIFE_LION`, and `POLYSUN_CALENDAR_LIFE_LEAD` -Copy the comparison implementation into the research artifact that needs it -before moving to 0.6.0. BREOS's supported degradation models are documented in +Archive the exact BREOS 0.5.1 tag or copy the comparison implementation into +the article's versioned research artifact before moving to 0.6.0. The module, +exports, constants, plots, and compatibility tests will then be deleted from +the shipped package. BREOS's supported degradation models are documented in [Degradation models](api/degradation-models.md). ## Undocumented plotting helpers diff --git a/tests/test_deprecations.py b/tests/test_deprecations.py index 309c276..a42813c 100644 --- a/tests/test_deprecations.py +++ b/tests/test_deprecations.py @@ -110,6 +110,15 @@ def test_polysun_entrypoint_emits_one_warning_at_the_user_call_site(): assert len(result) == 1 +def test_polysun_compatibility_module_disclaims_product_fidelity(): + module_doc = polysun_degradation.__doc__ or "" + + assert "Independent approximation" in module_doc + assert "not Polysun source code" in module_doc + assert "not derived from PerMod" in module_doc + assert "not been validated as a reproduction" in module_doc + + def test_deprecated_plot_warns_before_preserving_argument_validation(): with pytest.warns(DeprecationWarning, match=r"plot_loo_cv_summary.*BREOS 0\.6\.0"): with pytest.raises(TypeError): From 3a32e0dd623891f4733f32c40f58741d8c8874f2 Mon Sep 17 00:00:00 2001 From: Stravinci Date: Tue, 11 Aug 2026 14:38:49 +0100 Subject: [PATCH 5/6] Document pull request conventions --- AGENTS.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 98fe2af..b2693f3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,3 +29,9 @@ uv build - Avoid repo-relative runtime paths; packaged resources should be loaded through `breos.resources`. - Keep generated files out of git unless they are intentional release assets. - Add focused tests for public API behavior when touching defaults, packaged data, or serialization. + +## Pull requests + +- Use a specific, descriptive branch name with a conventional change-type prefix such as `feat/`, `fix/`, `refactor/`, or `docs/`. Do not use generic branch names such as `agents` or `codex`. +- Start every PR description with a plain, human-readable paragraph that explains the purpose of the change. +- Before drafting a PR description, review relevant previously closed PRs and follow the repository's established tone and structure. From 7258d0c87109c9f9f80ad9501fff71545ceb496f Mon Sep 17 00:00:00 2001 From: Stravinci Date: Tue, 11 Aug 2026 15:04:40 +0100 Subject: [PATCH 6/6] Record Python 3.14 release gate --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e963c87..7b4e706 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,10 @@ All notable changes to BREOS are documented here. Format follows [Keep a Changel approximation reconstructed from public documentation. Its generated plot legends now say "documentation-derived baseline" instead of presenting the series as Polysun output; calculations and compatibility APIs are unchanged. +- Updated branch protection on `develop` and `main` so the existing Python + 3.14 CI matrix job is required alongside Python 3.11–3.13. Python 3.14 had + run successfully since June, but the manually maintained required-check list + had not been updated when that matrix entry was added. ### Deprecated - Deprecated the unused `breos.numba_kernels` module and `breos[fast]` extra;