Skip to content

Commit

Permalink
Feature/automatic coupling model (#52)
Browse files Browse the repository at this point in the history
* Refactored Port classes to port.py

* Add automatic decision tree node coupling

* Cherry-picked benders_decomposed debug mode

* Fixed var name tree prefix

* Move invest pathway model to test only

* Allow for more flexible expressions between tree nodes

* Reset to a more rigid type of coupling constraints
  • Loading branch information
ianmnz authored Nov 27, 2024
1 parent fbe54ff commit 95e07ec
Show file tree
Hide file tree
Showing 24 changed files with 418 additions and 306 deletions.
2 changes: 1 addition & 1 deletion src/andromede/expression/port_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
PortFieldAggregatorNode,
PortFieldNode,
)
from andromede.model.model import PortFieldId
from andromede.model.port import PortFieldId


@dataclass(eq=True, frozen=True)
Expand Down
14 changes: 4 additions & 10 deletions src/andromede/libs/standard.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@
from andromede.expression.indexing_structure import IndexingStructure
from andromede.model.common import ProblemContext
from andromede.model.constraint import Constraint
from andromede.model.model import ModelPort, PortFieldDefinition, PortFieldId, model
from andromede.model.model import ModelPort, model
from andromede.model.parameter import float_parameter, int_parameter
from andromede.model.port import PortField, PortType
from andromede.model.port import PortField, PortFieldDefinition, PortFieldId, PortType
from andromede.model.variable import float_variable, int_variable

CONSTANT = IndexingStructure(False, False)
Expand Down Expand Up @@ -475,15 +475,9 @@
float_variable(
"invested_capa",
lower_bound=literal(0),
structure=CONSTANT,
context=ProblemContext.COUPLING,
),
float_variable(
"delta_invest",
lower_bound=literal(0),
upper_bound=param("max_invest"),
structure=CONSTANT,
context=ProblemContext.INVESTMENT,
context=ProblemContext.COUPLING,
),
],
ports=[ModelPort(port_type=BALANCE_PORT_TYPE, port_name="balance_port")],
Expand All @@ -503,5 +497,5 @@
objective_operational_contribution=(param("op_cost") * var("generation"))
.sum()
.expec(),
objective_investment_contribution=param("invest_cost") * var("delta_invest"),
objective_investment_contribution=param("invest_cost") * var("invested_capa"),
)
2 changes: 1 addition & 1 deletion src/andromede/libs/standard_sc.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
float_variable,
model,
)
from andromede.model.model import PortFieldDefinition, PortFieldId
from andromede.model.port import PortFieldDefinition, PortFieldId

"""
Simple Convertor model.
Expand Down
2 changes: 1 addition & 1 deletion src/andromede/model/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@
from .constraint import Constraint
from .model import Model, ModelPort, model
from .parameter import Parameter, float_parameter, int_parameter
from .port import PortField, PortType
from .port import PortField, PortFieldDefinition, PortFieldId, PortType
from .variable import Variable, float_variable, int_variable
125 changes: 2 additions & 123 deletions src/andromede/model/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,36 +19,13 @@
from dataclasses import dataclass, field, replace
from typing import Any, Dict, Iterable, Optional

from andromede.expression import (
AdditionNode,
ComparisonNode,
DivisionNode,
ExpressionNode,
ExpressionVisitor,
LiteralNode,
MultiplicationNode,
NegationNode,
ParameterNode,
SubstractionNode,
VariableNode,
)
from andromede.expression import ExpressionNode
from andromede.expression.degree import is_linear
from andromede.expression.expression import (
BinaryOperatorNode,
ComponentParameterNode,
ComponentVariableNode,
PortFieldAggregatorNode,
PortFieldNode,
ScenarioOperatorNode,
TimeAggregatorNode,
TimeOperatorNode,
)
from andromede.expression.indexing import IndexingStructureProvider, compute_indexation
from andromede.expression.indexing_structure import IndexingStructure
from andromede.expression.visitor import T, visit
from andromede.model.constraint import Constraint
from andromede.model.parameter import Parameter
from andromede.model.port import PortType
from andromede.model.port import PortFieldDefinition, PortFieldId, PortType
from andromede.model.variable import Variable


Expand Down Expand Up @@ -111,37 +88,6 @@ def replicate(self, /, **changes: Any) -> "ModelPort":
return replace(self, **changes)


@dataclass(frozen=True)
class PortFieldId:
port_name: str
field_name: str

def replicate(self, /, **changes: Any) -> "PortFieldId":
return replace(self, **changes)


@dataclass(frozen=True)
class PortFieldDefinition:
"""
Defines the values of one port field
"""

port_field: PortFieldId
definition: ExpressionNode

def __post_init__(self) -> None:
_validate_port_field_expression(self)

def replicate(self, /, **changes: Any) -> "PortFieldDefinition":
return replace(self, **changes)


def port_field_def(
port_name: str, field_name: str, definition: ExpressionNode
) -> PortFieldDefinition:
return PortFieldDefinition(PortFieldId(port_name, field_name), definition)


@dataclass(frozen=True)
class Model:
"""
Expand Down Expand Up @@ -238,70 +184,3 @@ def model(
if port_fields_definitions
else {},
)


