Skip to content

Architecture

Component Overview

Simulation Request Lifecycle

  1. API receives POST /simulate with params + run config
  2. Data loading — ScenarioManager retrieves price/solar/aFRR/FCR data from cache or SQLite
  3. Profile loading — If profile_id or wind_profile_id set, ProfileManager retrieves production data
  4. Initialization — BESSSimulator creates BatteryModel, DegradationModel, SolarModel, WindModel
  5. Data alignment — Price and profile timeseries aligned to 15-min intervals starting at startProject
  6. Optimization — MultiMarketOptimizer runs selected strategy over price data
  7. Degradation tracking — SimpleDegradationTracker updates battery state periodically (~weekly)
  8. Generation calculation — Solar/wind models compute generation and subsidy revenue
  9. Output — OutputHandler computes financial metrics and formats timeseries
  10. Response — JSON returned with summary, optional timeseries, optional yearly summaries

Database Design

The system uses a split database pattern to separate lightweight metadata from heavy timeseries data.

Metadata Databases

db/scenarios.db — scenario metadata:

ColumnTypeDescription
idINTEGER PK AUTOINCREMENTScenario ID
nameTEXTScenario name
countryTEXTCountry code
descriptionTEXTDescription
preloadBOOLEANAuto-load to cache on startup
has_price_dataBOOLEANPrice data imported
has_solar_dataBOOLEANSolar data imported
has_afrr_dataBOOLEANaFRR data imported
has_fcr_dataBOOLEANFCR data imported
record_countINTEGERNumber of records

db/profiles.db — profile metadata:

ColumnTypeDescription
idTEXT PKUUID (prevents ID reuse on DB recreation)
nameTEXTProfile name
profile_typeTEXTpv or wind
source_typeTEXTgenerated or imported
latitudeREALLocation
longitudeREALLocation
capacity_mwREALInstalled capacity
configTEXT (JSON)Generation configuration
preloadBOOLEANAuto-load to cache

Per-Entity Data Databases

Each scenario and profile gets its own SQLite database file:

  • db/scenario_data/scenario_{id}.db — price, aFRR, FCR, solar, and installed capacity tables
  • db/profile_data/profile_{uuid}.db — production timeseries and monthly statistics

Why Split?

  • Metadata queries stay fast — small DB, frequently queried for listings
  • Timeseries data is isolated — no single-DB bottleneck
  • Clean deletion — removing an entity = dropping a file
  • Independent caching — each entity can be cached or uncached separately

Caching

Both ScenarioManager and ProfileManager maintain in-memory caches:

PropertyDetail
StorageNumPy arrays for timeseries, Pydantic models for metadata
PreloadEntities with preload=True loaded on API startup
Manual controlPOST /{type}/{id}/cache/load and /cache/unload
Thread safetyPython RLock for concurrent access
Memory limitConfigurable (default 1000 MB total)
MonitoringGET /{type}/cache/stats reports cached count and memory

Multi-Year Execution

Sequential Mode (default)

Battery state carries between years. Degradation is continuous and accurate.

Parallel Mode (parallel_years=True)

Each year runs independently in a separate process, starting at 50% SOC. Degradation is estimated per year based on expected cycling. Faster but less accurate — no inter-year state carry-over.

Worker count is controlled by the SIMULATION_WORKERS environment variable (default: CPU count).

Source Files

FilePurpose
api.pyFastAPI REST API (~1400 lines)
bess_engine/simulator.pyMain simulation orchestrator
bess_engine/optimizer.pyMulti-market LP optimization engine
bess_engine/battery_model.pyBattery physics & SoC tracking
bess_engine/degradation.pyCycle + calendar degradation models
bess_engine/input_handler.pyInput validation & parsing
bess_engine/output_handler.pyResults formatting & financial calcs
bess_engine/solar_model.pySolar PV generation model
bess_engine/wind_model.pyWind generation model
bess_engine/scenario_manager/Market price data management
bess_engine/profile_manager/Solar/wind production profiles