BESS-Opt is a spatial optimization engine for utility-scale Battery Energy Storage System (BESS) plant design. It automates the placement of battery containers and their Medium-Voltage Stations (MVS) inside an arbitrary site polygon, honouring safety setbacks, non-buildable corridors, equipment clearances, and cable-length constraints — and then performs a global cable-routing reassignment to minimize copper.
What is traditionally a manual, multi-day CAD exercise is reduced to a parametric, repeatable run that produces engineering-grade KPIs (cluster count, total cable length, area saturation, plant power, plant energy) and an interactive layout that can be hand-tuned and exported.
The project is split into a headless core, a rendering layer, and two thin front-ends (notebook + app) that share the core verbatim — no duplicated logic.
core/ layout & calculations (no UI, no plotting deps)
viz/ matplotlib + plotly + text rendering (imports core)
app/ Streamlit UI wrapper (imports core + viz)
Layout.ipynb engineering notebook at the repo root (imports core + viz)
tests/ invariant tests
| Path | Role |
|---|---|
core/config.py |
Single source of truth: DEFAULT_EQUIPMENT + build_config(). Both modes build CONFIG here so they can never diverge. |
core/geometry.py |
Polygon handling (shapely), candidate grids, clearance generation, rotation, placement validity. |
core/placement.py |
Shared placement helpers (cluster growth, straggler fill, Hungarian cable reassignment) used by the packer and the co-located engine. |
core/colocation.py |
Co-located / paired-MVS hub engine (facility-location + hub-balanced assignment). |
core/metrics.py |
Layout KPIs and hub/civil-works metrics. |
core/sizing.py |
System sizing: total MW / MWh and 2H / 3H / 4H duration classification. |
core/serialization.py |
Layout ⇄ DataFrame round-trip and non-corrupting CSV export. |
core/packing.py |
Row/shelf packing engine (run_row_packing) — the primary layout optimizer. |
core/optimize.py |
run_colocated_optimization (co-located / paired-MVS scenario). |
viz/ |
matplotlib_plots, plotly_plots, compare (text table). |
app/app.py |
Streamlit UI — three-phase workflow; widgets → build_config → core → viz. |
Layout.ipynb |
Standalone engineering notebook at the repo root, no app dependency. |
tests/test_engine.py |
Invariant tests (no overlaps, capacity, in-bounds, sizing). |
- Back-to-Back Row Packing — the engine places containers flush at the exact per-side clearance (not on a lattice), so adjacent rows touch on their small
backclearance (e.g. 0.15 m), with a centred MVS row everymax_bess_per_mvsrows for short cable. - Full 0/90/180/270 Rotation — every container evaluates all four orientations, with asymmetric clearances remapped accordingly; 180°/270° are what enable back-to-back packing on the small clearance side.
- Row-Alignment Penalty — a configurable cost term coerces BESS units to inherit their parent MVS orientation and X/Y axis, producing clean architectural rows when uniformity matters.
- Global Min-Cost Reassignment — after greedy placement, a Hungarian solver re-pairs every BESS to the closest feasible MVS slot across the entire site, minimizing the true Euclidean cable sum subject to the per-MVS capacity cap.
- Collision-Aware Editing — manual edits in the UI re-run the placement validator so any out-of-bounds or overlapping component is flagged immediately.
| Engine | What it does |
|---|---|
Row Pack (core/packing.py, run_row_packing) |
Primary optimizer. Analytic back-to-back shelf packing adaptive to arbitrary concave polygons, evaluating all 4 rotations, with interleaved MVS rows and cable-cap-aware Hungarian assignment. Runs in ~1-3 s. |
Co-Located (core/optimize.py, run_colocated_optimization) |
Optional scenario that seeds MVS onto shared foundation pads (paired / hub) via facility-location before packing BESS, then balances cable across each hub. |
Python 3.12 is recommended. Core runtime dependencies:
pip install numpy shapely scipy matplotlib plotly streamlit pandasbuild_config is the single config constructor (equipment defaults to the
mandated DEFAULT_EQUIPMENT); the notebook and the app both use it.
from core import build_config, run_row_packing, size_system
from viz import plot_individual, print_comparison
CONFIG = build_config(
site_vertices=[(0, 0), (53.3, 0), (53.3, 90.4), (0, 90.4)],
non_buildable=[],
restricted=[],
max_bess_per_mvs=4,
max_cable_length=25,
grid_resolution=2.0,
bess_unit_mwh=5.0,
mvs_station_mw=2.5,
)
result = run_row_packing(CONFIG, verbose=True)
plot_individual(result, CONFIG)
# System sizing: total MW / MWh and 2H / 3H / 4H duration class.
m = result["metrics"]
print(size_system(m["bess_count"], m["mvs_count"],
CONFIG["bess_unit_mwh"], CONFIG["mvs_station_mw"]))Print the metrics table for a run:
print_comparison(CONFIG, result)The standalone notebook lives at Layout.ipynb (repo root)
and depends only on core + viz (no app).
streamlit run app/app.pyThe app exposes a three-phase workflow:
- Site definition — paste the property boundary, non-buildable corridors and restricted zones as vertex lists; tune BESS / MVS clearances, max BESS per MVS, and commercial MWh / MW scaling factors.
- Row-Pack optimization — the back-to-back packing engine runs and reports BESS / MVS count, plant energy, plant power, storage duration, total cable length, and area saturation.
- Deep-dive editor — click a unit on the interactive Plotly canvas (or pick from the dropdown) to nudge its X/Y coordinates, change orientation (0/90/180/270), reassign its MVS network, or delete it. Collisions are validated live. Layouts can be exported as a 1920×1080 PNG and the bill of quantities as a CSV report.
Benchmark results are cached per input hash, so re-entering Phase 2 with unchanged parameters is instantaneous.
| Key | Meaning |
|---|---|
site_vertices |
Ordered list of (x, y) tuples defining the outer property boundary. |
setback |
Inward buffer applied to the site before placement (metres). |
zones.non_buildable |
List of polygons where equipment may not be placed (access roads, cable corridors). |
zones.restricted |
List of polygons entirely excluded from the usable area. |
equipment.{BESS,MVS}.width/height |
Container footprint in metres. |
equipment.{BESS,MVS}.clearance |
Per-side clearance dictionary (front, back, left, right). |
max_bess_per_mvs |
Capacity cap for each MVS cluster. |
max_cable_length |
Maximum Euclidean BESS-to-MVS distance (set to 0 to disable). |
grid_resolution |
Coarse seeding grid spacing (metres). |
mvs_scoring_radius |
Radius used to score candidate MVS positions by nearby demand. |
min_mvs_spacing |
Minimum centre-to-centre spacing between MVS stations. |
bess_unit_mwh / mvs_station_mw |
Per-unit ratings used by core.sizing.size_system. |
colocation |
Co-located/hub parameters (group_size, pad_gap, balance_tolerance, hub_search_radius, target_hub_count). |
run_row_packing returns a dictionary containing the prepared site polygon, the non-buildable polygons, the lists of placed MVS and BESS objects (with footprint, clearance zone, rotation flag and assignment), and a metrics dictionary:
mvs_count,bess_count,full_mvstotal_cable,avg_cable,max_cable_usedbuildable_area,equipment_areaarea_saturation_pct,capacity_saturation_pct
python -m pytest tests/ -q # or: python tests/test_engine.pyThe suite runs every mode (plus the co-located engine) end-to-end on the default site and asserts the invariants any valid layout must satisfy: no footprint/clearance overlaps, per-MVS capacity respected, all equipment inside the site and out of non-buildable zones, and sane 2H/3H/4H sizing.
- Topographical awareness — Z-axis elevation tiering for sloped sites.
- Thermal heat-map overlays — ambient HVAC clearance scoring.
- DXF / GeoJSON export — direct hand-off to CAD and GIS pipelines.
- Multi-vendor equipment library — pluggable container catalogue.