class _PortFieldExpressionChecker(ExpressionVisitor[None]):
"""
Visits the whole expression to check there is no:
comparison, other port field, component-associated parametrs or variables...
"""

def literal(self, node: LiteralNode) -> None:
pass

def negation(self, node: NegationNode) -> None:
visit(node.operand, self)

def _visit_binary_op(self, node: BinaryOperatorNode) -> None:
visit(node.left, self)
visit(node.right, self)

def addition(self, node: AdditionNode) -> None:
self._visit_binary_op(node)

def substraction(self, node: SubstractionNode) -> None:
self._visit_binary_op(node)

def multiplication(self, node: MultiplicationNode) -> None:
self._visit_binary_op(node)

def division(self, node: DivisionNode) -> None:
self._visit_binary_op(node)

def comparison(self, node: ComparisonNode) -> None:
raise ValueError("Port definition cannot contain a comparison operator.")

def variable(self, node: VariableNode) -> None:
pass

def parameter(self, node: ParameterNode) -> None:
pass

def comp_parameter(self, node: ComponentParameterNode) -> None:
raise ValueError(
"Port definition must not contain a parameter associated to a component."
)

def comp_variable(self, node: ComponentVariableNode) -> None:
raise ValueError(
"Port definition must not contain a variable associated to a component."
)

def time_operator(self, node: TimeOperatorNode) -> None:
visit(node.operand, self)

def time_aggregator(self, node: TimeAggregatorNode) -> None:
visit(node.operand, self)

def scenario_operator(self, node: ScenarioOperatorNode) -> None:
visit(node.operand, self)

def port_field(self, node: PortFieldNode) -> None:
raise ValueError("Port definition cannot reference another port field.")

def port_field_aggregator(self, node: PortFieldAggregatorNode) -> None:
raise ValueError("Port definition cannot contain port field aggregation.")


def _validate_port_field_expression(definition: PortFieldDefinition) -> None:
visit(definition.definition, _PortFieldExpressionChecker())
127 changes: 125 additions & 2 deletions src/andromede/model/port.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,33 @@
#
# This file is part of the Antares project.

from dataclasses import dataclass
from typing import List
from dataclasses import dataclass, replace
from typing import Any, List

from andromede.expression import (
AdditionNode,
ComparisonNode,
DivisionNode,
ExpressionNode,
ExpressionVisitor,
LiteralNode,
MultiplicationNode,
NegationNode,
ParameterNode,
SubstractionNode,
VariableNode,
)
from andromede.expression.expression import (
BinaryOperatorNode,
ComponentParameterNode,
ComponentVariableNode,
PortFieldAggregatorNode,
PortFieldNode,
ScenarioOperatorNode,
TimeAggregatorNode,
TimeOperatorNode,
)
from andromede.expression.visitor import visit


@dataclass(frozen=True)
Expand All @@ -31,3 +56,101 @@ class PortType:

id: str
fields: List[PortField] # TODO: should we rename with "pin" ?


@dataclass(frozen=True)
class PortFieldId:
port_name: str
field_name: str

def replicate(self, /, **changes: Any) -> "PortFieldId":
return replace(self, **changes)


