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.
Files changed (92) hide show
  1. microgridspy/__init__.py +102 -0
  2. microgridspy/api.py +248 -0
  3. microgridspy/app/Home.py +256 -0
  4. microgridspy/app/__init__.py +7 -0
  5. microgridspy/app/assets/distribution_tool_card.jpeg +0 -0
  6. microgridspy/app/assets/intro_interface.png +0 -0
  7. microgridspy/app/assets/planning_tool_card.png +0 -0
  8. microgridspy/app/assets/pvgis_tool_card.png +0 -0
  9. microgridspy/app/assets/ramp_tool_card.png +0 -0
  10. microgridspy/app/assets/simulation_tool_card.png +0 -0
  11. microgridspy/app/launcher.py +31 -0
  12. microgridspy/app/multi_year_results_page.py +1233 -0
  13. microgridspy/app/page_helpers.py +41 -0
  14. microgridspy/app/pages/0_Project_Setup.py +1585 -0
  15. microgridspy/app/pages/1_Data_Audit_and_Visualization.py +1654 -0
  16. microgridspy/app/pages/2_Optimization.py +1046 -0
  17. microgridspy/app/pages/3_Results.py +1083 -0
  18. microgridspy/app/session_results.py +76 -0
  19. microgridspy/app/typical_year_results_page.py +1014 -0
  20. microgridspy/cli.py +142 -0
  21. microgridspy/data_pipeline/__init__.py +1 -0
  22. microgridspy/data_pipeline/battery_degradation_coefficients.py +358 -0
  23. microgridspy/data_pipeline/battery_degradation_model.py +108 -0
  24. microgridspy/data_pipeline/battery_loss_model.py +336 -0
  25. microgridspy/data_pipeline/generator_partial_load_model.py +82 -0
  26. microgridspy/data_pipeline/layer1_liion_coefficients.json +366 -0
  27. microgridspy/data_pipeline/loader.py +76 -0
  28. microgridspy/data_pipeline/multi_year_loader.py +75 -0
  29. microgridspy/data_pipeline/typical_year_loader.py +475 -0
  30. microgridspy/data_pipeline/typical_year_parsing.py +1295 -0
  31. microgridspy/data_pipeline/utils.py +176 -0
  32. microgridspy/errors.py +19 -0
  33. microgridspy/examples/demo_multi_year/inputs/README_inputs.md +49 -0
  34. microgridspy/examples/demo_multi_year/inputs/battery.yaml +105 -0
  35. microgridspy/examples/demo_multi_year/inputs/formulation.json +74 -0
  36. microgridspy/examples/demo_multi_year/inputs/generator.yaml +114 -0
  37. microgridspy/examples/demo_multi_year/inputs/load_demand.csv +8762 -0
  38. microgridspy/examples/demo_multi_year/inputs/renewables.yaml +88 -0
  39. microgridspy/examples/demo_multi_year/inputs/resource_availability.csv +8763 -0
  40. microgridspy/examples/demo_typical_year/inputs/README_inputs.md +51 -0
  41. microgridspy/examples/demo_typical_year/inputs/battery.yaml +102 -0
  42. microgridspy/examples/demo_typical_year/inputs/formulation.json +74 -0
  43. microgridspy/examples/demo_typical_year/inputs/generator.yaml +89 -0
  44. microgridspy/examples/demo_typical_year/inputs/load_demand.csv +8762 -0
  45. microgridspy/examples/demo_typical_year/inputs/renewables.yaml +87 -0
  46. microgridspy/examples/demo_typical_year/inputs/resource_availability.csv +8763 -0
  47. microgridspy/export/__init__.py +1 -0
  48. microgridspy/export/common.py +139 -0
  49. microgridspy/export/multi_year_results.py +2426 -0
  50. microgridspy/export/results_bundle.py +73 -0
  51. microgridspy/export/results_page_helpers.py +234 -0
  52. microgridspy/export/typical_year_reporting.py +914 -0
  53. microgridspy/export/typical_year_results.py +880 -0
  54. microgridspy/finance.py +51 -0
  55. microgridspy/io/__init__.py +0 -0
  56. microgridspy/io/csv_format.py +100 -0
  57. microgridspy/io/examples.py +93 -0
  58. microgridspy/io/formulation.py +19 -0
  59. microgridspy/io/input_labels.py +29 -0
  60. microgridspy/io/jsonio.py +58 -0
  61. microgridspy/io/paths.py +25 -0
  62. microgridspy/io/project_setup.py +384 -0
  63. microgridspy/io/templates.py +1644 -0
  64. microgridspy/io/utils.py +363 -0
  65. microgridspy/io/vintage_labels.py +114 -0
  66. microgridspy/multi_year_model/__init__.py +0 -0
  67. microgridspy/multi_year_model/constraints.py +848 -0
  68. microgridspy/multi_year_model/data.py +2235 -0
  69. microgridspy/multi_year_model/lifecycle.py +186 -0
  70. microgridspy/multi_year_model/model.py +402 -0
  71. microgridspy/multi_year_model/objective.py +389 -0
  72. microgridspy/multi_year_model/params.py +186 -0
  73. microgridspy/multi_year_model/sets.py +183 -0
  74. microgridspy/multi_year_model/variables.py +286 -0
  75. microgridspy/py.typed +0 -0
  76. microgridspy/typical_year_model/__init__.py +0 -0
  77. microgridspy/typical_year_model/constraints.py +547 -0
  78. microgridspy/typical_year_model/data.py +9 -0
  79. microgridspy/typical_year_model/model.py +379 -0
  80. microgridspy/typical_year_model/objective.py +355 -0
  81. microgridspy/typical_year_model/params.py +180 -0
  82. microgridspy/typical_year_model/sets.py +77 -0
  83. microgridspy/typical_year_model/variables.py +254 -0
  84. microgridspy/visualization/__init__.py +0 -0
  85. microgridspy/visualization/input_plots.py +93 -0
  86. microgridspy/visualization/plots.py +48 -0
  87. microgridspy-0.4.0.dist-info/METADATA +615 -0
  88. microgridspy-0.4.0.dist-info/RECORD +92 -0
  89. microgridspy-0.4.0.dist-info/WHEEL +4 -0
  90. microgridspy-0.4.0.dist-info/entry_points.txt +3 -0
  91. microgridspy-0.4.0.dist-info/licenses/AUTHORS +68 -0
  92. microgridspy-0.4.0.dist-info/licenses/LICENSE +287 -0
@@ -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)
@@ -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
+ """
@@ -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()