Tutorial 1 using Unit Commitment Example¶
This tutorial shows the different commands for using GemsPy python package for modeling and simulating an Unit Commitment problem.
We use in this tutorial a library of models called unit_commitment_tutorial_library, a pedagogical library inspired by the antares-legacy-models library. The yml file used in this tutorial is located at:
Installation of GemsPy¶
# Install required libraries
%pip install gemspy matplotlib --quiet --upgrade
Note: you may need to restart the kernel to use updated packages.
# Check the GemsPy version
%pip freeze | grep gemspy
gemspy==0.1.0
Note: you may need to restart the kernel to use updated packages.
Create a simple system composed of an area with load¶
Step 1: Configure an Area¶
In this step, we instantiate two models from the unit_commitment_tutorial_library:
- The
areamodel: represents an energy balance node. It holdsspillageandunsupplied_energyvariables, whose costs enter the objective function. - The
loadmodel: represents a consumption connected to a balance port, with time- and scenario-dependent load data.
Several types of components (production, load, storage, links...) can be connected to an area. We'll create an area with spillage cost and unsupplied energy cost parameters.
from gems.study.parsing import ComponentSchema, ComponentParameterSchema, SystemSchema
from gems.model.parsing import parse_yaml_library
from gems.model.resolve_library import resolve_library
from pathlib import Path
# Set tutorial directory structure
study_name = "Tutorial_Unit_Commitment_with_GemsPy"
base_path = Path.cwd()
library_folder = base_path / study_name
library_file = next(library_folder.glob("*.yml")).name
series_folder = base_path / study_name
# Read library file containing the models
print("LIBRARY READING")
print("\tLibrary path:", library_file)
with open(Path(library_folder, library_file)) as lib_file:
input_libraries = [parse_yaml_library(lib_file)]
library_name = input_libraries[0].id
print("\tLibrary loaded:", library_name)
result_lib = resolve_library(input_libraries)
print("COMPONENTS DEFINITION")
components = []
# Add an area component with spillage and ENS costs
components.append(
ComponentSchema(
id="my_area",
model=f"{library_name}.area",
parameters=[
ComponentParameterSchema(
id="spillage_cost",
time_dependent=False,
scenario_dependent=False,
value=1000),
ComponentParameterSchema(
id="ens_cost",
time_dependent=False,
scenario_dependent=False,
value=10000),
],
)
)
# Add a load component with load data from a data series
components.append(
ComponentSchema(
id="load",
model=f"{library_name}.load",
parameters=[
ComponentParameterSchema(
id="load",
time_dependent=True,
scenario_dependent=True,
value="load"),
],
)
)
print("\tComponents defined:\n\t", [comp.id for comp in components])
print("INPUT SYSTEM CREATION")
input_system = SystemSchema(
id = "my_system",
model_libraries=library_name,
components=components,
)
print("\tInput system created")
# Uncomment the following line to print the input system
# print("\tThe following input system has been created :")
# print(input_system)
LIBRARY READING Library path: tuto_antares_legacy_models.yml Library loaded: antares_legacy_models COMPONENTS DEFINITION Components defined: ['my_area', 'load'] INPUT SYSTEM CREATION Input system created
/home/docs/checkouts/readthedocs.org/user_builds/gems-energy/envs/latest/lib/python3.10/site-packages/gems/model/resolve_library.py:139: UserWarning: Objective contribution 'objective' has a scenario dimension but no explicit expec() operator. Expectation semantics (average over scenarios) are applied automatically. Add expec() explicitly to suppress this warning. _resolve_model(m, current_lib.port_types, current_lib.id)
Step 2: Configure the Generator (1 unit of 100 MW)¶
We now add a thermal model to the area. This model represents a thermal cluster with unit commitment variables (nb_units_on, nb_starting, nb_stopping) and enforces minimum up/down duration constraints.
The generator in this step has:
- 1 unit with a maximum capacity of 100 MW
- Generation cost of 50 €/MWh
print("NEW COMPONENT DEFINITION")
# Add a thermal generator component with technical and economic parameters
components.append(
ComponentSchema(
id="thermal_gen",
model=f"{library_name}.thermal",
parameters=[
ComponentParameterSchema(id="p_min_unit", time_dependent=False, scenario_dependent=False, value=0),
ComponentParameterSchema(id="p_max_unit", time_dependent=False, scenario_dependent=False, value=100),
ComponentParameterSchema(id="p_max_cluster", time_dependent=False, scenario_dependent=False, value=100),
ComponentParameterSchema(id="generation_cost", time_dependent=False, scenario_dependent=False, value=50),
ComponentParameterSchema(id="startup_cost", time_dependent=False, scenario_dependent=False, value=0),
ComponentParameterSchema(id="fixed_cost", time_dependent=False, scenario_dependent=False, value=0),
ComponentParameterSchema(id="d_min_up", time_dependent=False, scenario_dependent=False, value=1),
ComponentParameterSchema(id="d_min_down", time_dependent=False, scenario_dependent=False, value=1),
ComponentParameterSchema(id="unit_count", time_dependent=False, scenario_dependent=False, value=1),
ComponentParameterSchema(id="spinning", time_dependent=False, scenario_dependent=False, value=0),
ComponentParameterSchema(id="minimum_generation_modulation", time_dependent=False, scenario_dependent=False, value=0),
],
)
)
print("\tComponents defined:\n\t", [comp.id for comp in components])
print("INPUT SYSTEM CREATION")
input_system = SystemSchema(
id = "my_system",
model_libraries=library_name,
components=components,
)
print("\tInput system created")
# Uncomment the following lines to print the input system
# print("\tThe following input system has been created :")
# print(input_system)
NEW COMPONENT DEFINITION Components defined: ['my_area', 'load', 'thermal_gen'] INPUT SYSTEM CREATION Input system created
Step 3 : Configure the port connection¶
Then, we interconnect the two components by configuring the ports connection
from gems.study.parsing import PortConnectionsSchema
print("CONNECTIONS DEFINITION")
# Initialize connections list
connections = []
# Add connections between the area and the thermal generator
connections.append(
PortConnectionsSchema(
component1="my_area",
port1="balance_port",
component2="thermal_gen",
port2="balance_port",
)
)
# Add connection between the area and the load
connections.append(
PortConnectionsSchema(
component1="my_area",
port1="balance_port",
component2="load",
port2="balance_port",
)
)
print("\tConnections defined:")
for conn in connections:
print(f"\t\t{conn.component1}:{conn.port1} <-> {conn.component2}:{conn.port2}")
input_system = SystemSchema(
components=components,
connections=connections,
)
# Uncomment the following lines to print the input system
# print("\tThe following input system has been created :")
# print(input_system)
CONNECTIONS DEFINITION Connections defined: my_area:balance_port <-> thermal_gen:balance_port my_area:balance_port <-> load:balance_port
Step 4 : Run the study and Graph results¶
from gems.study.study import Study
from gems.session.session import SimulationSession
from gems.optim_config.parsing import OptimConfig, TimeScopeConfig, ScenarioScopeConfig
from gems.study.resolve_components import build_data_base, resolve_system
print("OPTIMIZATION PROBLEM BUILDING")
TimeSpan = 7 * 24 # one week with hourly time steps
system = resolve_system(input_system, resolve_library(input_libraries))
database = build_data_base(input_system, Path(series_folder))
study = Study(system=system, database=database)
optim_config = OptimConfig(
time_scope=TimeScopeConfig(first_time_step=0, last_time_step=TimeSpan - 1),
scenario_scope=ScenarioScopeConfig(nb_scenarios=1),
)
print("\t✓ Optimization problem built successfully")
print("SOLVING OPTIMIZATION PROBLEM")
try:
results = SimulationSession(study=study, optim_config=optim_config).run()
print("\t✓ Optimization problem solved and results extracted successfully")
except Exception as e:
print(f"Error solving optimization problem: {e}")
raise e
objective_value = results.data.loc[results.data["output"] == "objective-value", "value"].iloc[0]
print(f"\t✓ Objective value: {objective_value}")
OPTIMIZATION PROBLEM BUILDING ✓ Optimization problem built successfully SOLVING OPTIMIZATION PROBLEM
/home/docs/checkouts/readthedocs.org/user_builds/gems-energy/envs/latest/lib/python3.10/site-packages/gems/model/resolve_library.py:139: UserWarning: Objective contribution 'objective' has a scenario dimension but no explicit expec() operator. Expectation semantics (average over scenarios) are applied automatically. Add expec() explicitly to suppress this warning. _resolve_model(m, current_lib.port_types, current_lib.id)
Running HiGHS 1.14.0 (git hash: 7df0786): Copyright (c) 2026 under MIT licence terms
ERROR: getOptionIndex: Option "solver_logs" is unknown
LP has 1512 rows; 1176 cols; 4032 nonzeros
Coefficient ranges:
Matrix [1e+00, 1e+02]
Cost [5e+01, 1e+04]
Bound [1e+00, 1e+02]
RHS [1e+00, 1e+02]
Presolving model
1176 rows, 1008 cols, 3360 nonzeros 0s
790 rows, 933 cols, 2059 nonzeros 0s
Dependent equations search running on 168 equations with time limit of 1000.00s
Dependent equations search removed 0 rows and 0 nonzeros in 0.00s (limit = 1000.00s)
628 rows, 628 cols, 1592 nonzeros 0s
Presolve reductions: rows 628(-884); columns 628(-548); nonzeros 1592(-2440)
Solving the presolved LP
Using dual simplex solver
Iteration Objective Infeasibilities num(sum)
0 0.0000000000e+00 Ph1: 0(0) 0.0s
278 2.9520000000e+06 Pr: 0(0) 0.0s
Performed postsolve
Solving the original LP from the solution after postsolve
Model status : Optimal
Simplex iterations: 278
Objective value : 2.9520000000e+06
P-D objective error : 0.0000000000e+00
HiGHS run time : 0.01
✓ Optimization problem solved and results extracted successfully ✓ Objective value: 2952000.0
/home/docs/checkouts/readthedocs.org/user_builds/gems-energy/envs/latest/lib/python3.10/site-packages/linopy/common.py:492: UserWarning: Coordinates across variables not equal. Perform outer join. warn( /home/docs/checkouts/readthedocs.org/user_builds/gems-energy/envs/latest/lib/python3.10/site-packages/linopy/common.py:492: UserWarning: Coordinates across variables not equal. Perform outer join. warn(
import matplotlib.pyplot as plt
import io, base64
from IPython.display import display
thermal_gen_values = results.component("thermal_gen").output("generation").value(scenario_index=0)
unsupply_values = results.component("my_area").output("unsupplied_energy").value(scenario_index=0)
spilled_values = results.component("my_area").output("spillage").value(scenario_index=0)
plt.plot(thermal_gen_values, linewidth=2, color='blue', label='Thermal Generation')
plt.plot(unsupply_values, linewidth=2, color='red', label='Unsupplied Energy')
plt.plot(spilled_values, linewidth=2, color='green', label='Spilled Energy')
plt.xlabel('Time (hours)')
plt.ylabel('Power (MW)')
plt.title('Thermal Generation, Unsupplied Energy, and Spilled Energy Over One Week')
plt.legend(loc='lower right')
plt.grid(True, alpha=0.3)
plt.tight_layout()
buf = io.BytesIO()
plt.savefig(buf, format='png', bbox_inches='tight')
buf.seek(0)
display(
{'image/png': base64.b64encode(buf.read()).decode()},
raw=True,
metadata={'image/png': {'alt': 'Line chart showing thermal generation, unsupplied energy, and spilled energy over one week (168 hours). Thermal generation is capped at 100 MW and unsupplied energy appears when load exceeds this threshold.'}}
)
plt.close()
/home/docs/checkouts/readthedocs.org/user_builds/gems-energy/envs/latest/lib/python3.10/site-packages/gems/simulation/simulation_table.py:71: FutureWarning: Downcasting object dtype arrays on .fillna, .ffill, .bfill is deprecated and will change in a future version. Call result.infer_objects(copy=False) instead. To opt-in to the future behavior, set `pd.set_option('future.no_silent_downcasting', True)`
filtered[col_time] = filtered[col_time].fillna(0)
/home/docs/checkouts/readthedocs.org/user_builds/gems-energy/envs/latest/lib/python3.10/site-packages/gems/simulation/simulation_table.py:72: FutureWarning: Downcasting object dtype arrays on .fillna, .ffill, .bfill is deprecated and will change in a future version. Call result.infer_objects(copy=False) instead. To opt-in to the future behavior, set `pd.set_option('future.no_silent_downcasting', True)`
filtered[col_scenario] = filtered[col_scenario].fillna(0)
/home/docs/checkouts/readthedocs.org/user_builds/gems-energy/envs/latest/lib/python3.10/site-packages/gems/simulation/simulation_table.py:71: FutureWarning: Downcasting object dtype arrays on .fillna, .ffill, .bfill is deprecated and will change in a future version. Call result.infer_objects(copy=False) instead. To opt-in to the future behavior, set `pd.set_option('future.no_silent_downcasting', True)`
filtered[col_time] = filtered[col_time].fillna(0)
/home/docs/checkouts/readthedocs.org/user_builds/gems-energy/envs/latest/lib/python3.10/site-packages/gems/simulation/simulation_table.py:72: FutureWarning: Downcasting object dtype arrays on .fillna, .ffill, .bfill is deprecated and will change in a future version. Call result.infer_objects(copy=False) instead. To opt-in to the future behavior, set `pd.set_option('future.no_silent_downcasting', True)`
filtered[col_scenario] = filtered[col_scenario].fillna(0)
/home/docs/checkouts/readthedocs.org/user_builds/gems-energy/envs/latest/lib/python3.10/site-packages/gems/simulation/simulation_table.py:71: FutureWarning: Downcasting object dtype arrays on .fillna, .ffill, .bfill is deprecated and will change in a future version. Call result.infer_objects(copy=False) instead. To opt-in to the future behavior, set `pd.set_option('future.no_silent_downcasting', True)`
filtered[col_time] = filtered[col_time].fillna(0)
/home/docs/checkouts/readthedocs.org/user_builds/gems-energy/envs/latest/lib/python3.10/site-packages/gems/simulation/simulation_table.py:72: FutureWarning: Downcasting object dtype arrays on .fillna, .ffill, .bfill is deprecated and will change in a future version. Call result.infer_objects(copy=False) instead. To opt-in to the future behavior, set `pd.set_option('future.no_silent_downcasting', True)`
filtered[col_scenario] = filtered[col_scenario].fillna(0)
In these results, it's easy to see that the area has unsupplied energy while the load is above the 100 MW threshold of thermal power generation.
In the next section, a wind farm and a solar farm are interconnected to the area to supply enough energy.
Add Variable Renewable Energies in the system¶
Step 1 : Creation of components¶
We add two components using the renewable model. This model represents a variable renewable source whose generation is provided as a time- and scenario-dependent series, scaled by a nominal capacity.
In this section, the wind_farm and solar_farm components are added to our system:
- The wind_farm has a variable power generation between 0 and 60 MW
- The solar_farm has a variable power generation between 0 and 40 MW
print("NEW COMPONENTS DEFINITION")
# Add wind farm component with wind generation data from a data series
components.append(
ComponentSchema(
id="wind_farm",
model=f"{library_name}.renewable",
parameters=[
ComponentParameterSchema(id="nominal_capacity", time_dependent=False, scenario_dependent=False, value=60),
ComponentParameterSchema(id="unit_count", time_dependent=False, scenario_dependent=False, value=1),
ComponentParameterSchema(id="generation", time_dependent=True, scenario_dependent=True, value="wind"),
],
)
)
# Add connection between the area and the wind farm
connections.append(
PortConnectionsSchema(
component1="my_area",
port1="balance_port",
component2="wind_farm",
port2="balance_port",
)
)
# Add solar farm component with solar generation data from a data series
components.append(
ComponentSchema(
id="solar_farm",
model=f"{library_name}.renewable",
parameters=[
ComponentParameterSchema(id="nominal_capacity", time_dependent=False, scenario_dependent=False, value=40),
ComponentParameterSchema(id="unit_count", time_dependent=False, scenario_dependent=False, value=1),
ComponentParameterSchema(id="generation", time_dependent=True, scenario_dependent=True, value="solar"),
],
)
)
print("\tComponents defined:\n\t", [comp.id for comp in components])
print("CONNECTIONS DEFINITION")
# Add connection between the area and the solar farm
connections.append(
PortConnectionsSchema(
component1="my_area",
port1="balance_port",
component2="solar_farm",
port2="balance_port",
)
)
print("\tConnections defined:")
for conn in connections:
print(f"\t\t{conn.component1}:{conn.port1} <-> {conn.component2}:{conn.port2}")
input_system = SystemSchema(
id = "my_system",
components=components,
connections=connections,
)
# Uncomment the following lines to print the input system
# print("\tThe following input system has been created :")
# print(input_system)
NEW COMPONENTS DEFINITION Components defined: ['my_area', 'load', 'thermal_gen', 'wind_farm', 'solar_farm'] CONNECTIONS DEFINITION Connections defined: my_area:balance_port <-> thermal_gen:balance_port my_area:balance_port <-> load:balance_port my_area:balance_port <-> wind_farm:balance_port my_area:balance_port <-> solar_farm:balance_port
Step 2 : Results visualization¶
print("OPTIMIZATION PROBLEM BUILDING")
system = resolve_system(input_system, resolve_library(input_libraries))
database = build_data_base(input_system, Path(series_folder))
study = Study(system=system, database=database)
optim_config = OptimConfig(
time_scope=TimeScopeConfig(first_time_step=0, last_time_step=TimeSpan - 1),
scenario_scope=ScenarioScopeConfig(nb_scenarios=1),
)
print("\t✓ Optimization problem built successfully")
print("SOLVING OPTIMIZATION PROBLEM")
try:
results = SimulationSession(study=study, optim_config=optim_config).run()
print("\t✓ Optimization problem solved and results extracted successfully")
except Exception as e:
print(f"Error solving optimization problem: {e}")
raise e
objective_value = results.data.loc[results.data["output"] == "objective-value", "value"].iloc[0]
print(f"\t✓ Objective value: {objective_value}")
OPTIMIZATION PROBLEM BUILDING ✓ Optimization problem built successfully SOLVING OPTIMIZATION PROBLEM
/home/docs/checkouts/readthedocs.org/user_builds/gems-energy/envs/latest/lib/python3.10/site-packages/gems/model/resolve_library.py:139: UserWarning: Objective contribution 'objective' has a scenario dimension but no explicit expec() operator. Expectation semantics (average over scenarios) are applied automatically. Add expec() explicitly to suppress this warning. _resolve_model(m, current_lib.port_types, current_lib.id)
Running HiGHS 1.14.0 (git hash: 7df0786): Copyright (c) 2026 under MIT licence terms
ERROR: getOptionIndex: Option "solver_logs" is unknown
LP has 1512 rows; 1176 cols; 4032 nonzeros
Coefficient ranges:
Matrix [1e+00, 1e+02]
Cost [5e+01, 1e+04]
Bound [1e+00, 1e+02]
RHS [1e+00, 1e+02]
Presolving model
1176 rows, 1008 cols, 3360 nonzeros 0s
840 rows, 1008 cols, 2184 nonzeros 0s
Dependent equations search running on 168 equations with time limit of 1000.00s
Dependent equations search removed 0 rows and 0 nonzeros in 0.00s (limit = 1000.00s)
669 rows, 669 cols, 1674 nonzeros 0s
Presolve reductions: rows 669(-843); columns 669(-507); nonzeros 1674(-2358)
Solving the presolved LP
Using dual simplex solver
Iteration Objective Infeasibilities num(sum)
0 0.0000000000e+00 Ph1: 0(0) 0.0s
344 4.7740000000e+05 Pr: 0(0) 0.0s
Performed postsolve
Solving the original LP from the solution after postsolve
Model status : Optimal
Simplex iterations: 344
Objective value : 4.7740000000e+05
P-D objective error : 0.0000000000e+00
HiGHS run time : 0.01
✓ Optimization problem solved and results extracted successfully ✓ Objective value: 477400.0
/home/docs/checkouts/readthedocs.org/user_builds/gems-energy/envs/latest/lib/python3.10/site-packages/linopy/common.py:492: UserWarning: Coordinates across variables not equal. Perform outer join. warn( /home/docs/checkouts/readthedocs.org/user_builds/gems-energy/envs/latest/lib/python3.10/site-packages/linopy/common.py:492: UserWarning: Coordinates across variables not equal. Perform outer join. warn(
import matplotlib.pyplot as plt
import io, base64
from IPython.display import display
thermal_gen_values = results.component("thermal_gen").output("generation").value(scenario_index=0)
unsupply_values = results.component("my_area").output("unsupplied_energy").value(scenario_index=0)
spilled_values = results.component("my_area").output("spillage").value(scenario_index=0)
plt.plot(thermal_gen_values, linewidth=2, color='blue', label='Thermal Generation')
plt.plot(unsupply_values, linewidth=2, color='red', label='Unsupplied Energy')
plt.plot(spilled_values, linewidth=2, color='green', label='Spilled Energy')
plt.xlabel('Time (hours)')
plt.ylabel('Power (MW)')
plt.title('Thermal Generation, Unsupplied Energy, and Spilled Energy Over One Week')
plt.legend(loc='lower right')
plt.grid(True, alpha=0.3)
plt.tight_layout()
buf = io.BytesIO()
plt.savefig(buf, format='png', bbox_inches='tight')
buf.seek(0)
display(
{'image/png': base64.b64encode(buf.read()).decode()},
raw=True,
metadata={'image/png': {'alt': 'Line chart showing thermal generation, unsupplied energy, and spilled energy over one week after adding wind and solar farms. Unsupplied energy drops to zero thanks to renewable generation covering the load.'}}
)
plt.close()
/home/docs/checkouts/readthedocs.org/user_builds/gems-energy/envs/latest/lib/python3.10/site-packages/gems/simulation/simulation_table.py:71: FutureWarning: Downcasting object dtype arrays on .fillna, .ffill, .bfill is deprecated and will change in a future version. Call result.infer_objects(copy=False) instead. To opt-in to the future behavior, set `pd.set_option('future.no_silent_downcasting', True)`
filtered[col_time] = filtered[col_time].fillna(0)
/home/docs/checkouts/readthedocs.org/user_builds/gems-energy/envs/latest/lib/python3.10/site-packages/gems/simulation/simulation_table.py:72: FutureWarning: Downcasting object dtype arrays on .fillna, .ffill, .bfill is deprecated and will change in a future version. Call result.infer_objects(copy=False) instead. To opt-in to the future behavior, set `pd.set_option('future.no_silent_downcasting', True)`
filtered[col_scenario] = filtered[col_scenario].fillna(0)
/home/docs/checkouts/readthedocs.org/user_builds/gems-energy/envs/latest/lib/python3.10/site-packages/gems/simulation/simulation_table.py:71: FutureWarning: Downcasting object dtype arrays on .fillna, .ffill, .bfill is deprecated and will change in a future version. Call result.infer_objects(copy=False) instead. To opt-in to the future behavior, set `pd.set_option('future.no_silent_downcasting', True)`
filtered[col_time] = filtered[col_time].fillna(0)
/home/docs/checkouts/readthedocs.org/user_builds/gems-energy/envs/latest/lib/python3.10/site-packages/gems/simulation/simulation_table.py:72: FutureWarning: Downcasting object dtype arrays on .fillna, .ffill, .bfill is deprecated and will change in a future version. Call result.infer_objects(copy=False) instead. To opt-in to the future behavior, set `pd.set_option('future.no_silent_downcasting', True)`
filtered[col_scenario] = filtered[col_scenario].fillna(0)
/home/docs/checkouts/readthedocs.org/user_builds/gems-energy/envs/latest/lib/python3.10/site-packages/gems/simulation/simulation_table.py:71: FutureWarning: Downcasting object dtype arrays on .fillna, .ffill, .bfill is deprecated and will change in a future version. Call result.infer_objects(copy=False) instead. To opt-in to the future behavior, set `pd.set_option('future.no_silent_downcasting', True)`
filtered[col_time] = filtered[col_time].fillna(0)
/home/docs/checkouts/readthedocs.org/user_builds/gems-energy/envs/latest/lib/python3.10/site-packages/gems/simulation/simulation_table.py:72: FutureWarning: Downcasting object dtype arrays on .fillna, .ffill, .bfill is deprecated and will change in a future version. Call result.infer_objects(copy=False) instead. To opt-in to the future behavior, set `pd.set_option('future.no_silent_downcasting', True)`
filtered[col_scenario] = filtered[col_scenario].fillna(0)
By adding the renewable generation to the system, there is no more unsupplied energy.
In the next section, the thermal generator will have several units, forcing the system to implement unit commitment.
Add Unit Commitment Constraints¶
Step 1 : Add Units in the thermal cluster¶
We replace the previous thermal component with a new instance of the thermal model configured with 10 units.
For each unit:
- the minimum power generated is set to 1 MW
- the maximum power generated is set to 10 MW
- the startup cost is set to 20 €
- the fixed cost is set to 10 €
# Delete the previous thermal generator component
components = [comp for comp in components if comp.id != 'thermal_gen']
# Delete the previous connection between the area and the thermal generator
connections = [conn for conn in connections if not (conn.component2 == 'thermal_gen' or conn.component1 == 'thermal_gen')]
input_system = SystemSchema(
id = "my_system",
components=components,
connections=connections,
)
# Uncomment the following lines to print the input system to verify the changes
# print("\tThe following input system has been created :")
# print(input_system)
print("NEW COMPONENT DEFINITION")
# Create a new thermal generator with different parameters
components.append(
ComponentSchema(
id="thermal_gen_UC",
model=f"{library_name}.thermal",
parameters=[
ComponentParameterSchema(id="p_min_unit", time_dependent=False, scenario_dependent=False, value=1),
ComponentParameterSchema(id="p_max_unit", time_dependent=False, scenario_dependent=False, value=10),
ComponentParameterSchema(id="p_max_cluster", time_dependent=False, scenario_dependent=False, value=100),
ComponentParameterSchema(id="generation_cost", time_dependent=False, scenario_dependent=False, value=50),
ComponentParameterSchema(id="startup_cost", time_dependent=False, scenario_dependent=False, value=20),
ComponentParameterSchema(id="fixed_cost", time_dependent=False, scenario_dependent=False, value=10),
ComponentParameterSchema(id="d_min_up", time_dependent=False, scenario_dependent=False, value=1),
ComponentParameterSchema(id="d_min_down", time_dependent=False, scenario_dependent=False, value=1),
ComponentParameterSchema(id="unit_count", time_dependent=False, scenario_dependent=False, value=10),
ComponentParameterSchema(id="spinning", time_dependent=False, scenario_dependent=False, value=0),
ComponentParameterSchema(id="minimum_generation_modulation", time_dependent=False, scenario_dependent=False, value=0),
],
)
)
# Add connections
connections.append(
PortConnectionsSchema(
component1="my_area",
port1="balance_port",
component2="thermal_gen_UC",
port2="balance_port",
)
)
print("\tComponents defined:\n\t", [comp.id for comp in components])
print("\tConnections defined:")
for conn in connections:
print(f"\t\t{conn.component1}:{conn.port1} <-> {conn.component2}:{conn.port2}")
input_system = SystemSchema(
id = "my_system",
components=components,
connections=connections,
)
NEW COMPONENT DEFINITION Components defined: ['my_area', 'load', 'wind_farm', 'solar_farm', 'thermal_gen_UC'] Connections defined: my_area:balance_port <-> load:balance_port my_area:balance_port <-> wind_farm:balance_port my_area:balance_port <-> solar_farm:balance_port my_area:balance_port <-> thermal_gen_UC:balance_port
Step 2 : Results Visualization¶
print("OPTIMIZATION PROBLEM BUILDING")
system = resolve_system(input_system, resolve_library(input_libraries))
database = build_data_base(input_system, Path(series_folder))
study = Study(system=system, database=database)
optim_config = OptimConfig(
time_scope=TimeScopeConfig(first_time_step=0, last_time_step=TimeSpan - 1),
scenario_scope=ScenarioScopeConfig(nb_scenarios=1),
)
print("\t✓ Optimization problem built successfully")
print("SOLVING OPTIMIZATION PROBLEM")
try:
results = SimulationSession(study=study, optim_config=optim_config).run()
print("\t✓ Optimization problem solved and results extracted successfully")
except Exception as e:
print(f"Error solving optimization problem: {e}")
raise e
objective_value = results.data.loc[results.data["output"] == "objective-value", "value"].iloc[0]
print(f"\t✓ Objective value: {objective_value}")
OPTIMIZATION PROBLEM BUILDING ✓ Optimization problem built successfully SOLVING OPTIMIZATION PROBLEM
/home/docs/checkouts/readthedocs.org/user_builds/gems-energy/envs/latest/lib/python3.10/site-packages/gems/model/resolve_library.py:139: UserWarning: Objective contribution 'objective' has a scenario dimension but no explicit expec() operator. Expectation semantics (average over scenarios) are applied automatically. Add expec() explicitly to suppress this warning. _resolve_model(m, current_lib.port_types, current_lib.id)
Running HiGHS 1.14.0 (git hash: 7df0786): Copyright (c) 2026 under MIT licence terms
ERROR: getOptionIndex: Option "solver_logs" is unknown
LP has 1512 rows; 1176 cols; 4200 nonzeros
Coefficient ranges:
Matrix [1e+00, 1e+01]
Cost [1e+01, 1e+04]
Bound [1e+01, 1e+02]
RHS [5e+00, 1e+02]
Presolving model
1344 rows, 1008 cols, 3696 nonzeros 0s
1005 rows, 1002 cols, 2511 nonzeros 0s
Presolve reductions: rows 1005(-507); columns 1002(-174); nonzeros 2511(-1689)
Solving the presolved LP
Using dual simplex solver
Iteration Objective Infeasibilities num(sum)
0 0.0000000000e+00 Ph1: 0(0) 0.0s
498 4.8841600000e+05 Pr: 0(0) 0.0s
Performed postsolve
Solving the original LP from the solution after postsolve
Model status : Optimal
Simplex iterations: 498
Objective value : 4.8841600000e+05
P-D objective error : 0.0000000000e+00
HiGHS run time : 0.01
✓ Optimization problem solved and results extracted successfully ✓ Objective value: 488416.0
/home/docs/checkouts/readthedocs.org/user_builds/gems-energy/envs/latest/lib/python3.10/site-packages/linopy/common.py:492: UserWarning: Coordinates across variables not equal. Perform outer join. warn( /home/docs/checkouts/readthedocs.org/user_builds/gems-energy/envs/latest/lib/python3.10/site-packages/linopy/common.py:492: UserWarning: Coordinates across variables not equal. Perform outer join. warn(
import matplotlib.pyplot as plt
import io, base64
from IPython.display import display
thermal_gen_values = results.component("thermal_gen_UC").output("generation").value(scenario_index=0)
thermal_nb_units_values = results.component("thermal_gen_UC").output("nb_units_on").value(scenario_index=0)
unsupply_values = results.component("my_area").output("unsupplied_energy").value(scenario_index=0)
spilled_values = results.component("my_area").output("spillage").value(scenario_index=0)
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8))
ax1.plot(thermal_gen_values, linewidth=2, label='Thermal Generation')
ax1.plot(unsupply_values, linewidth=2, color='red', label='Unsupplied Energy')
ax1.plot(spilled_values, linewidth=2, color='green', label='Spilled Energy')
ax1.set_xlabel('Time (hours)')
ax1.set_ylabel('Power (MW)')
ax1.set_title('Thermal Generation and Unsupplied Energy Over One Week')
ax1.legend()
ax1.grid(True, alpha=0.3)
ax2.bar(thermal_nb_units_values.index, thermal_nb_units_values.values, color='steelblue', alpha=0.7)
ax2.set_xlabel('Time (hours)')
ax2.set_ylabel('Number of Units On')
ax2.set_title('Unit Commitment Decisions Over One Week')
ax2.grid(True, alpha=0.3, axis='y')
plt.tight_layout()
buf = io.BytesIO()
fig.savefig(buf, format='png', bbox_inches='tight')
buf.seek(0)
display(
{'image/png': base64.b64encode(buf.read()).decode()},
raw=True,
metadata={'image/png': {'alt': 'Two subplots: top shows thermal generation and unsupplied energy over one week; bottom shows a bar chart of the number of thermal units committed (on) at each hour, illustrating unit commitment decisions driven by renewable generation variability.'}}
)
plt.close(fig)
/home/docs/checkouts/readthedocs.org/user_builds/gems-energy/envs/latest/lib/python3.10/site-packages/gems/simulation/simulation_table.py:71: FutureWarning: Downcasting object dtype arrays on .fillna, .ffill, .bfill is deprecated and will change in a future version. Call result.infer_objects(copy=False) instead. To opt-in to the future behavior, set `pd.set_option('future.no_silent_downcasting', True)`
filtered[col_time] = filtered[col_time].fillna(0)
/home/docs/checkouts/readthedocs.org/user_builds/gems-energy/envs/latest/lib/python3.10/site-packages/gems/simulation/simulation_table.py:72: FutureWarning: Downcasting object dtype arrays on .fillna, .ffill, .bfill is deprecated and will change in a future version. Call result.infer_objects(copy=False) instead. To opt-in to the future behavior, set `pd.set_option('future.no_silent_downcasting', True)`
filtered[col_scenario] = filtered[col_scenario].fillna(0)
/home/docs/checkouts/readthedocs.org/user_builds/gems-energy/envs/latest/lib/python3.10/site-packages/gems/simulation/simulation_table.py:71: FutureWarning: Downcasting object dtype arrays on .fillna, .ffill, .bfill is deprecated and will change in a future version. Call result.infer_objects(copy=False) instead. To opt-in to the future behavior, set `pd.set_option('future.no_silent_downcasting', True)`
filtered[col_time] = filtered[col_time].fillna(0)
/home/docs/checkouts/readthedocs.org/user_builds/gems-energy/envs/latest/lib/python3.10/site-packages/gems/simulation/simulation_table.py:72: FutureWarning: Downcasting object dtype arrays on .fillna, .ffill, .bfill is deprecated and will change in a future version. Call result.infer_objects(copy=False) instead. To opt-in to the future behavior, set `pd.set_option('future.no_silent_downcasting', True)`
filtered[col_scenario] = filtered[col_scenario].fillna(0)
/home/docs/checkouts/readthedocs.org/user_builds/gems-energy/envs/latest/lib/python3.10/site-packages/gems/simulation/simulation_table.py:71: FutureWarning: Downcasting object dtype arrays on .fillna, .ffill, .bfill is deprecated and will change in a future version. Call result.infer_objects(copy=False) instead. To opt-in to the future behavior, set `pd.set_option('future.no_silent_downcasting', True)`
filtered[col_time] = filtered[col_time].fillna(0)
/home/docs/checkouts/readthedocs.org/user_builds/gems-energy/envs/latest/lib/python3.10/site-packages/gems/simulation/simulation_table.py:72: FutureWarning: Downcasting object dtype arrays on .fillna, .ffill, .bfill is deprecated and will change in a future version. Call result.infer_objects(copy=False) instead. To opt-in to the future behavior, set `pd.set_option('future.no_silent_downcasting', True)`
filtered[col_scenario] = filtered[col_scenario].fillna(0)
/home/docs/checkouts/readthedocs.org/user_builds/gems-energy/envs/latest/lib/python3.10/site-packages/gems/simulation/simulation_table.py:71: FutureWarning: Downcasting object dtype arrays on .fillna, .ffill, .bfill is deprecated and will change in a future version. Call result.infer_objects(copy=False) instead. To opt-in to the future behavior, set `pd.set_option('future.no_silent_downcasting', True)`
filtered[col_time] = filtered[col_time].fillna(0)
/home/docs/checkouts/readthedocs.org/user_builds/gems-energy/envs/latest/lib/python3.10/site-packages/gems/simulation/simulation_table.py:72: FutureWarning: Downcasting object dtype arrays on .fillna, .ffill, .bfill is deprecated and will change in a future version. Call result.infer_objects(copy=False) instead. To opt-in to the future behavior, set `pd.set_option('future.no_silent_downcasting', True)`
filtered[col_scenario] = filtered[col_scenario].fillna(0)
The results illustrate how the number of thermal units generating power changes over the simulation week, reflecting the unit commitment feature.
Indeed, the number of thermal units on fluctuated throughout the wind and solar power generation.