@dataclass(frozen=True)
class PortFieldDefinition:
"""
Defines the values of one port field
"""

port_field: PortFieldId
definition: ExpressionNode

def __post_init__(self) -> None:
_validate_port_field_expression(self)

def replicate(self, /, **changes: Any) -> "PortFieldDefinition":
return replace(self, **changes)


def port_field_def(
port_name: str, field_name: str, definition: ExpressionNode
) -> PortFieldDefinition:
return PortFieldDefinition(PortFieldId(port_name, field_name), definition)


class _PortFieldExpressionChecker(ExpressionVisitor[None]):
"""
Visits the whole expression to check there is no:
comparison, other port field, component-associated parametrs or variables...
"""

def literal(self, node: LiteralNode) -> None:
pass

def negation(self, node: NegationNode) -> None:
visit(node.operand, self)

def _visit_binary_op(self, node: BinaryOperatorNode) -> None:
visit(node.left, self)
visit(node.right, self)

def addition(self, node: AdditionNode) -> None:
self._visit_binary_op(node)

def substraction(self, node: SubstractionNode) -> None:
self._visit_binary_op(node)

def multiplication(self, node: MultiplicationNode) -> None:
self._visit_binary_op(node)

def division(self, node: DivisionNode) -> None:
self._visit_binary_op(node)

def comparison(self, node: ComparisonNode) -> None:
raise ValueError("Port definition cannot contain a comparison operator.")

def variable(self, node: VariableNode) -> None:
pass

def parameter(self, node: ParameterNode) -> None:
pass

def comp_parameter(self, node: ComponentParameterNode) -> None:
raise ValueError(
"Port definition must not contain a parameter associated to a component."
)

def comp_variable(self, node: ComponentVariableNode) -> None:
raise ValueError(
"Port definition must not contain a variable associated to a component."
)

def time_operator(self, node: TimeOperatorNode) -> None:
visit(node.operand, self)

def time_aggregator(self, node: TimeAggregatorNode) -> None:
visit(node.operand, self)

def scenario_operator(self, node: ScenarioOperatorNode) -> None:
visit(node.operand, self)

def port_field(self, node: PortFieldNode) -> None:
raise ValueError("Port definition cannot reference another port field.")

def port_field_aggregator(self, node: PortFieldAggregatorNode) -> None:
raise ValueError("Port definition cannot contain port field aggregation.")


def _validate_port_field_expression(definition: PortFieldDefinition) -> None:
visit(definition.definition, _PortFieldExpressionChecker())
2 changes: 1 addition & 1 deletion src/andromede/model/resolve_library.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
model,
)
from andromede.model.library import Library, library
from andromede.model.model import PortFieldDefinition, port_field_def
from andromede.model.port import PortFieldDefinition, port_field_def
from andromede.model.parsing import (
InputConstraint,
InputField,
Expand Down
8 changes: 5 additions & 3 deletions src/andromede/simulation/benders_decomposed.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,6 @@ def build_benders_decomposed_problem(
*,
border_management: BlockBorderManagement = BlockBorderManagement.CYCLE,
solver_id: str = "GLOP",
coupling_network: Network = Network(""),
struct_filename: str = "structure.txt",
) -> BendersDecomposedProblem:
"""
Expand All @@ -232,22 +231,25 @@ def build_benders_decomposed_problem(
"""

if not decision_tree_root.is_leaves_prob_sum_one():
raise RuntimeError("Decision tree must have leaves' probability sum equal one!")
raise ValueError("Decision tree leaves' probability must sum one!")

null_time_block = TimeBlock(
0, [0]
) # Not necessary for master, but list must be non-empty
null_scenario = 0 # Not necessary for master

decision_tree_root._add_coupling_constraints()

coupler = build_problem(
coupling_network,
decision_tree_root.coupling_network,
database,
null_time_block,
null_scenario,
problem_name="coupler",
solver_id=solver_id,
build_strategy=InvestmentProblemStrategy(),
risk_strategy=ExpectedValue(0.0),
use_full_var_name=False,
)

masters = [] # Benders Decomposed Master Problem
Expand Down
Loading

0 comments on commit 95e07ec

Please sign in to comment.