A Python library for PV and battery energy-system simulation and optimization, designed for research and engineering applications.
- Weather data: Fetch TMY data from PVGIS/NSRDB and historical data from Open-Meteo. Support for hourly and 15-minute resolutions with Makima interpolation.
- PV production: DC and AC power calculations using pvlib (CEC single-diode model), with a small catalog of example modules and full support for custom module parameters.
- Battery simulation: Energy balance with calendar and cycle aging models (Naumann 2020, Lam 2025) and field-calibrated LFP parameters. Optional approximate Numba kernels for fast standalone screening studies.
- Economics: NPV, LCOE, breakeven analysis, and cost projections with configurable tariffs and inflation.
- Monte Carlo: Multi-year weather-year and demand resampling for NPV, payback, grid-independence, LCOE, and battery SoH distributions.
- Optimization: Multi-objective PV/battery sizing with pymoo (NSGA-II), tilt optimization via grid search or Brent's method, simple battery sizing sweeps, and ZEB sizing.
- Emissions: CO2 savings calculations and projections.
- Visualization: Publication-ready plots for energy balances, degradation, breakeven, Pareto fronts, and more.
- Load profiles: Bundled demandlib-derived H0 examples, plus support for user-supplied BDEW, E-REDES, REE, and custom profiles.
pip install breosOr with uv:
uv add breos # as a project dependency
uvx breos --version # run the CLI without installingOptional feature groups keep the default install focused on core PV + battery simulation:
pip install "breos[plots]" # publication plots
pip install "breos[optimization]" # pymoo multi-objective sizing
pip install "breos[weather]" # Open-Meteo historical weather fetching
pip install "breos[fast]" # approximate Numba screening kernels (not used by App)
pip install "breos[validation]" # Excel / Arrow dependencies for local validation workTo install from source instead:
git clone https://github.com/Str4vinci/breos.git
cd breos
pip install -e .import breos
app = breos.App({
"location": "porto", # preset or {"latitude": ..., "longitude": ..., "timezone": ...}
"n_modules": 10,
"annual_consumption_kwh": 4000,
"battery_kwh": 5.0, # 0 for no battery
"cost_preset": "residential_pt",
"emissions_country": "PT",
})
app.simulate()
result = app.result()
print(f"Grid independence: {result['grid_independence_pct']:.1f}%")
print(f"Payback: {result['payback_year']} years")
print(f"NPV savings: {result['npv_savings_eur']:,.0f} EUR")
print(f"CO2 avoided: {result['co2_avoided_total_kg']:,.0f} kg")result() returns a plain Python dict (JSON-serializable, no pandas). See Configuration for all options.
For real studies, bring your own weather/API access where required, licensed load profiles, PV module/system data, and cost/tariff assumptions. The packaged defaults are intended to make the tool runnable, not to certify a project.
Run a simulation without writing Python:
breos run \
--location porto \
--n-modules 10 \
--annual-consumption-kwh 4000 \
--battery-kwh 5.0 \
--cost-preset residential-pt \
--emissions-country pt \
--output result.jsonThe CLI writes the same JSON-serializable result returned by App.result().
You can also pass a TOML or JSON config file. A minimal quickstart.toml
looks like this:
location = "porto"
n_modules = 10
annual_consumption_kwh = 4000
battery_kwh = 5.0
load_profile = "demandlib_h0"
cost_preset = "residential_pt"
emissions_country = "PT"
projection_years = 20
resolution = "h"breos validate-config quickstart.toml
breos run --config quickstart.toml --dry-run
breos run --config quickstart.toml --output result.jsonThe full simulation may fetch PVGIS TMY weather data and needs internet access
unless a matching local weather cache is present. Source checkouts include the
same config at configs/examples/quickstart.toml.
Discover packaged option keys:
breos list locations
breos list modules
breos list cost-presets
breos list emissions
breos list load-profilesRun a parameter grid from a normal config plus a [sweep] section:
location = "porto"
n_modules = 10
annual_consumption_kwh = 4000
battery_kwh = 0.0
cost_preset = "residential_pt"
[sweep]
n_modules = [8, 10, 12]
battery_kwh = [0.0, 5.0]breos sweep --config configs/examples/sweep.toml --output sweep_results.csvThe command runs every parameter combination and writes one CSV row per run, including the varied parameters, resolved system sizing, BREOS version, and top-level scalar result metrics.
Run a Monte Carlo study over weather-year and demand uncertainty:
breos montecarlo \
--config configs/examples/montecarlo.toml \
--weather-file weather/porto_historical_2005_2024_openmeteo.csv \
--runs 100 \
--plotsThe command writes one row per trajectory to monte_carlo_results.csv and,
with --plots, saves payback, NPV, grid-independence, final-SoH, and LCOE
distributions in plots/. BREOS does not bundle the multi-year historical
weather file; provide your own CSV or keep it in the git-ignored local
weather/ directory.
Multi-objective PV/battery sizing is available from Python with
breos.optimize_system_multi_objective(...) after installing
breos[optimization]. It returns an OptimizationResult whose
details["pareto"] table contains the NSGA-II Pareto solutions.
For non-bundled RLPs, put licensed CSVs in a local directory and pass it through config or flags:
breos run --config configs/examples/external-rlp.toml --rlp-directory external_rlpOnly location, annual_consumption_kwh, and either n_modules or
pv_arrays are required. Common keys are:
| Key | Default | Description |
|---|---|---|
location |
required | Preset key ("porto", "berlin", ...) or dict with latitude, longitude, timezone |
n_modules |
required unless pv_arrays is set |
Number of PV modules |
annual_consumption_kwh |
required | Annual electricity demand (kWh) |
battery_kwh |
0.0 |
Nominal battery capacity in kWh (0 = no battery). The SOC window sets the usable share — see Modeling conventions |
pv_arrays |
None |
Multiple roof faces, each with modules, module, tilt, and azimuth |
pv_module |
None |
Module name from catalogue (None = default) |
load_profile |
"1" |
Bundled demandlib-derived H0 profile; "demandlib_h0" is the friendly alias |
resolution |
"h" |
Time resolution ("h" or "15min") |
cost_preset |
None |
Cost preset key from packaged defaults; editable examples live in configs/base/ |
emissions_country |
None |
Country code for CO2 calculations ("PT", "DE", "ES", ...) |
projection_years |
20 |
Economic projection horizon |
tilt, azimuth |
auto | Fixed-array orientation; defaults are estimated from latitude |
inverter_loading_ratio |
1.25 |
DC/AC oversizing ratio; also sets the inverter AC rating that clips production |
calendar_model |
"naumann_lam_field_calibrated" |
Battery aging model; default is v1 field calibration |
rlp_directory |
None |
Directory containing licensed external RLP CSVs for non-bundled load profiles |
See docs/getting-started/configuration.md for every option, including tracking, the sky-diffusion (transposition) model, PV loss overrides, battery SOC limits, tariffs, inflation, and discounting.
- System losses: every DC production calculation applies pvlib's PVWatts
losses with BREOS defaults of soiling 2%, shading 3%, mismatch 2%, wiring 2%,
connections 0.5%, LID 1.5%, nameplate 1%, and availability 3% — about 14.1%
combined (
breos.solar.DEFAULT_PVWATTS_LOSSES). Age-based degradation is added separately per simulation year. Override individual components withpv_loss_overrides(App) orloss_overrides(solar functions). - PV model background: BREOS uses pvlib for solar position, irradiance
transposition, cell temperature, and PV performance model pieces. The
sky-diffusion transposition model is selectable via
transposition_model(defaultisotropic), withalbedo/surface_typefor ground reflectance andmodel_perezfor the Perez coefficient set (see configuration docs). See docs/resources.md for pvlib and PV model references. - Inverter: the energy balance applies a flat
inverter_efficiencyand clips AC output (PV and battery discharge combined) at the inverter rating implied byinverter_loading_ratio— the same rating used for inverter CAPEX. DC surplus above the rating can still charge a DC-coupled battery. - Battery SOC window:
battery_kwhis the nominal pack capacity. The energy balance only cycles the battery betweenbattery_min_socandbattery_max_soc, so the effective storage swing isbattery_kwh × (battery_max_soc − battery_min_soc)— 80% of nominal with the defaults. Battery datasheets usually advertise usable capacity; to match a spec sheet, enterusable / 0.8or widen the SOC window. Aging is evaluated on absolute SOC, so the window also shapes degradation results — the defaults reflect the operating range the field-calibrated aging parameters were fit for. - Battery degradation calibration:
calendar_model = "naumann_lam_field_calibrated"is the stable default and maps to the v1 field calibration. The explicit alias"naumann_lam_field_calibrated_v1"is equivalent. The v2 option fixes LamEaandnwhile fittingk0andbto the field data, available as"naumann_lam_field_calibrated_v2".
app.result() returns a dict whose main fields are listed below; see
docs/getting-started/interpreting-results.md
for the full key reference, including system echo fields (pv_kwp,
consumption_kwh, ...), grid flows, and battery replacement details.
| Key | Description |
|---|---|
pv_production_kwh |
Year 1 PV production |
grid_independence_pct |
Year 1 grid independence (%) |
self_consumption_pct |
Year 1 self-consumption ratio (%) |
total_investment_eur |
Total CAPEX |
payback_year |
Payback year (None if not reached) |
npv_savings_eur |
NPV savings over projection period |
lcoe_eur_kwh |
Levelized cost of electricity from system CAPEX, O&M, simulated replacements, and discounted PV production |
co2_avoided_year1_kg |
Year 1 CO2 avoided |
co2_avoided_total_kg |
Lifetime CO2 avoided |
battery_soh_end_pct |
Battery state of health at end (if battery) |
monthly |
Year 1 monthly balance rows for PV, load, imports, exports, and self-consumption |
financial |
Yearly financial projection rows, including year 0 investment |
yearly |
List of per-year dicts with detailed breakdown |
Use pv_arrays when a roof has panels on different faces or orientations:
app = breos.App({
"location": "porto",
"annual_consumption_kwh": 4000,
"pv_arrays": [
{"modules": 8, "module": "Erlangen_445W", "tilt": 10, "azimuth": 90},
{"modules": 8, "module": "Erlangen_445W", "tilt": 10, "azimuth": 270},
],
})
app.simulate()BREOS calculates production per array and combines the DC output before the energy balance, so east-west and pitched-roof layouts are not collapsed into a single representative tilt/azimuth.
For full control over individual simulation steps, use the lower-level modules directly:
from breos.weather import fetch_tmy_weather_data
from breos.solar import calculate_pv_production_dc, PVModuleParams
from breos.battery import simulate_energy_balance, BatteryConfig
from breos.load_profiles import load_profile
from breos.economics import calculate_costs, cost_analysis_projection
from pvlib.location import Location
# Each module can be used independently
weather, metadata = fetch_tmy_weather_data(41.15, -8.63, timezone="Europe/Lisbon")
location = Location(41.15, -8.63, tz='Europe/Lisbon')
pv_dc = calculate_pv_production_dc(weather, location, tilt=35, surface_azimuth=180, n_modules=10)
# ...BREOS 0.3.3 focuses on PV and stationary-battery simulation, economic
analysis, emissions, Monte Carlo uncertainty studies, PV/battery sizing, and
serial parameter sweeps. The public API is centered on breos.App, with
lower-level modules available for users who need to assemble their own study
pipeline.
BREOS uses Open-Meteo for historical weather data. Open-Meteo is free for non-commercial use. For commercial applications, please review their pricing and terms.
Two working-directory conventions to be aware of:
- A
weather/directory in the current working directory is scanned before any PVGIS fetch — a file matching the location preset name is used silently instead of fetching. Remove or rename it to force a fresh fetch. - Historical Open-Meteo fetches cache responses in a
.cache.sqlitefile in the current working directory (30-day expiry).
Library modules report progress (file discovery, saved files, conversions)
through the standard logging module under the breos.* logger names —
enable them with logging.basicConfig(level=logging.INFO) or silence them
per module. Functions with a verbose flag still print to stdout when asked.
The public package bundles only demandlib-derived H0 example profiles. E-REDES, REE, and direct BDEW CSVs are supported as user-provided files through rlp_directory, but are not redistributed in this repository because their public source terms do not clearly grant package redistribution rights. See ATTRIBUTIONS.md and docs/legal/load-profile-data.md.
See docs/resources.md for links to PV modelling references, RLP sources, weather/solar-resource APIs, and input assumptions to record.
If you use BREOS in your research, please cite the preprint:
@misc{rodrigues2026breos,
author = {Rodrigues, L. and Delgado, J. M. P. Q. and Mendes, A. and Guimar{\~a}es, A. S.},
title = {A Modular, Open-Source Python Framework for Household PV-Battery Sizing: Validation, Multi-Objective Optimisation, and Uncertainty Analysis},
year = {2026},
doi = {10.2139/ssrn.7032064},
url = {https://papers.ssrn.com/sol3/papers.cfm?abstract_id=7032064},
note = {SSRN preprint}
}You may also cite the software directly:
@software{breos,
author = {Rodrigues, Leonardo},
title = {BREOS: Building Renewable Energy Optimization Software},
year = {2026},
url = {https://github.com/Str4vinci/breos}
}See ROADMAP.md for planned architectural work and capability extensions.
See CONTRIBUTING.md for guidelines.
BREOS uses develop as the default development branch. Feature work should be
done on separate branches and opened as pull requests into develop.
The main branch tracks stable releases only. Use main or the GitHub Releases
page when you want the latest stable version.
For questions, collaboration, or access to additional modules, reach out at lrodrigues@fe.up.pt.
BSD 3-Clause License. See LICENSE for details.