microgridspy 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- microgridspy/__init__.py +102 -0
- microgridspy/api.py +248 -0
- microgridspy/app/Home.py +256 -0
- microgridspy/app/__init__.py +7 -0
- microgridspy/app/assets/distribution_tool_card.jpeg +0 -0
- microgridspy/app/assets/intro_interface.png +0 -0
- microgridspy/app/assets/planning_tool_card.png +0 -0
- microgridspy/app/assets/pvgis_tool_card.png +0 -0
- microgridspy/app/assets/ramp_tool_card.png +0 -0
- microgridspy/app/assets/simulation_tool_card.png +0 -0
- microgridspy/app/launcher.py +31 -0
- microgridspy/app/multi_year_results_page.py +1233 -0
- microgridspy/app/page_helpers.py +41 -0
- microgridspy/app/pages/0_Project_Setup.py +1585 -0
- microgridspy/app/pages/1_Data_Audit_and_Visualization.py +1654 -0
- microgridspy/app/pages/2_Optimization.py +1046 -0
- microgridspy/app/pages/3_Results.py +1083 -0
- microgridspy/app/session_results.py +76 -0
- microgridspy/app/typical_year_results_page.py +1014 -0
- microgridspy/cli.py +142 -0
- microgridspy/data_pipeline/__init__.py +1 -0
- microgridspy/data_pipeline/battery_degradation_coefficients.py +358 -0
- microgridspy/data_pipeline/battery_degradation_model.py +108 -0
- microgridspy/data_pipeline/battery_loss_model.py +336 -0
- microgridspy/data_pipeline/generator_partial_load_model.py +82 -0
- microgridspy/data_pipeline/layer1_liion_coefficients.json +366 -0
- microgridspy/data_pipeline/loader.py +76 -0
- microgridspy/data_pipeline/multi_year_loader.py +75 -0
- microgridspy/data_pipeline/typical_year_loader.py +475 -0
- microgridspy/data_pipeline/typical_year_parsing.py +1295 -0
- microgridspy/data_pipeline/utils.py +176 -0
- microgridspy/errors.py +19 -0
- microgridspy/examples/demo_multi_year/inputs/README_inputs.md +49 -0
- microgridspy/examples/demo_multi_year/inputs/battery.yaml +105 -0
- microgridspy/examples/demo_multi_year/inputs/formulation.json +74 -0
- microgridspy/examples/demo_multi_year/inputs/generator.yaml +114 -0
- microgridspy/examples/demo_multi_year/inputs/load_demand.csv +8762 -0
- microgridspy/examples/demo_multi_year/inputs/renewables.yaml +88 -0
- microgridspy/examples/demo_multi_year/inputs/resource_availability.csv +8763 -0
- microgridspy/examples/demo_typical_year/inputs/README_inputs.md +51 -0
- microgridspy/examples/demo_typical_year/inputs/battery.yaml +102 -0
- microgridspy/examples/demo_typical_year/inputs/formulation.json +74 -0
- microgridspy/examples/demo_typical_year/inputs/generator.yaml +89 -0
- microgridspy/examples/demo_typical_year/inputs/load_demand.csv +8762 -0
- microgridspy/examples/demo_typical_year/inputs/renewables.yaml +87 -0
- microgridspy/examples/demo_typical_year/inputs/resource_availability.csv +8763 -0
- microgridspy/export/__init__.py +1 -0
- microgridspy/export/common.py +139 -0
- microgridspy/export/multi_year_results.py +2426 -0
- microgridspy/export/results_bundle.py +73 -0
- microgridspy/export/results_page_helpers.py +234 -0
- microgridspy/export/typical_year_reporting.py +914 -0
- microgridspy/export/typical_year_results.py +880 -0
- microgridspy/finance.py +51 -0
- microgridspy/io/__init__.py +0 -0
- microgridspy/io/csv_format.py +100 -0
- microgridspy/io/examples.py +93 -0
- microgridspy/io/formulation.py +19 -0
- microgridspy/io/input_labels.py +29 -0
- microgridspy/io/jsonio.py +58 -0
- microgridspy/io/paths.py +25 -0
- microgridspy/io/project_setup.py +384 -0
- microgridspy/io/templates.py +1644 -0
- microgridspy/io/utils.py +363 -0
- microgridspy/io/vintage_labels.py +114 -0
- microgridspy/multi_year_model/__init__.py +0 -0
- microgridspy/multi_year_model/constraints.py +848 -0
- microgridspy/multi_year_model/data.py +2235 -0
- microgridspy/multi_year_model/lifecycle.py +186 -0
- microgridspy/multi_year_model/model.py +402 -0
- microgridspy/multi_year_model/objective.py +389 -0
- microgridspy/multi_year_model/params.py +186 -0
- microgridspy/multi_year_model/sets.py +183 -0
- microgridspy/multi_year_model/variables.py +286 -0
- microgridspy/py.typed +0 -0
- microgridspy/typical_year_model/__init__.py +0 -0
- microgridspy/typical_year_model/constraints.py +547 -0
- microgridspy/typical_year_model/data.py +9 -0
- microgridspy/typical_year_model/model.py +379 -0
- microgridspy/typical_year_model/objective.py +355 -0
- microgridspy/typical_year_model/params.py +180 -0
- microgridspy/typical_year_model/sets.py +77 -0
- microgridspy/typical_year_model/variables.py +254 -0
- microgridspy/visualization/__init__.py +0 -0
- microgridspy/visualization/input_plots.py +93 -0
- microgridspy/visualization/plots.py +48 -0
- microgridspy-0.4.0.dist-info/METADATA +615 -0
- microgridspy-0.4.0.dist-info/RECORD +92 -0
- microgridspy-0.4.0.dist-info/WHEEL +4 -0
- microgridspy-0.4.0.dist-info/entry_points.txt +3 -0
- microgridspy-0.4.0.dist-info/licenses/AUTHORS +68 -0
- microgridspy-0.4.0.dist-info/licenses/LICENSE +287 -0
microgridspy/__init__.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""MicroGridsPy: bottom-up optimization tool for planning mini-grids.
|
|
2
|
+
|
|
3
|
+
Public API:
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
import microgridspy as mgp
|
|
7
|
+
|
|
8
|
+
# one-liner: solve and get analysis-ready tables
|
|
9
|
+
results = mgp.solve("demo_typical_year", solver="highs").results()
|
|
10
|
+
results.kpis # pandas DataFrame
|
|
11
|
+
mgp.export_results(results) # write CSV/Excel to the project folder
|
|
12
|
+
|
|
13
|
+
# or drive the models directly
|
|
14
|
+
from microgridspy import TypicalYearModel
|
|
15
|
+
model = TypicalYearModel("demo_typical_year")
|
|
16
|
+
model.solve_single_objective(solver="highs")
|
|
17
|
+
summary = model.results_summary()
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The two formulations are named ``typical_year`` and ``multi_year`` everywhere —
|
|
21
|
+
in ``formulation.json``, in ``create_project(formulation=...)`` and in the model
|
|
22
|
+
class names.
|
|
23
|
+
|
|
24
|
+
The Streamlit GUI lives in `microgridspy.app` and is installed only with
|
|
25
|
+
the ``[gui]`` extra; importing this package never imports Streamlit.
|
|
26
|
+
|
|
27
|
+
Stability: ``TypicalYearModel``, ``MultiYearModel`` and ``InputValidationError``
|
|
28
|
+
are the stable core. The results dataclasses are provisional (their tables may
|
|
29
|
+
grow) until the 1.0 release.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
from importlib.metadata import PackageNotFoundError
|
|
35
|
+
from importlib.metadata import version as _version
|
|
36
|
+
|
|
37
|
+
# --- Convenience API --------------------------------------------------------
|
|
38
|
+
from microgridspy.api import (
|
|
39
|
+
export_results,
|
|
40
|
+
list_input_timeseries,
|
|
41
|
+
load_inputs,
|
|
42
|
+
load_results,
|
|
43
|
+
plot_input_timeseries,
|
|
44
|
+
solve,
|
|
45
|
+
solve_example,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# --- Workspace helpers ------------------------------------------------------
|
|
49
|
+
from microgridspy.errors import InputValidationError
|
|
50
|
+
from microgridspy.export.multi_year_results import MultiYearResults
|
|
51
|
+
|
|
52
|
+
# --- Structured results (provisional) ---------------------------------------
|
|
53
|
+
from microgridspy.export.typical_year_results import TypicalYearResults
|
|
54
|
+
from microgridspy.io.examples import list_examples, load_example
|
|
55
|
+
|
|
56
|
+
# --- Project scaffolding & management ---------------------------------------
|
|
57
|
+
from microgridspy.io.project_setup import (
|
|
58
|
+
copy_project,
|
|
59
|
+
create_project,
|
|
60
|
+
delete_project,
|
|
61
|
+
rename_project,
|
|
62
|
+
validate_project,
|
|
63
|
+
)
|
|
64
|
+
from microgridspy.io.templates import TemplateSettings
|
|
65
|
+
from microgridspy.io.utils import list_projects, project_exists, project_paths, set_workspace
|
|
66
|
+
from microgridspy.multi_year_model.model import MultiYearModel
|
|
67
|
+
|
|
68
|
+
# --- Models (stable core) ---------------------------------------------------
|
|
69
|
+
from microgridspy.typical_year_model.model import TypicalYearModel
|
|
70
|
+
|
|
71
|
+
__all__ = [
|
|
72
|
+
"TypicalYearModel",
|
|
73
|
+
"MultiYearModel",
|
|
74
|
+
"solve",
|
|
75
|
+
"solve_example",
|
|
76
|
+
"list_examples",
|
|
77
|
+
"load_example",
|
|
78
|
+
"load_results",
|
|
79
|
+
"export_results",
|
|
80
|
+
"load_inputs",
|
|
81
|
+
"list_input_timeseries",
|
|
82
|
+
"plot_input_timeseries",
|
|
83
|
+
"create_project",
|
|
84
|
+
"validate_project",
|
|
85
|
+
"delete_project",
|
|
86
|
+
"copy_project",
|
|
87
|
+
"rename_project",
|
|
88
|
+
"TemplateSettings",
|
|
89
|
+
"TypicalYearResults",
|
|
90
|
+
"MultiYearResults",
|
|
91
|
+
"set_workspace",
|
|
92
|
+
"list_projects",
|
|
93
|
+
"project_paths",
|
|
94
|
+
"project_exists",
|
|
95
|
+
"InputValidationError",
|
|
96
|
+
"__version__",
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
__version__ = _version("microgridspy")
|
|
101
|
+
except PackageNotFoundError: # not installed (e.g. running from a source checkout)
|
|
102
|
+
__version__ = "0.0.0+unknown"
|
microgridspy/api.py
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""High-level convenience API for MicroGridsPy.
|
|
2
|
+
|
|
3
|
+
These are thin wrappers over the model, export, and IO layers that give library
|
|
4
|
+
users a short path from a project name to solved, analysis-ready results:
|
|
5
|
+
|
|
6
|
+
```python
|
|
7
|
+
import microgridspy as mgp
|
|
8
|
+
|
|
9
|
+
results = mgp.solve("demo_typical_year", solver="highs").results()
|
|
10
|
+
results.kpis # pandas DataFrame
|
|
11
|
+
mgp.export_results(results) # write CSV/Excel to the project folder
|
|
12
|
+
```
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import TYPE_CHECKING, Any
|
|
20
|
+
|
|
21
|
+
import xarray as xr
|
|
22
|
+
|
|
23
|
+
from microgridspy.errors import InputValidationError
|
|
24
|
+
from microgridspy.export.multi_year_results import MultiYearResults
|
|
25
|
+
from microgridspy.export.typical_year_results import TypicalYearResults
|
|
26
|
+
from microgridspy.io.formulation import MULTI_YEAR, TYPICAL_YEAR, VALID_FORMULATIONS
|
|
27
|
+
from microgridspy.multi_year_model.model import MultiYearModel
|
|
28
|
+
from microgridspy.typical_year_model.model import TypicalYearModel
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from matplotlib.figure import Figure
|
|
32
|
+
|
|
33
|
+
AnyModel = TypicalYearModel | MultiYearModel
|
|
34
|
+
AnyResults = TypicalYearResults | MultiYearResults
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _detect_formulation(project_name: str) -> str:
|
|
38
|
+
"""Read ``core_formulation`` from a project's ``formulation.json``."""
|
|
39
|
+
from microgridspy.io.utils import project_paths
|
|
40
|
+
|
|
41
|
+
fpath = project_paths(project_name).formulation_json
|
|
42
|
+
if not fpath.exists():
|
|
43
|
+
raise InputValidationError(
|
|
44
|
+
f"Cannot detect formulation: {fpath} not found. "
|
|
45
|
+
f"Pass formulation='{TYPICAL_YEAR}' or '{MULTI_YEAR}' explicitly."
|
|
46
|
+
)
|
|
47
|
+
try:
|
|
48
|
+
raw = json.loads(fpath.read_text(encoding="utf-8"))
|
|
49
|
+
except json.JSONDecodeError as exc:
|
|
50
|
+
raise InputValidationError(f"Cannot parse {fpath}: {exc}") from exc
|
|
51
|
+
return str(raw.get("core_formulation", TYPICAL_YEAR))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _model_for(project_name: str, formulation: str) -> AnyModel:
|
|
55
|
+
if formulation == TYPICAL_YEAR:
|
|
56
|
+
return TypicalYearModel(project_name)
|
|
57
|
+
if formulation == MULTI_YEAR:
|
|
58
|
+
return MultiYearModel(project_name)
|
|
59
|
+
raise InputValidationError(
|
|
60
|
+
f"Unknown formulation {formulation!r}. Expected one of {', '.join(VALID_FORMULATIONS)}."
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def solve(
|
|
65
|
+
project_name: str,
|
|
66
|
+
*,
|
|
67
|
+
formulation: str | None = None,
|
|
68
|
+
solver: str = "highs",
|
|
69
|
+
**solver_kwargs: Any,
|
|
70
|
+
) -> AnyModel:
|
|
71
|
+
"""Build and solve a project, returning the solved model.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
project_name: the project folder in the active workspace.
|
|
75
|
+
formulation: ``"typical_year"`` or ``"multi_year"``; if ``None`` it is read
|
|
76
|
+
from the project's ``formulation.json``.
|
|
77
|
+
solver: ``"highs"`` (open source) or ``"gurobi"`` (licensed).
|
|
78
|
+
**solver_kwargs: forwarded to
|
|
79
|
+
`TypicalYearModel.solve_single_objective()` (e.g. ``solver_params``,
|
|
80
|
+
``problem_fn``, ``log_file_path``).
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
The solved model instance, ready for `results()` /
|
|
84
|
+
`results_summary()`.
|
|
85
|
+
"""
|
|
86
|
+
if formulation is None:
|
|
87
|
+
formulation = _detect_formulation(project_name)
|
|
88
|
+
model = _model_for(project_name, formulation)
|
|
89
|
+
model.solve_single_objective(solver=solver, **solver_kwargs)
|
|
90
|
+
return model
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def solve_example(
|
|
94
|
+
name: str = "demo_typical_year",
|
|
95
|
+
*,
|
|
96
|
+
solver: str = "highs",
|
|
97
|
+
dest: str | None = None,
|
|
98
|
+
overwrite: bool = True,
|
|
99
|
+
**solver_kwargs: Any,
|
|
100
|
+
) -> AnyModel:
|
|
101
|
+
"""Load a bundled example project and solve it, end-to-end.
|
|
102
|
+
|
|
103
|
+
A one-call quick start that works straight after ``pip install`` (no repository
|
|
104
|
+
clone needed): it copies the example into the active workspace and solves it.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
name: the bundled example to run (see
|
|
108
|
+
`microgridspy.list_examples()`); ``"demo_typical_year"`` or
|
|
109
|
+
``"demo_multi_year"``.
|
|
110
|
+
solver: ``"highs"`` (open source) or ``"gurobi"`` (licensed).
|
|
111
|
+
dest: destination project name; defaults to ``name``.
|
|
112
|
+
overwrite: replace the destination project if it already exists (default
|
|
113
|
+
True, so the example is re-runnable).
|
|
114
|
+
**solver_kwargs: forwarded to the model's ``solve_single_objective``.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
The solved model instance, ready for `results()` / `results_summary()`.
|
|
118
|
+
"""
|
|
119
|
+
from microgridspy.io.examples import load_example
|
|
120
|
+
|
|
121
|
+
project = load_example(name, dest=dest, overwrite=overwrite)
|
|
122
|
+
return solve(project, solver=solver, **solver_kwargs)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def load_results(
|
|
126
|
+
project_name: str,
|
|
127
|
+
*,
|
|
128
|
+
formulation: str | None = None,
|
|
129
|
+
) -> AnyResults | None:
|
|
130
|
+
"""Load a previously saved run's results from the project's ``results/`` folder.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
project_name: the project to read.
|
|
134
|
+
formulation: ``"typical_year"`` or ``"multi_year"``; auto-detected from
|
|
135
|
+
``formulation.json`` when ``None``.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
The structured results object, or ``None`` if no saved run exists.
|
|
139
|
+
"""
|
|
140
|
+
from microgridspy.export.results_page_helpers import (
|
|
141
|
+
load_multi_year_results_from_files,
|
|
142
|
+
load_typical_year_results_from_files,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
if formulation is None:
|
|
146
|
+
formulation = _detect_formulation(project_name)
|
|
147
|
+
if formulation == MULTI_YEAR:
|
|
148
|
+
return load_multi_year_results_from_files(project_name)
|
|
149
|
+
return load_typical_year_results_from_files(project_name)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def export_results(results: AnyResults, out_dir: Path | None = None) -> dict[str, str]:
|
|
153
|
+
"""Write a results object to CSV/Excel files.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
results: a `TypicalYearResults` or `MultiYearResults`.
|
|
157
|
+
out_dir: destination directory; when ``None``, writes to the project's
|
|
158
|
+
``results/`` folder.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Mapping of output name to the written file path.
|
|
162
|
+
"""
|
|
163
|
+
if isinstance(results, MultiYearResults):
|
|
164
|
+
from microgridspy.export.multi_year_results import export_multi_year_results_package
|
|
165
|
+
|
|
166
|
+
return export_multi_year_results_package(results=results, out_dir=out_dir)
|
|
167
|
+
if isinstance(results, TypicalYearResults):
|
|
168
|
+
from microgridspy.export.typical_year_results import export_typical_year_results_package
|
|
169
|
+
|
|
170
|
+
return export_typical_year_results_package(results=results, out_dir=out_dir)
|
|
171
|
+
raise TypeError(
|
|
172
|
+
f"export_results expects TypicalYearResults or MultiYearResults, got {type(results).__name__}"
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def load_inputs(project_name: str, *, formulation: str | None = None) -> xr.Dataset:
|
|
177
|
+
"""Assemble and return a project's input dataset, without solving.
|
|
178
|
+
|
|
179
|
+
Builds the sets and data layers (the same inputs a model would use), so you
|
|
180
|
+
can inspect the assembled xarray Dataset directly.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
project_name: the project to read.
|
|
184
|
+
formulation: ``"typical_year"`` or ``"multi_year"``; auto-detected when None.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
xr.Dataset: the assembled input dataset.
|
|
188
|
+
"""
|
|
189
|
+
if formulation is None:
|
|
190
|
+
formulation = _detect_formulation(project_name)
|
|
191
|
+
model = _model_for(project_name, formulation)
|
|
192
|
+
model._initialize_data() # builds sets + data, no optimization model
|
|
193
|
+
return model.data
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def list_input_timeseries(project_name: str, *, formulation: str | None = None) -> list[str]:
|
|
197
|
+
"""List the time-series input variables available to plot for a project."""
|
|
198
|
+
from microgridspy.visualization.input_plots import list_timeseries_options
|
|
199
|
+
|
|
200
|
+
ds = load_inputs(project_name, formulation=formulation)
|
|
201
|
+
return [opt.variable for opt in list_timeseries_options(ds)]
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def plot_input_timeseries(
|
|
205
|
+
project_name: str,
|
|
206
|
+
variable: str | None = None,
|
|
207
|
+
*,
|
|
208
|
+
formulation: str | None = None,
|
|
209
|
+
scenario: str | None = None,
|
|
210
|
+
year: str | int | None = None,
|
|
211
|
+
**selectors: Any,
|
|
212
|
+
) -> tuple[Figure, Figure]:
|
|
213
|
+
"""Plot an input time series as ``(hourly, daily)`` matplotlib figures.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
project_name: the project to read.
|
|
217
|
+
variable: the time-series variable to plot; defaults to the first
|
|
218
|
+
available (see `list_input_timeseries()`).
|
|
219
|
+
formulation: auto-detected when None.
|
|
220
|
+
scenario: optional scenario selector when the variable has that dimension.
|
|
221
|
+
year: optional year selector when the variable has that dimension.
|
|
222
|
+
**selectors: further dimension selectors (e.g. ``resource="solar"``).
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
tuple[matplotlib.figure.Figure, matplotlib.figure.Figure]: hourly and
|
|
226
|
+
average-daily figures.
|
|
227
|
+
"""
|
|
228
|
+
from microgridspy.visualization.input_plots import (
|
|
229
|
+
build_timeseries_figures,
|
|
230
|
+
list_timeseries_options,
|
|
231
|
+
slice_timeseries,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
ds = load_inputs(project_name, formulation=formulation)
|
|
235
|
+
names = [opt.variable for opt in list_timeseries_options(ds)]
|
|
236
|
+
if not names:
|
|
237
|
+
raise InputValidationError(f"No plottable time-series inputs found for '{project_name}'.")
|
|
238
|
+
if variable is None:
|
|
239
|
+
variable = names[0]
|
|
240
|
+
elif variable not in names:
|
|
241
|
+
raise InputValidationError(
|
|
242
|
+
f"'{variable}' is not a plottable time series for '{project_name}'. Available: {names}"
|
|
243
|
+
)
|
|
244
|
+
da = slice_timeseries(
|
|
245
|
+
ds, variable=variable, scenario=scenario, year=year, selectors=selectors or None
|
|
246
|
+
)
|
|
247
|
+
label = variable.replace("_", " ")
|
|
248
|
+
return build_timeseries_figures(da, title_prefix=label, y_label=label)
|
microgridspy/app/Home.py
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import streamlit as st
|
|
6
|
+
|
|
7
|
+
st.set_page_config(
|
|
8
|
+
page_title="MicroGridsPy - Planning",
|
|
9
|
+
layout="wide",
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
ASSETS_DIR = Path(__file__).resolve().parent / "assets"
|
|
14
|
+
REPOSITORY_URL = "https://github.com/AleOnori98/microgridspy-planning"
|
|
15
|
+
RAMP_URL = "https://github.com/AleOnori98/RAMP-Streamlit"
|
|
16
|
+
PVGIS_URL = "https://github.com/AleOnori98/PVGIS-Streamlit-App"
|
|
17
|
+
LV_TOPOLOGY_URL = "https://github.com/AleOnori98/LV-Distribution-Topology-Streamlit"
|
|
18
|
+
# Official documentation site (Read the Docs).
|
|
19
|
+
DOCS_URL = "https://microgridspy-package-docs.readthedocs.io/en/latest/"
|
|
20
|
+
|
|
21
|
+
ECOSYSTEM_TOOL_ROWS = (
|
|
22
|
+
(
|
|
23
|
+
{
|
|
24
|
+
"title": "RAMP Demand Model",
|
|
25
|
+
"description": "Bottom-up stochastic demand assessment for appliances, households, and community load evolution.",
|
|
26
|
+
"image_name": "ramp_tool_card.png",
|
|
27
|
+
"badge": "Upstream input layer",
|
|
28
|
+
"repo_url": RAMP_URL,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"title": "PVGIS Resource Assessment",
|
|
32
|
+
"description": "Solar and wind resource estimation used to build planning-ready renewable input profiles.",
|
|
33
|
+
"image_name": "pvgis_tool_card.png",
|
|
34
|
+
"badge": "Upstream input layer",
|
|
35
|
+
"repo_url": PVGIS_URL,
|
|
36
|
+
},
|
|
37
|
+
),
|
|
38
|
+
(
|
|
39
|
+
{
|
|
40
|
+
"title": "LV Distribution Topology Tool",
|
|
41
|
+
"description": "Distribution network layout, pole placement, and topology design for the physical electrification layer.",
|
|
42
|
+
"image_name": "distribution_tool_card.jpeg",
|
|
43
|
+
"badge": "Network design layer",
|
|
44
|
+
"repo_url": LV_TOPOLOGY_URL,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"title": "Dispatch Simulation Module",
|
|
48
|
+
"description": "Detailed operational analysis starting from a predefined system design, useful for operational realism and control studies.",
|
|
49
|
+
"image_name": "simulation_tool_card.png",
|
|
50
|
+
"badge": "Operational analysis layer",
|
|
51
|
+
},
|
|
52
|
+
),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
APP_PAGE_LINKS = (
|
|
56
|
+
("pages/0_Project_Setup.py", "1. Project Setup"),
|
|
57
|
+
("pages/1_Data_Audit_and_Visualization.py", "2. Data Audit and Visualization"),
|
|
58
|
+
("pages/2_Optimization.py", "3. Optimization"),
|
|
59
|
+
("pages/3_Results.py", "4. Results"),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
REPOSITORY_REFERENCES = (
|
|
63
|
+
"`README.md`: overall project scope and workflow",
|
|
64
|
+
"`docs/data-reference/data-contract.md`: canonical dataset contract",
|
|
65
|
+
"`projects/`: your project folders and input templates (created here at runtime)",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
USEFUL_LINKS = (("Official documentation", DOCS_URL),)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _asset(name: str) -> str:
|
|
72
|
+
return str(ASSETS_DIR / name)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _inject_css() -> None:
|
|
76
|
+
st.markdown(
|
|
77
|
+
"""
|
|
78
|
+
<style>
|
|
79
|
+
:root {
|
|
80
|
+
--mgpy-ink: #153243;
|
|
81
|
+
--mgpy-muted: #5b6b73;
|
|
82
|
+
--mgpy-accent: #1f7a8c;
|
|
83
|
+
--mgpy-gold: #f4b942;
|
|
84
|
+
--mgpy-surface: #f7fbfc;
|
|
85
|
+
--mgpy-border: rgba(21, 50, 67, 0.10);
|
|
86
|
+
}
|
|
87
|
+
.featured-card {
|
|
88
|
+
display: block;
|
|
89
|
+
height: 0.01rem;
|
|
90
|
+
margin: 0;
|
|
91
|
+
padding: 0;
|
|
92
|
+
opacity: 0;
|
|
93
|
+
}
|
|
94
|
+
div[data-testid="stVerticalBlockBorderWrapper"]:has(.featured-card) {
|
|
95
|
+
border: 2px solid rgba(47, 128, 237, 0.95) !important;
|
|
96
|
+
box-shadow: 0 8px 24px rgba(47, 128, 237, 0.10);
|
|
97
|
+
background: linear-gradient(180deg, #ffffff 0%, #f7fbff 100%);
|
|
98
|
+
}
|
|
99
|
+
.featured-kicker {
|
|
100
|
+
display: inline-block;
|
|
101
|
+
padding: 0.25rem 0.6rem;
|
|
102
|
+
border-radius: 999px;
|
|
103
|
+
background: rgba(47, 128, 237, 0.12);
|
|
104
|
+
color: #2f80ed;
|
|
105
|
+
font-size: 0.82rem;
|
|
106
|
+
font-weight: 700;
|
|
107
|
+
letter-spacing: 0.04em;
|
|
108
|
+
margin-bottom: 0.4rem;
|
|
109
|
+
}
|
|
110
|
+
div[data-testid="stButton"]:has(button[kind="primary"]) button {
|
|
111
|
+
background: linear-gradient(135deg, #2f80ed 0%, #1f7a8c 100%);
|
|
112
|
+
border: 1px solid #2f80ed;
|
|
113
|
+
color: white;
|
|
114
|
+
font-weight: 700;
|
|
115
|
+
}
|
|
116
|
+
</style>
|
|
117
|
+
""",
|
|
118
|
+
unsafe_allow_html=True,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _render_featured_planning_card() -> None:
|
|
123
|
+
with st.container(border=True):
|
|
124
|
+
st.markdown('<div class="featured-card"></div>', unsafe_allow_html=True)
|
|
125
|
+
col_image, col_body = st.columns([1.15, 1.45], gap="large")
|
|
126
|
+
with col_image:
|
|
127
|
+
st.image(_asset("planning_tool_card.png"), width="stretch")
|
|
128
|
+
with col_body:
|
|
129
|
+
st.markdown(
|
|
130
|
+
'<div class="featured-kicker">Core planning engine</div>', unsafe_allow_html=True
|
|
131
|
+
)
|
|
132
|
+
st.markdown("### MicroGridsPy Planning")
|
|
133
|
+
st.write(
|
|
134
|
+
"Techno-economic optimization of mini-grid systems under deterministic or stochastic assumptions. "
|
|
135
|
+
"Use it to size renewables, batteries, generators, and grid interaction with either a representative typical year "
|
|
136
|
+
"or a multi-year dynamic formulation with capacity expansion."
|
|
137
|
+
)
|
|
138
|
+
st.markdown(f"[GitHub repository]({REPOSITORY_URL})")
|
|
139
|
+
if st.button("Open Project Setup", type="primary", key="open_project_setup"):
|
|
140
|
+
st.switch_page("pages/0_Project_Setup.py")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _tool_card(
|
|
144
|
+
*,
|
|
145
|
+
title: str,
|
|
146
|
+
description: str,
|
|
147
|
+
image_name: str,
|
|
148
|
+
badge: str,
|
|
149
|
+
repo_url: str | None = None,
|
|
150
|
+
) -> None:
|
|
151
|
+
with st.container(border=True):
|
|
152
|
+
st.image(_asset(image_name), width="stretch")
|
|
153
|
+
st.caption(badge)
|
|
154
|
+
st.markdown(f"**{title}**")
|
|
155
|
+
st.write(description)
|
|
156
|
+
if repo_url:
|
|
157
|
+
st.markdown(f"[GitHub repository]({repo_url})")
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _render_markdown_bullets(title: str, items: tuple[str, ...]) -> None:
|
|
161
|
+
bullet_lines = "\n".join(f"- {item}" for item in items)
|
|
162
|
+
st.markdown(f"**{title}**\n\n{bullet_lines}")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _render_ecosystem() -> None:
|
|
166
|
+
st.title("Welcome to MicroGridsPy!")
|
|
167
|
+
st.markdown(
|
|
168
|
+
"**MicroGridsPy Planning** is the techno-economic optimization layer of the MicroGridsPy ecosystem."
|
|
169
|
+
)
|
|
170
|
+
st.markdown(
|
|
171
|
+
"It supports off-grid and weak-grid mini-grid design, combining renewable generation, batteries, generators, "
|
|
172
|
+
"optional grid interaction, stochastic scenarios, and both typical-year and multi-year capacity-expansion formulations. "
|
|
173
|
+
"The broader ecosystem connects resource assessment, demand modelling, distribution design, planning, and detailed operational analysis into one coherent workflow."
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
_render_featured_planning_card()
|
|
177
|
+
st.subheader("Ecosystem tools")
|
|
178
|
+
for tool_row in ECOSYSTEM_TOOL_ROWS:
|
|
179
|
+
columns = st.columns(len(tool_row), gap="large")
|
|
180
|
+
for column, tool in zip(columns, tool_row):
|
|
181
|
+
with column:
|
|
182
|
+
_tool_card(**tool)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _render_resources() -> None:
|
|
186
|
+
st.subheader("Resources and Navigation")
|
|
187
|
+
st.write(
|
|
188
|
+
"Use this application as the planning workspace inside the broader ecosystem. "
|
|
189
|
+
"The links below help you start a new project and locate the main reference material already available in this repository."
|
|
190
|
+
)
|
|
191
|
+
st.write("")
|
|
192
|
+
|
|
193
|
+
c1, c2 = st.columns([1, 3.0], gap="large")
|
|
194
|
+
with c1:
|
|
195
|
+
st.markdown("**Start here in this app**")
|
|
196
|
+
for page_path, label in APP_PAGE_LINKS:
|
|
197
|
+
st.page_link(page_path, label=label)
|
|
198
|
+
|
|
199
|
+
with c2:
|
|
200
|
+
_render_markdown_bullets("Repository references", REPOSITORY_REFERENCES)
|
|
201
|
+
|
|
202
|
+
st.write("")
|
|
203
|
+
st.markdown("**At a glance**")
|
|
204
|
+
info_cols = st.columns(3, gap="medium")
|
|
205
|
+
with info_cols[0]:
|
|
206
|
+
st.info(
|
|
207
|
+
"Planning modes: Typical-year for compact investment studies, multi-year for dynamic expansion and long-horizon planning."
|
|
208
|
+
)
|
|
209
|
+
with info_cols[1]:
|
|
210
|
+
st.info(
|
|
211
|
+
"Backend: Python + Streamlit frontend, Linopy optimization backend, CSV/YAML/JSON project workflow."
|
|
212
|
+
)
|
|
213
|
+
with info_cols[2]:
|
|
214
|
+
st.info(
|
|
215
|
+
"Use together: Resource, demand, planning, network, and dispatch modules can be combined at increasing levels of detail."
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
st.write("")
|
|
219
|
+
st.markdown("**Documentation**")
|
|
220
|
+
st.info(f"Full documentation is available at [{DOCS_URL}]({DOCS_URL}).")
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _render_footer() -> None:
|
|
224
|
+
st.subheader("Contacts")
|
|
225
|
+
st.markdown("**Active Developer**")
|
|
226
|
+
st.markdown(
|
|
227
|
+
"""
|
|
228
|
+
**Alessandro Onori** , alessandro.onori@polimi.it
|
|
229
|
+
*Core Linopy optimization model, modeling advancements, and Streamlit UI development*
|
|
230
|
+
"""
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
st.markdown("**Technical Advisors**")
|
|
234
|
+
st.markdown(
|
|
235
|
+
"""
|
|
236
|
+
- Nicolò Stevanato, nicolo.stevanato@polimi.it, Politecnico di Milano
|
|
237
|
+
- Riccardo Mereu, riccardo.mereu@polimi.it, Politecnico di Milano
|
|
238
|
+
- Emanuela Colombo, emanuela.colombo@polimi.it, Politecnico di Milano
|
|
239
|
+
"""
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
st.subheader("License")
|
|
243
|
+
st.markdown(
|
|
244
|
+
"Open-source research codebase. Refer to the repository materials for the current licensing terms."
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def render_home_page() -> None:
|
|
249
|
+
_inject_css()
|
|
250
|
+
_render_ecosystem()
|
|
251
|
+
_render_resources()
|
|
252
|
+
st.divider()
|
|
253
|
+
_render_footer()
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
render_home_page()
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""MicroGridsPy Streamlit GUI.
|
|
2
|
+
|
|
3
|
+
This subpackage contains the optional graphical interface. Its extra
|
|
4
|
+
dependency (streamlit) is installed only via the ``[gui]`` extra:
|
|
5
|
+
``pip install "microgridspy[gui]"``. Importing the library core
|
|
6
|
+
(``import microgridspy``) never imports anything from here.
|
|
7
|
+
"""
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Console entry point: ``microgridspy-gui`` launches the Streamlit app from anywhere.
|
|
2
|
+
|
|
3
|
+
Registered in ``pyproject.toml`` under ``[project.scripts]`` as
|
|
4
|
+
``microgridspy-gui = "microgridspy.app.launcher:main"``. It locates the
|
|
5
|
+
packaged ``Home.py`` via `importlib.resources`, so it works regardless of
|
|
6
|
+
where the package was installed or which directory the user runs it from.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import sys
|
|
12
|
+
from importlib import resources
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main() -> None:
|
|
16
|
+
try:
|
|
17
|
+
from streamlit.web import cli as stcli # streamlit is only needed for the GUI
|
|
18
|
+
except ModuleNotFoundError as exc: # pragma: no cover - trivial guard
|
|
19
|
+
raise SystemExit(
|
|
20
|
+
"The MicroGridsPy GUI requires the optional 'gui' extra.\n"
|
|
21
|
+
'Install it with: pip install "microgridspy[gui]"'
|
|
22
|
+
) from exc
|
|
23
|
+
|
|
24
|
+
# Locate the packaged Home.py regardless of install location.
|
|
25
|
+
with resources.as_file(resources.files("microgridspy.app") / "Home.py") as home:
|
|
26
|
+
sys.argv = ["streamlit", "run", str(home)]
|
|
27
|
+
sys.exit(stcli.main())
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
if __name__ == "__main__":
|
|
31
|
+
main()
|