PyPSA to SMS++ conversion step by step¶
This notebook describes how to perform the conversion step by step: first convert the PyPSA model into a SMS++ object, then optimize it and finally read back a solved PyPSA network with the results from SMS++.
In particular:
- Create a PyPSA network
- Convert the PyPSA network to a SMS++ object and explore the SMS++ object
- Optimize the SMS++ object
- Read back the results into a PyPSA network
1. Creation of the PyPSA model (from UCBlock)¶
We first create a simple PyPSA model.
import pypsa
import pandas as pd
import pypsa2smspp
The following code creates the desired pypsa model.¶
n = pypsa.Network()
n.set_snapshots(pd.date_range("2024-01-01T00:00", "2024-01-01T23:00", freq="h"))
# Add carriers
n.add("Carrier", "AC")
# Add buses
n_buses = 2
for b in range(n_buses):
n.add("Bus", f"bus{b}", carrier="AC")
# Add lines in a radial topology using bidirectional links
n_lines = n_buses - 1
for l in range(n_lines):
n.add(
"Link",
f"line{l}",
bus0=f"bus{l}",
bus1=f"bus{l+1}",
length=1,
capital_cost=1000,
p_min_pu=-1,
p_nom_extendable=True,
)
# Add a load to each bus
n_loads = n_buses
for l in range(n_loads):
n.add("Load", f"load{l}", bus=f"bus{l}", p_set=pd.Series(100, index=n.snapshots))
# Add a generator to the first bus
n.add(
"Generator",
"gen0",
bus="bus0",
p_nom_extendable=True,
capital_cost=1000,
marginal_cost=1,
)
n_clean = n.copy()
n.optimize(solver_name="highs")
/tmp/ipykernel_1900/3144390136.py:1: FutureWarning: The default value of `include_objective_constant` will change from True to False in version 2.0. Set `include_objective_constant` explicitly to suppress this warning. Using False improves LP numerical conditioning by not including the objective constant as a variable. n.optimize(solver_name="highs")
INFO:linopy.model: Solve problem using Highs solver
INFO:linopy.io: Writing time: 0.04s
INFO:linopy.constants: Optimization successful: Status: ok Termination condition: optimal Solution: 50 primals, 146 duals Objective: 3.05e+05 Solver model: available Solver message: Optimal
INFO:pypsa.optimization.optimize:The shadow-prices of the constraints Generator-ext-p-lower, Generator-ext-p-upper, Link-ext-p-lower, Link-ext-p-upper were not assigned to the network.
Running HiGHS 1.11.0 (git hash: 364c83a): Copyright (c) 2025 HiGHS under MIT licence terms LP linopy-problem-ant48rj_ has 146 rows; 50 cols; 242 nonzeros Coefficient ranges: Matrix [1e+00, 1e+00] Cost [1e+00, 1e+03] Bound [0e+00, 0e+00] RHS [1e+02, 1e+02] Presolving model 72 rows, 2 cols, 72 nonzeros 0s 0 rows, 0 cols, 0 nonzeros 0s Presolve : Reductions: rows 0(-146); columns 0(-50); elements 0(-242) - Reduced to empty Solving the original LP from the solution after postsolve Model name : linopy-problem-ant48rj_ Model status : Optimal Objective value : 3.0480000000e+05 P-D objective error : 0.0000000000e+00 HiGHS run time : 0.00
('ok', 'optimal')
2. Create a SMS++ object from the PyPSA model¶
The following code creates the SMS++ object from the PyPSA model, as a pysmspp.SMSNetwork object, and explores it.
If the user wants to use only this object or edit it before the processing, they can do so at this point. Be aware that editing the SMS++ object may lead to inconsistencies with the original PyPSA model and hence may cause issues when reading back the results into a PyPSA network.
tran = pypsa2smspp.Transformation()
verbose = True
sms_network = tran.create_model(n_clean, verbose=verbose)
[START] consistency_check [ OK ] consistency_check (0.000s) [START] direct [ OK ] direct (0.115s) [START] prepare_tssb_interface [ OK ] prepare_tssb_interface (0.000s) [START] convert_to_blocks [ OK ] convert_to_blocks (0.096s)
Showcase the pysmspp object that is created by the conversion process.
sms_network
SMSNetwork Object Block object Attributes (1): SMS++_file_type Dimensions (0): None Variables (0): None Blocks (1): Block_0
and its inner block structure using the print_tree function from pysmspp.
sms_network.print_tree(show_all=True)
SMSNetwork
Attributes (1): SMS++_file_type=1
└── Block_0 [UCBlock]
Dimensions (6): TimeHorizon=24, NumberUnits=1, NumberElectricalGenerators=1, NumberNodes=2, NumberLines=1, NumberNetworks=1
Variables (11): ActivePowerDemand, NodeName, LineName, GeneratorNode, StartLine, ... (11 total)
Attributes (1): id=0
├── UnitBlock_0 [IntermittentUnitBlock]
│ Variables (8): MaxPower, MinPower, ActivePowerCost, MinGeneration, MaxGeneration, ... (8 total)
│ Attributes (1): name=gen0
└── NetworkBlock_0 [DesignNetworkBlock]
Dimensions (2): NumberDesignLines=1, NumberSubNetwork=24
Variables (4): InvestmentCost, MinCapacityDesign, MaxCapacityDesign, DesignLines
3. Execute the optimization on the transformation object¶
tran.optimize(verbose=verbose)
[START] optimize Executing command: ucblock_solver temp_test_case.nc -S uc_solverconfig.txt -c /home/docs/checkouts/readthedocs.org/user_builds/pypsa2smspp/conda/latest/lib/python3.13/site-packages/pysmspp/data/configs/UCBlock/ -p /home/docs/checkouts/readthedocs.org/user_builds/pypsa2smspp/checkouts/latest/docs/examples/output/ -O /home/docs/checkouts/readthedocs.org/user_builds/pypsa2smspp/checkouts/latest/docs/examples/output/solution_test_case.nc Using a default Block configuration Running HiGHS 1.14.0 (git hash: n/a): Copyright (c) 2026 under MIT licence terms Solver: HiGHSMILPSolver LP has 240 rows; 122 cols; 432 nonzeros Coefficient ranges: Matrix [1e+00, 1e+00] Cost [1e+00, 1e+03] Bound [1e+07, 1e+09] RHS [1e+02, 1e+02] WARNING: Problem has some excessively large column bounds WARNING: Consider scaling the bounds by 1e-3, or setting the user_bound_scale option to -10 Presolving model 0 rows, 0 cols, 0 nonzeros 0s 0 rows, 0 cols, 0 nonzeros 0s Presolve reductions: rows 0(-240); columns 0(-122); nonzeros 0(-432) - Reduced to empty Performed postsolve Solving the original LP from the solution after postsolve Model status : Optimal Objective value : 3.0480000000e+05 P-D objective error : 0.0000000000e+00 HiGHS run time : 0.00 Elapsed time: 9.86211000e-04 s Status = 10 (Success) Upper bound = 3.04800000e+05 Lower bound = 3.04800000e+05 ----- IntermittentUnitBlock 0 ----- Function value = 2.04800000e+05 Capacity = 2.00000000e+11 Active power = [ 2.00000000e+02 2.00000000e+02 2.00000000e+02 2.00000000e+02 2.00000000e+02 2.00000000e+02 2.00000000e+02 2.00000000e+02 2.00000000e+02 2.00000000e+02 2.00000000e+02 2.00000000e+02 2.00000000e+02 2.00000000e+02 2.00000000e+02 2.00000000e+02 2.00000000e+02 2.00000000e+02 2.00000000e+02 2.00000000e+02 2.00000000e+02 2.00000000e+02 2.00000000e+02 2.00000000e+02 ] ----- DesignNetworkBlock 0 ----- Function value = 1.00000000e+05 Design variables = [ line 0 : 100 ] --- DCNetworkBlock 0 --- Power flow = [ 1.00000000e+02 ] Auxiliary variable = [ 1.00000000e+02 ] --- DCNetworkBlock 1 --- Power flow = [ 1.00000000e+02 ] Auxiliary variable = [ 1.00000000e+02 ] --- DCNetworkBlock 2 --- Power flow = [ 1.00000000e+02 ] Auxiliary variable = [ 1.00000000e+02 ] --- DCNetworkBlock 3 --- Power flow = [ 1.00000000e+02 ] Auxiliary variable = [ 1.00000000e+02 ] --- DCNetworkBlock 4 --- Power flow = [ 1.00000000e+02 ] Auxiliary variable = [ 1.00000000e+02 ] --- DCNetworkBlock 5 --- Power flow = [ 1.00000000e+02 ] Auxiliary variable = [ 1.00000000e+02 ] --- DCNetworkBlock 6 --- Power flow = [ 1.00000000e+02 ] Auxiliary variable = [ 1.00000000e+02 ] --- DCNetworkBlock 7 --- Power flow = [ 1.00000000e+02 ] Auxiliary variable = [ 1.00000000e+02 ] --- DCNetworkBlock 8 --- Power flow = [ 1.00000000e+02 ] Auxiliary variable = [ 1.00000000e+02 ] --- DCNetworkBlock 9 --- Power flow = [ 1.00000000e+02 ] Auxiliary variable = [ 1.00000000e+02 ] --- DCNetworkBlock 10 --- Power flow = [ 1.00000000e+02 ] Auxiliary variable = [ 1.00000000e+02 ] --- DCNetworkBlock 11 --- Power flow = [ 1.00000000e+02 ] Auxiliary variable = [ 1.00000000e+02 ] --- DCNetworkBlock 12 --- Power flow = [ 1.00000000e+02 ] Auxiliary variable = [ 1.00000000e+02 ] --- DCNetworkBlock 13 --- Power flow = [ 1.00000000e+02 ] Auxiliary variable = [ 1.00000000e+02 ] --- DCNetworkBlock 14 --- Power flow = [ 1.00000000e+02 ] Auxiliary variable = [ 1.00000000e+02 ] --- DCNetworkBlock 15 --- Power flow = [ 1.00000000e+02 ] Auxiliary variable = [ 1.00000000e+02 ] --- DCNetworkBlock 16 --- Power flow = [ 1.00000000e+02 ] Auxiliary variable = [ 1.00000000e+02 ] --- DCNetworkBlock 17 --- Power flow = [ 1.00000000e+02 ] Auxiliary variable = [ 1.00000000e+02 ] --- DCNetworkBlock 18 --- Power flow = [ 1.00000000e+02 ] Auxiliary variable = [ 1.00000000e+02 ] --- DCNetworkBlock 19 --- Power flow = [ 1.00000000e+02 ] Auxiliary variable = [ 1.00000000e+02 ] --- DCNetworkBlock 20 --- Power flow = [ 1.00000000e+02 ] Auxiliary variable = [ 1.00000000e+02 ] --- DCNetworkBlock 21 --- Power flow = [ 1.00000000e+02 ] Auxiliary variable = [ 1.00000000e+02 ] --- DCNetworkBlock 22 --- Power flow = [ 1.00000000e+02 ] Auxiliary variable = [ 1.00000000e+02 ] --- DCNetworkBlock 23 --- Power flow = [ 1.00000000e+02 ] Auxiliary variable = [ 1.00000000e+02 ]
Peak CPU Usage: 0.00 % Peak Memory Usage: 0.72 MB Total Time: 0.20 seconds [ OK ] optimize (0.226s)
UCBlockSolver solver_name=ucblock_solver status=10 (Success) configfile=/home/docs/checkouts/readthedocs.org/user_builds/pypsa2smspp/conda/latest/lib/python3.13/site-packages/pysmspp/data/configs/UCBlock/uc_solverconfig.txt fp_network=/home/docs/checkouts/readthedocs.org/user_builds/pypsa2smspp/checkouts/latest/docs/examples/output/temp_test_case.nc fp_solution=/home/docs/checkouts/readthedocs.org/user_builds/pypsa2smspp/checkouts/latest/docs/examples/output/solution_test_case.nc
4. Perform the conversion back to a PyPSA network and explore the results¶
n_smspp = tran.retrieve_solution(n_clean, verbose=verbose)
n_smspp
[START] parse_solution_to_unitblocks [split] No merged DCNetworkBlock entries found; nothing to do. [ OK ] parse_solution_to_unitblocks (0.000s) [START] inverse_transformation [ OK ] inverse_transformation (0.017s)
PyPSA Network 'Unnamed Network' ------------------------------- Components: - Bus: 2 - Carrier: 1 - Generator: 1 - Link: 1 - Load: 2 Snapshots: 24
5. Check the results¶
Retrieve PyPSA statistics
n_stat = n.statistics.energy_balance(comps=["Generator"]).droplevel([0, 2])
n_stat
carrier - 4800.0 dtype: float64
Retrieve SMS++ statistics
n_parsed_stat = n_smspp.statistics.energy_balance(comps=["Generator"]).droplevel([0, 2])
n_parsed_stat
carrier - 4800.0 dtype: float64
Calculate difference between the two
error_stat = n_stat - n_parsed_stat
merged_stat = pd.concat([n_stat, n_parsed_stat, error_stat], axis=1)
merged_stat.columns = ["pypsa", "smspp", "difference"]
merged_stat
| pypsa | smspp | difference | |
|---|---|---|---|
| carrier | |||
| - | 4800.0 | 4800.0 | 0.0 |