pypsa2smspp 0.0.1__tar.gz

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.
@@ -0,0 +1,2 @@
1
+ include pypsa2smspp/data/*
2
+ include README.md LICENSE
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.4
2
+ Name: pypsa2smspp
3
+ Version: 0.0.1
4
+ Summary: Bi-directional interface between PyPSA and SMS++
5
+ Author-email: Alessandro Pampado <alessandro.pampado@ing.unipi.it>, Davide Fioriti <davide.fioriti@unipi.it>, Unipi developers <davide.fioriti@unipi.it>
6
+ Project-URL: Homepage, https://github.com/SPSUnipi/pypsa2smspp
7
+ Project-URL: Issues, https://github.com/SPSUnipi/pypsa2smspp/issues
8
+ Classifier: Programming Language :: Python :: 3.12
9
+ Classifier: Programming Language :: Python :: 3.13
10
+ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.12
13
+ Description-Content-Type: text/markdown
14
+ Requires-Dist: numpy
15
+ Requires-Dist: pandas<3,>=0.24
16
+ Requires-Dist: pypsa>=1
17
+ Requires-Dist: xarray
18
+ Requires-Dist: openpyxl
19
+ Requires-Dist: pysmspp>=0.0.2
20
+ Provides-Extra: dev
21
+ Requires-Dist: pytest; extra == "dev"
22
+ Requires-Dist: highspy; extra == "dev"
23
+ Requires-Dist: gurobipy; extra == "dev"
24
+ Requires-Dist: coverage; extra == "dev"
25
+ Requires-Dist: pre-commit; extra == "dev"
26
+ Requires-Dist: ncompare; extra == "dev"
27
+ Requires-Dist: mypy; extra == "dev"
28
+ Provides-Extra: docs
29
+ Requires-Dist: numpydoc; extra == "docs"
30
+ Requires-Dist: sphinx; extra == "docs"
31
+ Requires-Dist: sphinx-book-theme; extra == "docs"
32
+ Requires-Dist: sphinx-rtd-theme; extra == "docs"
33
+ Requires-Dist: pydata-sphinx-theme; extra == "docs"
34
+ Requires-Dist: sphinx-reredirects; extra == "docs"
35
+ Requires-Dist: nbsphinx; extra == "docs"
36
+ Requires-Dist: nbsphinx-link; extra == "docs"
37
+ Requires-Dist: scikit-learn; extra == "docs"
38
+ Requires-Dist: ipython; extra == "docs"
39
+ Requires-Dist: ipykernel; extra == "docs"
40
+
41
+ # pypsa2smspp
42
+
43
+ [![Tests](https://github.com/SPSUnipi/pypsa2smspp/actions/workflows/test.yml/badge.svg)](https://github.com/SPSUnipi/pypsa2smspp/actions/workflows/test.yml)
44
+
45
+ This package aims at providing a python interface between [PyPSA](https://github.com/PyPSA/pypsa) and [SMS++](https://gitlab.com/smspp/smspp-project) models using a simple python interface.
46
+ The package aims to support:
47
+ - Convert a PyPSA model to SMS++
48
+ - Execute the optimization of the so-created SMS++ model
49
+ - Parse the solution from the SMS++ model to PyPSA
50
+
51
+
52
+ ## How to develop
53
+
54
+ 1. First, clone the repository using git:
55
+
56
+ ```bash
57
+ git clone https://github.com/SPSUnipi/pypsa2smspp
58
+ ```
59
+
60
+ 2. Create a virtual environment using venv or conda.
61
+ For exaample, using venv:
62
+
63
+ ```bash
64
+ python -m venv .venv
65
+ source .venv/bin/activate
66
+ ```
67
+
68
+ Alternatively, using conda:
69
+
70
+ ```bash
71
+ conda create -n pypsa2smspp python=3.10
72
+ conda activate pypsa2smspp
73
+ ```
74
+
75
+ 3. Install the required packages and pre-commit hooks:
76
+
77
+ ```bash
78
+ pip install -e .[dev]
79
+ pre-commit install
80
+ ```
81
+
82
+ Note that the `-e` command line option installs the package in editable mode, so that changes to the source code are immediately available in the environment being used. The `[dev]` option installs the packages required for development. The `pre-commit install` command installs the pre-commit hooks, which are used to check the code before committing to ensure code quality standards.
83
+
84
+ 4. Develop and test the code. For testing, please run:
85
+
86
+ ```bash
87
+ pytest
88
+ ```
@@ -0,0 +1,48 @@
1
+ # pypsa2smspp
2
+
3
+ [![Tests](https://github.com/SPSUnipi/pypsa2smspp/actions/workflows/test.yml/badge.svg)](https://github.com/SPSUnipi/pypsa2smspp/actions/workflows/test.yml)
4
+
5
+ This package aims at providing a python interface between [PyPSA](https://github.com/PyPSA/pypsa) and [SMS++](https://gitlab.com/smspp/smspp-project) models using a simple python interface.
6
+ The package aims to support:
7
+ - Convert a PyPSA model to SMS++
8
+ - Execute the optimization of the so-created SMS++ model
9
+ - Parse the solution from the SMS++ model to PyPSA
10
+
11
+
12
+ ## How to develop
13
+
14
+ 1. First, clone the repository using git:
15
+
16
+ ```bash
17
+ git clone https://github.com/SPSUnipi/pypsa2smspp
18
+ ```
19
+
20
+ 2. Create a virtual environment using venv or conda.
21
+ For exaample, using venv:
22
+
23
+ ```bash
24
+ python -m venv .venv
25
+ source .venv/bin/activate
26
+ ```
27
+
28
+ Alternatively, using conda:
29
+
30
+ ```bash
31
+ conda create -n pypsa2smspp python=3.10
32
+ conda activate pypsa2smspp
33
+ ```
34
+
35
+ 3. Install the required packages and pre-commit hooks:
36
+
37
+ ```bash
38
+ pip install -e .[dev]
39
+ pre-commit install
40
+ ```
41
+
42
+ Note that the `-e` command line option installs the package in editable mode, so that changes to the source code are immediately available in the environment being used. The `[dev]` option installs the packages required for development. The `pre-commit install` command installs the pre-commit hooks, which are used to check the code before committing to ensure code quality standards.
43
+
44
+ 4. Develop and test the code. For testing, please run:
45
+
46
+ ```bash
47
+ pytest
48
+ ```
@@ -0,0 +1,103 @@
1
+ [build-system]
2
+ requires = ["setuptools>=64"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pypsa2smspp"
7
+ version = "0.0.1"
8
+ authors = [
9
+ { name="Alessandro Pampado", email="alessandro.pampado@ing.unipi.it"},
10
+ { name="Davide Fioriti", email="davide.fioriti@unipi.it" },
11
+ { name="Unipi developers", email="davide.fioriti@unipi.it" },
12
+ ]
13
+ description = "Bi-directional interface between PyPSA and SMS++"
14
+ readme = "README.md"
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3.12",
17
+ "Programming Language :: Python :: 3.13",
18
+ "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
19
+ "Operating System :: OS Independent",
20
+ ]
21
+
22
+ requires-python = ">=3.12"
23
+
24
+ dependencies = [
25
+ "numpy",
26
+ "pandas>=0.24, <3",
27
+ "pypsa>=1",
28
+ "xarray",
29
+ "openpyxl",
30
+ "pysmspp>=0.0.2",
31
+ ]
32
+
33
+ [project.urls]
34
+ Homepage = "https://github.com/SPSUnipi/pypsa2smspp"
35
+ Issues = "https://github.com/SPSUnipi/pypsa2smspp/issues"
36
+
37
+ [project.optional-dependencies]
38
+ dev = [
39
+ "pytest",
40
+ "highspy",
41
+ "gurobipy",
42
+ "coverage",
43
+ "pre-commit",
44
+ "ncompare",
45
+ "mypy",
46
+ ]
47
+ docs=[
48
+ "numpydoc",
49
+ "sphinx",
50
+ "sphinx-book-theme",
51
+ "sphinx-rtd-theme",
52
+ "pydata-sphinx-theme",
53
+ "sphinx-reredirects",
54
+ "nbsphinx",
55
+ "nbsphinx-link",
56
+ "scikit-learn",
57
+ "ipython",
58
+ "ipykernel",
59
+ ]
60
+
61
+ [tool.setuptools]
62
+ packages = ["pypsa2smspp"]
63
+
64
+ [tool.setuptools_scm]
65
+ version_scheme = "no-guess-dev"
66
+
67
+ [tool.setuptools.package-data]
68
+ "pysmspp" = ["py.typed"]
69
+
70
+ # Pytest settings
71
+
72
+ [tool.pytest.ini_options]
73
+ filterwarnings = [
74
+ "error::DeprecationWarning", # Raise all DeprecationWarnings as errors
75
+ "error::FutureWarning", # Raise all FutureWarnings as errors
76
+ ]
77
+
78
+ # Coverage settings
79
+
80
+ [tool.coverage.run]
81
+ branch = true
82
+ source = ["pypsa2smspp"]
83
+ omit = ["test/*"]
84
+ [tool.coverage.report]
85
+ exclude_also = [
86
+ "if TYPE_CHECKING:",
87
+ ]
88
+
89
+ # Static type checker settings
90
+ [tool.mypy]
91
+ exclude = ['dev/*', 'examples/*', 'doc/*']
92
+ ignore_missing_imports = true
93
+ no_implicit_optional = true
94
+ warn_unused_ignores = true
95
+ show_error_code_links = true
96
+
97
+ [[tool.mypy.overrides]]
98
+ module = "pypsa2smspp.*"
99
+ disallow_untyped_defs = true
100
+ check_untyped_defs = true
101
+
102
+ # [tool.setuptools.dynamic]
103
+ # version = {attr = "pysmspp.__version__"}
@@ -0,0 +1,63 @@
1
+
2
+ """
3
+ pypsa2smspp package init
4
+
5
+ Exposes high-level transformation and network correction utilities.
6
+ """
7
+
8
+ import logging
9
+
10
+ logger = logging.getLogger("pypsa2smspp")
11
+ logger.setLevel(logging.INFO)
12
+
13
+ if not logger.handlers: # 👈 evita di aggiungere più handler
14
+ console_handler = logging.StreamHandler()
15
+ console_handler.setLevel(logging.DEBUG)
16
+
17
+ formatter = logging.Formatter(
18
+ '[%(asctime)s] %(levelname)s - %(name)s - %(message)s'
19
+ )
20
+ console_handler.setFormatter(formatter)
21
+
22
+ logger.addHandler(console_handler)
23
+
24
+ # opzionale ma consigliato in una libreria:
25
+ logger.propagate = False
26
+
27
+
28
+
29
+ # Transformation logic (PyPSA ↔ SMS++)
30
+ from pypsa2smspp.transformation import Transformation
31
+ from pypsa2smspp.transformation_config import TransformationConfig
32
+
33
+ # PyPSA network correction tools
34
+ from pypsa2smspp.network_correction import (
35
+ clean_marginal_cost,
36
+ clean_global_constraints,
37
+ clean_e_sum,
38
+ clean_efficiency_link,
39
+ clean_ciclicity_storage,
40
+ clean_marginal_cost_intermittent,
41
+ clean_storage_units,
42
+ clean_stores,
43
+ parse_txt_file,
44
+ clean_p_min_pu,
45
+ one_bus_network,
46
+ )
47
+
48
+ __all__ = [
49
+ "Transformation",
50
+ "TransformationConfig",
51
+ # network correction tools
52
+ "clean_marginal_cost",
53
+ "clean_global_constraints",
54
+ "clean_e_sum",
55
+ "clean_efficiency_link",
56
+ "clean_ciclicity_storage",
57
+ "clean_marginal_cost_intermittent",
58
+ "clean_storage_units",
59
+ "clean_stores",
60
+ "parse_txt_file",
61
+ "clean_p_min_pu",
62
+ "one_bus_network",
63
+ ]
@@ -0,0 +1,59 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Created on Thu Jun 26 15:50:01 2025
4
+
5
+ @author: aless
6
+ """
7
+
8
+ """
9
+ constants.py
10
+
11
+ This module contains structural constants used throughout the PyPSA2SMSpp
12
+ transformation process. These values define generic mappings and categories
13
+ used repeatedly in the Transformation class and related utilities.
14
+
15
+ They are not meant to be modified by the user and do not depend on any
16
+ specific instance or configuration of the network.
17
+ """
18
+
19
+ # Dictionary mapping internal shorthand dimensions to full SMS++ dimension names
20
+ conversion_dict = {
21
+ "T": "TimeHorizon",
22
+ "NU": "NumberUnits",
23
+ "NE": "NumberElectricalGenerators",
24
+ "N": "NumberNodes",
25
+ "L": "NumberLines",
26
+ "Li": "Links",
27
+ "NA": "NumberArcs",
28
+ "NR": "NumberReservoirs",
29
+ "NP": "TotalNumberPieces",
30
+ "Nass": "NumAssets",
31
+ "NB": "NumberBranches",
32
+ "NDL": "NumberDesignLines",
33
+ "NDLL": "NumberDesignLines_lines",
34
+ "NDLLi": "NumberDesignLines_links",
35
+ "1": 1
36
+ }
37
+
38
+ # Mapping from PyPSA component types to their nominal attribute
39
+ nominal_attrs = {
40
+ "Generator": "p_nom",
41
+ "Line": "s_nom",
42
+ "Transformer": "s_nom",
43
+ "Link": "p_nom",
44
+ "Store": "e_nom",
45
+ "StorageUnit": "p_nom",
46
+ }
47
+
48
+ # List of renewable carriers used to identify IntermittentUnitBlocks
49
+ renewable_carriers = [
50
+ "solar",
51
+ "solar-hsat",
52
+ "onwind",
53
+ "offwind-ac",
54
+ "offwind-dc",
55
+ "offwind-float",
56
+ "PV",
57
+ "wind",
58
+ "ror"
59
+ ]
@@ -0,0 +1,22 @@
1
+ transformation:
2
+ merge_links: true
3
+ expansion_ucblock: false # To include with investmentblock... Collect everything with mode
4
+ max_hours_stores: 1
5
+
6
+ run:
7
+ mode: investmentblock # ucblock | investmentblock
8
+
9
+ smspp:
10
+ ucblock:
11
+ output_prefix: uc
12
+ template: UCBlock/uc_solverconfig_grb
13
+ investmentblock:
14
+ output_prefix: inv
15
+ template: InvestmentBlock/BSPar.txt
16
+ inner_block_name: InvestmentBlock
17
+ log_executable_call: true
18
+
19
+ io:
20
+ workdir: output
21
+ name: test_case
22
+ overwrite: true
@@ -0,0 +1,143 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Created on Thu Jun 26 15:55:15 2025
4
+
5
+ @author: aless
6
+ """
7
+
8
+ """
9
+ inverse.py
10
+
11
+ This module handles the inverse transformation from an SMS++ solution
12
+ back into a PyPSA-compatible xarray.Dataset. It reconstructs time-series
13
+ and scalar variables from SMS unit blocks and prepares them for use with
14
+ assign_solution in PyPSA.
15
+ """
16
+
17
+ import numpy as np
18
+ import xarray as xr
19
+
20
+
21
+ def normalize_key(key: str) -> str:
22
+ """
23
+ Normalizes a key string by converting to lowercase and replacing spaces with underscores.
24
+ """
25
+ return key.lower().replace(" ", "_")
26
+
27
+
28
+ def component_definition(n, unit_block: dict) -> str:
29
+ """
30
+ Maps a unit block type to its corresponding PyPSA component name.
31
+ """
32
+ block = unit_block['block']
33
+ match block:
34
+ case "IntermittentUnitBlock":
35
+ return "Generator"
36
+ case "ThermalUnitBlock":
37
+ return "Generator"
38
+ case "HydroUnitBlock":
39
+ return "StorageUnit"
40
+ case "BatteryUnitBlock":
41
+ return "StorageUnit" if unit_block['name'] in n.storage_units.index else "Store"
42
+ case "DCNetworkBlock_lines":
43
+ return "Line"
44
+ case "DCNetworkBlock_links":
45
+ return "Link"
46
+ case "SlackUnitBlock":
47
+ return "Generator"
48
+ case _:
49
+ raise ValueError(f"Unknown unit block type: {block}")
50
+
51
+
52
+ def evaluate_function(func, normalized_keys, unit_block, df):
53
+ """
54
+ Evaluates a callable inverse function by extracting arguments from unit block or network.
55
+ """
56
+ param_names = func.__code__.co_varnames[:func.__code__.co_argcount]
57
+ args = []
58
+ for param in param_names:
59
+ param = normalize_key(param)
60
+ if param in normalized_keys:
61
+ args.append(unit_block[normalized_keys[param]])
62
+ else:
63
+ args.append(df.loc[unit_block['name']][param])
64
+ return func(*args)
65
+
66
+
67
+ def dataarray_components(n, value, component, unit_block, key):
68
+ """
69
+ Build xarray dims/coords compliant with PyPSA 1.0 assign_solution():
70
+ - static per-unit: dims -> ('name',)
71
+ - time series: dims -> ('snapshot','name')
72
+ """
73
+ # Unmask masked arrays; use NaN for masked scalars
74
+ if isinstance(value, np.ma.MaskedArray):
75
+ value = value.filled(np.nan)
76
+
77
+ value = np.asarray(value)
78
+ unit_name = unit_block["name"]
79
+
80
+ if value.ndim == 0:
81
+ # scalar -> one value for this unit: ('name',)
82
+ value = value.reshape(1)
83
+ dims = ["name"]
84
+ coords = {"name": [unit_name]}
85
+
86
+ elif value.ndim == 1:
87
+ if len(value) == len(n.snapshots):
88
+ # (T,) -> ('snapshot','name')
89
+ value = value[:, np.newaxis] # (T,1)
90
+ dims = ["snapshot", "name"]
91
+ coords = {"snapshot": n.snapshots, "name": [unit_name]}
92
+ else:
93
+ # vector but not time-based -> treat as per-unit static ('name',)
94
+ # (se è un vettore con più elementi non-temporali, somma o valida prima)
95
+ value = np.array([value.sum()]) if value.size > 1 else value.reshape(1)
96
+ dims = ["name"]
97
+ coords = {"name": [unit_name]}
98
+
99
+ elif value.ndim == 2:
100
+ T = len(n.snapshots)
101
+ if value.shape == (T, 1):
102
+ dims = ["snapshot", "name"]
103
+ coords = {"snapshot": n.snapshots, "name": [unit_name]}
104
+ elif value.shape == (1, T):
105
+ value = value.T
106
+ dims = ["snapshot", "name"]
107
+ coords = {"snapshot": n.snapshots, "name": [unit_name]}
108
+ else:
109
+ raise ValueError(f"Unsupported shape for variable {key}: {value.shape}")
110
+ else:
111
+ raise ValueError(f"Unsupported ndim for variable {key}: {value.ndim}")
112
+
113
+ var_name = f"{component}-{key}" # e.g., 'Generator-p', 'Store-e_nom', 'Link-p'
114
+ return value, dims, coords, var_name
115
+
116
+
117
+
118
+ def block_to_dataarrays(n, unit_name, unit_block, component, config) -> dict:
119
+ """
120
+ Transforms a unit block into a dictionary of DataArrays.
121
+ """
122
+ attr_name = f"{unit_block['block']}_inverse"
123
+ converted_dict = {}
124
+ normalized_keys = {normalize_key(k): k for k in unit_block.keys()}
125
+
126
+ if hasattr(config, attr_name):
127
+ unitblock_parameters = getattr(config, attr_name)
128
+ else:
129
+ print(f"Block {unit_block['block']} not yet implemented")
130
+ return {}
131
+
132
+ df = getattr(n, config.component_mapping[component])
133
+
134
+ for key, func in unitblock_parameters.items():
135
+ if callable(func):
136
+ value = evaluate_function(func, normalized_keys, unit_block, df)
137
+ if isinstance(value, np.ndarray) and value.ndim == 2 and all(dim > 1 for dim in value.shape):
138
+ value = value.sum(axis=0)
139
+ value, dims, coords, var_name = dataarray_components(n, value, component, unit_block, key)
140
+ converted_dict[var_name] = xr.DataArray(value, dims=dims, coords=coords, name=var_name)
141
+
142
+ return converted_dict
143
+