Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change scenario representation in optimization context #49

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/andromede/simulation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@
BendersDecomposedProblem,
build_benders_decomposed_problem,
)
from .optimization import BlockBorderManagement, OptimizationProblem, build_problem
from .optimization import (
BlockBorderManagement,
OptimizationProblem,
build_problem,
scenario_playlist,
)
from .output_values import BendersSolution, OutputValues
from .runner import BendersRunner, MergeMPSRunner
from .strategy import MergedProblemStrategy, ModelSelectionStrategy
Expand Down
5 changes: 3 additions & 2 deletions src/andromede/simulation/benders_decomposed.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ def build_benders_decomposed_problem(
network: Network,
database: DataBase,
blocks: List[TimeBlock],
scenarios: int,
scenarios: List[int],
*,
border_management: BlockBorderManagement = BlockBorderManagement.CYCLE,
solver_id: str = "GLOP",
Expand All @@ -217,6 +217,7 @@ def build_benders_decomposed_problem(

Returns a Benders Decomposed problem
"""
null_scenario: List[int] = []

# Benders Decomposed Master Problem
master = build_problem(
Expand All @@ -225,7 +226,7 @@ def build_benders_decomposed_problem(
null_time_block := TimeBlock( # Not necessary for master, but list must be non-empty
0, [0]
),
null_scenario := 0, # Not necessary for master
null_scenario, # Not necessary for master
problem_name="master",
border_management=border_management,
solver_id=solver_id,
Expand Down
32 changes: 23 additions & 9 deletions src/andromede/simulation/optimization.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from abc import ABC, abstractmethod
from dataclasses import dataclass
from enum import Enum
from typing import Dict, Iterable, List, Optional
from typing import Dict, Iterable, List, Optional, Union

import ortools.linear_solver.pywraplp as lp

Expand Down Expand Up @@ -69,8 +69,9 @@ def _get_parameter_value(
name: str,
) -> float:
data = context.database.get_data(component_id, name)
absolute_scenario = context.scenario_to_absolute_scenario(scenario)
absolute_timestep = context.block_timestep_to_absolute_timestep(block_timestep)
return data.get_value(absolute_timestep, scenario)
return data.get_value(absolute_timestep, absolute_scenario)


# TODO: Maybe add the notion of constant parameter in the model
Expand Down Expand Up @@ -304,7 +305,7 @@ def __init__(
network: Network,
database: DataBase,
block: TimeBlock,
scenarios: int,
scenarios: List[int],
border_management: BlockBorderManagement,
):
self._network = network
Expand All @@ -323,9 +324,12 @@ def network(self) -> Network:
return self._network

@property
def scenarios(self) -> int:
def scenarios(self) -> List[int]:
return self._scenarios

def scenario_length(self) -> int:
return len(self._scenarios)

def block_length(self) -> int:
return len(self._block.timesteps)

Expand All @@ -337,6 +341,12 @@ def connection_fields_expressions(self) -> Dict[PortFieldKey, List[ExpressionNod
def block_timestep_to_absolute_timestep(self, block_timestep: int) -> int:
return self._block.timesteps[block_timestep]

def scenario_to_absolute_scenario(self, scenario: int) -> int:
if len(self.scenarios) == 0:
return 0
else:
return self.scenarios[scenario]

@property
def database(self) -> DataBase:
return self._database
Expand All @@ -351,7 +361,7 @@ def get_time_indices(self, index_structure: IndexingStructure) -> Iterable[int]:
return range(self.block_length()) if index_structure.time else range(1)

def get_scenario_indices(self, index_structure: IndexingStructure) -> Iterable[int]:
return range(self.scenarios) if index_structure.scenario else range(1)
return range(self.scenario_length()) if index_structure.scenario else range(1)

# TODO: API to improve, variable_structure guides which of the indices block_timestep and scenario should be used
def get_component_variable(
Expand Down Expand Up @@ -509,8 +519,8 @@ def _create_objective(
for term in linear_expr.terms.values():
# TODO : How to handle the scenario operator in a general manner ?
if isinstance(term.scenario_operator, Expectation):
weight = 1 / opt_context.scenarios
scenario_ids = range(opt_context.scenarios)
weight = 1 / opt_context.scenario_length()
scenario_ids = range(opt_context.scenario_length())
else:
weight = 1
scenario_ids = range(1)
Expand Down Expand Up @@ -761,7 +771,7 @@ def _create_variables(self) -> None:
scenario_suffix = (
f"_s{scenario}"
if var_indexing.is_scenario_varying()
and (self.context.scenarios > 1)
and (self.context.scenario_length() > 1)
else ""
)

Expand Down Expand Up @@ -853,7 +863,7 @@ def build_problem(
network: Network,
database: DataBase,
block: TimeBlock,
scenarios: int,
scenarios: List[int],
*,
problem_name: str = "optimization_problem",
border_management: BlockBorderManagement = BlockBorderManagement.CYCLE,
Expand All @@ -872,3 +882,7 @@ def build_problem(
)

return OptimizationProblem(problem_name, solver, opt_context, problem_strategy)


def scenario_playlist(scenarios: int) -> List[int]:
return list(range(scenarios))
17 changes: 11 additions & 6 deletions tests/functional/test_andromede.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
OutputValues,
TimeBlock,
build_problem,
scenario_playlist,
)
from andromede.study import (
ConstantData,
Expand Down Expand Up @@ -89,7 +90,7 @@ def test_timeseries() -> None:
time_block = TimeBlock(1, [0, 1])
scenarios = 1

problem = build_problem(network, database, time_block, scenarios)
problem = build_problem(network, database, time_block, scenario_playlist(scenarios))
status = problem.solver.Solve()

assert status == problem.solver.OPTIMAL
Expand Down Expand Up @@ -161,15 +162,15 @@ def test_variable_bound() -> None:

network = create_one_node_network(generator_model)
database = create_simple_database(max_generation=200)
problem = build_problem(network, database, TimeBlock(1, [0]), 1)
problem = build_problem(network, database, TimeBlock(1, [0]), scenario_playlist(1))
status = problem.solver.Solve()

assert status == problem.solver.OPTIMAL
assert problem.solver.Objective().Value() == 3000

network = create_one_node_network(generator_model)
database = create_simple_database(max_generation=80)
problem = build_problem(network, database, TimeBlock(1, [0]), 1)
problem = build_problem(network, database, TimeBlock(1, [0]), scenario_playlist(1))
status = problem.solver.Solve()
assert status == problem.solver.INFEASIBLE # Infeasible

Expand All @@ -179,15 +180,19 @@ def test_variable_bound() -> None:
ValueError,
match="Upper and lower bounds of variable G_generation have the same value: 0",
):
problem = build_problem(network, database, TimeBlock(1, [0]), 1)
problem = build_problem(
network, database, TimeBlock(1, [0]), scenario_playlist(1)
)

network = create_one_node_network(generator_model)
database = create_simple_database(max_generation=-10)
with pytest.raises(
ValueError,
match=r"Upper bound \(-10\) must be strictly greater than lower bound \(0\) for variable G_generation",
):
problem = build_problem(network, database, TimeBlock(1, [0]), 1)
problem = build_problem(
network, database, TimeBlock(1, [0]), scenario_playlist(1)
)


def generate_data(
Expand Down Expand Up @@ -251,7 +256,7 @@ def short_term_storage_base(efficiency: float, horizon: int) -> None:
network,
database,
time_blocks[0],
scenarios,
scenario_playlist(scenarios),
border_management=BlockBorderManagement.CYCLE,
)
status = problem.solver.Solve()
Expand Down
21 changes: 14 additions & 7 deletions tests/functional/test_andromede_yml.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
OutputValues,
TimeBlock,
build_problem,
scenario_playlist,
)
from andromede.study import (
ConstantData,
Expand Down Expand Up @@ -82,7 +83,9 @@ def test_basic_balance(lib: Library) -> None:
network.connect(PortRef(gen, "balance_port"), PortRef(node, "balance_port"))

scenarios = 1
problem = build_problem(network, database, TimeBlock(1, [0]), scenarios)
problem = build_problem(
network, database, TimeBlock(1, [0]), scenario_playlist(scenarios)
)
status = problem.solver.Solve()

assert status == problem.solver.OPTIMAL
Expand Down Expand Up @@ -134,7 +137,9 @@ def test_link(lib: Library) -> None:
network.connect(PortRef(link, "out_port"), PortRef(node2, "balance_port"))

scenarios = 1
problem = build_problem(network, database, TimeBlock(1, [0]), scenarios)
problem = build_problem(
network, database, TimeBlock(1, [0]), scenario_playlist(scenarios)
)
status = problem.solver.Solve()

assert status == problem.solver.OPTIMAL
Expand Down Expand Up @@ -192,7 +197,9 @@ def test_stacking_generation(lib: Library) -> None:
network.connect(PortRef(gen2, "balance_port"), PortRef(node1, "balance_port"))

scenarios = 1
problem = build_problem(network, database, TimeBlock(1, [0]), scenarios)
problem = build_problem(
network, database, TimeBlock(1, [0]), scenario_playlist(scenarios)
)
status = problem.solver.Solve()

assert status == problem.solver.OPTIMAL
Expand Down Expand Up @@ -232,7 +239,7 @@ def test_spillage(lib: Library) -> None:
network.connect(PortRef(gen1, "balance_port"), PortRef(node, "balance_port"))
network.connect(PortRef(spillage, "balance_port"), PortRef(node, "balance_port"))

problem = build_problem(network, database, TimeBlock(0, [1]), 1)
problem = build_problem(network, database, TimeBlock(0, [1]), scenario_playlist(1))
status = problem.solver.Solve()

assert status == problem.solver.OPTIMAL
Expand Down Expand Up @@ -322,7 +329,7 @@ def test_min_up_down_times(lib: Library) -> None:
network,
database,
time_block,
scenarios,
scenario_playlist(scenarios),
border_management=BlockBorderManagement.CYCLE,
)
status = problem.solver.Solve()
Expand Down Expand Up @@ -380,7 +387,7 @@ def test_changing_demand(lib: Library) -> None:
network,
database,
time_block,
scenarios,
scenario_playlist(scenarios),
border_management=BlockBorderManagement.CYCLE,
)
status = problem.solver.Solve()
Expand Down Expand Up @@ -472,7 +479,7 @@ def test_min_up_down_times_2(lib: Library) -> None:
network,
database,
time_block,
scenarios,
scenario_playlist(scenarios),
border_management=BlockBorderManagement.CYCLE,
)
status = problem.solver.Solve()
Expand Down
22 changes: 15 additions & 7 deletions tests/functional/test_performance.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
NODE_BALANCE_MODEL,
)
from andromede.model import float_parameter, float_variable, model
from andromede.simulation import TimeBlock, build_problem
from andromede.simulation import TimeBlock, build_problem, scenario_playlist
from andromede.study import (
ConstantData,
DataBase,
Expand Down Expand Up @@ -70,7 +70,9 @@ def test_large_sum_inside_model_with_loop() -> None:
cost_model = create_component(model=SIMPLE_COST_MODEL, id="simple_cost")
network.add_component(cost_model)

problem = build_problem(network, database, time_blocks[0], scenarios)
problem = build_problem(
network, database, time_blocks[0], scenario_playlist(scenarios)
)
status = problem.solver.Solve()

assert status == problem.solver.OPTIMAL
Expand Down Expand Up @@ -106,7 +108,9 @@ def test_large_sum_outside_model_with_loop() -> None:
)
network.add_component(simple_model)

problem = build_problem(network, database, time_blocks[0], scenarios)
problem = build_problem(
network, database, time_blocks[0], scenario_playlist(scenarios)
)
status = problem.solver.Solve()

assert status == problem.solver.OPTIMAL
Expand Down Expand Up @@ -151,7 +155,9 @@ def test_large_sum_inside_model_with_sum_operator() -> None:
cost_model = create_component(model=SIMPLE_COST_MODEL, id="simple_cost")
network.add_component(cost_model)

problem = build_problem(network, database, time_blocks[0], scenarios)
problem = build_problem(
network, database, time_blocks[0], scenario_playlist(scenarios)
)
status = problem.solver.Solve()

assert status == problem.solver.OPTIMAL
Expand Down Expand Up @@ -197,7 +203,9 @@ def test_large_sum_of_port_connections() -> None:
)

with pytest.raises(RecursionError, match="maximum recursion depth exceeded"):
problem = build_problem(network, database, time_block, scenarios)
problem = build_problem(
network, database, time_block, scenario_playlist(scenarios)
)

# Won't run because last statement will raise the error
status = problem.solver.Solve()
Expand Down Expand Up @@ -235,7 +243,7 @@ def test_basic_balance_on_whole_year() -> None:
network.connect(PortRef(demand, "balance_port"), PortRef(node, "balance_port"))
network.connect(PortRef(gen, "balance_port"), PortRef(node, "balance_port"))

problem = build_problem(network, database, time_block, scenarios)
problem = build_problem(network, database, time_block, scenario_playlist(scenarios))
status = problem.solver.Solve()

assert status == problem.solver.OPTIMAL
Expand Down Expand Up @@ -273,7 +281,7 @@ def test_basic_balance_on_whole_year_with_large_sum() -> None:
network.connect(PortRef(demand, "balance_port"), PortRef(node, "balance_port"))
network.connect(PortRef(gen, "balance_port"), PortRef(node, "balance_port"))

problem = build_problem(network, database, time_block, scenarios)
problem = build_problem(network, database, time_block, scenario_playlist(scenarios))
status = problem.solver.Solve()

assert status == problem.solver.OPTIMAL
Expand Down
6 changes: 3 additions & 3 deletions tests/functional/test_stochastic.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
THERMAL_CLUSTER_MODEL_DHD,
THERMAL_CLUSTER_MODEL_HD,
)
from andromede.simulation import TimeBlock, build_problem
from andromede.simulation import TimeBlock, build_problem, scenario_playlist
from andromede.study import (
ConstantData,
DataBase,
Expand Down Expand Up @@ -123,7 +123,7 @@ def test_stochastic_model_with_HD_for_thermal_startup(
network.connect(PortRef(peak, "balance_port"), PortRef(node, "balance_port"))

for block in time_blocks: # TODO : To manage blocks simply for now
problem = build_problem(network, database, block, scenarios)
problem = build_problem(network, database, block, scenario_playlist(scenarios))
status = problem.solver.Solve()

assert (
Expand Down Expand Up @@ -189,7 +189,7 @@ def test_stochastic_model_with_DH_for_thermal_startup(
network.connect(PortRef(peak, "balance_port"), PortRef(node, "balance_port"))

for block in time_blocks: # TODO : To manage blocks simply for now
problem = build_problem(network, database, block, scenarios)
problem = build_problem(network, database, block, scenario_playlist(scenarios))
status = problem.solver.Solve()

assert (
Expand Down
Loading
Loading