diff --git a/.gitignore b/.gitignore index e1b8586c..97927d8a 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,5 @@ venv coverage.xml bin outputs +build +src/andromede_modeling_prototype.egg-info/ diff --git a/grammar/Expr.g4 b/grammar/Expr.g4 index 072bf52e..9b21de7b 100644 --- a/grammar/Expr.g4 +++ b/grammar/Expr.g4 @@ -25,11 +25,11 @@ expr | expr op=('/' | '*') expr # muldiv | expr op=('+' | '-') expr # addsub | expr COMPARISON expr # comparison + | 'sum' '(' expr ')' # allTimeSum + | 'sum' '(' from=shift '..' to=shift ',' expr ')' # timeSum | IDENTIFIER '(' expr ')' # function - | IDENTIFIER '[' shift (',' shift)* ']' # timeShift - | IDENTIFIER '[' expr (',' expr )* ']' # timeIndex - | IDENTIFIER '[' shift1=shift '..' shift2=shift ']' # timeShiftRange - | IDENTIFIER '[' expr '..' expr ']' # timeRange + | IDENTIFIER '[' shift ']' # timeShift + | IDENTIFIER '[' expr ']' # timeIndex ; atom diff --git a/pyproject.toml b/pyproject.toml index 29dfbbb8..d2a95f32 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,9 @@ dependencies = [ "ortools" ] +[project.scripts] +andromede-simulator = "andromede.main.main:main_cli" + [tool.setuptools.packages.find] # All the following settings are optional: where = ["src"] diff --git a/src/andromede/expression/__init__.py b/src/andromede/expression/__init__.py index 2fe9b94d..3d949b6e 100644 --- a/src/andromede/expression/__init__.py +++ b/src/andromede/expression/__init__.py @@ -28,7 +28,6 @@ MultiplicationNode, NegationNode, ParameterNode, - SubstractionNode, VariableNode, literal, param, diff --git a/src/andromede/expression/context_adder.py b/src/andromede/expression/context_adder.py index 812e95f7..07ee97fd 100644 --- a/src/andromede/expression/context_adder.py +++ b/src/andromede/expression/context_adder.py @@ -18,6 +18,8 @@ ComponentVariableNode, ExpressionNode, ParameterNode, + ProblemParameterNode, + ProblemVariableNode, VariableNode, ) from .visitor import visit @@ -48,6 +50,16 @@ def comp_parameter(self, node: ComponentParameterNode) -> ExpressionNode: "This expression has already been associated to another component." ) + def pb_variable(self, node: ProblemVariableNode) -> ExpressionNode: + raise ValueError( + "This expression has already been associated to another component." + ) + + def pb_parameter(self, node: ProblemParameterNode) -> ExpressionNode: + raise ValueError( + "This expression has already been associated to another component." + ) + def add_component_context(id: str, expression: ExpressionNode) -> ExpressionNode: return visit(expression, ContextAdder(id)) diff --git a/src/andromede/expression/copy.py b/src/andromede/expression/copy.py index c135ee59..184a4354 100644 --- a/src/andromede/expression/copy.py +++ b/src/andromede/expression/copy.py @@ -11,30 +11,27 @@ # This file is part of the Antares project. from dataclasses import dataclass -from typing import List, Union, cast +from typing import List, cast from .expression import ( - AdditionNode, + AllTimeSumNode, ComparisonNode, ComponentParameterNode, ComponentVariableNode, - DivisionNode, ExpressionNode, - ExpressionRange, - InstancesTimeIndex, LiteralNode, - MultiplicationNode, - NegationNode, ParameterNode, PortFieldAggregatorNode, PortFieldNode, + ProblemParameterNode, + ProblemVariableNode, ScenarioOperatorNode, - SubstractionNode, - TimeAggregatorNode, - TimeOperatorNode, + TimeEvalNode, + TimeShiftNode, + TimeSumNode, VariableNode, ) -from .visitor import ExpressionVisitor, ExpressionVisitorOperations, T, visit +from .visitor import ExpressionVisitorOperations, visit @dataclass(frozen=True) @@ -63,38 +60,31 @@ def comp_variable(self, node: ComponentVariableNode) -> ExpressionNode: def comp_parameter(self, node: ComponentParameterNode) -> ExpressionNode: return ComponentParameterNode(node.component_id, node.name) - def copy_expression_range( - self, expression_range: ExpressionRange - ) -> ExpressionRange: - return ExpressionRange( - start=visit(expression_range.start, self), - stop=visit(expression_range.stop, self), - step=visit(expression_range.step, self) - if expression_range.step is not None - else None, + def pb_variable(self, node: ProblemVariableNode) -> ExpressionNode: + return ProblemVariableNode( + node.component_id, node.name, node.time_index, node.scenario_index ) - def copy_instances_index( - self, instances_index: InstancesTimeIndex - ) -> InstancesTimeIndex: - expressions = instances_index.expressions - if isinstance(expressions, ExpressionRange): - return InstancesTimeIndex(self.copy_expression_range(expressions)) - if isinstance(expressions, list): - expressions_list = cast(List[ExpressionNode], expressions) - copy = [visit(e, self) for e in expressions_list] - return InstancesTimeIndex(copy) - raise ValueError("Unexpected type in instances index") - - def time_operator(self, node: TimeOperatorNode) -> ExpressionNode: - return TimeOperatorNode( + def pb_parameter(self, node: ProblemParameterNode) -> ExpressionNode: + return ProblemParameterNode( + node.component_id, node.name, node.time_index, node.scenario_index + ) + + def time_shift(self, node: TimeShiftNode) -> ExpressionNode: + return TimeShiftNode(visit(node.operand, self), visit(node.time_shift, self)) + + def time_eval(self, node: TimeEvalNode) -> ExpressionNode: + return TimeShiftNode(visit(node.operand, self), visit(node.eval_time, self)) + + def time_sum(self, node: TimeSumNode) -> ExpressionNode: + return TimeSumNode( visit(node.operand, self), - node.name, - self.copy_instances_index(node.instances_index), + visit(node.from_time, self), + visit(node.to_time, self), ) - def time_aggregator(self, node: TimeAggregatorNode) -> ExpressionNode: - return TimeAggregatorNode(visit(node.operand, self), node.name, node.stay_roll) + def all_time_sum(self, node: AllTimeSumNode) -> ExpressionNode: + return AllTimeSumNode(visit(node.operand, self)) def scenario_operator(self, node: ScenarioOperatorNode) -> ExpressionNode: return ScenarioOperatorNode(visit(node.operand, self), node.name) diff --git a/src/andromede/expression/degree.py b/src/andromede/expression/degree.py index cfd175cd..597d08ef 100644 --- a/src/andromede/expression/degree.py +++ b/src/andromede/expression/degree.py @@ -12,11 +12,16 @@ import andromede.expression.scenario_operator from andromede.expression.expression import ( + AllTimeSumNode, ComponentParameterNode, ComponentVariableNode, PortFieldAggregatorNode, PortFieldNode, - TimeOperatorNode, + ProblemParameterNode, + ProblemVariableNode, + TimeEvalNode, + TimeShiftNode, + TimeSumNode, ) from .expression import ( @@ -29,8 +34,6 @@ NegationNode, ParameterNode, ScenarioOperatorNode, - SubstractionNode, - TimeAggregatorNode, VariableNode, ) from .visitor import ExpressionVisitor, T, visit @@ -49,10 +52,8 @@ def negation(self, node: NegationNode) -> int: # TODO: Take into account simplification that can occur with literal coefficient for add, sub, mult, div def addition(self, node: AdditionNode) -> int: - return max(visit(node.left, self), visit(node.right, self)) - - def substraction(self, node: SubstractionNode) -> int: - return max(visit(node.left, self), visit(node.right, self)) + degrees = [visit(o, self) for o in node.operands] + return max(degrees) def multiplication(self, node: MultiplicationNode) -> int: return visit(node.left, self) + visit(node.right, self) @@ -78,17 +79,23 @@ def comp_variable(self, node: ComponentVariableNode) -> int: def comp_parameter(self, node: ComponentParameterNode) -> int: return 0 - def time_operator(self, node: TimeOperatorNode) -> int: - if node.name in ["TimeShift", "TimeEvaluation"]: - return visit(node.operand, self) - else: - return NotImplemented - - def time_aggregator(self, node: TimeAggregatorNode) -> int: - if node.name in ["TimeSum"]: - return visit(node.operand, self) - else: - return NotImplemented + def pb_variable(self, node: ProblemVariableNode) -> int: + return 1 + + def pb_parameter(self, node: ProblemParameterNode) -> int: + return 0 + + def time_shift(self, node: TimeShiftNode) -> int: + return visit(node.operand, self) + + def time_eval(self, node: TimeEvalNode) -> int: + return visit(node.operand, self) + + def time_sum(self, node: TimeSumNode) -> int: + return visit(node.operand, self) + + def all_time_sum(self, node: AllTimeSumNode) -> int: + return visit(node.operand, self) def scenario_operator(self, node: ScenarioOperatorNode) -> int: scenario_operator_cls = getattr( diff --git a/src/andromede/expression/equality.py b/src/andromede/expression/equality.py index a2deeb27..29983037 100644 --- a/src/andromede/expression/equality.py +++ b/src/andromede/expression/equality.py @@ -23,18 +23,21 @@ MultiplicationNode, NegationNode, ParameterNode, - SubstractionNode, VariableNode, ) from andromede.expression.expression import ( + AllTimeSumNode, BinaryOperatorNode, - ExpressionRange, - InstancesTimeIndex, + ComponentParameterNode, + ComponentVariableNode, PortFieldAggregatorNode, PortFieldNode, + ProblemParameterNode, + ProblemVariableNode, ScenarioOperatorNode, - TimeAggregatorNode, - TimeOperatorNode, + TimeEvalNode, + TimeShiftNode, + TimeSumNode, ) @@ -62,8 +65,6 @@ def visit(self, left: ExpressionNode, right: ExpressionNode) -> bool: return self.negation(left, right) if isinstance(left, AdditionNode) and isinstance(right, AdditionNode): return self.addition(left, right) - if isinstance(left, SubstractionNode) and isinstance(right, SubstractionNode): - return self.substraction(left, right) if isinstance(left, DivisionNode) and isinstance(right, DivisionNode): return self.division(left, right) if isinstance(left, MultiplicationNode) and isinstance( @@ -76,12 +77,30 @@ def visit(self, left: ExpressionNode, right: ExpressionNode) -> bool: return self.variable(left, right) if isinstance(left, ParameterNode) and isinstance(right, ParameterNode): return self.parameter(left, right) - if isinstance(left, TimeOperatorNode) and isinstance(right, TimeOperatorNode): - return self.time_operator(left, right) - if isinstance(left, TimeAggregatorNode) and isinstance( - right, TimeAggregatorNode + if isinstance(left, ComponentVariableNode) and isinstance( + right, ComponentVariableNode ): - return self.time_aggregator(left, right) + return self.comp_variable(left, right) + if isinstance(left, ComponentParameterNode) and isinstance( + right, ComponentParameterNode + ): + return self.comp_parameter(left, right) + if isinstance(left, ProblemVariableNode) and isinstance( + right, ProblemVariableNode + ): + return self.problem_variable(left, right) + if isinstance(left, ProblemParameterNode) and isinstance( + right, ProblemParameterNode + ): + return self.problem_parameter(left, right) + if isinstance(left, TimeShiftNode) and isinstance(right, TimeShiftNode): + return self.time_shift(left, right) + if isinstance(left, TimeEvalNode) and isinstance(right, TimeEvalNode): + return self.time_eval(left, right) + if isinstance(left, TimeSumNode) and isinstance(right, TimeSumNode): + return self.time_sum(left, right) + if isinstance(left, AllTimeSumNode) and isinstance(right, AllTimeSumNode): + return self.all_time_sum(left, right) if isinstance(left, ScenarioOperatorNode) and isinstance( right, ScenarioOperatorNode ): @@ -108,10 +127,11 @@ def negation(self, left: NegationNode, right: NegationNode) -> bool: return self.visit(left.operand, right.operand) def addition(self, left: AdditionNode, right: AdditionNode) -> bool: - return self._visit_operands(left, right) - - def substraction(self, left: SubstractionNode, right: SubstractionNode) -> bool: - return self._visit_operands(left, right) + left_ops = left.operands + right_ops = right.operands + return len(left_ops) == len(right_ops) and all( + self.visit(l, r) for l, r in zip(left_ops, right_ops) + ) def multiplication( self, left: MultiplicationNode, right: MultiplicationNode @@ -130,42 +150,56 @@ def variable(self, left: VariableNode, right: VariableNode) -> bool: def parameter(self, left: ParameterNode, right: ParameterNode) -> bool: return left.name == right.name - def expression_range(self, left: ExpressionRange, right: ExpressionRange) -> bool: - if not self.visit(left.start, right.start): - return False - if not self.visit(left.stop, right.stop): - return False - if left.step is not None and right.step is not None: - return self.visit(left.step, right.step) - return left.step is None and right.step is None + def comp_variable( + self, left: ComponentVariableNode, right: ComponentVariableNode + ) -> bool: + return left.name == right.name and left.component_id == right.component_id - def instances_index(self, lhs: InstancesTimeIndex, rhs: InstancesTimeIndex) -> bool: - if isinstance(lhs.expressions, ExpressionRange) and isinstance( - rhs.expressions, ExpressionRange - ): - return self.expression_range(lhs.expressions, rhs.expressions) - if isinstance(lhs.expressions, list) and isinstance(rhs.expressions, list): - return len(lhs.expressions) == len(rhs.expressions) and all( - self.visit(l, r) for l, r in zip(lhs.expressions, rhs.expressions) - ) - return False + def comp_parameter( + self, left: ComponentParameterNode, right: ComponentParameterNode + ) -> bool: + return left.name == right.name and left.component_id == right.component_id - def time_operator(self, left: TimeOperatorNode, right: TimeOperatorNode) -> bool: + def problem_variable( + self, left: ProblemVariableNode, right: ProblemVariableNode + ) -> bool: return ( left.name == right.name - and self.instances_index(left.instances_index, right.instances_index) - and self.visit(left.operand, right.operand) + and left.component_id == right.component_id + and left.time_index == right.time_index + and left.scenario_index == right.scenario_index ) - def time_aggregator( - self, left: TimeAggregatorNode, right: TimeAggregatorNode + def problem_parameter( + self, left: ProblemParameterNode, right: ProblemParameterNode ) -> bool: return ( left.name == right.name - and left.stay_roll == right.stay_roll + and left.component_id == right.component_id + and left.time_index == right.time_index + and left.scenario_index == right.scenario_index + ) + + def time_shift(self, left: TimeShiftNode, right: TimeShiftNode) -> bool: + return self.visit(left.time_shift, right.time_shift) and self.visit( + left.operand, right.operand + ) + + def time_eval(self, left: TimeEvalNode, right: TimeEvalNode) -> bool: + return self.visit(left.eval_time, right.eval_time) and self.visit( + left.operand, right.operand + ) + + def time_sum(self, left: TimeSumNode, right: TimeSumNode) -> bool: + return ( + self.visit(left.from_time, right.from_time) + and self.visit(left.to_time, right.to_time) and self.visit(left.operand, right.operand) ) + def all_time_sum(self, left: AllTimeSumNode, right: AllTimeSumNode) -> bool: + return self.visit(left.operand, right.operand) + def scenario_operator( self, left: ScenarioOperatorNode, right: ScenarioOperatorNode ) -> bool: diff --git a/src/andromede/expression/evaluate.py b/src/andromede/expression/evaluate.py index b51c0e86..a7f524e9 100644 --- a/src/andromede/expression/evaluate.py +++ b/src/andromede/expression/evaluate.py @@ -15,28 +15,28 @@ from typing import Dict from andromede.expression.expression import ( + AllTimeSumNode, ComponentParameterNode, ComponentVariableNode, PortFieldAggregatorNode, PortFieldNode, - TimeOperatorNode, + ProblemParameterNode, + ProblemVariableNode, + TimeEvalNode, + TimeShiftNode, + TimeSumNode, ) from .expression import ( - AdditionNode, ComparisonNode, - DivisionNode, ExpressionNode, LiteralNode, - MultiplicationNode, - NegationNode, ParameterNode, ScenarioOperatorNode, - SubstractionNode, - TimeAggregatorNode, VariableNode, ) -from .visitor import ExpressionVisitor, ExpressionVisitorOperations, T, visit +from .indexing import IndexingStructureProvider +from .visitor import ExpressionVisitorOperations, visit class ValueProvider(ABC): @@ -61,11 +61,6 @@ def get_component_variable_value(self, component_id: str, name: str) -> float: def get_component_parameter_value(self, component_id: str, name: str) -> float: ... - # TODO: Should this really be an abstract method ? Or maybe, only the Provider in _make_value_provider should implement it. And the context attribute in the InstancesIndexVisitor is a ValueProvider that implements the parameter_is_constant_over_time method. Maybe create a child class of ValueProvider like TimeValueProvider ? - @abstractmethod - def parameter_is_constant_over_time(self, name: str) -> bool: - ... - @dataclass(frozen=True) class EvaluationContext(ValueProvider): @@ -89,9 +84,6 @@ def get_component_variable_value(self, component_id: str, name: str) -> float: def get_component_parameter_value(self, component_id: str, name: str) -> float: raise NotImplementedError() - def parameter_is_constant_over_time(self, name: str) -> bool: - raise NotImplementedError() - @dataclass(frozen=True) class EvaluationVisitor(ExpressionVisitorOperations[float]): @@ -120,10 +112,22 @@ def comp_parameter(self, node: ComponentParameterNode) -> float: def comp_variable(self, node: ComponentVariableNode) -> float: return self.context.get_component_variable_value(node.component_id, node.name) - def time_operator(self, node: TimeOperatorNode) -> float: + def pb_parameter(self, node: ProblemParameterNode) -> float: + raise ValueError("Should not reach here.") + + def pb_variable(self, node: ProblemVariableNode) -> float: + raise ValueError("Should not reach here.") + + def time_shift(self, node: TimeShiftNode) -> float: + raise NotImplementedError() + + def time_eval(self, node: TimeEvalNode) -> float: raise NotImplementedError() - def time_aggregator(self, node: TimeAggregatorNode) -> float: + def time_sum(self, node: TimeSumNode) -> float: + raise NotImplementedError() + + def all_time_sum(self, node: AllTimeSumNode) -> float: raise NotImplementedError() def scenario_operator(self, node: ScenarioOperatorNode) -> float: @@ -138,26 +142,3 @@ def port_field_aggregator(self, node: PortFieldAggregatorNode) -> float: def evaluate(expression: ExpressionNode, value_provider: ValueProvider) -> float: return visit(expression, EvaluationVisitor(value_provider)) - - -@dataclass(frozen=True) -class InstancesIndexVisitor(EvaluationVisitor): - """ - Evaluates an expression given as instances index which should have no variable and constant parameter values. - """ - - def variable(self, node: VariableNode) -> float: - raise ValueError("An instance index expression cannot contain variable") - - def parameter(self, node: ParameterNode) -> float: - if not self.context.parameter_is_constant_over_time(node.name): - raise ValueError( - "Parameter given in an instance index expression must be constant over time" - ) - return self.context.get_parameter_value(node.name) - - def time_operator(self, node: TimeOperatorNode) -> float: - raise ValueError("An instance index expression cannot contain time operator") - - def time_aggregator(self, node: TimeAggregatorNode) -> float: - raise ValueError("An instance index expression cannot contain time aggregator") diff --git a/src/andromede/expression/evaluate_parameters.py b/src/andromede/expression/evaluate_parameters.py index 7c734260..e6e9cfe6 100644 --- a/src/andromede/expression/evaluate_parameters.py +++ b/src/andromede/expression/evaluate_parameters.py @@ -12,16 +12,13 @@ from abc import ABC, abstractmethod from dataclasses import dataclass -from typing import List -from andromede.expression.evaluate import InstancesIndexVisitor, ValueProvider +from andromede.expression.evaluate import ValueProvider from .copy import CopyVisitor from .expression import ( ComponentParameterNode, ExpressionNode, - ExpressionRange, - InstancesTimeIndex, LiteralNode, ParameterNode, ) @@ -61,39 +58,3 @@ def resolve_parameters( expression: ExpressionNode, parameter_provider: ParameterValueProvider ) -> ExpressionNode: return visit(expression, ParameterResolver(parameter_provider)) - - -def float_to_int(value: float) -> int: - if isinstance(value, int) or value.is_integer(): - return int(value) - else: - raise ValueError(f"{value} is not an integer.") - - -def evaluate_time_id(expr: ExpressionNode, value_provider: ValueProvider) -> int: - float_time_id = visit(expr, InstancesIndexVisitor(value_provider)) - try: - time_id = float_to_int(float_time_id) - except ValueError: - print(f"{expr} does not represent an integer time index.") - return time_id - - -def get_time_ids_from_instances_index( - instances_index: InstancesTimeIndex, value_provider: ValueProvider -) -> List[int]: - time_ids = [] - if isinstance(instances_index.expressions, list): # List[ExpressionNode] - for expr in instances_index.expressions: - time_ids.append(evaluate_time_id(expr, value_provider)) - - elif isinstance(instances_index.expressions, ExpressionRange): # ExpressionRange - start_id = evaluate_time_id(instances_index.expressions.start, value_provider) - stop_id = evaluate_time_id(instances_index.expressions.stop, value_provider) - step_id = 1 - if instances_index.expressions.step is not None: - step_id = evaluate_time_id(instances_index.expressions.step, value_provider) - # ExpressionRange includes stop_id whereas range excludes it - time_ids = list(range(start_id, stop_id + 1, step_id)) - - return time_ids diff --git a/src/andromede/expression/expression.py b/src/andromede/expression/expression.py index 5b962caa..d0e6c43a 100644 --- a/src/andromede/expression/expression.py +++ b/src/andromede/expression/expression.py @@ -15,17 +15,13 @@ """ import enum import inspect -from dataclasses import dataclass, field +from dataclasses import dataclass from typing import Any, Callable, List, Optional, Sequence, Union import andromede.expression.port_operator import andromede.expression.scenario_operator -import andromede.expression.time_operator - -class Instances(enum.Enum): - SIMPLE = "SIMPLE" - MULTIPLE = "MULTIPLE" +AnyExpression = Union[int, float, "ExpressionNode"] @dataclass(frozen=True) @@ -40,22 +36,33 @@ class ExpressionNode: >>> expr = -var('x') + 5 / param('p') """ - instances: Instances = field(init=False, default=Instances.SIMPLE) - def __neg__(self) -> "ExpressionNode": return NegationNode(self) def __add__(self, rhs: Any) -> "ExpressionNode": - return _apply_if_node(rhs, lambda x: AdditionNode(self, x)) + lhs = self + operands = [] + rhs = _wrap_in_node(rhs) + operands.extend(lhs.operands if isinstance(lhs, AdditionNode) else [lhs]) + operands.extend(rhs.operands if isinstance(rhs, AdditionNode) else [rhs]) + return AdditionNode(operands) def __radd__(self, lhs: Any) -> "ExpressionNode": - return _apply_if_node(lhs, lambda x: AdditionNode(x, self)) + lhs = _wrap_in_node(lhs) + return lhs + self def __sub__(self, rhs: Any) -> "ExpressionNode": - return _apply_if_node(rhs, lambda x: SubstractionNode(self, x)) + lhs = self + operands = [] + rhs = _wrap_in_node(rhs) + operands.extend(lhs.operands if isinstance(lhs, AdditionNode) else [lhs]) + right_operands = rhs.operands if isinstance(rhs, AdditionNode) else [rhs] + operands.extend([-o for o in right_operands]) + return AdditionNode(operands) def __rsub__(self, lhs: Any) -> "ExpressionNode": - return _apply_if_node(lhs, lambda x: SubstractionNode(x, self)) + lhs = _wrap_in_node(lhs) + return lhs + self def __mul__(self, rhs: Any) -> "ExpressionNode": return _apply_if_node(rhs, lambda x: MultiplicationNode(self, x)) @@ -82,13 +89,20 @@ def __ge__(self, rhs: Any) -> "ExpressionNode": def __eq__(self, rhs: Any) -> "ExpressionNode": # type: ignore return _apply_if_node(rhs, lambda x: ComparisonNode(self, x, Comparator.EQUAL)) - def sum(self) -> "ExpressionNode": - if isinstance(self, TimeOperatorNode): - return TimeAggregatorNode(self, "TimeSum", stay_roll=True) - else: - return _apply_if_node( - self, lambda x: TimeAggregatorNode(x, "TimeSum", stay_roll=False) - ) + def time_sum( + self, + from_shift: Optional[AnyExpression] = None, + to_shift: Optional[AnyExpression] = None, + ) -> "ExpressionNode": + if from_shift is None and to_shift is None: + return AllTimeSumNode(self) + if from_shift is None or to_shift is None: + raise ValueError("Both time bounds of a time sum must be defined.") + return TimeSumNode( + operand=self, + from_time=_wrap_in_node(from_shift), + to_time=_wrap_in_node(to_shift), + ) def sum_connections(self) -> "ExpressionNode": if isinstance(self, PortFieldNode): @@ -97,29 +111,11 @@ def sum_connections(self) -> "ExpressionNode": f"sum_connections() applies only for PortFieldNode, whereas the current node is of type {type(self)}." ) - def shift( - self, - expressions: Union[ - int, "ExpressionNode", List["ExpressionNode"], "ExpressionRange" - ], - ) -> "ExpressionNode": - return _apply_if_node( - self, - lambda x: TimeOperatorNode(x, "TimeShift", InstancesTimeIndex(expressions)), - ) + def shift(self, shift: AnyExpression) -> "ExpressionNode": + return TimeShiftNode(self, _wrap_in_node(shift)) - def eval( - self, - expressions: Union[ - int, "ExpressionNode", List["ExpressionNode"], "ExpressionRange" - ], - ) -> "ExpressionNode": - return _apply_if_node( - self, - lambda x: TimeOperatorNode( - x, "TimeEvaluation", InstancesTimeIndex(expressions) - ), - ) + def eval(self, time: AnyExpression) -> "ExpressionNode": + return TimeEvalNode(self, _wrap_in_node(time)) def expec(self) -> "ExpressionNode": return _apply_if_node(self, lambda x: ScenarioOperatorNode(x, "Expectation")) @@ -195,6 +191,102 @@ def comp_param(component_id: str, name: str) -> ComponentParameterNode: return ComponentParameterNode(component_id, name) +@dataclass(frozen=True) +class TimeIndex: + pass + + +@dataclass(frozen=True) +class NoTimeIndex(TimeIndex): + """ + Some values do not depend on the timestep, this index should be used for them + (think one variable for all timesteps). + """ + + pass + + +@dataclass(frozen=True) +class TimeShift(TimeIndex): + """ + Represents the current timestep + a shift. + + This should only be used for nodes that actually depend on the timestep, + never for time-independent nodes (constant parameters ...). + """ + + timeshift: int + + +@dataclass(frozen=True) +class TimeStep(TimeIndex): + """ + Represents a given timestep, independently of the current timestep. + + This should only be used for nodes that actually depend on the timestep, + never for time-independent nodes (constant parameters ...). + """ + + timestep: int + + +@dataclass(frozen=True) +class ScenarioIndex: + pass + + +@dataclass(frozen=True) +class NoScenarioIndex(ScenarioIndex): + """ + Some values do not depend on the scenario, this index should be used for them + (think one variable for all timesteps). + """ + + pass + + +@dataclass(frozen=True) +class CurrentScenarioIndex(ScenarioIndex): + """ + Represents the current scenario. + + This should only be used for nodes that actually depend on the scenario, + never for scenario-independent nodes (constant parameters ...). + """ + + pass + + +@dataclass(frozen=True) +class OneScenarioIndex(ScenarioIndex): + """ + Represents a given scenario out of all scenarios. + + This should only be used for nodes that actually depend on the scenario, + never for scenario-independent nodes (constant parameters ...). + """ + + scenario: int + + +@dataclass(frozen=True, eq=False) +class ProblemParameterNode(ExpressionNode): + """ + Represents one parameter of the optimization problem + """ + + component_id: str + name: str + time_index: TimeIndex + scenario_index: ScenarioIndex + + +def problem_param( + component_id: str, name: str, time_index: TimeIndex, scenario_index: ScenarioIndex +) -> ProblemParameterNode: + return ProblemParameterNode(component_id, name, time_index, scenario_index) + + @dataclass(frozen=True, eq=False) class ComponentVariableNode(ExpressionNode): """ @@ -213,6 +305,24 @@ def comp_var(component_id: str, name: str) -> ComponentVariableNode: return ComponentVariableNode(component_id, name) +@dataclass(frozen=True, eq=False) +class ProblemVariableNode(ExpressionNode): + """ + Represents one variable of the optimization problem + """ + + component_id: str + name: str + time_index: TimeIndex + scenario_index: ScenarioIndex + + +def problem_var( + component_id: str, name: str, time_index: TimeIndex, scenario_index: ScenarioIndex +) -> ProblemVariableNode: + return ProblemVariableNode(component_id, name, time_index, scenario_index) + + @dataclass(frozen=True, eq=False) class LiteralNode(ExpressionNode): value: float @@ -234,9 +344,6 @@ def is_non_negative(expr: ExpressionNode) -> bool: class UnaryOperatorNode(ExpressionNode): operand: ExpressionNode - def __post_init__(self) -> None: - object.__setattr__(self, "instances", self.operand.instances) - @dataclass(frozen=True, eq=False) class PortFieldAggregatorNode(UnaryOperatorNode): @@ -266,18 +373,6 @@ class BinaryOperatorNode(ExpressionNode): left: ExpressionNode right: ExpressionNode - def __post_init__(self) -> None: - binary_operator_post_init(self, "apply binary operation with") - - -def binary_operator_post_init(node: BinaryOperatorNode, operation: str) -> None: - if node.left.instances != node.right.instances: - raise ValueError( - f"Cannot {operation} {node.left} and {node.right} as they do not have the same number of instances." - ) - else: - object.__setattr__(node, "instances", node.left.instances) - class Comparator(enum.Enum): LESS_THAN = "LESS_THAN" @@ -289,148 +384,46 @@ class Comparator(enum.Enum): class ComparisonNode(BinaryOperatorNode): comparator: Comparator - def __post_init__(self) -> None: - binary_operator_post_init(self, "compare") - - -@dataclass(frozen=True, eq=False) -class AdditionNode(BinaryOperatorNode): - def __post_init__(self) -> None: - binary_operator_post_init(self, "add") - @dataclass(frozen=True, eq=False) -class SubstractionNode(BinaryOperatorNode): - def __post_init__(self) -> None: - binary_operator_post_init(self, "substract") +class AdditionNode(ExpressionNode): + operands: List[ExpressionNode] @dataclass(frozen=True, eq=False) class MultiplicationNode(BinaryOperatorNode): - def __post_init__(self) -> None: - binary_operator_post_init(self, "multiply") + pass @dataclass(frozen=True, eq=False) class DivisionNode(BinaryOperatorNode): - def __post_init__(self) -> None: - binary_operator_post_init(self, "divide") + pass @dataclass(frozen=True, eq=False) -class ExpressionRange: - start: ExpressionNode - stop: ExpressionNode - step: Optional[ExpressionNode] = None - - def __post_init__(self) -> None: - for attribute in self.__dict__: - value = getattr(self, attribute) - object.__setattr__( - self, attribute, _wrap_in_node(value) if value is not None else value - ) - +class TimeShiftNode(UnaryOperatorNode): + time_shift: ExpressionNode -IntOrExpr = Union[int, ExpressionNode] - -def expression_range( - start: IntOrExpr, stop: IntOrExpr, step: Optional[IntOrExpr] = None -) -> ExpressionRange: - return ExpressionRange( - start=_wrap_in_node(start), - stop=_wrap_in_node(stop), - step=None if step is None else _wrap_in_node(step), - ) - - -@dataclass -class InstancesTimeIndex: - """ - Defines a set of time indices on which a time operator operates. - - In particular, it defines time indices created by the shift operator. - - The actual indices can either be defined as a time range defined by - 2 expression, or as a list of expressions. - """ - - expressions: Union[List[ExpressionNode], ExpressionRange] - - def __init__( - self, - expressions: Union[int, ExpressionNode, List[ExpressionNode], ExpressionRange], - ) -> None: - if not isinstance(expressions, (int, ExpressionNode, list, ExpressionRange)): - raise TypeError( - f"{expressions} must be of type among {{int, ExpressionNode, List[ExpressionNode], ExpressionRange}}" - ) - if isinstance(expressions, list) and not all( - isinstance(x, ExpressionNode) for x in expressions - ): - raise TypeError( - f"All elements of {expressions} must be of type ExpressionNode" - ) - - if isinstance(expressions, (int, ExpressionNode)): - self.expressions = [_wrap_in_node(expressions)] - else: - self.expressions = expressions - - def is_simple(self) -> bool: - if isinstance(self.expressions, list): - return len(self.expressions) == 1 - else: - # TODO: We could also check that if a range only includes literal nodes, compute the length of the range, if it's one return True. This is more complicated, I do not know if we want to do this - return False +@dataclass(frozen=True, eq=False) +class TimeEvalNode(UnaryOperatorNode): + eval_time: ExpressionNode @dataclass(frozen=True, eq=False) -class TimeOperatorNode(UnaryOperatorNode): - name: str - instances_index: InstancesTimeIndex - - def __post_init__(self) -> None: - valid_names = [ - cls.__name__ - for _, cls in inspect.getmembers( - andromede.expression.time_operator, inspect.isclass - ) - if issubclass(cls, andromede.expression.time_operator.TimeOperator) - ] - if self.name not in valid_names: - raise ValueError( - f"{self.name} is not a valid time aggregator, valid time aggregators are {valid_names}" - ) - if self.operand.instances == Instances.SIMPLE: - if self.instances_index.is_simple(): - object.__setattr__(self, "instances", Instances.SIMPLE) - else: - object.__setattr__(self, "instances", Instances.MULTIPLE) - else: - raise ValueError( - "Cannot apply time operator on an expression that already represents multiple instances" - ) +class TimeSumNode(UnaryOperatorNode): + from_time: ExpressionNode + to_time: ExpressionNode @dataclass(frozen=True, eq=False) -class TimeAggregatorNode(UnaryOperatorNode): - name: str - stay_roll: bool +class AllTimeSumNode(UnaryOperatorNode): + """ + Separate from time sum node because it's actually a quite different operation: + In particular, this changes the time indexing. + """ - def __post_init__(self) -> None: - valid_names = [ - cls.__name__ - for _, cls in inspect.getmembers( - andromede.expression.time_operator, inspect.isclass - ) - if issubclass(cls, andromede.expression.time_operator.TimeAggregator) - ] - if self.name not in valid_names: - raise ValueError( - f"{self.name} is not a valid time aggregator, valid time aggregators are {valid_names}" - ) - object.__setattr__(self, "instances", Instances.SIMPLE) + pass @dataclass(frozen=True, eq=False) @@ -449,7 +442,6 @@ def __post_init__(self) -> None: raise ValueError( f"{self.name} is not a valid scenario operator, valid scenario operators are {valid_names}" ) - object.__setattr__(self, "instances", Instances.SIMPLE) def sum_expressions(expressions: Sequence[ExpressionNode]) -> ExpressionNode: @@ -457,4 +449,4 @@ def sum_expressions(expressions: Sequence[ExpressionNode]) -> ExpressionNode: return LiteralNode(0) if len(expressions) == 1: return expressions[0] - return expressions[0] + sum_expressions(expressions[1:]) + return AdditionNode([e for e in expressions]) diff --git a/src/andromede/expression/indexing.py b/src/andromede/expression/indexing.py index 11051dd5..4bc8471c 100644 --- a/src/andromede/expression/indexing.py +++ b/src/andromede/expression/indexing.py @@ -12,12 +12,13 @@ from abc import ABC, abstractmethod from dataclasses import dataclass +from typing import List -import andromede.expression.time_operator from andromede.expression.indexing_structure import IndexingStructure from .expression import ( AdditionNode, + AllTimeSumNode, ComparisonNode, ComponentParameterNode, ComponentVariableNode, @@ -29,10 +30,12 @@ ParameterNode, PortFieldAggregatorNode, PortFieldNode, + ProblemParameterNode, + ProblemVariableNode, ScenarioOperatorNode, - SubstractionNode, - TimeAggregatorNode, - TimeOperatorNode, + TimeEvalNode, + TimeShiftNode, + TimeSumNode, VariableNode, ) from .visitor import ExpressionVisitor, T, visit @@ -74,20 +77,32 @@ def literal(self, node: LiteralNode) -> IndexingStructure: def negation(self, node: NegationNode) -> IndexingStructure: return visit(node.operand, self) - def addition(self, node: AdditionNode) -> IndexingStructure: - return visit(node.left, self) | visit(node.right, self) + def _combine(self, operands: List[ExpressionNode]) -> IndexingStructure: + if not operands: + return IndexingStructure(False, False) + res = visit(operands[0], self) + if res.is_time_scenario_varying(): + return res + for o in operands[1:]: + res = res | visit(o, self) + if res.is_time_scenario_varying(): + return res + return res - def substraction(self, node: SubstractionNode) -> IndexingStructure: - return visit(node.left, self) | visit(node.right, self) + def addition(self, node: AdditionNode) -> IndexingStructure: + # performance note: + # here we don't need to visit all nodes, we can stop as soon as + # index is true/true + return self._combine(node.operands) def multiplication(self, node: MultiplicationNode) -> IndexingStructure: - return visit(node.left, self) | visit(node.right, self) + return self._combine([node.left, node.right]) def division(self, node: DivisionNode) -> IndexingStructure: - return visit(node.left, self) | visit(node.right, self) + return self._combine([node.left, node.right]) def comparison(self, node: ComparisonNode) -> IndexingStructure: - return visit(node.left, self) | visit(node.right, self) + return self._combine([node.left, node.right]) def variable(self, node: VariableNode) -> IndexingStructure: time = self.context.get_variable_structure(node.name).time == True @@ -109,18 +124,27 @@ def comp_parameter(self, node: ComponentParameterNode) -> IndexingStructure: node.component_id, node.name ) - def time_operator(self, node: TimeOperatorNode) -> IndexingStructure: - time_operator_cls = getattr(andromede.expression.time_operator, node.name) - if time_operator_cls.rolling(): - return visit(node.operand, self) - else: - return IndexingStructure(False, visit(node.operand, self).scenario) - - def time_aggregator(self, node: TimeAggregatorNode) -> IndexingStructure: - if node.stay_roll: - return visit(node.operand, self) - else: - return IndexingStructure(False, visit(node.operand, self).scenario) + def pb_variable(self, node: ProblemVariableNode) -> IndexingStructure: + raise ValueError( + "Not relevant to compute indexation on already instantiated problem variables." + ) + + def pb_parameter(self, node: ProblemParameterNode) -> IndexingStructure: + raise ValueError( + "Not relevant to compute indexation on already instantiated problem parameters." + ) + + def time_shift(self, node: TimeShiftNode) -> IndexingStructure: + return visit(node.operand, self) + + def time_eval(self, node: TimeEvalNode) -> IndexingStructure: + return visit(node.operand, self) + + def time_sum(self, node: TimeSumNode) -> IndexingStructure: + return visit(node.operand, self) + + def all_time_sum(self, node: AllTimeSumNode) -> IndexingStructure: + return IndexingStructure(False, visit(node.operand, self).scenario) def scenario_operator(self, node: ScenarioOperatorNode) -> IndexingStructure: return IndexingStructure(visit(node.operand, self).time, False) diff --git a/src/andromede/expression/operators_expansion.py b/src/andromede/expression/operators_expansion.py new file mode 100644 index 00000000..13e2d40d --- /dev/null +++ b/src/andromede/expression/operators_expansion.py @@ -0,0 +1,240 @@ +import dataclasses +from dataclasses import dataclass +from typing import Callable, TypeVar, Union + +from andromede.expression import CopyVisitor, ExpressionNode, sum_expressions, visit +from andromede.expression.expression import ( + AllTimeSumNode, + ComponentParameterNode, + ComponentVariableNode, + CurrentScenarioIndex, + NoScenarioIndex, + NoTimeIndex, + OneScenarioIndex, + ProblemParameterNode, + ProblemVariableNode, + ScenarioOperatorNode, + TimeEvalNode, + TimeShift, + TimeShiftNode, + TimeStep, + TimeSumNode, + problem_param, + problem_var, +) +from andromede.expression.indexing import IndexingStructureProvider + +ExpressionEvaluator = Callable[[ExpressionNode], int] + + +@dataclass(frozen=True) +class ProblemDimensions: + """ + Dimensions for the simulation window + """ + + timesteps_count: int + scenarios_count: int + + +@dataclass(frozen=True) +class ProblemIndex: + """ + Index of an object in the simulation window. + """ + + timestep: int + scenario: int + + +@dataclass(frozen=True) +class OperatorsExpansion(CopyVisitor): + """ + Replaces aggregators (time sum, expectations ...) by their + arithmetic expansion. + + This will allow to easily translate it to a plain linear expression later on, + without complex handling of operators. + + The obtained expression only contains `ProblemVariableNode` for variables + and `ProblemParameterNode` parameters. + """ + + timesteps_count: int + scenarios_count: int + evaluator: ExpressionEvaluator + structure_provider: IndexingStructureProvider + + def comp_variable(self, node: ComponentVariableNode) -> ExpressionNode: + structure = self.structure_provider.get_component_variable_structure( + node.component_id, node.name + ) + time_index = TimeShift(0) if structure.time else NoTimeIndex() + scenario_index = ( + CurrentScenarioIndex() if structure.scenario else NoScenarioIndex() + ) + return problem_var(node.component_id, node.name, time_index, scenario_index) + + def comp_parameter(self, node: ComponentParameterNode) -> ExpressionNode: + structure = self.structure_provider.get_component_parameter_structure( + node.component_id, node.name + ) + time_index = TimeShift(0) if structure.time else NoTimeIndex() + scenario_index = ( + CurrentScenarioIndex() if structure.scenario else NoScenarioIndex() + ) + return problem_param(node.component_id, node.name, time_index, scenario_index) + + def time_shift(self, node: TimeShiftNode) -> ExpressionNode: + shift = self.evaluator(node.time_shift) + operand = visit(node.operand, self) + return apply_timeshift(operand, shift) + + def time_eval(self, node: TimeEvalNode) -> ExpressionNode: + timestep = self.evaluator(node.eval_time) + operand = visit(node.operand, self) + return apply_timestep(operand, timestep) + + def time_sum(self, node: TimeSumNode) -> ExpressionNode: + from_shift = self.evaluator(node.from_time) + to_shift = self.evaluator(node.to_time) + operand = visit(node.operand, self) + nodes = [] + for t in range(from_shift, to_shift + 1): + nodes.append(apply_timeshift(operand, t)) + return sum_expressions(nodes) + + def all_time_sum(self, node: AllTimeSumNode) -> ExpressionNode: + nodes = [] + operand = visit(node.operand, self) + for t in range(self.timesteps_count): + # if we sum previously "evaluated" variables for example x[0], it's ok + nodes.append(apply_timestep(operand, t, allow_existing=True)) + return sum_expressions(nodes) + + def scenario_operator(self, node: ScenarioOperatorNode) -> ExpressionNode: + if node.name != "Expectation": + raise ValueError(f"Scenario operator not supported: {node.name}") + nodes = [] + operand = visit(node.operand, self) + for t in range(self.scenarios_count): + nodes.append(apply_scenario(operand, t)) + return sum_expressions(nodes) / self.scenarios_count + + def pb_parameter(self, node: ProblemParameterNode) -> ExpressionNode: + raise ValueError("Should not reach") + + def pb_variable(self, node: ProblemVariableNode) -> ExpressionNode: + raise ValueError("Should not reach") + + +def expand_operators( + expression: ExpressionNode, + dimensions: ProblemDimensions, + evaluator: ExpressionEvaluator, + structure_provider: IndexingStructureProvider, +) -> ExpressionNode: + return visit( + expression, + OperatorsExpansion( + dimensions.timesteps_count, + dimensions.scenarios_count, + evaluator, + structure_provider, + ), + ) + + +TimeIndexedNode = TypeVar( + "TimeIndexedNode", bound=Union[ProblemParameterNode, ProblemVariableNode] +) + + +@dataclass(frozen=True) +class ApplyTimeShift(CopyVisitor): + """ + Shifts all underlying expressions. + """ + + timeshift: int + + def _apply_timeshift(self, node: TimeIndexedNode) -> TimeIndexedNode: + current_index = node.time_index + if isinstance(current_index, TimeShift): + return dataclasses.replace( + node, time_index=TimeShift(current_index.timeshift + self.timeshift) + ) + if isinstance(current_index, TimeStep): + return dataclasses.replace( + node, time_index=TimeStep(current_index.timestep + self.timeshift) + ) + if isinstance(current_index, NoTimeIndex): + return node + raise ValueError("Unknown time index type.") + + def pb_parameter(self, node: ProblemParameterNode) -> ProblemParameterNode: + return self._apply_timeshift(node) + + def pb_variable(self, node: ProblemVariableNode) -> ProblemVariableNode: + return self._apply_timeshift(node) + + +def apply_timeshift(expression: ExpressionNode, timeshift: int) -> ExpressionNode: + return visit(expression, ApplyTimeShift(timeshift)) + + +@dataclass(frozen=True) +class ApplyTimeStep(CopyVisitor): + """ + Applies timestep to all underlying expressions. + """ + + timestep: int + allow_existing: bool = False + + def _apply_timestep(self, node: TimeIndexedNode) -> TimeIndexedNode: + current_index = node.time_index + if isinstance(current_index, TimeShift): + return dataclasses.replace( + node, time_index=TimeStep(current_index.timeshift + self.timestep) + ) + if isinstance(current_index, TimeStep): + if not self.allow_existing: + raise ValueError( + "Cannot override a previously defined timestep (for example (x[0])[1])." + ) + return node + if isinstance(current_index, NoTimeIndex): + return node + raise ValueError("Unknown time index type.") + + def pb_parameter(self, node: ProblemParameterNode) -> ExpressionNode: + return self._apply_timestep(node) + + def pb_variable(self, node: ProblemVariableNode) -> ExpressionNode: + return self._apply_timestep(node) + + +def apply_timestep( + expression: ExpressionNode, timestep: int, allow_existing: bool = False +) -> ExpressionNode: + return visit(expression, ApplyTimeStep(timestep, allow_existing)) + + +@dataclass(frozen=True) +class ApplyScenario(CopyVisitor): + scenario: int + + def pb_parameter(self, node: ProblemParameterNode) -> ExpressionNode: + if isinstance(node.scenario_index, NoScenarioIndex): + return node + return dataclasses.replace(node, scenario_index=OneScenarioIndex(self.scenario)) + + def pb_variable(self, node: ProblemVariableNode) -> ExpressionNode: + if isinstance(node.scenario_index, NoScenarioIndex): + return node + return dataclasses.replace(node, scenario_index=OneScenarioIndex(self.scenario)) + + +def apply_scenario(expression: ExpressionNode, scenario: int) -> ExpressionNode: + return visit(expression, ApplyScenario(scenario)) diff --git a/src/andromede/expression/parsing/antlr/Expr.interp b/src/andromede/expression/parsing/antlr/Expr.interp index bf05ae28..c715291b 100644 --- a/src/andromede/expression/parsing/antlr/Expr.interp +++ b/src/andromede/expression/parsing/antlr/Expr.interp @@ -7,10 +7,11 @@ null '/' '*' '+' -'[' +'sum' +'..' ',' +'[' ']' -'..' null 't' null @@ -30,6 +31,7 @@ null null null null +null NUMBER TIME IDENTIFIER @@ -46,4 +48,4 @@ right_expr atn: -[4, 1, 16, 131, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5, 1, 37, 8, 1, 10, 1, 12, 1, 40, 9, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5, 1, 49, 8, 1, 10, 1, 12, 1, 52, 9, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 70, 8, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5, 1, 81, 8, 1, 10, 1, 12, 1, 84, 9, 1, 1, 2, 1, 2, 3, 2, 88, 8, 2, 1, 3, 1, 3, 3, 3, 92, 8, 3, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 3, 4, 102, 8, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 5, 4, 110, 8, 4, 10, 4, 12, 4, 113, 9, 4, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 3, 5, 121, 8, 5, 1, 5, 1, 5, 1, 5, 5, 5, 126, 8, 5, 10, 5, 12, 5, 129, 9, 5, 1, 5, 0, 3, 2, 8, 10, 6, 0, 2, 4, 6, 8, 10, 0, 2, 1, 0, 5, 6, 2, 0, 2, 2, 7, 7, 144, 0, 12, 1, 0, 0, 0, 2, 69, 1, 0, 0, 0, 4, 87, 1, 0, 0, 0, 6, 89, 1, 0, 0, 0, 8, 101, 1, 0, 0, 0, 10, 120, 1, 0, 0, 0, 12, 13, 3, 2, 1, 0, 13, 14, 5, 0, 0, 1, 14, 1, 1, 0, 0, 0, 15, 16, 6, 1, -1, 0, 16, 70, 3, 4, 2, 0, 17, 18, 5, 14, 0, 0, 18, 19, 5, 1, 0, 0, 19, 70, 5, 14, 0, 0, 20, 21, 5, 2, 0, 0, 21, 70, 3, 2, 1, 10, 22, 23, 5, 3, 0, 0, 23, 24, 3, 2, 1, 0, 24, 25, 5, 4, 0, 0, 25, 70, 1, 0, 0, 0, 26, 27, 5, 14, 0, 0, 27, 28, 5, 3, 0, 0, 28, 29, 3, 2, 1, 0, 29, 30, 5, 4, 0, 0, 30, 70, 1, 0, 0, 0, 31, 32, 5, 14, 0, 0, 32, 33, 5, 8, 0, 0, 33, 38, 3, 6, 3, 0, 34, 35, 5, 9, 0, 0, 35, 37, 3, 6, 3, 0, 36, 34, 1, 0, 0, 0, 37, 40, 1, 0, 0, 0, 38, 36, 1, 0, 0, 0, 38, 39, 1, 0, 0, 0, 39, 41, 1, 0, 0, 0, 40, 38, 1, 0, 0, 0, 41, 42, 5, 10, 0, 0, 42, 70, 1, 0, 0, 0, 43, 44, 5, 14, 0, 0, 44, 45, 5, 8, 0, 0, 45, 50, 3, 2, 1, 0, 46, 47, 5, 9, 0, 0, 47, 49, 3, 2, 1, 0, 48, 46, 1, 0, 0, 0, 49, 52, 1, 0, 0, 0, 50, 48, 1, 0, 0, 0, 50, 51, 1, 0, 0, 0, 51, 53, 1, 0, 0, 0, 52, 50, 1, 0, 0, 0, 53, 54, 5, 10, 0, 0, 54, 70, 1, 0, 0, 0, 55, 56, 5, 14, 0, 0, 56, 57, 5, 8, 0, 0, 57, 58, 3, 6, 3, 0, 58, 59, 5, 11, 0, 0, 59, 60, 3, 6, 3, 0, 60, 61, 5, 10, 0, 0, 61, 70, 1, 0, 0, 0, 62, 63, 5, 14, 0, 0, 63, 64, 5, 8, 0, 0, 64, 65, 3, 2, 1, 0, 65, 66, 5, 11, 0, 0, 66, 67, 3, 2, 1, 0, 67, 68, 5, 10, 0, 0, 68, 70, 1, 0, 0, 0, 69, 15, 1, 0, 0, 0, 69, 17, 1, 0, 0, 0, 69, 20, 1, 0, 0, 0, 69, 22, 1, 0, 0, 0, 69, 26, 1, 0, 0, 0, 69, 31, 1, 0, 0, 0, 69, 43, 1, 0, 0, 0, 69, 55, 1, 0, 0, 0, 69, 62, 1, 0, 0, 0, 70, 82, 1, 0, 0, 0, 71, 72, 10, 8, 0, 0, 72, 73, 7, 0, 0, 0, 73, 81, 3, 2, 1, 9, 74, 75, 10, 7, 0, 0, 75, 76, 7, 1, 0, 0, 76, 81, 3, 2, 1, 8, 77, 78, 10, 6, 0, 0, 78, 79, 5, 15, 0, 0, 79, 81, 3, 2, 1, 7, 80, 71, 1, 0, 0, 0, 80, 74, 1, 0, 0, 0, 80, 77, 1, 0, 0, 0, 81, 84, 1, 0, 0, 0, 82, 80, 1, 0, 0, 0, 82, 83, 1, 0, 0, 0, 83, 3, 1, 0, 0, 0, 84, 82, 1, 0, 0, 0, 85, 88, 5, 12, 0, 0, 86, 88, 5, 14, 0, 0, 87, 85, 1, 0, 0, 0, 87, 86, 1, 0, 0, 0, 88, 5, 1, 0, 0, 0, 89, 91, 5, 13, 0, 0, 90, 92, 3, 8, 4, 0, 91, 90, 1, 0, 0, 0, 91, 92, 1, 0, 0, 0, 92, 7, 1, 0, 0, 0, 93, 94, 6, 4, -1, 0, 94, 95, 7, 1, 0, 0, 95, 102, 3, 4, 2, 0, 96, 97, 7, 1, 0, 0, 97, 98, 5, 3, 0, 0, 98, 99, 3, 2, 1, 0, 99, 100, 5, 4, 0, 0, 100, 102, 1, 0, 0, 0, 101, 93, 1, 0, 0, 0, 101, 96, 1, 0, 0, 0, 102, 111, 1, 0, 0, 0, 103, 104, 10, 4, 0, 0, 104, 105, 7, 0, 0, 0, 105, 110, 3, 10, 5, 0, 106, 107, 10, 3, 0, 0, 107, 108, 7, 1, 0, 0, 108, 110, 3, 10, 5, 0, 109, 103, 1, 0, 0, 0, 109, 106, 1, 0, 0, 0, 110, 113, 1, 0, 0, 0, 111, 109, 1, 0, 0, 0, 111, 112, 1, 0, 0, 0, 112, 9, 1, 0, 0, 0, 113, 111, 1, 0, 0, 0, 114, 115, 6, 5, -1, 0, 115, 116, 5, 3, 0, 0, 116, 117, 3, 2, 1, 0, 117, 118, 5, 4, 0, 0, 118, 121, 1, 0, 0, 0, 119, 121, 3, 4, 2, 0, 120, 114, 1, 0, 0, 0, 120, 119, 1, 0, 0, 0, 121, 127, 1, 0, 0, 0, 122, 123, 10, 3, 0, 0, 123, 124, 7, 0, 0, 0, 124, 126, 3, 10, 5, 4, 125, 122, 1, 0, 0, 0, 126, 129, 1, 0, 0, 0, 127, 125, 1, 0, 0, 0, 127, 128, 1, 0, 0, 0, 128, 11, 1, 0, 0, 0, 129, 127, 1, 0, 0, 0, 12, 38, 50, 69, 80, 82, 87, 91, 101, 109, 111, 120, 127] \ No newline at end of file +[4, 1, 17, 117, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 56, 8, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5, 1, 67, 8, 1, 10, 1, 12, 1, 70, 9, 1, 1, 2, 1, 2, 3, 2, 74, 8, 2, 1, 3, 1, 3, 3, 3, 78, 8, 3, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 3, 4, 88, 8, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 5, 4, 96, 8, 4, 10, 4, 12, 4, 99, 9, 4, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 3, 5, 107, 8, 5, 1, 5, 1, 5, 1, 5, 5, 5, 112, 8, 5, 10, 5, 12, 5, 115, 9, 5, 1, 5, 0, 3, 2, 8, 10, 6, 0, 2, 4, 6, 8, 10, 0, 2, 1, 0, 5, 6, 2, 0, 2, 2, 7, 7, 128, 0, 12, 1, 0, 0, 0, 2, 55, 1, 0, 0, 0, 4, 73, 1, 0, 0, 0, 6, 75, 1, 0, 0, 0, 8, 87, 1, 0, 0, 0, 10, 106, 1, 0, 0, 0, 12, 13, 3, 2, 1, 0, 13, 14, 5, 0, 0, 1, 14, 1, 1, 0, 0, 0, 15, 16, 6, 1, -1, 0, 16, 56, 3, 4, 2, 0, 17, 18, 5, 15, 0, 0, 18, 19, 5, 1, 0, 0, 19, 56, 5, 15, 0, 0, 20, 21, 5, 2, 0, 0, 21, 56, 3, 2, 1, 10, 22, 23, 5, 3, 0, 0, 23, 24, 3, 2, 1, 0, 24, 25, 5, 4, 0, 0, 25, 56, 1, 0, 0, 0, 26, 27, 5, 8, 0, 0, 27, 28, 5, 3, 0, 0, 28, 29, 3, 2, 1, 0, 29, 30, 5, 4, 0, 0, 30, 56, 1, 0, 0, 0, 31, 32, 5, 8, 0, 0, 32, 33, 5, 3, 0, 0, 33, 34, 3, 6, 3, 0, 34, 35, 5, 9, 0, 0, 35, 36, 3, 6, 3, 0, 36, 37, 5, 10, 0, 0, 37, 38, 3, 2, 1, 0, 38, 39, 5, 4, 0, 0, 39, 56, 1, 0, 0, 0, 40, 41, 5, 15, 0, 0, 41, 42, 5, 3, 0, 0, 42, 43, 3, 2, 1, 0, 43, 44, 5, 4, 0, 0, 44, 56, 1, 0, 0, 0, 45, 46, 5, 15, 0, 0, 46, 47, 5, 11, 0, 0, 47, 48, 3, 6, 3, 0, 48, 49, 5, 12, 0, 0, 49, 56, 1, 0, 0, 0, 50, 51, 5, 15, 0, 0, 51, 52, 5, 11, 0, 0, 52, 53, 3, 2, 1, 0, 53, 54, 5, 12, 0, 0, 54, 56, 1, 0, 0, 0, 55, 15, 1, 0, 0, 0, 55, 17, 1, 0, 0, 0, 55, 20, 1, 0, 0, 0, 55, 22, 1, 0, 0, 0, 55, 26, 1, 0, 0, 0, 55, 31, 1, 0, 0, 0, 55, 40, 1, 0, 0, 0, 55, 45, 1, 0, 0, 0, 55, 50, 1, 0, 0, 0, 56, 68, 1, 0, 0, 0, 57, 58, 10, 8, 0, 0, 58, 59, 7, 0, 0, 0, 59, 67, 3, 2, 1, 9, 60, 61, 10, 7, 0, 0, 61, 62, 7, 1, 0, 0, 62, 67, 3, 2, 1, 8, 63, 64, 10, 6, 0, 0, 64, 65, 5, 16, 0, 0, 65, 67, 3, 2, 1, 7, 66, 57, 1, 0, 0, 0, 66, 60, 1, 0, 0, 0, 66, 63, 1, 0, 0, 0, 67, 70, 1, 0, 0, 0, 68, 66, 1, 0, 0, 0, 68, 69, 1, 0, 0, 0, 69, 3, 1, 0, 0, 0, 70, 68, 1, 0, 0, 0, 71, 74, 5, 13, 0, 0, 72, 74, 5, 15, 0, 0, 73, 71, 1, 0, 0, 0, 73, 72, 1, 0, 0, 0, 74, 5, 1, 0, 0, 0, 75, 77, 5, 14, 0, 0, 76, 78, 3, 8, 4, 0, 77, 76, 1, 0, 0, 0, 77, 78, 1, 0, 0, 0, 78, 7, 1, 0, 0, 0, 79, 80, 6, 4, -1, 0, 80, 81, 7, 1, 0, 0, 81, 88, 3, 4, 2, 0, 82, 83, 7, 1, 0, 0, 83, 84, 5, 3, 0, 0, 84, 85, 3, 2, 1, 0, 85, 86, 5, 4, 0, 0, 86, 88, 1, 0, 0, 0, 87, 79, 1, 0, 0, 0, 87, 82, 1, 0, 0, 0, 88, 97, 1, 0, 0, 0, 89, 90, 10, 4, 0, 0, 90, 91, 7, 0, 0, 0, 91, 96, 3, 10, 5, 0, 92, 93, 10, 3, 0, 0, 93, 94, 7, 1, 0, 0, 94, 96, 3, 10, 5, 0, 95, 89, 1, 0, 0, 0, 95, 92, 1, 0, 0, 0, 96, 99, 1, 0, 0, 0, 97, 95, 1, 0, 0, 0, 97, 98, 1, 0, 0, 0, 98, 9, 1, 0, 0, 0, 99, 97, 1, 0, 0, 0, 100, 101, 6, 5, -1, 0, 101, 102, 5, 3, 0, 0, 102, 103, 3, 2, 1, 0, 103, 104, 5, 4, 0, 0, 104, 107, 1, 0, 0, 0, 105, 107, 3, 4, 2, 0, 106, 100, 1, 0, 0, 0, 106, 105, 1, 0, 0, 0, 107, 113, 1, 0, 0, 0, 108, 109, 10, 3, 0, 0, 109, 110, 7, 0, 0, 0, 110, 112, 3, 10, 5, 4, 111, 108, 1, 0, 0, 0, 112, 115, 1, 0, 0, 0, 113, 111, 1, 0, 0, 0, 113, 114, 1, 0, 0, 0, 114, 11, 1, 0, 0, 0, 115, 113, 1, 0, 0, 0, 10, 55, 66, 68, 73, 77, 87, 95, 97, 106, 113] diff --git a/src/andromede/expression/parsing/antlr/Expr.tokens b/src/andromede/expression/parsing/antlr/Expr.tokens index 9401c83a..86857744 100644 --- a/src/andromede/expression/parsing/antlr/Expr.tokens +++ b/src/andromede/expression/parsing/antlr/Expr.tokens @@ -9,11 +9,12 @@ T__7=8 T__8=9 T__9=10 T__10=11 -NUMBER=12 -TIME=13 -IDENTIFIER=14 -COMPARISON=15 -WS=16 +T__11=12 +NUMBER=13 +TIME=14 +IDENTIFIER=15 +COMPARISON=16 +WS=17 '.'=1 '-'=2 '('=3 @@ -21,8 +22,9 @@ WS=16 '/'=5 '*'=6 '+'=7 -'['=8 -','=9 -']'=10 -'..'=11 -'t'=13 +'sum'=8 +'..'=9 +','=10 +'['=11 +']'=12 +'t'=14 diff --git a/src/andromede/expression/parsing/antlr/ExprLexer.interp b/src/andromede/expression/parsing/antlr/ExprLexer.interp index 2e85e1b7..59bb73c6 100644 --- a/src/andromede/expression/parsing/antlr/ExprLexer.interp +++ b/src/andromede/expression/parsing/antlr/ExprLexer.interp @@ -7,10 +7,11 @@ null '/' '*' '+' -'[' +'sum' +'..' ',' +'[' ']' -'..' null 't' null @@ -30,6 +31,7 @@ null null null null +null NUMBER TIME IDENTIFIER @@ -48,6 +50,7 @@ T__7 T__8 T__9 T__10 +T__11 DIGIT CHAR CHAR_OR_DIGIT @@ -65,4 +68,4 @@ mode names: DEFAULT_MODE atn: -[4, 0, 16, 103, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15, 7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 2, 18, 7, 18, 1, 0, 1, 0, 1, 1, 1, 1, 1, 2, 1, 2, 1, 3, 1, 3, 1, 4, 1, 4, 1, 5, 1, 5, 1, 6, 1, 6, 1, 7, 1, 7, 1, 8, 1, 8, 1, 9, 1, 9, 1, 10, 1, 10, 1, 10, 1, 11, 1, 11, 1, 12, 1, 12, 1, 13, 1, 13, 3, 13, 69, 8, 13, 1, 14, 4, 14, 72, 8, 14, 11, 14, 12, 14, 73, 1, 14, 1, 14, 4, 14, 78, 8, 14, 11, 14, 12, 14, 79, 3, 14, 82, 8, 14, 1, 15, 1, 15, 1, 16, 1, 16, 5, 16, 88, 8, 16, 10, 16, 12, 16, 91, 9, 16, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 3, 17, 98, 8, 17, 1, 18, 1, 18, 1, 18, 1, 18, 0, 0, 19, 1, 1, 3, 2, 5, 3, 7, 4, 9, 5, 11, 6, 13, 7, 15, 8, 17, 9, 19, 10, 21, 11, 23, 0, 25, 0, 27, 0, 29, 12, 31, 13, 33, 14, 35, 15, 37, 16, 1, 0, 3, 1, 0, 48, 57, 3, 0, 65, 90, 95, 95, 97, 122, 3, 0, 9, 10, 13, 13, 32, 32, 106, 0, 1, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0, 0, 9, 1, 0, 0, 0, 0, 11, 1, 0, 0, 0, 0, 13, 1, 0, 0, 0, 0, 15, 1, 0, 0, 0, 0, 17, 1, 0, 0, 0, 0, 19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 0, 29, 1, 0, 0, 0, 0, 31, 1, 0, 0, 0, 0, 33, 1, 0, 0, 0, 0, 35, 1, 0, 0, 0, 0, 37, 1, 0, 0, 0, 1, 39, 1, 0, 0, 0, 3, 41, 1, 0, 0, 0, 5, 43, 1, 0, 0, 0, 7, 45, 1, 0, 0, 0, 9, 47, 1, 0, 0, 0, 11, 49, 1, 0, 0, 0, 13, 51, 1, 0, 0, 0, 15, 53, 1, 0, 0, 0, 17, 55, 1, 0, 0, 0, 19, 57, 1, 0, 0, 0, 21, 59, 1, 0, 0, 0, 23, 62, 1, 0, 0, 0, 25, 64, 1, 0, 0, 0, 27, 68, 1, 0, 0, 0, 29, 71, 1, 0, 0, 0, 31, 83, 1, 0, 0, 0, 33, 85, 1, 0, 0, 0, 35, 97, 1, 0, 0, 0, 37, 99, 1, 0, 0, 0, 39, 40, 5, 46, 0, 0, 40, 2, 1, 0, 0, 0, 41, 42, 5, 45, 0, 0, 42, 4, 1, 0, 0, 0, 43, 44, 5, 40, 0, 0, 44, 6, 1, 0, 0, 0, 45, 46, 5, 41, 0, 0, 46, 8, 1, 0, 0, 0, 47, 48, 5, 47, 0, 0, 48, 10, 1, 0, 0, 0, 49, 50, 5, 42, 0, 0, 50, 12, 1, 0, 0, 0, 51, 52, 5, 43, 0, 0, 52, 14, 1, 0, 0, 0, 53, 54, 5, 91, 0, 0, 54, 16, 1, 0, 0, 0, 55, 56, 5, 44, 0, 0, 56, 18, 1, 0, 0, 0, 57, 58, 5, 93, 0, 0, 58, 20, 1, 0, 0, 0, 59, 60, 5, 46, 0, 0, 60, 61, 5, 46, 0, 0, 61, 22, 1, 0, 0, 0, 62, 63, 7, 0, 0, 0, 63, 24, 1, 0, 0, 0, 64, 65, 7, 1, 0, 0, 65, 26, 1, 0, 0, 0, 66, 69, 3, 25, 12, 0, 67, 69, 3, 23, 11, 0, 68, 66, 1, 0, 0, 0, 68, 67, 1, 0, 0, 0, 69, 28, 1, 0, 0, 0, 70, 72, 3, 23, 11, 0, 71, 70, 1, 0, 0, 0, 72, 73, 1, 0, 0, 0, 73, 71, 1, 0, 0, 0, 73, 74, 1, 0, 0, 0, 74, 81, 1, 0, 0, 0, 75, 77, 5, 46, 0, 0, 76, 78, 3, 23, 11, 0, 77, 76, 1, 0, 0, 0, 78, 79, 1, 0, 0, 0, 79, 77, 1, 0, 0, 0, 79, 80, 1, 0, 0, 0, 80, 82, 1, 0, 0, 0, 81, 75, 1, 0, 0, 0, 81, 82, 1, 0, 0, 0, 82, 30, 1, 0, 0, 0, 83, 84, 5, 116, 0, 0, 84, 32, 1, 0, 0, 0, 85, 89, 3, 25, 12, 0, 86, 88, 3, 27, 13, 0, 87, 86, 1, 0, 0, 0, 88, 91, 1, 0, 0, 0, 89, 87, 1, 0, 0, 0, 89, 90, 1, 0, 0, 0, 90, 34, 1, 0, 0, 0, 91, 89, 1, 0, 0, 0, 92, 98, 5, 61, 0, 0, 93, 94, 5, 62, 0, 0, 94, 98, 5, 61, 0, 0, 95, 96, 5, 60, 0, 0, 96, 98, 5, 61, 0, 0, 97, 92, 1, 0, 0, 0, 97, 93, 1, 0, 0, 0, 97, 95, 1, 0, 0, 0, 98, 36, 1, 0, 0, 0, 99, 100, 7, 2, 0, 0, 100, 101, 1, 0, 0, 0, 101, 102, 6, 18, 0, 0, 102, 38, 1, 0, 0, 0, 7, 0, 68, 73, 79, 81, 89, 97, 1, 6, 0, 0] \ No newline at end of file +[4, 0, 17, 109, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15, 7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 2, 18, 7, 18, 2, 19, 7, 19, 1, 0, 1, 0, 1, 1, 1, 1, 1, 2, 1, 2, 1, 3, 1, 3, 1, 4, 1, 4, 1, 5, 1, 5, 1, 6, 1, 6, 1, 7, 1, 7, 1, 7, 1, 7, 1, 8, 1, 8, 1, 8, 1, 9, 1, 9, 1, 10, 1, 10, 1, 11, 1, 11, 1, 12, 1, 12, 1, 13, 1, 13, 1, 14, 1, 14, 3, 14, 75, 8, 14, 1, 15, 4, 15, 78, 8, 15, 11, 15, 12, 15, 79, 1, 15, 1, 15, 4, 15, 84, 8, 15, 11, 15, 12, 15, 85, 3, 15, 88, 8, 15, 1, 16, 1, 16, 1, 17, 1, 17, 5, 17, 94, 8, 17, 10, 17, 12, 17, 97, 9, 17, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 3, 18, 104, 8, 18, 1, 19, 1, 19, 1, 19, 1, 19, 0, 0, 20, 1, 1, 3, 2, 5, 3, 7, 4, 9, 5, 11, 6, 13, 7, 15, 8, 17, 9, 19, 10, 21, 11, 23, 12, 25, 0, 27, 0, 29, 0, 31, 13, 33, 14, 35, 15, 37, 16, 39, 17, 1, 0, 3, 1, 0, 48, 57, 3, 0, 65, 90, 95, 95, 97, 122, 3, 0, 9, 10, 13, 13, 32, 32, 112, 0, 1, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0, 0, 9, 1, 0, 0, 0, 0, 11, 1, 0, 0, 0, 0, 13, 1, 0, 0, 0, 0, 15, 1, 0, 0, 0, 0, 17, 1, 0, 0, 0, 0, 19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 0, 23, 1, 0, 0, 0, 0, 31, 1, 0, 0, 0, 0, 33, 1, 0, 0, 0, 0, 35, 1, 0, 0, 0, 0, 37, 1, 0, 0, 0, 0, 39, 1, 0, 0, 0, 1, 41, 1, 0, 0, 0, 3, 43, 1, 0, 0, 0, 5, 45, 1, 0, 0, 0, 7, 47, 1, 0, 0, 0, 9, 49, 1, 0, 0, 0, 11, 51, 1, 0, 0, 0, 13, 53, 1, 0, 0, 0, 15, 55, 1, 0, 0, 0, 17, 59, 1, 0, 0, 0, 19, 62, 1, 0, 0, 0, 21, 64, 1, 0, 0, 0, 23, 66, 1, 0, 0, 0, 25, 68, 1, 0, 0, 0, 27, 70, 1, 0, 0, 0, 29, 74, 1, 0, 0, 0, 31, 77, 1, 0, 0, 0, 33, 89, 1, 0, 0, 0, 35, 91, 1, 0, 0, 0, 37, 103, 1, 0, 0, 0, 39, 105, 1, 0, 0, 0, 41, 42, 5, 46, 0, 0, 42, 2, 1, 0, 0, 0, 43, 44, 5, 45, 0, 0, 44, 4, 1, 0, 0, 0, 45, 46, 5, 40, 0, 0, 46, 6, 1, 0, 0, 0, 47, 48, 5, 41, 0, 0, 48, 8, 1, 0, 0, 0, 49, 50, 5, 47, 0, 0, 50, 10, 1, 0, 0, 0, 51, 52, 5, 42, 0, 0, 52, 12, 1, 0, 0, 0, 53, 54, 5, 43, 0, 0, 54, 14, 1, 0, 0, 0, 55, 56, 5, 115, 0, 0, 56, 57, 5, 117, 0, 0, 57, 58, 5, 109, 0, 0, 58, 16, 1, 0, 0, 0, 59, 60, 5, 46, 0, 0, 60, 61, 5, 46, 0, 0, 61, 18, 1, 0, 0, 0, 62, 63, 5, 44, 0, 0, 63, 20, 1, 0, 0, 0, 64, 65, 5, 91, 0, 0, 65, 22, 1, 0, 0, 0, 66, 67, 5, 93, 0, 0, 67, 24, 1, 0, 0, 0, 68, 69, 7, 0, 0, 0, 69, 26, 1, 0, 0, 0, 70, 71, 7, 1, 0, 0, 71, 28, 1, 0, 0, 0, 72, 75, 3, 27, 13, 0, 73, 75, 3, 25, 12, 0, 74, 72, 1, 0, 0, 0, 74, 73, 1, 0, 0, 0, 75, 30, 1, 0, 0, 0, 76, 78, 3, 25, 12, 0, 77, 76, 1, 0, 0, 0, 78, 79, 1, 0, 0, 0, 79, 77, 1, 0, 0, 0, 79, 80, 1, 0, 0, 0, 80, 87, 1, 0, 0, 0, 81, 83, 5, 46, 0, 0, 82, 84, 3, 25, 12, 0, 83, 82, 1, 0, 0, 0, 84, 85, 1, 0, 0, 0, 85, 83, 1, 0, 0, 0, 85, 86, 1, 0, 0, 0, 86, 88, 1, 0, 0, 0, 87, 81, 1, 0, 0, 0, 87, 88, 1, 0, 0, 0, 88, 32, 1, 0, 0, 0, 89, 90, 5, 116, 0, 0, 90, 34, 1, 0, 0, 0, 91, 95, 3, 27, 13, 0, 92, 94, 3, 29, 14, 0, 93, 92, 1, 0, 0, 0, 94, 97, 1, 0, 0, 0, 95, 93, 1, 0, 0, 0, 95, 96, 1, 0, 0, 0, 96, 36, 1, 0, 0, 0, 97, 95, 1, 0, 0, 0, 98, 104, 5, 61, 0, 0, 99, 100, 5, 62, 0, 0, 100, 104, 5, 61, 0, 0, 101, 102, 5, 60, 0, 0, 102, 104, 5, 61, 0, 0, 103, 98, 1, 0, 0, 0, 103, 99, 1, 0, 0, 0, 103, 101, 1, 0, 0, 0, 104, 38, 1, 0, 0, 0, 105, 106, 7, 2, 0, 0, 106, 107, 1, 0, 0, 0, 107, 108, 6, 19, 0, 0, 108, 40, 1, 0, 0, 0, 7, 0, 74, 79, 85, 87, 95, 103, 1, 6, 0, 0] diff --git a/src/andromede/expression/parsing/antlr/ExprLexer.py b/src/andromede/expression/parsing/antlr/ExprLexer.py index 1ad7f368..b78b3c77 100644 --- a/src/andromede/expression/parsing/antlr/ExprLexer.py +++ b/src/andromede/expression/parsing/antlr/ExprLexer.py @@ -1,4 +1,4 @@ -# Generated from Expr.g4 by ANTLR 4.13.1 +# Generated from Expr.g4 by ANTLR 4.13.2 import sys from io import StringIO @@ -14,8 +14,8 @@ def serializedATN(): return [ 4, 0, - 16, - 103, + 17, + 109, 6, -1, 2, @@ -94,6 +94,10 @@ def serializedATN(): 18, 7, 18, + 2, + 19, + 7, + 19, 1, 0, 1, @@ -127,15 +131,19 @@ def serializedATN(): 1, 7, 1, + 7, + 1, + 7, + 1, 8, 1, 8, 1, - 9, + 8, 1, 9, 1, - 10, + 9, 1, 10, 1, @@ -152,76 +160,65 @@ def serializedATN(): 13, 1, 13, - 3, - 13, - 69, - 8, - 13, 1, 14, - 4, - 14, - 72, - 8, - 14, - 11, + 1, 14, - 12, + 3, 14, - 73, - 1, + 75, + 8, 14, 1, - 14, + 15, 4, - 14, + 15, 78, 8, - 14, + 15, 11, - 14, + 15, 12, - 14, + 15, 79, - 3, - 14, - 82, - 8, - 14, 1, 15, 1, 15, - 1, - 16, - 1, - 16, - 5, - 16, - 88, + 4, + 15, + 84, 8, - 16, - 10, - 16, + 15, + 11, + 15, 12, + 15, + 85, + 3, + 15, + 88, + 8, + 15, + 1, 16, - 91, - 9, + 1, 16, 1, 17, 1, 17, - 1, + 5, 17, - 1, + 94, + 8, 17, - 1, + 10, 17, - 3, + 12, 17, - 98, - 8, + 97, + 9, 17, 1, 18, @@ -231,9 +228,24 @@ def serializedATN(): 18, 1, 18, + 1, + 18, + 3, + 18, + 104, + 8, + 18, + 1, + 19, + 1, + 19, + 1, + 19, + 1, + 19, 0, 0, - 19, + 20, 1, 1, 3, @@ -257,13 +269,13 @@ def serializedATN(): 21, 11, 23, - 0, + 12, 25, 0, 27, 0, 29, - 12, + 0, 31, 13, 33, @@ -272,6 +284,8 @@ def serializedATN(): 15, 37, 16, + 39, + 17, 1, 0, 3, @@ -295,7 +309,7 @@ def serializedATN(): 13, 32, 32, - 106, + 112, 0, 1, 1, @@ -363,7 +377,7 @@ def serializedATN(): 0, 0, 0, - 29, + 23, 1, 0, 0, @@ -392,128 +406,128 @@ def serializedATN(): 0, 0, 0, - 1, + 0, 39, 1, 0, 0, 0, - 3, + 1, 41, 1, 0, 0, 0, - 5, + 3, 43, 1, 0, 0, 0, - 7, + 5, 45, 1, 0, 0, 0, - 9, + 7, 47, 1, 0, 0, 0, - 11, + 9, 49, 1, 0, 0, 0, - 13, + 11, 51, 1, 0, 0, 0, - 15, + 13, 53, 1, 0, 0, 0, - 17, + 15, 55, 1, 0, 0, 0, + 17, + 59, + 1, + 0, + 0, + 0, 19, - 57, + 62, 1, 0, 0, 0, 21, - 59, + 64, 1, 0, 0, 0, 23, - 62, + 66, 1, 0, 0, 0, 25, - 64, + 68, 1, 0, 0, 0, 27, - 68, + 70, 1, 0, 0, 0, 29, - 71, + 74, 1, 0, 0, 0, 31, - 83, + 77, 1, 0, 0, 0, 33, - 85, + 89, 1, 0, 0, 0, 35, - 97, + 91, 1, 0, 0, 0, 37, - 99, + 103, 1, 0, 0, 0, 39, - 40, - 5, - 46, - 0, - 0, - 40, - 2, + 105, 1, 0, 0, @@ -521,11 +535,11 @@ def serializedATN(): 41, 42, 5, - 45, + 46, 0, 0, 42, - 4, + 2, 1, 0, 0, @@ -533,11 +547,11 @@ def serializedATN(): 43, 44, 5, - 40, + 45, 0, 0, 44, - 6, + 4, 1, 0, 0, @@ -545,11 +559,11 @@ def serializedATN(): 45, 46, 5, - 41, + 40, 0, 0, 46, - 8, + 6, 1, 0, 0, @@ -557,11 +571,11 @@ def serializedATN(): 47, 48, 5, - 47, + 41, 0, 0, 48, - 10, + 8, 1, 0, 0, @@ -569,11 +583,11 @@ def serializedATN(): 49, 50, 5, - 42, + 47, 0, 0, 50, - 12, + 10, 1, 0, 0, @@ -581,11 +595,11 @@ def serializedATN(): 51, 52, 5, - 43, + 42, 0, 0, 52, - 14, + 12, 1, 0, 0, @@ -593,11 +607,11 @@ def serializedATN(): 53, 54, 5, - 91, + 43, 0, 0, 54, - 16, + 14, 1, 0, 0, @@ -605,23 +619,23 @@ def serializedATN(): 55, 56, 5, - 44, + 115, 0, 0, 56, - 18, - 1, - 0, + 57, + 5, + 117, 0, 0, 57, 58, 5, - 93, + 109, 0, 0, 58, - 20, + 16, 1, 0, 0, @@ -639,112 +653,106 @@ def serializedATN(): 0, 0, 61, - 22, + 18, 1, 0, 0, 0, 62, 63, - 7, - 0, + 5, + 44, 0, 0, 63, - 24, + 20, 1, 0, 0, 0, 64, 65, - 7, - 1, + 5, + 91, 0, 0, 65, - 26, + 22, 1, 0, 0, 0, 66, - 69, - 3, - 25, - 12, - 0, 67, - 69, - 3, - 23, - 11, + 5, + 93, 0, - 68, - 66, + 0, + 67, + 24, 1, 0, 0, 0, 68, - 67, - 1, + 69, + 7, 0, 0, 0, 69, - 28, + 26, 1, 0, 0, 0, 70, - 72, - 3, - 23, - 11, - 0, 71, - 70, - 1, - 0, - 0, - 0, - 72, - 73, + 7, 1, 0, 0, - 0, - 73, 71, + 28, 1, 0, 0, 0, + 72, + 75, + 3, + 27, + 13, + 0, 73, + 75, + 3, + 25, + 12, + 0, 74, + 72, 1, 0, 0, 0, 74, - 81, + 73, 1, 0, 0, 0, 75, - 77, - 5, - 46, + 30, + 1, + 0, 0, 0, 76, 78, 3, - 23, - 11, + 25, + 12, 0, 77, 76, @@ -771,75 +779,75 @@ def serializedATN(): 0, 0, 80, - 82, + 87, 1, 0, 0, 0, 81, - 75, - 1, + 83, + 5, + 46, 0, 0, + 82, + 84, + 3, + 25, + 12, 0, - 81, + 83, 82, 1, 0, 0, 0, - 82, - 30, + 84, + 85, 1, 0, 0, 0, + 85, 83, - 84, - 5, - 116, - 0, - 0, - 84, - 32, 1, 0, 0, 0, 85, - 89, - 3, - 25, - 12, + 86, + 1, + 0, + 0, 0, 86, 88, - 3, - 27, - 13, + 1, + 0, + 0, 0, 87, - 86, + 81, 1, 0, 0, 0, + 87, 88, - 91, 1, 0, 0, 0, - 89, - 87, + 88, + 32, 1, 0, 0, 0, 89, 90, - 1, - 0, + 5, + 116, 0, 0, 90, @@ -849,97 +857,139 @@ def serializedATN(): 0, 0, 91, - 89, + 95, + 3, + 27, + 13, + 0, + 92, + 94, + 3, + 29, + 14, + 0, + 93, + 92, + 1, + 0, + 0, + 0, + 94, + 97, + 1, + 0, + 0, + 0, + 95, + 93, + 1, + 0, + 0, + 0, + 95, + 96, + 1, + 0, + 0, + 0, + 96, + 36, + 1, + 0, + 0, + 0, + 97, + 95, 1, 0, 0, 0, - 92, 98, + 104, 5, 61, 0, 0, - 93, - 94, + 99, + 100, 5, 62, 0, 0, - 94, - 98, + 100, + 104, 5, 61, 0, 0, - 95, - 96, + 101, + 102, 5, 60, 0, 0, - 96, - 98, + 102, + 104, 5, 61, 0, 0, - 97, - 92, + 103, + 98, 1, 0, 0, 0, - 97, - 93, + 103, + 99, 1, 0, 0, 0, - 97, - 95, + 103, + 101, 1, 0, 0, 0, - 98, - 36, + 104, + 38, 1, 0, 0, 0, - 99, - 100, + 105, + 106, 7, 2, 0, 0, - 100, - 101, + 106, + 107, 1, 0, 0, 0, - 101, - 102, + 107, + 108, 6, - 18, + 19, 0, 0, - 102, - 38, + 108, + 40, 1, 0, 0, 0, 7, 0, - 68, - 73, + 74, 79, - 81, - 89, - 97, + 85, + 87, + 95, + 103, 1, 6, 0, @@ -963,11 +1013,12 @@ class ExprLexer(Lexer): T__8 = 9 T__9 = 10 T__10 = 11 - NUMBER = 12 - TIME = 13 - IDENTIFIER = 14 - COMPARISON = 15 - WS = 16 + T__11 = 12 + NUMBER = 13 + TIME = 14 + IDENTIFIER = 15 + COMPARISON = 16 + WS = 17 channelNames = ["DEFAULT_TOKEN_CHANNEL", "HIDDEN"] @@ -982,10 +1033,11 @@ class ExprLexer(Lexer): "'/'", "'*'", "'+'", - "'['", + "'sum'", + "'..'", "','", + "'['", "']'", - "'..'", "'t'", ] @@ -1003,6 +1055,7 @@ class ExprLexer(Lexer): "T__8", "T__9", "T__10", + "T__11", "DIGIT", "CHAR", "CHAR_OR_DIGIT", @@ -1017,7 +1070,7 @@ class ExprLexer(Lexer): def __init__(self, input=None, output: TextIO = sys.stdout): super().__init__(input, output) - self.checkVersion("4.13.1") + self.checkVersion("4.13.2") self._interp = LexerATNSimulator( self, self.atn, self.decisionsToDFA, PredictionContextCache() ) diff --git a/src/andromede/expression/parsing/antlr/ExprLexer.tokens b/src/andromede/expression/parsing/antlr/ExprLexer.tokens index 9401c83a..86857744 100644 --- a/src/andromede/expression/parsing/antlr/ExprLexer.tokens +++ b/src/andromede/expression/parsing/antlr/ExprLexer.tokens @@ -9,11 +9,12 @@ T__7=8 T__8=9 T__9=10 T__10=11 -NUMBER=12 -TIME=13 -IDENTIFIER=14 -COMPARISON=15 -WS=16 +T__11=12 +NUMBER=13 +TIME=14 +IDENTIFIER=15 +COMPARISON=16 +WS=17 '.'=1 '-'=2 '('=3 @@ -21,8 +22,9 @@ WS=16 '/'=5 '*'=6 '+'=7 -'['=8 -','=9 -']'=10 -'..'=11 -'t'=13 +'sum'=8 +'..'=9 +','=10 +'['=11 +']'=12 +'t'=14 diff --git a/src/andromede/expression/parsing/antlr/ExprParser.py b/src/andromede/expression/parsing/antlr/ExprParser.py index 8f312fe9..085c5390 100644 --- a/src/andromede/expression/parsing/antlr/ExprParser.py +++ b/src/andromede/expression/parsing/antlr/ExprParser.py @@ -1,4 +1,4 @@ -# Generated from Expr.g4 by ANTLR 4.13.1 +# Generated from Expr.g4 by ANTLR 4.13.2 # encoding: utf-8 import sys from io import StringIO @@ -15,8 +15,8 @@ def serializedATN(): return [ 4, 1, - 16, - 131, + 17, + 117, 2, 0, 7, @@ -89,18 +89,6 @@ def serializedATN(): 1, 1, 1, - 5, - 1, - 37, - 8, - 1, - 10, - 1, - 12, - 1, - 40, - 9, - 1, 1, 1, 1, @@ -115,26 +103,6 @@ def serializedATN(): 1, 1, 1, - 5, - 1, - 49, - 8, - 1, - 10, - 1, - 12, - 1, - 52, - 9, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, 1, 1, 1, @@ -161,7 +129,7 @@ def serializedATN(): 1, 3, 1, - 70, + 56, 8, 1, 1, @@ -184,14 +152,14 @@ def serializedATN(): 1, 5, 1, - 81, + 67, 8, 1, 10, 1, 12, 1, - 84, + 70, 9, 1, 1, @@ -200,7 +168,7 @@ def serializedATN(): 2, 3, 2, - 88, + 74, 8, 2, 1, @@ -209,7 +177,7 @@ def serializedATN(): 3, 3, 3, - 92, + 78, 8, 3, 1, @@ -230,7 +198,7 @@ def serializedATN(): 4, 3, 4, - 102, + 88, 8, 4, 1, @@ -247,14 +215,14 @@ def serializedATN(): 4, 5, 4, - 110, + 96, 8, 4, 10, 4, 12, 4, - 113, + 99, 9, 4, 1, @@ -271,7 +239,7 @@ def serializedATN(): 5, 3, 5, - 121, + 107, 8, 5, 1, @@ -282,14 +250,14 @@ def serializedATN(): 5, 5, 5, - 126, + 112, 8, 5, 10, 5, 12, 5, - 129, + 115, 9, 5, 1, @@ -318,7 +286,7 @@ def serializedATN(): 2, 7, 7, - 144, + 128, 0, 12, 1, @@ -326,31 +294,31 @@ def serializedATN(): 0, 0, 2, - 69, + 55, 1, 0, 0, 0, 4, - 87, + 73, 1, 0, 0, 0, 6, - 89, + 75, 1, 0, 0, 0, 8, - 101, + 87, 1, 0, 0, 0, 10, - 120, + 106, 1, 0, 0, @@ -380,7 +348,7 @@ def serializedATN(): -1, 0, 16, - 70, + 56, 3, 4, 2, @@ -388,7 +356,7 @@ def serializedATN(): 17, 18, 5, - 14, + 15, 0, 0, 18, @@ -398,9 +366,9 @@ def serializedATN(): 0, 0, 19, - 70, + 56, 5, - 14, + 15, 0, 0, 20, @@ -410,7 +378,7 @@ def serializedATN(): 0, 0, 21, - 70, + 56, 3, 2, 1, @@ -434,7 +402,7 @@ def serializedATN(): 0, 0, 25, - 70, + 56, 1, 0, 0, @@ -442,7 +410,7 @@ def serializedATN(): 26, 27, 5, - 14, + 8, 0, 0, 27, @@ -464,7 +432,7 @@ def serializedATN(): 0, 0, 30, - 70, + 56, 1, 0, 0, @@ -472,17 +440,17 @@ def serializedATN(): 31, 32, 5, - 14, + 8, 0, 0, 32, 33, 5, - 8, + 3, 0, 0, 33, - 38, + 34, 3, 6, 3, @@ -494,708 +462,610 @@ def serializedATN(): 0, 0, 35, - 37, + 36, 3, 6, 3, 0, 36, - 34, - 1, - 0, - 0, - 0, 37, - 40, - 1, - 0, + 5, + 10, 0, 0, + 37, 38, - 36, + 3, + 2, 1, 0, - 0, - 0, 38, 39, - 1, - 0, + 5, + 4, 0, 0, 39, - 41, + 56, 1, 0, 0, 0, 40, - 38, - 1, - 0, + 41, + 5, + 15, 0, 0, 41, 42, 5, - 10, + 3, 0, 0, 42, - 70, + 43, + 3, + 2, 1, 0, - 0, - 0, 43, 44, 5, - 14, + 4, 0, 0, 44, - 45, - 5, - 8, - 0, - 0, - 45, - 50, - 3, - 2, + 56, 1, 0, - 46, - 47, - 5, - 9, 0, 0, - 47, - 49, - 3, - 2, - 1, - 0, - 48, + 45, 46, - 1, - 0, - 0, - 0, - 49, - 52, - 1, - 0, - 0, - 0, - 50, - 48, - 1, - 0, - 0, - 0, - 50, - 51, - 1, - 0, - 0, - 0, - 51, - 53, - 1, - 0, - 0, - 0, - 52, - 50, - 1, - 0, - 0, - 0, - 53, - 54, - 5, - 10, - 0, - 0, - 54, - 70, - 1, - 0, - 0, - 0, - 55, - 56, - 5, - 14, - 0, - 0, - 56, - 57, 5, - 8, - 0, + 15, 0, - 57, - 58, - 3, - 6, - 3, 0, - 58, - 59, + 46, + 47, 5, 11, 0, 0, - 59, - 60, + 47, + 48, 3, 6, 3, 0, - 60, - 61, + 48, + 49, 5, - 10, + 12, 0, 0, - 61, - 70, + 49, + 56, 1, 0, 0, 0, - 62, - 63, - 5, - 14, - 0, - 0, - 63, - 64, + 50, + 51, 5, - 8, + 15, 0, 0, - 64, - 65, - 3, - 2, - 1, - 0, - 65, - 66, + 51, + 52, 5, 11, 0, 0, - 66, - 67, + 52, + 53, 3, 2, 1, 0, - 67, - 68, + 53, + 54, 5, - 10, + 12, 0, 0, - 68, - 70, + 54, + 56, 1, 0, 0, 0, - 69, + 55, 15, 1, 0, 0, 0, - 69, + 55, 17, 1, 0, 0, 0, - 69, + 55, 20, 1, 0, 0, 0, - 69, + 55, 22, 1, 0, 0, 0, - 69, + 55, 26, 1, 0, 0, 0, - 69, + 55, 31, 1, 0, 0, 0, - 69, - 43, + 55, + 40, 1, 0, 0, 0, - 69, 55, + 45, 1, 0, 0, 0, - 69, - 62, + 55, + 50, 1, 0, 0, 0, - 70, - 82, + 56, + 68, 1, 0, 0, 0, - 71, - 72, + 57, + 58, 10, 8, 0, 0, - 72, - 73, + 58, + 59, 7, 0, 0, 0, - 73, - 81, + 59, + 67, 3, 2, 1, 9, - 74, - 75, + 60, + 61, 10, 7, 0, 0, - 75, - 76, + 61, + 62, 7, 1, 0, 0, - 76, - 81, + 62, + 67, 3, 2, 1, 8, - 77, - 78, + 63, + 64, 10, 6, 0, 0, - 78, - 79, + 64, + 65, 5, - 15, + 16, 0, 0, - 79, - 81, + 65, + 67, 3, 2, 1, 7, - 80, - 71, + 66, + 57, 1, 0, 0, 0, - 80, - 74, + 66, + 60, 1, 0, 0, 0, - 80, - 77, + 66, + 63, 1, 0, 0, 0, - 81, - 84, + 67, + 70, 1, 0, 0, 0, - 82, - 80, + 68, + 66, 1, 0, 0, 0, - 82, - 83, + 68, + 69, 1, 0, 0, 0, - 83, + 69, 3, 1, 0, 0, 0, - 84, - 82, + 70, + 68, 1, 0, 0, 0, - 85, - 88, + 71, + 74, 5, - 12, + 13, 0, 0, - 86, - 88, + 72, + 74, 5, - 14, + 15, 0, 0, - 87, - 85, + 73, + 71, 1, 0, 0, 0, - 87, - 86, + 73, + 72, 1, 0, 0, 0, - 88, + 74, 5, 1, 0, 0, 0, - 89, - 91, + 75, + 77, 5, - 13, + 14, 0, 0, - 90, - 92, + 76, + 78, 3, 8, 4, 0, - 91, - 90, + 77, + 76, 1, 0, 0, 0, - 91, - 92, + 77, + 78, 1, 0, 0, 0, - 92, + 78, 7, 1, 0, 0, 0, - 93, - 94, + 79, + 80, 6, 4, -1, 0, - 94, - 95, + 80, + 81, 7, 1, 0, 0, - 95, - 102, + 81, + 88, 3, 4, 2, 0, - 96, - 97, + 82, + 83, 7, 1, 0, 0, - 97, - 98, + 83, + 84, 5, 3, 0, 0, - 98, - 99, + 84, + 85, 3, 2, 1, 0, - 99, - 100, + 85, + 86, 5, 4, 0, 0, - 100, - 102, + 86, + 88, 1, 0, 0, 0, - 101, - 93, + 87, + 79, 1, 0, 0, 0, - 101, - 96, + 87, + 82, 1, 0, 0, 0, - 102, - 111, + 88, + 97, 1, 0, 0, 0, - 103, - 104, + 89, + 90, 10, 4, 0, 0, - 104, - 105, + 90, + 91, 7, 0, 0, 0, - 105, - 110, + 91, + 96, 3, 10, 5, 0, - 106, - 107, + 92, + 93, 10, 3, 0, 0, - 107, - 108, + 93, + 94, 7, 1, 0, 0, - 108, - 110, + 94, + 96, 3, 10, 5, 0, - 109, - 103, + 95, + 89, 1, 0, 0, 0, - 109, - 106, + 95, + 92, 1, 0, 0, 0, - 110, - 113, + 96, + 99, 1, 0, 0, 0, - 111, - 109, + 97, + 95, 1, 0, 0, 0, - 111, - 112, + 97, + 98, 1, 0, 0, 0, - 112, + 98, 9, 1, 0, 0, 0, - 113, - 111, + 99, + 97, 1, 0, 0, 0, - 114, - 115, + 100, + 101, 6, 5, -1, 0, - 115, - 116, + 101, + 102, 5, 3, 0, 0, - 116, - 117, + 102, + 103, 3, 2, 1, 0, - 117, - 118, + 103, + 104, 5, 4, 0, 0, - 118, - 121, + 104, + 107, 1, 0, 0, 0, - 119, - 121, + 105, + 107, 3, 4, 2, 0, - 120, - 114, + 106, + 100, 1, 0, 0, 0, - 120, - 119, + 106, + 105, 1, 0, 0, 0, - 121, - 127, + 107, + 113, 1, 0, 0, 0, - 122, - 123, + 108, + 109, 10, 3, 0, 0, - 123, - 124, + 109, + 110, 7, 0, 0, 0, - 124, - 126, + 110, + 112, 3, 10, 5, 4, - 125, - 122, + 111, + 108, 1, 0, 0, 0, - 126, - 129, + 112, + 115, 1, 0, 0, 0, - 127, - 125, + 113, + 111, 1, 0, 0, 0, - 127, - 128, + 113, + 114, 1, 0, 0, 0, - 128, + 114, 11, 1, 0, 0, 0, - 129, - 127, + 115, + 113, 1, 0, 0, 0, - 12, - 38, - 50, - 69, - 80, - 82, + 10, + 55, + 66, + 68, + 73, + 77, 87, - 91, - 101, - 109, - 111, - 120, - 127, + 95, + 97, + 106, + 113, ] @@ -1217,10 +1087,11 @@ class ExprParser(Parser): "'/'", "'*'", "'+'", - "'['", + "'sum'", + "'..'", "','", + "'['", "']'", - "'..'", "", "'t'", ] @@ -1238,6 +1109,7 @@ class ExprParser(Parser): "", "", "", + "", "NUMBER", "TIME", "IDENTIFIER", @@ -1266,15 +1138,16 @@ class ExprParser(Parser): T__8 = 9 T__9 = 10 T__10 = 11 - NUMBER = 12 - TIME = 13 - IDENTIFIER = 14 - COMPARISON = 15 - WS = 16 + T__11 = 12 + NUMBER = 13 + TIME = 14 + IDENTIFIER = 15 + COMPARISON = 16 + WS = 17 def __init__(self, input: TokenStream, output: TextIO = sys.stdout): super().__init__(input, output) - self.checkVersion("4.13.1") + self.checkVersion("4.13.2") self._interp = ParserATNSimulator( self, self.atn, self.decisionsToDFA, self.sharedContextCache ) @@ -1336,6 +1209,30 @@ def getRuleIndex(self): def copyFrom(self, ctx: ParserRuleContext): super().copyFrom(ctx) + class TimeSumContext(ExprContext): + def __init__( + self, parser, ctx: ParserRuleContext + ): # actually a ExprParser.ExprContext + super().__init__(parser) + self.from_ = None # ShiftContext + self.to = None # ShiftContext + self.copyFrom(ctx) + + def expr(self): + return self.getTypedRuleContext(ExprParser.ExprContext, 0) + + def shift(self, i: int = None): + if i is None: + return self.getTypedRuleContexts(ExprParser.ShiftContext) + else: + return self.getTypedRuleContext(ExprParser.ShiftContext, i) + + def accept(self, visitor: ParseTreeVisitor): + if hasattr(visitor, "visitTimeSum"): + return visitor.visitTimeSum(self) + else: + return visitor.visitChildren(self) + class NegationContext(ExprContext): def __init__( self, parser, ctx: ParserRuleContext @@ -1394,11 +1291,8 @@ def __init__( def IDENTIFIER(self): return self.getToken(ExprParser.IDENTIFIER, 0) - def expr(self, i: int = None): - if i is None: - return self.getTypedRuleContexts(ExprParser.ExprContext) - else: - return self.getTypedRuleContext(ExprParser.ExprContext, i) + def expr(self): + return self.getTypedRuleContext(ExprParser.ExprContext, 0) def accept(self, visitor: ParseTreeVisitor): if hasattr(visitor, "visitTimeIndex"): @@ -1428,6 +1322,22 @@ def accept(self, visitor: ParseTreeVisitor): else: return visitor.visitChildren(self) + class AllTimeSumContext(ExprContext): + def __init__( + self, parser, ctx: ParserRuleContext + ): # actually a ExprParser.ExprContext + super().__init__(parser) + self.copyFrom(ctx) + + def expr(self): + return self.getTypedRuleContext(ExprParser.ExprContext, 0) + + def accept(self, visitor: ParseTreeVisitor): + if hasattr(visitor, "visitAllTimeSum"): + return visitor.visitAllTimeSum(self) + else: + return visitor.visitChildren(self) + class TimeShiftContext(ExprContext): def __init__( self, parser, ctx: ParserRuleContext @@ -1438,11 +1348,8 @@ def __init__( def IDENTIFIER(self): return self.getToken(ExprParser.IDENTIFIER, 0) - def shift(self, i: int = None): - if i is None: - return self.getTypedRuleContexts(ExprParser.ShiftContext) - else: - return self.getTypedRuleContext(ExprParser.ShiftContext, i) + def shift(self): + return self.getTypedRuleContext(ExprParser.ShiftContext, 0) def accept(self, visitor: ParseTreeVisitor): if hasattr(visitor, "visitTimeShift"): @@ -1489,30 +1396,6 @@ def accept(self, visitor: ParseTreeVisitor): else: return visitor.visitChildren(self) - class TimeShiftRangeContext(ExprContext): - def __init__( - self, parser, ctx: ParserRuleContext - ): # actually a ExprParser.ExprContext - super().__init__(parser) - self.shift1 = None # ShiftContext - self.shift2 = None # ShiftContext - self.copyFrom(ctx) - - def IDENTIFIER(self): - return self.getToken(ExprParser.IDENTIFIER, 0) - - def shift(self, i: int = None): - if i is None: - return self.getTypedRuleContexts(ExprParser.ShiftContext) - else: - return self.getTypedRuleContext(ExprParser.ShiftContext, i) - - def accept(self, visitor: ParseTreeVisitor): - if hasattr(visitor, "visitTimeShiftRange"): - return visitor.visitTimeShiftRange(self) - else: - return visitor.visitChildren(self) - class PortFieldContext(ExprContext): def __init__( self, parser, ctx: ParserRuleContext @@ -1552,28 +1435,6 @@ def accept(self, visitor: ParseTreeVisitor): else: return visitor.visitChildren(self) - class TimeRangeContext(ExprContext): - def __init__( - self, parser, ctx: ParserRuleContext - ): # actually a ExprParser.ExprContext - super().__init__(parser) - self.copyFrom(ctx) - - def IDENTIFIER(self): - return self.getToken(ExprParser.IDENTIFIER, 0) - - def expr(self, i: int = None): - if i is None: - return self.getTypedRuleContexts(ExprParser.ExprContext) - else: - return self.getTypedRuleContext(ExprParser.ExprContext, i) - - def accept(self, visitor: ParseTreeVisitor): - if hasattr(visitor, "visitTimeRange"): - return visitor.visitTimeRange(self) - else: - return visitor.visitChildren(self) - def expr(self, _p: int = 0): _parentctx = self._ctx _parentState = self.state @@ -1584,9 +1445,9 @@ def expr(self, _p: int = 0): self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 69 + self.state = 55 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input, 2, self._ctx) + la_ = self._interp.adaptivePredict(self._input, 0, self._ctx) if la_ == 1: localctx = ExprParser.UnsignedAtomContext(self, localctx) self._ctx = localctx @@ -1631,11 +1492,11 @@ def expr(self, _p: int = 0): pass elif la_ == 5: - localctx = ExprParser.FunctionContext(self, localctx) + localctx = ExprParser.AllTimeSumContext(self, localctx) self._ctx = localctx _prevctx = localctx self.state = 26 - self.match(ExprParser.IDENTIFIER) + self.match(ExprParser.T__7) self.state = 27 self.match(ExprParser.T__2) self.state = 28 @@ -1645,105 +1506,81 @@ def expr(self, _p: int = 0): pass elif la_ == 6: - localctx = ExprParser.TimeShiftContext(self, localctx) + localctx = ExprParser.TimeSumContext(self, localctx) self._ctx = localctx _prevctx = localctx self.state = 31 - self.match(ExprParser.IDENTIFIER) - self.state = 32 self.match(ExprParser.T__7) + self.state = 32 + self.match(ExprParser.T__2) self.state = 33 - self.shift() - self.state = 38 - self._errHandler.sync(self) - _la = self._input.LA(1) - while _la == 9: - self.state = 34 - self.match(ExprParser.T__8) - self.state = 35 - self.shift() - self.state = 40 - self._errHandler.sync(self) - _la = self._input.LA(1) - - self.state = 41 + localctx.from_ = self.shift() + self.state = 34 + self.match(ExprParser.T__8) + self.state = 35 + localctx.to = self.shift() + self.state = 36 self.match(ExprParser.T__9) + self.state = 37 + self.expr(0) + self.state = 38 + self.match(ExprParser.T__3) pass elif la_ == 7: - localctx = ExprParser.TimeIndexContext(self, localctx) + localctx = ExprParser.FunctionContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 43 + self.state = 40 self.match(ExprParser.IDENTIFIER) - self.state = 44 - self.match(ExprParser.T__7) - self.state = 45 + self.state = 41 + self.match(ExprParser.T__2) + self.state = 42 self.expr(0) - self.state = 50 - self._errHandler.sync(self) - _la = self._input.LA(1) - while _la == 9: - self.state = 46 - self.match(ExprParser.T__8) - self.state = 47 - self.expr(0) - self.state = 52 - self._errHandler.sync(self) - _la = self._input.LA(1) - - self.state = 53 - self.match(ExprParser.T__9) + self.state = 43 + self.match(ExprParser.T__3) pass elif la_ == 8: - localctx = ExprParser.TimeShiftRangeContext(self, localctx) + localctx = ExprParser.TimeShiftContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 55 + self.state = 45 self.match(ExprParser.IDENTIFIER) - self.state = 56 - self.match(ExprParser.T__7) - self.state = 57 - localctx.shift1 = self.shift() - self.state = 58 + self.state = 46 self.match(ExprParser.T__10) - self.state = 59 - localctx.shift2 = self.shift() - self.state = 60 - self.match(ExprParser.T__9) + self.state = 47 + self.shift() + self.state = 48 + self.match(ExprParser.T__11) pass elif la_ == 9: - localctx = ExprParser.TimeRangeContext(self, localctx) + localctx = ExprParser.TimeIndexContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 62 + self.state = 50 self.match(ExprParser.IDENTIFIER) - self.state = 63 - self.match(ExprParser.T__7) - self.state = 64 - self.expr(0) - self.state = 65 + self.state = 51 self.match(ExprParser.T__10) - self.state = 66 + self.state = 52 self.expr(0) - self.state = 67 - self.match(ExprParser.T__9) + self.state = 53 + self.match(ExprParser.T__11) pass self._ctx.stop = self._input.LT(-1) - self.state = 82 + self.state = 68 self._errHandler.sync(self) - _alt = self._interp.adaptivePredict(self._input, 4, self._ctx) + _alt = self._interp.adaptivePredict(self._input, 2, self._ctx) while _alt != 2 and _alt != ATN.INVALID_ALT_NUMBER: if _alt == 1: if self._parseListeners is not None: self.triggerExitRuleEvent() _prevctx = localctx - self.state = 80 + self.state = 66 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input, 3, self._ctx) + la_ = self._interp.adaptivePredict(self._input, 1, self._ctx) if la_ == 1: localctx = ExprParser.MuldivContext( self, ExprParser.ExprContext(self, _parentctx, _parentState) @@ -1751,14 +1588,14 @@ def expr(self, _p: int = 0): self.pushNewRecursionContext( localctx, _startState, self.RULE_expr ) - self.state = 71 + self.state = 57 if not self.precpred(self._ctx, 8): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException( self, "self.precpred(self._ctx, 8)" ) - self.state = 72 + self.state = 58 localctx.op = self._input.LT(1) _la = self._input.LA(1) if not (_la == 5 or _la == 6): @@ -1766,7 +1603,7 @@ def expr(self, _p: int = 0): else: self._errHandler.reportMatch(self) self.consume() - self.state = 73 + self.state = 59 self.expr(9) pass @@ -1777,14 +1614,14 @@ def expr(self, _p: int = 0): self.pushNewRecursionContext( localctx, _startState, self.RULE_expr ) - self.state = 74 + self.state = 60 if not self.precpred(self._ctx, 7): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException( self, "self.precpred(self._ctx, 7)" ) - self.state = 75 + self.state = 61 localctx.op = self._input.LT(1) _la = self._input.LA(1) if not (_la == 2 or _la == 7): @@ -1792,7 +1629,7 @@ def expr(self, _p: int = 0): else: self._errHandler.reportMatch(self) self.consume() - self.state = 76 + self.state = 62 self.expr(8) pass @@ -1803,22 +1640,22 @@ def expr(self, _p: int = 0): self.pushNewRecursionContext( localctx, _startState, self.RULE_expr ) - self.state = 77 + self.state = 63 if not self.precpred(self._ctx, 6): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException( self, "self.precpred(self._ctx, 6)" ) - self.state = 78 + self.state = 64 self.match(ExprParser.COMPARISON) - self.state = 79 + self.state = 65 self.expr(7) pass - self.state = 84 + self.state = 70 self._errHandler.sync(self) - _alt = self._interp.adaptivePredict(self._input, 4, self._ctx) + _alt = self._interp.adaptivePredict(self._input, 2, self._ctx) except RecognitionException as re: localctx.exception = re @@ -1879,19 +1716,19 @@ def atom(self): localctx = ExprParser.AtomContext(self, self._ctx, self.state) self.enterRule(localctx, 4, self.RULE_atom) try: - self.state = 87 + self.state = 73 self._errHandler.sync(self) token = self._input.LA(1) - if token in [12]: + if token in [13]: localctx = ExprParser.NumberContext(self, localctx) self.enterOuterAlt(localctx, 1) - self.state = 85 + self.state = 71 self.match(ExprParser.NUMBER) pass - elif token in [14]: + elif token in [15]: localctx = ExprParser.IdentifierContext(self, localctx) self.enterOuterAlt(localctx, 2) - self.state = 86 + self.state = 72 self.match(ExprParser.IDENTIFIER) pass else: @@ -1935,13 +1772,13 @@ def shift(self): self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 89 + self.state = 75 self.match(ExprParser.TIME) - self.state = 91 + self.state = 77 self._errHandler.sync(self) _la = self._input.LA(1) if _la == 2 or _la == 7: - self.state = 90 + self.state = 76 self.shift_expr(0) except RecognitionException as re: @@ -2051,15 +1888,15 @@ def shift_expr(self, _p: int = 0): self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 101 + self.state = 87 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input, 7, self._ctx) + la_ = self._interp.adaptivePredict(self._input, 5, self._ctx) if la_ == 1: localctx = ExprParser.SignedAtomContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 94 + self.state = 80 localctx.op = self._input.LT(1) _la = self._input.LA(1) if not (_la == 2 or _la == 7): @@ -2067,7 +1904,7 @@ def shift_expr(self, _p: int = 0): else: self._errHandler.reportMatch(self) self.consume() - self.state = 95 + self.state = 81 self.atom() pass @@ -2075,7 +1912,7 @@ def shift_expr(self, _p: int = 0): localctx = ExprParser.SignedExpressionContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 96 + self.state = 82 localctx.op = self._input.LT(1) _la = self._input.LA(1) if not (_la == 2 or _la == 7): @@ -2083,26 +1920,26 @@ def shift_expr(self, _p: int = 0): else: self._errHandler.reportMatch(self) self.consume() - self.state = 97 + self.state = 83 self.match(ExprParser.T__2) - self.state = 98 + self.state = 84 self.expr(0) - self.state = 99 + self.state = 85 self.match(ExprParser.T__3) pass self._ctx.stop = self._input.LT(-1) - self.state = 111 + self.state = 97 self._errHandler.sync(self) - _alt = self._interp.adaptivePredict(self._input, 9, self._ctx) + _alt = self._interp.adaptivePredict(self._input, 7, self._ctx) while _alt != 2 and _alt != ATN.INVALID_ALT_NUMBER: if _alt == 1: if self._parseListeners is not None: self.triggerExitRuleEvent() _prevctx = localctx - self.state = 109 + self.state = 95 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input, 8, self._ctx) + la_ = self._interp.adaptivePredict(self._input, 6, self._ctx) if la_ == 1: localctx = ExprParser.ShiftMuldivContext( self, @@ -2113,14 +1950,14 @@ def shift_expr(self, _p: int = 0): self.pushNewRecursionContext( localctx, _startState, self.RULE_shift_expr ) - self.state = 103 + self.state = 89 if not self.precpred(self._ctx, 4): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException( self, "self.precpred(self._ctx, 4)" ) - self.state = 104 + self.state = 90 localctx.op = self._input.LT(1) _la = self._input.LA(1) if not (_la == 5 or _la == 6): @@ -2128,7 +1965,7 @@ def shift_expr(self, _p: int = 0): else: self._errHandler.reportMatch(self) self.consume() - self.state = 105 + self.state = 91 self.right_expr(0) pass @@ -2142,14 +1979,14 @@ def shift_expr(self, _p: int = 0): self.pushNewRecursionContext( localctx, _startState, self.RULE_shift_expr ) - self.state = 106 + self.state = 92 if not self.precpred(self._ctx, 3): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException( self, "self.precpred(self._ctx, 3)" ) - self.state = 107 + self.state = 93 localctx.op = self._input.LT(1) _la = self._input.LA(1) if not (_la == 2 or _la == 7): @@ -2157,13 +1994,13 @@ def shift_expr(self, _p: int = 0): else: self._errHandler.reportMatch(self) self.consume() - self.state = 108 + self.state = 94 self.right_expr(0) pass - self.state = 113 + self.state = 99 self._errHandler.sync(self) - _alt = self._interp.adaptivePredict(self._input, 9, self._ctx) + _alt = self._interp.adaptivePredict(self._input, 7, self._ctx) except RecognitionException as re: localctx.exception = re @@ -2250,7 +2087,7 @@ def right_expr(self, _p: int = 0): self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 120 + self.state = 106 self._errHandler.sync(self) token = self._input.LA(1) if token in [3]: @@ -2258,27 +2095,27 @@ def right_expr(self, _p: int = 0): self._ctx = localctx _prevctx = localctx - self.state = 115 + self.state = 101 self.match(ExprParser.T__2) - self.state = 116 + self.state = 102 self.expr(0) - self.state = 117 + self.state = 103 self.match(ExprParser.T__3) pass - elif token in [12, 14]: + elif token in [13, 15]: localctx = ExprParser.RightAtomContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 119 + self.state = 105 self.atom() pass else: raise NoViableAltException(self) self._ctx.stop = self._input.LT(-1) - self.state = 127 + self.state = 113 self._errHandler.sync(self) - _alt = self._interp.adaptivePredict(self._input, 11, self._ctx) + _alt = self._interp.adaptivePredict(self._input, 9, self._ctx) while _alt != 2 and _alt != ATN.INVALID_ALT_NUMBER: if _alt == 1: if self._parseListeners is not None: @@ -2291,14 +2128,14 @@ def right_expr(self, _p: int = 0): self.pushNewRecursionContext( localctx, _startState, self.RULE_right_expr ) - self.state = 122 + self.state = 108 if not self.precpred(self._ctx, 3): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException( self, "self.precpred(self._ctx, 3)" ) - self.state = 123 + self.state = 109 localctx.op = self._input.LT(1) _la = self._input.LA(1) if not (_la == 5 or _la == 6): @@ -2306,11 +2143,11 @@ def right_expr(self, _p: int = 0): else: self._errHandler.reportMatch(self) self.consume() - self.state = 124 + self.state = 110 self.right_expr(4) - self.state = 129 + self.state = 115 self._errHandler.sync(self) - _alt = self._interp.adaptivePredict(self._input, 11, self._ctx) + _alt = self._interp.adaptivePredict(self._input, 9, self._ctx) except RecognitionException as re: localctx.exception = re diff --git a/src/andromede/expression/parsing/antlr/ExprVisitor.py b/src/andromede/expression/parsing/antlr/ExprVisitor.py index 0e924349..db080cbf 100644 --- a/src/andromede/expression/parsing/antlr/ExprVisitor.py +++ b/src/andromede/expression/parsing/antlr/ExprVisitor.py @@ -1,4 +1,4 @@ -# Generated from Expr.g4 by ANTLR 4.13.1 +# Generated from Expr.g4 by ANTLR 4.13.2 from antlr4 import * if "." in __name__: @@ -14,6 +14,10 @@ class ExprVisitor(ParseTreeVisitor): def visitFullexpr(self, ctx: ExprParser.FullexprContext): return self.visitChildren(ctx) + # Visit a parse tree produced by ExprParser#timeSum. + def visitTimeSum(self, ctx: ExprParser.TimeSumContext): + return self.visitChildren(ctx) + # Visit a parse tree produced by ExprParser#negation. def visitNegation(self, ctx: ExprParser.NegationContext): return self.visitChildren(ctx) @@ -34,6 +38,10 @@ def visitTimeIndex(self, ctx: ExprParser.TimeIndexContext): def visitComparison(self, ctx: ExprParser.ComparisonContext): return self.visitChildren(ctx) + # Visit a parse tree produced by ExprParser#allTimeSum. + def visitAllTimeSum(self, ctx: ExprParser.AllTimeSumContext): + return self.visitChildren(ctx) + # Visit a parse tree produced by ExprParser#timeShift. def visitTimeShift(self, ctx: ExprParser.TimeShiftContext): return self.visitChildren(ctx) @@ -46,10 +54,6 @@ def visitFunction(self, ctx: ExprParser.FunctionContext): def visitAddsub(self, ctx: ExprParser.AddsubContext): return self.visitChildren(ctx) - # Visit a parse tree produced by ExprParser#timeShiftRange. - def visitTimeShiftRange(self, ctx: ExprParser.TimeShiftRangeContext): - return self.visitChildren(ctx) - # Visit a parse tree produced by ExprParser#portField. def visitPortField(self, ctx: ExprParser.PortFieldContext): return self.visitChildren(ctx) @@ -58,10 +62,6 @@ def visitPortField(self, ctx: ExprParser.PortFieldContext): def visitMuldiv(self, ctx: ExprParser.MuldivContext): return self.visitChildren(ctx) - # Visit a parse tree produced by ExprParser#timeRange. - def visitTimeRange(self, ctx: ExprParser.TimeRangeContext): - return self.visitChildren(ctx) - # Visit a parse tree produced by ExprParser#number. def visitNumber(self, ctx: ExprParser.NumberContext): return self.visitChildren(ctx) diff --git a/src/andromede/expression/parsing/parse_expression.py b/src/andromede/expression/parsing/parse_expression.py index e96a70f1..add7544e 100644 --- a/src/andromede/expression/parsing/parse_expression.py +++ b/src/andromede/expression/parsing/parse_expression.py @@ -12,17 +12,12 @@ from dataclasses import dataclass from typing import Set -from antlr4 import CommonTokenStream, DiagnosticErrorListener, InputStream +from antlr4 import CommonTokenStream, InputStream from antlr4.error.ErrorStrategy import BailErrorStrategy from andromede.expression import ExpressionNode, literal, param, var from andromede.expression.equality import expressions_equal -from andromede.expression.expression import ( - Comparator, - ComparisonNode, - ExpressionRange, - PortFieldNode, -) +from andromede.expression.expression import Comparator, ComparisonNode, PortFieldNode from andromede.expression.parsing.antlr.ExprLexer import ExprLexer from andromede.expression.parsing.antlr.ExprParser import ExprParser from andromede.expression.parsing.antlr.ExprVisitor import ExprVisitor @@ -125,31 +120,27 @@ def visitComparison(self, ctx: ExprParser.ComparisonContext) -> ExpressionNode: # Visit a parse tree produced by ExprParser#timeShift. def visitTimeIndex(self, ctx: ExprParser.TimeIndexContext) -> ExpressionNode: - shifted_expr = self._convert_identifier(ctx.IDENTIFIER().getText()) # type: ignore - time_shifts = [e.accept(self) for e in ctx.expr()] # type: ignore - return shifted_expr.eval(time_shifts) - - # Visit a parse tree produced by ExprParser#rangeTimeShift. - def visitTimeRange(self, ctx: ExprParser.TimeRangeContext) -> ExpressionNode: - shifted_expr = self._convert_identifier(ctx.IDENTIFIER().getText()) # type: ignore - expressions = [e.accept(self) for e in ctx.expr()] # type: ignore - return shifted_expr.eval(ExpressionRange(expressions[0], expressions[1])) + expr = self._convert_identifier(ctx.IDENTIFIER().getText()) # type: ignore + eval_time = ctx.expr().accept(self) # type: ignore + return expr.eval(eval_time) def visitTimeShift(self, ctx: ExprParser.TimeShiftContext) -> ExpressionNode: shifted_expr = self._convert_identifier(ctx.IDENTIFIER().getText()) # type: ignore - time_shifts = [s.accept(self) for s in ctx.shift()] # type: ignore + time_shift = ctx.shift().accept(self) # type: ignore # specifics for x[t] ... - if len(time_shifts) == 1 and expressions_equal(time_shifts[0], literal(0)): + if expressions_equal(time_shift, literal(0)): return shifted_expr - return shifted_expr.shift(time_shifts) + return shifted_expr.shift(time_shift) - def visitTimeShiftRange( - self, ctx: ExprParser.TimeShiftRangeContext - ) -> ExpressionNode: - shifted_expr = self._convert_identifier(ctx.IDENTIFIER().getText()) # type: ignore - shift1 = ctx.shift1.accept(self) # type: ignore - shift2 = ctx.shift2.accept(self) # type: ignore - return shifted_expr.shift(ExpressionRange(shift1, shift2)) + def visitTimeSum(self, ctx: ExprParser.TimeSumContext) -> ExpressionNode: + shifted_expr = ctx.expr().accept(self) # type: ignore + from_shift = ctx.from_.accept(self) # type: ignore + to_shift = ctx.to.accept(self) # type: ignore + return shifted_expr.time_sum(from_shift, to_shift) + + def visitAllTimeSum(self, ctx: ExprParser.AllTimeSumContext) -> ExpressionNode: + shifted_expr = ctx.expr().accept(self) # type: ignore + return shifted_expr.time_sum() # Visit a parse tree produced by ExprParser#function. def visitFunction(self, ctx: ExprParser.FunctionContext) -> ExpressionNode: @@ -228,7 +219,6 @@ def visitRightAtom(self, ctx: ExprParser.RightAtomContext) -> ExpressionNode: _FUNCTIONS = { - "sum": ExpressionNode.sum, "sum_connections": ExpressionNode.sum_connections, "expec": ExpressionNode.expec, } diff --git a/src/andromede/expression/port_resolver.py b/src/andromede/expression/port_resolver.py index b09cae7a..66ab8f5d 100644 --- a/src/andromede/expression/port_resolver.py +++ b/src/andromede/expression/port_resolver.py @@ -10,15 +10,12 @@ # # This file is part of the Antares project. -from abc import ABC, abstractmethod from dataclasses import dataclass from typing import Dict, List from andromede.expression import CopyVisitor, sum_expressions, visit from andromede.expression.expression import ( - AdditionNode, ExpressionNode, - LiteralNode, PortFieldAggregatorNode, PortFieldNode, ) diff --git a/src/andromede/expression/print.py b/src/andromede/expression/print.py index c01ae76f..469a24e5 100644 --- a/src/andromede/expression/print.py +++ b/src/andromede/expression/print.py @@ -14,13 +14,18 @@ from typing import Dict from andromede.expression.expression import ( + AllTimeSumNode, ComponentParameterNode, ComponentVariableNode, ExpressionNode, PortFieldAggregatorNode, PortFieldNode, + ProblemParameterNode, + ProblemVariableNode, + TimeEvalNode, + TimeShiftNode, + TimeSumNode, ) -from andromede.expression.visitor import T from .expression import ( AdditionNode, @@ -32,9 +37,6 @@ NegationNode, ParameterNode, ScenarioOperatorNode, - SubstractionNode, - TimeAggregatorNode, - TimeOperatorNode, VariableNode, ) from .visitor import ExpressionVisitor, visit @@ -61,14 +63,15 @@ def negation(self, node: NegationNode) -> str: return f"-({visit(node.operand, self)})" def addition(self, node: AdditionNode) -> str: - left_value = visit(node.left, self) - right_value = visit(node.right, self) - return f"({left_value} + {right_value})" - - def substraction(self, node: SubstractionNode) -> str: - left_value = visit(node.left, self) - right_value = visit(node.right, self) - return f"({left_value} - {right_value})" + if len(node.operands) == 0: + return "" + res = visit(node.operands[0], self) + for o in node.operands[1:]: + if isinstance(o, NegationNode): + res += f" - {visit(o.operand, self)}" + else: + res += f" + {visit(o, self)}" + return f"({res})" def multiplication(self, node: MultiplicationNode) -> str: left_value = visit(node.left, self) @@ -98,12 +101,25 @@ def comp_variable(self, node: ComponentVariableNode) -> str: def comp_parameter(self, node: ComponentParameterNode) -> str: return f"{node.component_id}.{node.name}" - # TODO: Add pretty print for node.instances_index - def time_operator(self, node: TimeOperatorNode) -> str: - return f"({visit(node.operand, self)}.{str(node.name)}({node.instances_index}))" + def pb_variable(self, node: ProblemVariableNode) -> str: + # TODO + return f"{node.component_id}.{node.name}" + + def pb_parameter(self, node: ProblemParameterNode) -> str: + # TODO + return f"{node.component_id}.{node.name}" + + def time_shift(self, node: TimeShiftNode) -> str: + return f"({visit(node.operand, self)}.shift({visit(node.time_shift, self)}))" + + def time_eval(self, node: TimeEvalNode) -> str: + return f"({visit(node.operand, self)}.eval({visit(node.eval_time, self)}))" + + def time_sum(self, node: TimeSumNode) -> str: + return f"({visit(node.operand, self)}.time_sum({visit(node.from_time, self)}, {visit(node.to_time, self)}))" - def time_aggregator(self, node: TimeAggregatorNode) -> str: - return f"({visit(node.operand, self)}.{str(node.name)}({node.stay_roll}))" + def all_time_sum(self, node: AllTimeSumNode) -> str: + return f"({visit(node.operand, self)}.time_sum())" def scenario_operator(self, node: ScenarioOperatorNode) -> str: return f"({visit(node.operand, self)}.{str(node.name)})" diff --git a/src/andromede/expression/time_operator.py b/src/andromede/expression/time_operator.py deleted file mode 100644 index 63059528..00000000 --- a/src/andromede/expression/time_operator.py +++ /dev/null @@ -1,110 +0,0 @@ -# Copyright (c) 2024, RTE (https://www.rte-france.com) -# -# See AUTHORS.txt -# -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. -# -# SPDX-License-Identifier: MPL-2.0 -# -# This file is part of the Antares project. - -""" -Operators that allow temporal manipulation of expressions -""" - -from abc import ABC, abstractmethod -from dataclasses import dataclass -from typing import Any, List, Tuple - - -@dataclass(frozen=True) -class TimeOperator(ABC): - """ - A time operator on an expression is charactirized by two attributes: - - time_ids: int, List[int] or range, is the list of time indices to which the operator applies - - is_rolling: bool, if true, this means that the time_ids are to be understood relatively to the current timestep of the context AND that the represented expression will have to be instanciated for all timesteps. Otherwise, the time_ids are "absolute" times and the expression only has to be instantiated once. - """ - - time_ids: List[int] - - @classmethod - @abstractmethod - def rolling(cls) -> bool: - raise NotImplementedError - - def __post_init__(self) -> None: - if isinstance(self.time_ids, int): - object.__setattr__(self, "time_ids", [self.time_ids]) - elif isinstance(self.time_ids, range): - object.__setattr__(self, "time_ids", list(self.time_ids)) - - def key(self) -> Tuple[int, ...]: - return tuple(self.time_ids) - - def size(self) -> int: - return len(self.time_ids) - - -@dataclass(frozen=True) -class TimeShift(TimeOperator): - """ - Time shift of variables - - Examples: - >>> x.shift([1, 2, 4]) represents the vector of variables (x[t+1], x[t+2], x[t+4]) - """ - - def __str__(self) -> str: - return f"shift({self.time_ids})" - - def __eq__(self, other: Any) -> bool: - return isinstance(other, TimeShift) and self.time_ids == other.time_ids - - def __hash__(self) -> int: - return hash(self.key()) - - @classmethod - def rolling(cls) -> bool: - return True - - -@dataclass(frozen=True) -class TimeEvaluation(TimeOperator): - """ - Absolute time evalaution of variables - - Examples: - >>> x.eval([1, 2, 4]) represents the vector of variables (x[1], x[2], x[4]) - """ - - def __str__(self) -> str: - return f"eval({self.time_ids})" - - def __eq__(self, other: Any) -> bool: - return isinstance(other, TimeEvaluation) and self.time_ids == other.time_ids - - def __hash__(self) -> int: - return hash(self.key()) - - @classmethod - def rolling(cls) -> bool: - return False - - -@dataclass(frozen=True) -class TimeAggregator: - stay_roll: bool - - def size(self) -> int: - return 1 - - -@dataclass(frozen=True) -class TimeSum(TimeAggregator): - def __str__(self) -> str: - return f"sum({self.stay_roll})" - - def __eq__(self, other: Any) -> bool: - return isinstance(other, TimeSum) and self.stay_roll == other.stay_roll diff --git a/src/andromede/expression/visitor.py b/src/andromede/expression/visitor.py index 25bbfb02..351be9e5 100644 --- a/src/andromede/expression/visitor.py +++ b/src/andromede/expression/visitor.py @@ -19,6 +19,7 @@ from andromede.expression.expression import ( AdditionNode, + AllTimeSumNode, ComparisonNode, ComponentParameterNode, ComponentVariableNode, @@ -30,10 +31,12 @@ ParameterNode, PortFieldAggregatorNode, PortFieldNode, + ProblemParameterNode, + ProblemVariableNode, ScenarioOperatorNode, - SubstractionNode, - TimeAggregatorNode, - TimeOperatorNode, + TimeEvalNode, + TimeShiftNode, + TimeSumNode, VariableNode, ) @@ -61,10 +64,6 @@ def negation(self, node: NegationNode) -> T: def addition(self, node: AdditionNode) -> T: ... - @abstractmethod - def substraction(self, node: SubstractionNode) -> T: - ... - @abstractmethod def multiplication(self, node: MultiplicationNode) -> T: ... @@ -94,11 +93,27 @@ def comp_variable(self, node: ComponentVariableNode) -> T: ... @abstractmethod - def time_operator(self, node: TimeOperatorNode) -> T: + def pb_parameter(self, node: ProblemParameterNode) -> T: + ... + + @abstractmethod + def pb_variable(self, node: ProblemVariableNode) -> T: + ... + + @abstractmethod + def time_shift(self, node: TimeShiftNode) -> T: + ... + + @abstractmethod + def time_eval(self, node: TimeEvalNode) -> T: + ... + + @abstractmethod + def time_sum(self, node: TimeSumNode) -> T: ... @abstractmethod - def time_aggregator(self, node: TimeAggregatorNode) -> T: + def all_time_sum(self, node: AllTimeSumNode) -> T: ... @abstractmethod @@ -130,20 +145,26 @@ def visit(root: ExpressionNode, visitor: ExpressionVisitor[T]) -> T: return visitor.comp_parameter(root) elif isinstance(root, ComponentVariableNode): return visitor.comp_variable(root) + elif isinstance(root, ProblemParameterNode): + return visitor.pb_parameter(root) + elif isinstance(root, ProblemVariableNode): + return visitor.pb_variable(root) elif isinstance(root, AdditionNode): return visitor.addition(root) elif isinstance(root, MultiplicationNode): return visitor.multiplication(root) elif isinstance(root, DivisionNode): return visitor.division(root) - elif isinstance(root, SubstractionNode): - return visitor.substraction(root) elif isinstance(root, ComparisonNode): return visitor.comparison(root) - elif isinstance(root, TimeOperatorNode): - return visitor.time_operator(root) - elif isinstance(root, TimeAggregatorNode): - return visitor.time_aggregator(root) + elif isinstance(root, TimeShiftNode): + return visitor.time_shift(root) + elif isinstance(root, TimeEvalNode): + return visitor.time_eval(root) + elif isinstance(root, TimeSumNode): + return visitor.time_sum(root) + elif isinstance(root, AllTimeSumNode): + return visitor.all_time_sum(root) elif isinstance(root, ScenarioOperatorNode): return visitor.scenario_operator(root) elif isinstance(root, PortFieldNode): @@ -192,14 +213,11 @@ def negation(self, node: NegationNode) -> T_op: return -visit(node.operand, self) def addition(self, node: AdditionNode) -> T_op: - left_value = visit(node.left, self) - right_value = visit(node.right, self) - return left_value + right_value - - def substraction(self, node: SubstractionNode) -> T_op: - left_value = visit(node.left, self) - right_value = visit(node.right, self) - return left_value - right_value + operands = [visit(o, self) for o in node.operands] + res = operands[0] + for o in operands[1:]: + res = res + o + return res def multiplication(self, node: MultiplicationNode) -> T_op: left_value = visit(node.left, self) diff --git a/src/andromede/libs/standard.py b/src/andromede/libs/standard.py index b48025ae..dc033441 100644 --- a/src/andromede/libs/standard.py +++ b/src/andromede/libs/standard.py @@ -14,7 +14,7 @@ The standard module contains the definition of standard models. """ from andromede.expression import literal, param, var -from andromede.expression.expression import ExpressionRange, port_field +from andromede.expression.expression import port_field from andromede.expression.indexing_structure import IndexingStructure from andromede.model.common import ProblemContext from andromede.model.constraint import Constraint @@ -61,7 +61,7 @@ param("spillage_cost") * var("spillage") + param("ens_cost") * var("unsupplied_energy") ) - .sum() + .time_sum() .expec(), ) @@ -130,7 +130,7 @@ ), ], objective_operational_contribution=(param("cost") * var("generation")) - .sum() + .time_sum() .expec(), ) @@ -160,7 +160,7 @@ ), # To test both ways of setting constraints ], objective_operational_contribution=(param("cost") * var("generation")) - .sum() + .time_sum() .expec(), ) @@ -189,11 +189,44 @@ ), Constraint( name="Total storage", - expression=var("generation").sum() <= param("full_storage"), + expression=var("generation").time_sum() <= param("full_storage"), ), ], objective_operational_contribution=(param("cost") * var("generation")) - .sum() + .time_sum() + .expec(), +) + +""" +A model for a linear cost generation limited by a maximum generation per time-step +and total generation in whole period. It considers a full storage with no replenishing +""" +GENERATOR_MODEL_WITH_STORAGE = model( + id="GEN", + parameters=[ + float_parameter("p_max", CONSTANT), + float_parameter("cost", CONSTANT), + float_parameter("full_storage", CONSTANT), + ], + variables=[float_variable("generation", lower_bound=literal(0))], + ports=[ModelPort(port_type=BALANCE_PORT_TYPE, port_name="balance_port")], + port_fields_definitions=[ + PortFieldDefinition( + port_field=PortFieldId("balance_port", "flow"), + definition=var("generation"), + ) + ], + constraints=[ + Constraint( + name="Max generation", expression=var("generation") <= param("p_max") + ), + Constraint( + name="Total storage", + expression=var("generation").time_sum() <= param("full_storage"), + ), + ], + objective_operational_contribution=(param("cost") * var("generation")) + .time_sum() .expec(), ) @@ -255,22 +288,17 @@ ), Constraint( "Min up time", - var("nb_start") - .shift(ExpressionRange(-param("d_min_up") + 1, literal(0))) - .sum() + var("nb_start").time_sum(-param("d_min_up") + 1, literal(0)) <= var("nb_on"), ), Constraint( "Min down time", - var("nb_stop") - .shift(ExpressionRange(-param("d_min_down") + 1, literal(0))) - .sum() + var("nb_stop").time_sum(-param("d_min_down") + 1, literal(0)) <= param("nb_units_max").shift(-param("d_min_down")) - var("nb_on"), ), - # It also works by writing ExpressionRange(-param("d_min_down") + 1, 0) as ExpressionRange's __post_init__ wraps integers to literal nodes. However, MyPy does not seem to infer that ExpressionRange's attributes are necessarily of ExpressionNode type and raises an error if the arguments in the constructor are integer (whereas it runs correctly), this why we specify it here with literal(0) instead of 0. ], objective_operational_contribution=(param("cost") * var("generation")) - .sum() + .time_sum() .expec(), ) @@ -332,21 +360,17 @@ ), Constraint( "Min up time", - var("nb_start") - .shift(ExpressionRange(-param("d_min_up") + 1, literal(0))) - .sum() + var("nb_start").time_sum(-param("d_min_up") + 1, literal(0)) <= var("nb_on"), ), Constraint( "Min down time", - var("nb_stop") - .shift(ExpressionRange(-param("d_min_down") + 1, literal(0))) - .sum() + var("nb_stop").time_sum(-param("d_min_down") + 1, literal(0)) <= param("nb_units_max").shift(-param("d_min_down")) - var("nb_on"), ), ], objective_operational_contribution=(param("cost") * var("generation")) - .sum() + .time_sum() .expec(), ) @@ -361,7 +385,9 @@ definition=-var("spillage"), ) ], - objective_operational_contribution=(param("cost") * var("spillage")).sum().expec(), + objective_operational_contribution=(param("cost") * var("spillage")) + .time_sum() + .expec(), ) UNSUPPLIED_ENERGY_MODEL = model( @@ -376,7 +402,7 @@ ) ], objective_operational_contribution=(param("cost") * var("unsupplied_energy")) - .sum() + .time_sum() .expec(), ) @@ -456,7 +482,7 @@ Constraint(name="Max generation", expression=var("generation") <= var("p_max")) ], objective_operational_contribution=(param("op_cost") * var("generation")) - .sum() + .time_sum() .expec(), objective_investment_contribution=param("invest_cost") * var("p_max"), ) @@ -495,7 +521,7 @@ ) ], objective_operational_contribution=(param("op_cost") * var("generation")) - .sum() + .time_sum() .expec(), objective_investment_contribution=param("invest_cost") * var("invested_capa"), ) diff --git a/src/andromede/libs/standard_sc.py b/src/andromede/libs/standard_sc.py index b1411ab3..9e24b99a 100644 --- a/src/andromede/libs/standard_sc.py +++ b/src/andromede/libs/standard_sc.py @@ -170,11 +170,11 @@ definition=var("p") * param("emission_rate"), ), ], - objective_operational_contribution=(param("cost") * var("p")).sum().expec(), + objective_operational_contribution=(param("cost") * var("p")).time_sum().expec(), ) """ -Model of the CO² quota. +Model of the CO² quota. It takes a set a CO² emissions as input. It forces the sum of those emissions to be smaller than a predefined quota. """ QUOTA_CO2_MODEL = model( @@ -286,6 +286,6 @@ + param("Pgrad+s_penality") * var("Pgrad+s") + param("Pgrad-s_penality") * var("Pgrad-s") ) - .sum() + .time_sum() .expec(), ) diff --git a/src/andromede/main/main.py b/src/andromede/main/main.py new file mode 100644 index 00000000..33d5784f --- /dev/null +++ b/src/andromede/main/main.py @@ -0,0 +1,100 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + + +from pathlib import Path +from typing import List, Optional + +from andromede.model.library import Library +from andromede.model.parsing import parse_yaml_library +from andromede.model.resolve_library import resolve_library +from andromede.simulation import TimeBlock, build_problem +from andromede.study import DataBase +from andromede.study.parsing import parse_cli, parse_yaml_components +from andromede.study.resolve_components import ( + NetworkComponents, + build_data_base, + build_network, + consistency_check, + resolve_components_and_cnx, +) + + +class AntaresTimeSeriesImportError(Exception): + pass + + +def input_models(model_paths: List[Path]) -> Library: + yaml_libraries = [] + yaml_library_ids = set() + + for path in model_paths: + with path.open("r") as file: + yaml_lib = parse_yaml_library(file) + + if yaml_lib.id in yaml_library_ids: + raise ValueError(f"The identifier '{yaml_lib.id}' is defined twice") + + yaml_libraries.append(yaml_lib) + yaml_library_ids.add(yaml_lib.id) + + return resolve_library(yaml_libraries) + + +def input_database(study_path: Path, timeseries_path: Optional[Path]) -> DataBase: + with study_path.open() as comp: + return build_data_base(parse_yaml_components(comp), timeseries_path) + + +def input_components(study_path: Path, model: Library) -> NetworkComponents: + with study_path.open() as comp: + return resolve_components_and_cnx(parse_yaml_components(comp), model) + + +def main_cli() -> None: + parsed_args = parse_cli() + + models = input_models(parsed_args.models_path) + components = input_components(parsed_args.components_path, models) + consistency_check(components.components, models.models) + + try: + database = input_database( + parsed_args.components_path, parsed_args.timeseries_path + ) + + except UnboundLocalError: + raise AntaresTimeSeriesImportError( + f"An error occurred while importing time series." + ) + + network = build_network(components) + + timeblock = TimeBlock(1, list(range(parsed_args.duration))) + scenario = parsed_args.nb_scenarios + + try: + problem = build_problem(network, database, timeblock, scenario) + + except IndexError as e: + raise IndexError( + f"{e}. Did parameters '--duration' and '--scenario' were correctly set?" + ) + + status = problem.solver.Solve() + print("status : ", status) + + print("final average cost : ", problem.solver.Objective().Value()) + + +if __name__ == "__main__": + main_cli() diff --git a/src/andromede/model/parsing.py b/src/andromede/model/parsing.py index ef039ef5..1a4f702e 100644 --- a/src/andromede/model/parsing.py +++ b/src/andromede/model/parsing.py @@ -97,6 +97,7 @@ class InputModel(ModifiedBaseModel): class InputLibrary(ModifiedBaseModel): id: str + dependencies: List[str] = Field(default_factory=list) port_types: List[InputPortType] = Field(default_factory=list) models: List[InputModel] = Field(default_factory=list) description: Optional[str] = None diff --git a/src/andromede/model/port.py b/src/andromede/model/port.py index c81753aa..cf1951fd 100644 --- a/src/andromede/model/port.py +++ b/src/andromede/model/port.py @@ -23,18 +23,21 @@ MultiplicationNode, NegationNode, ParameterNode, - SubstractionNode, VariableNode, ) from andromede.expression.expression import ( + AllTimeSumNode, BinaryOperatorNode, ComponentParameterNode, ComponentVariableNode, PortFieldAggregatorNode, PortFieldNode, + ProblemParameterNode, + ProblemVariableNode, ScenarioOperatorNode, - TimeAggregatorNode, - TimeOperatorNode, + TimeEvalNode, + TimeShiftNode, + TimeSumNode, ) from andromede.expression.visitor import visit @@ -106,10 +109,8 @@ def _visit_binary_op(self, node: BinaryOperatorNode) -> None: 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) + for n in node.operands: + visit(n, self) def multiplication(self, node: MultiplicationNode) -> None: self._visit_binary_op(node) @@ -136,10 +137,26 @@ def comp_variable(self, node: ComponentVariableNode) -> None: "Port definition must not contain a variable associated to a component." ) - def time_operator(self, node: TimeOperatorNode) -> None: + def pb_parameter(self, node: ProblemParameterNode) -> None: + raise ValueError( + "Port definition must not contain a parameter associated to a component." + ) + + def pb_variable(self, node: ProblemVariableNode) -> None: + raise ValueError( + "Port definition must not contain a variable associated to a component." + ) + + def time_shift(self, node: TimeShiftNode) -> None: + visit(node.operand, self) + + def time_eval(self, node: TimeEvalNode) -> None: + visit(node.operand, self) + + def time_sum(self, node: TimeSumNode) -> None: visit(node.operand, self) - def time_aggregator(self, node: TimeAggregatorNode) -> None: + def all_time_sum(self, node: AllTimeSumNode) -> None: visit(node.operand, self) def scenario_operator(self, node: ScenarioOperatorNode) -> None: diff --git a/src/andromede/model/resolve_library.py b/src/andromede/model/resolve_library.py index 38d2d4ea..e1daa9a1 100644 --- a/src/andromede/model/resolve_library.py +++ b/src/andromede/model/resolve_library.py @@ -9,7 +9,7 @@ # SPDX-License-Identifier: MPL-2.0 # # This file is part of the Antares project. -from typing import Dict, List, Optional +from typing import Dict, List, Optional, Set from andromede.expression import ExpressionNode, literal from andromede.expression.indexing_structure import IndexingStructure @@ -29,8 +29,7 @@ Variable, model, ) -from andromede.model.library import Library, library -from andromede.model.port import PortFieldDefinition, port_field_def +from andromede.model.library import Library from andromede.model.parsing import ( InputConstraint, InputField, @@ -42,10 +41,11 @@ InputPortType, InputVariable, ) +from andromede.model.port import PortFieldDefinition, port_field_def def resolve_library( - input_lib: InputLibrary, preloaded_libraries: Optional[List[Library]] = None + input_libs: List[InputLibrary], preloaded_libs: Optional[List[Library]] = None ) -> Library: """ Converts parsed data into an actually usable library of models. @@ -53,14 +53,62 @@ def resolve_library( - resolves references between models and ports - parses expressions and resolves references to variables/params """ - if preloaded_libraries is None: - preloaded_libraries = [] - port_types = [_convert_port_type(p) for p in input_lib.port_types] - for lib in preloaded_libraries: - port_types.extend(lib.port_types.values()) - port_types_dict = dict((p.id, p) for p in port_types) - models = [_resolve_model(m, port_types_dict) for m in input_lib.models] - return library(port_types, models) + yaml_lib_dict = dict((l.id, l) for l in input_libs) + + preloaded_port_types = {} + if preloaded_libs: + for preloaded_lib in preloaded_libs: + preloaded_port_types.update(preloaded_lib.port_types) + + output_lib = Library(port_types=preloaded_port_types, models={}) + + todo: List[str] = list(yaml_lib_dict) + done: Set[str] = set() + import_stack: List[str] = [] + + while todo: + next_lib_id = todo.pop() + + if next_lib_id in done: + continue + else: + import_stack.append(next_lib_id) + + while import_stack: + cur_lib = yaml_lib_dict[import_stack[-1]] + dependencies = set(cur_lib.dependencies) - done + + if dependencies: + first_dependency = dependencies.pop() + + if first_dependency in import_stack: + raise Exception("Circular import in yaml libraries") + import_stack.append(first_dependency) + + else: + port_types = [_convert_port_type(p) for p in cur_lib.port_types] + port_types_dict = dict((p.id, p) for p in port_types) + + if output_lib.port_types.keys() & port_types_dict.keys(): + raise Exception( + f"Port(s) : {str(output_lib.port_types.keys() & port_types_dict.keys())} is(are) defined twice." + ) + output_lib.port_types.update(port_types_dict) + + models = [ + _resolve_model(m, output_lib.port_types) for m in cur_lib.models + ] + + models_dict = dict((m.id, m) for m in models) + if output_lib.models.keys() & models_dict.keys(): + raise Exception( + f"Model(s) : {str(output_lib.models.keys() & models_dict.keys())} is(are) defined twice" + ) + output_lib.models.update(models_dict) + + done.add(import_stack.pop()) + + return output_lib def _convert_field(field: InputField) -> PortField: diff --git a/src/andromede/simulation/linear_expression.py b/src/andromede/simulation/linear_expression.py index 430cb9f3..44217610 100644 --- a/src/andromede/simulation/linear_expression.py +++ b/src/andromede/simulation/linear_expression.py @@ -14,12 +14,17 @@ Specific modelling for "instantiated" linear expressions, with only variables and literal coefficients. """ -from dataclasses import dataclass, field +import dataclasses +from dataclasses import dataclass from typing import Callable, Dict, List, Optional, TypeVar, Union -from andromede.expression.indexing_structure import IndexingStructure -from andromede.expression.scenario_operator import ScenarioOperator -from andromede.expression.time_operator import TimeAggregator, TimeOperator +from andromede.expression.expression import ( + OneScenarioIndex, + ScenarioIndex, + TimeIndex, + TimeShift, + TimeStep, +) T = TypeVar("T") @@ -42,6 +47,75 @@ def is_minus_one(value: float) -> bool: return is_close_abs(value, -1, EPS) +@dataclass(frozen=True) +class TimeExpansion: + """ + Carries knowledge of which timesteps this term refers to. + Simplest one is "only the current timestep" + """ + + def get_timesteps(self, current_timestep: int, block_length: int) -> List[int]: + return [current_timestep] + + def apply(self, other: "TimeExpansion") -> "TimeExpansion": + """ + Apply another time expansion on this one. + For example, a shift of -1 applied to a shift one +1 could provide + a no-op TimeExpansion. Not yet supported for now, though. + """ + return other + + +@dataclass(frozen=True) +class AllTimeExpansion(TimeExpansion): + def get_timesteps(self, current_timestep: int, block_length: int) -> List[int]: + return [t for t in range(block_length)] + + def apply(self, other: "TimeExpansion") -> "TimeExpansion": + raise ValueError("No time operation allowed on all-time sum.") + + +@dataclass(frozen=True) +class TimeEvalExpansion(TimeExpansion): + timestep: int + + def get_timesteps(self, current_timestep: int, block_length: int) -> List[int]: + return [self.timestep] + + def apply(self, other: "TimeExpansion") -> "TimeExpansion": + raise ValueError( + "Time operation on evaluated expression not supported for now." + ) + + +@dataclass(frozen=True) +class TimeShiftExpansion(TimeExpansion): + shift: int + + def get_timesteps(self, current_timestep: int, block_length: int) -> List[int]: + return [current_timestep + self.shift] + + def apply(self, other: "TimeExpansion") -> "TimeExpansion": + raise ValueError("Time operation on shifted expression not supported for now.") + + +@dataclass(frozen=True) +class TimeSumExpansion(TimeExpansion): + from_shift: int + to_shift: int + + def get_timesteps(self, current_timestep: int, block_length: int) -> List[int]: + return [ + t + for t in range( + current_timestep + self.from_shift, current_timestep + self.to_shift + ) + ] + + def apply(self, other: "TimeExpansion") -> "TimeExpansion": + raise ValueError("Time operation on time-sums not supported for now.") + + @dataclass(frozen=True) class TermKey: @@ -51,9 +125,47 @@ class TermKey: component_id: str variable_name: str - time_operator: Optional[TimeOperator] - time_aggregator: Optional[TimeAggregator] - scenario_operator: Optional[ScenarioOperator] + time_index: Optional[int] + scenario_index: Optional[int] + + +def _str_for_coeff(coeff: float) -> str: + if is_one(coeff): + return "+" + elif is_minus_one(coeff): + return "-" + else: + return "{:+g}".format(coeff) + + +def _time_index_to_str(time_index: TimeIndex) -> str: + if isinstance(time_index, TimeShift): + if time_index.timeshift == 0: + return "t" + elif time_index.timeshift > 0: + return f"t + {time_index.timeshift}" + else: + return f"t - {-time_index.timeshift}" + if isinstance(time_index, TimeStep): + return f"{time_index.timestep}" + return "" + + +def _scenario_index_to_str(scenario_index: ScenarioIndex) -> str: + if isinstance(scenario_index, OneScenarioIndex): + return f"{scenario_index.scenario}" + return "" + + +def _str_for_time_expansion(exp: TimeExpansion) -> str: + if isinstance(exp, TimeShiftExpansion): + return f".shift({exp.shift})" + elif isinstance(exp, TimeSumExpansion): + return f".sum({exp.from_shift}, {exp.to_shift})" + elif isinstance(exp, AllTimeExpansion): + return ".sum()" + else: + return "" @dataclass(frozen=True) @@ -69,136 +181,69 @@ class Term: coefficient: float component_id: str variable_name: str - structure: IndexingStructure = field( - default=IndexingStructure(time=True, scenario=True) - ) - time_operator: Optional[TimeOperator] = None - time_aggregator: Optional[TimeAggregator] = None - scenario_operator: Optional[ScenarioOperator] = None + time_index: Optional[int] + scenario_index: Optional[int] # TODO: It may be useful to define __add__, __sub__, etc on terms, which should return a linear expression ? def is_zero(self) -> bool: return is_zero(self.coefficient) - def str_for_coeff(self) -> str: - str_for_coeff = "" - if is_one(self.coefficient): - str_for_coeff = "+" - elif is_minus_one(self.coefficient): - str_for_coeff = "-" - else: - str_for_coeff = "{:+g}".format(self.coefficient) - return str_for_coeff - def __str__(self) -> str: # Useful for debugging tests - result = self.str_for_coeff() + str(self.variable_name) - if self.time_operator is not None: - result += f".{str(self.time_operator)}" - if self.time_aggregator is not None: - result += f".{str(self.time_aggregator)}" - if self.scenario_operator is not None: - result += f".{str(self.scenario_operator)}" - return result + return repr(self) - def number_of_instances(self) -> int: - if self.time_aggregator is not None: - return self.time_aggregator.size() - else: - if self.time_operator is not None: - return self.time_operator.size() - else: - return 1 + def __repr__(self) -> str: + # Useful for debugging tests + result = ( + f"{_str_for_coeff(self.coefficient)}{self.component_id}.{self.variable_name}" + f"[{self.time_index},{self.scenario_index}]" + ) + return result def generate_key(term: Term) -> TermKey: return TermKey( term.component_id, term.variable_name, - term.time_operator, - term.time_aggregator, - term.scenario_operator, + term.time_index, + term.scenario_index, ) def _merge_dicts( lhs: Dict[TermKey, Term], rhs: Dict[TermKey, Term], - merge_func: Callable[[Term, Term], Term], - neutral: float, + merge_func: Callable[[Optional[Term], Optional[Term]], Term], ) -> Dict[TermKey, Term]: res = {} - for k, v in lhs.items(): - res[k] = merge_func( - v, - rhs.get( - k, - Term( - neutral, - v.component_id, - v.variable_name, - v.structure, - v.time_operator, - v.time_aggregator, - v.scenario_operator, - ), - ), - ) - for k, v in rhs.items(): + for k, left in lhs.items(): + right = rhs.get(k, None) + res[k] = merge_func(left, right) + for k, right in rhs.items(): if k not in lhs: - res[k] = merge_func( - Term( - neutral, - v.component_id, - v.variable_name, - v.structure, - v.time_operator, - v.time_aggregator, - v.scenario_operator, - ), - v, - ) + res[k] = merge_func(None, right) return res -def _merge_is_possible(lhs: Term, rhs: Term) -> None: - if lhs.component_id != rhs.component_id or lhs.variable_name != rhs.variable_name: - raise ValueError("Cannot merge terms for different variables") - if ( - lhs.time_operator != rhs.time_operator - or lhs.time_aggregator != rhs.time_aggregator - or lhs.scenario_operator != rhs.scenario_operator - ): - raise ValueError("Cannot merge terms with different operators") - if lhs.structure != rhs.structure: - raise ValueError("Cannot merge terms with different structures") - - -def _add_terms(lhs: Term, rhs: Term) -> Term: - _merge_is_possible(lhs, rhs) - return Term( - lhs.coefficient + rhs.coefficient, - lhs.component_id, - lhs.variable_name, - lhs.structure, - lhs.time_operator, - lhs.time_aggregator, - lhs.scenario_operator, - ) +def _add_terms(lhs: Optional[Term], rhs: Optional[Term]) -> Term: + if lhs is not None and rhs is not None: + return dataclasses.replace(rhs, coefficient=lhs.coefficient + rhs.coefficient) + elif lhs is not None and rhs is None: + return lhs + elif lhs is None and rhs is not None: + return rhs + raise ValueError("Cannot add 2 null terms.") -def _substract_terms(lhs: Term, rhs: Term) -> Term: - _merge_is_possible(lhs, rhs) - return Term( - lhs.coefficient - rhs.coefficient, - lhs.component_id, - lhs.variable_name, - lhs.structure, - lhs.time_operator, - lhs.time_aggregator, - lhs.scenario_operator, - ) +def _substract_terms(lhs: Optional[Term], rhs: Optional[Term]) -> Term: + if lhs is not None and rhs is not None: + return dataclasses.replace(lhs, coefficient=lhs.coefficient - rhs.coefficient) + elif lhs is not None and rhs is None: + return lhs + elif lhs is None and rhs is not None: + return dataclasses.replace(rhs, coefficient=-rhs.coefficient) + raise ValueError("Cannot subtract 2 null terms.") class LinearExpression: @@ -258,14 +303,14 @@ def str_for_constant(self) -> str: else: return "{:+g}".format(self.constant) - def __str__(self) -> str: + def __repr__(self) -> str: # Useful for debugging tests result = "" if self.is_zero(): result += "0" else: for term in self.terms.values(): - result += str(term) + result += repr(term) result += self.str_for_constant() @@ -283,7 +328,7 @@ def __iadd__(self, rhs: "LinearExpression") -> "LinearExpression": if not isinstance(rhs, LinearExpression): return NotImplemented self.constant += rhs.constant - aggregated_terms = _merge_dicts(self.terms, rhs.terms, _add_terms, 0) + aggregated_terms = _merge_dicts(self.terms, rhs.terms, _add_terms) self.terms = aggregated_terms self.remove_zeros_from_terms() return self @@ -298,7 +343,7 @@ def __isub__(self, rhs: "LinearExpression") -> "LinearExpression": if not isinstance(rhs, LinearExpression): return NotImplemented self.constant -= rhs.constant - aggregated_terms = _merge_dicts(self.terms, rhs.terms, _substract_terms, 0) + aggregated_terms = _merge_dicts(self.terms, rhs.terms, _substract_terms) self.terms = aggregated_terms self.remove_zeros_from_terms() return self @@ -339,10 +384,8 @@ def __imul__(self, rhs: "LinearExpression") -> "LinearExpression": term.coefficient * const_expr.constant, term.component_id, term.variable_name, - term.structure, - term.time_operator, - term.time_aggregator, - term.scenario_operator, + term.time_index, + term.scenario_index, ) _copy_expression(left_expr, self) return self @@ -371,10 +414,8 @@ def __itruediv__(self, rhs: "LinearExpression") -> "LinearExpression": term.coefficient / rhs.constant, term.component_id, term.variable_name, - term.structure, - term.time_operator, - term.time_aggregator, - term.scenario_operator, + term.time_index, + term.scenario_index, ) return self @@ -391,26 +432,6 @@ def remove_zeros_from_terms(self) -> None: if is_close_abs(term.coefficient, 0, EPS): del self.terms[term_key] - def is_valid(self) -> bool: - nb_instances = None - for term in self.terms.values(): - term_instances = term.number_of_instances() - if nb_instances is None: - nb_instances = term_instances - else: - if term_instances != nb_instances: - raise ValueError( - "The terms of the linear expression {self} do not have the same number of instances" - ) - return True - - def number_of_instances(self) -> int: - if self.is_valid(): - # All terms have the same number of instances, just pick one - return self.terms[next(iter(self.terms))].number_of_instances() - else: - raise ValueError(f"{self} is not a valid linear expression") - def _copy_expression(src: LinearExpression, dst: LinearExpression) -> None: dst.terms = src.terms diff --git a/src/andromede/simulation/linearize.py b/src/andromede/simulation/linearize.py index 9fe6738a..045b8c5b 100644 --- a/src/andromede/simulation/linearize.py +++ b/src/andromede/simulation/linearize.py @@ -10,137 +10,273 @@ # # This file is part of the Antares project. -import dataclasses +from abc import ABC, abstractmethod from dataclasses import dataclass -from typing import Optional +from typing import Any, Dict, List, Optional, Union -import andromede.expression.scenario_operator -import andromede.expression.time_operator -from andromede.expression.evaluate import ValueProvider -from andromede.expression.evaluate_parameters import get_time_ids_from_instances_index +from andromede.expression import ( + AdditionNode, + DivisionNode, + ExpressionVisitor, + MultiplicationNode, + NegationNode, +) from andromede.expression.expression import ( + AllTimeSumNode, ComparisonNode, ComponentParameterNode, ComponentVariableNode, + CurrentScenarioIndex, ExpressionNode, LiteralNode, + NoScenarioIndex, + NoTimeIndex, + OneScenarioIndex, ParameterNode, PortFieldAggregatorNode, PortFieldNode, + ProblemParameterNode, + ProblemVariableNode, + ScenarioIndex, ScenarioOperatorNode, - TimeAggregatorNode, - TimeOperatorNode, + TimeEvalNode, + TimeIndex, + TimeShift, + TimeShiftNode, + TimeStep, + TimeSumNode, VariableNode, ) -from andromede.expression.indexing import IndexingStructureProvider -from andromede.expression.visitor import ExpressionVisitorOperations, T, visit -from andromede.simulation.linear_expression import LinearExpression, Term, generate_key +from andromede.expression.visitor import visit +from andromede.simulation.linear_expression import LinearExpression, Term, TermKey + + +class ParameterGetter(ABC): + @abstractmethod + def get_parameter_value( + self, + component_id: str, + parameter_name: str, + timestep: Optional[int], + scenario: Optional[int], + ) -> float: + pass + + +@dataclass +class MutableTerm: + coefficient: float + component_id: str + variable_name: str + time_index: Optional[int] + scenario_index: Optional[int] + + def to_key(self) -> TermKey: + return TermKey( + self.component_id, + self.variable_name, + self.time_index, + self.scenario_index, + ) + + def to_term(self) -> Term: + return Term( + self.coefficient, + self.component_id, + self.variable_name, + self.time_index, + self.scenario_index, + ) + + +@dataclass +class LinearExpressionData: + terms: List[MutableTerm] + constant: float + + def build(self) -> LinearExpression: + res_terms: Dict[TermKey, Any] = {} + for t in self.terms: + k = t.to_key() + if k in res_terms: + current_t = res_terms[k] + current_t.coefficient += t.coefficient + else: + res_terms[k] = t + for k, v in res_terms.items(): + res_terms[k] = v.to_term() + return LinearExpression(res_terms, self.constant) @dataclass(frozen=True) -class LinearExpressionBuilder(ExpressionVisitorOperations[LinearExpression]): +class LinearExpressionBuilder(ExpressionVisitor[LinearExpressionData]): """ Reduces a generic expression to a linear expression. - Parameters should have been evaluated first. + The input expression must respect the constraints of the output of + the operators expansion expression: + it must only contain `ProblemVariableNode` for variables + and `ProblemParameterNode` parameters. It cannot contain anymore + time aggregators or scenario aggregators, nor port-related nodes. """ - structure_provider: IndexingStructureProvider - value_provider: Optional[ValueProvider] = None + # TODO: linear expressions should be re-usable for different timesteps and scenarios + timestep: Optional[int] + scenario: Optional[int] + value_provider: Optional[ParameterGetter] = None + + def negation(self, node: NegationNode) -> LinearExpressionData: + operand = visit(node.operand, self) + operand.constant = -operand.constant + for t in operand.terms: + t.coefficient = -t.coefficient + return operand + + def addition(self, node: AdditionNode) -> LinearExpressionData: + operands = [visit(o, self) for o in node.operands] + terms = [] + constant: float = 0 + for o in operands: + constant += o.constant + terms.extend(o.terms) + return LinearExpressionData(terms=terms, constant=constant) + + def multiplication(self, node: MultiplicationNode) -> LinearExpressionData: + lhs = visit(node.left, self) + rhs = visit(node.right, self) + if not lhs.terms: + multiplier = lhs.constant + actual_expr = rhs + elif not rhs.terms: + multiplier = rhs.constant + actual_expr = lhs + else: + raise ValueError( + "At least one operand of a multiplication must be a constant expression." + ) + actual_expr.constant *= multiplier + for t in actual_expr.terms: + t.coefficient *= multiplier + return actual_expr + + def division(self, node: DivisionNode) -> LinearExpressionData: + lhs = visit(node.left, self) + rhs = visit(node.right, self) + if rhs.terms: + raise ValueError( + "The second operand of a division must be a constant expression." + ) + divider = rhs.constant + actual_expr = lhs + actual_expr.constant /= divider + for t in actual_expr.terms: + t.coefficient /= divider + return actual_expr - def literal(self, node: LiteralNode) -> LinearExpression: - return LinearExpression([], node.value) + def _get_timestep(self, time_index: TimeIndex) -> Optional[int]: + if isinstance(time_index, TimeShift): + if self.timestep is None: + raise ValueError("Cannot shift a time-independent expression.") + return self.timestep + time_index.timeshift + if isinstance(time_index, TimeStep): + return time_index.timestep + if isinstance(time_index, NoTimeIndex): + return None + else: + raise TypeError(f"Type {type(time_index)} is not a valid TimeIndex type.") - def comparison(self, node: ComparisonNode) -> LinearExpression: + def _get_scenario(self, scenario_index: ScenarioIndex) -> Optional[int]: + if isinstance(scenario_index, OneScenarioIndex): + return scenario_index.scenario + elif isinstance(scenario_index, CurrentScenarioIndex): + return self.scenario + elif isinstance(scenario_index, NoScenarioIndex): + return None + else: + raise TypeError( + f"Type {type(scenario_index)} is not a valid ScenarioIndex type." + ) + + def literal(self, node: LiteralNode) -> LinearExpressionData: + return LinearExpressionData([], node.value) + + def comparison(self, node: ComparisonNode) -> LinearExpressionData: raise ValueError("Linear expression cannot contain a comparison operator.") - def variable(self, node: VariableNode) -> LinearExpression: + def variable(self, node: VariableNode) -> LinearExpressionData: raise ValueError( "Variables need to be associated with their component ID before linearization." ) - def parameter(self, node: ParameterNode) -> LinearExpression: + def parameter(self, node: ParameterNode) -> LinearExpressionData: raise ValueError("Parameters must be evaluated before linearization.") - def comp_variable(self, node: ComponentVariableNode) -> LinearExpression: - return LinearExpression( + def comp_variable(self, node: ComponentVariableNode) -> LinearExpressionData: + raise ValueError( + "Variables need to be associated with their timestep/scenario before linearization." + ) + + def pb_variable(self, node: ProblemVariableNode) -> LinearExpressionData: + return LinearExpressionData( [ - Term( + MutableTerm( 1, node.component_id, node.name, - self.structure_provider.get_component_variable_structure( - node.component_id, node.name - ), + time_index=self._get_timestep(node.time_index), + scenario_index=self._get_scenario(node.scenario_index), ) ], 0, ) - def comp_parameter(self, node: ComponentParameterNode) -> LinearExpression: - raise ValueError("Parameters must be evaluated before linearization.") - - def time_operator(self, node: TimeOperatorNode) -> LinearExpression: - if self.value_provider is None: - raise ValueError( - "A value provider must be specified to linearize a time operator node. This is required in order to evaluate the value of potential parameters used to specified the time ids on which the time operator applies." - ) + def comp_parameter(self, node: ComponentParameterNode) -> LinearExpressionData: + raise ValueError( + "Parameters need to be associated with their timestep/scenario before linearization." + ) - operand_expr = visit(node.operand, self) - time_operator_cls = getattr(andromede.expression.time_operator, node.name) - time_ids = get_time_ids_from_instances_index( - node.instances_index, self.value_provider + def pb_parameter(self, node: ProblemParameterNode) -> LinearExpressionData: + # TODO SL: not the best place to do this. + # in the future, we should evaluate coefficients of variables as time vectors once for all timesteps + time_index = self._get_timestep(node.time_index) + scenario_index = self._get_scenario(node.scenario_index) + return LinearExpressionData( + [], + self._value_provider().get_parameter_value( + node.component_id, node.name, time_index, scenario_index + ), ) - result_terms = {} - for term in operand_expr.terms.values(): - term_with_operator = dataclasses.replace( - term, time_operator=time_operator_cls(time_ids) - ) - result_terms[generate_key(term_with_operator)] = term_with_operator - - # TODO: How can we apply a shift on a parameter ? It seems impossible for now as parameters must already be evaluated... - result_expr = LinearExpression(result_terms, operand_expr.constant) - return result_expr - - def time_aggregator(self, node: TimeAggregatorNode) -> LinearExpression: - # TODO: Very similar to time_operator, may be factorized - operand_expr = visit(node.operand, self) - time_aggregator_cls = getattr(andromede.expression.time_operator, node.name) - result_terms = {} - for term in operand_expr.terms.values(): - term_with_operator = dataclasses.replace( - term, time_aggregator=time_aggregator_cls(node.stay_roll) - ) - result_terms[generate_key(term_with_operator)] = term_with_operator + def time_eval(self, node: TimeEvalNode) -> LinearExpressionData: + raise ValueError("Time operators need to be expanded before linearization.") - result_expr = LinearExpression(result_terms, operand_expr.constant) - return result_expr + def time_shift(self, node: TimeShiftNode) -> LinearExpressionData: + raise ValueError("Time operators need to be expanded before linearization.") - def scenario_operator(self, node: ScenarioOperatorNode) -> LinearExpression: - scenario_operator_cls = getattr( - andromede.expression.scenario_operator, node.name - ) - if scenario_operator_cls.degree() > 1: - raise ValueError( - f"Cannot linearize expression with a non-linear operator: {scenario_operator_cls.__name__}" - ) + def time_sum(self, node: TimeSumNode) -> LinearExpressionData: + raise ValueError("Time operators need to be expanded before linearization.") + + def all_time_sum(self, node: AllTimeSumNode) -> LinearExpressionData: + raise ValueError("Time operators need to be expanded before linearization.") - operand_expr = visit(node.operand, self) - result_terms = {} - for term in operand_expr.terms.values(): - term_with_operator = dataclasses.replace( - term, scenario_operator=scenario_operator_cls() + def _value_provider(self) -> ParameterGetter: + if self.value_provider is None: + raise ValueError( + "A value provider must be specified to linearize a time operator node." + " This is required in order to evaluate the value of potential parameters" + " used to specified the time ids on which the time operator applies." ) - result_terms[generate_key(term_with_operator)] = term_with_operator + return self.value_provider - result_expr = LinearExpression(result_terms, operand_expr.constant) - return result_expr + def scenario_operator(self, node: ScenarioOperatorNode) -> LinearExpressionData: + raise ValueError("Scenario operators need to be expanded before linearization.") - def port_field(self, node: PortFieldNode) -> LinearExpression: + def port_field(self, node: PortFieldNode) -> LinearExpressionData: raise ValueError("Port fields must be replaced before linearization.") - def port_field_aggregator(self, node: PortFieldAggregatorNode) -> LinearExpression: + def port_field_aggregator( + self, node: PortFieldAggregatorNode + ) -> LinearExpressionData: raise ValueError( "Port fields aggregators must be replaced before linearization." ) @@ -148,9 +284,13 @@ def port_field_aggregator(self, node: PortFieldAggregatorNode) -> LinearExpressi def linearize_expression( expression: ExpressionNode, - structure_provider: IndexingStructureProvider, - value_provider: Optional[ValueProvider] = None, + timestep: Optional[int], + scenario: Optional[int], + value_provider: Optional[ParameterGetter] = None, ) -> LinearExpression: return visit( - expression, LinearExpressionBuilder(structure_provider, value_provider) - ) + expression, + LinearExpressionBuilder( + value_provider=value_provider, timestep=timestep, scenario=scenario + ), + ).build() diff --git a/src/andromede/simulation/optimization.py b/src/andromede/simulation/optimization.py index d974ed52..3f5463c3 100644 --- a/src/andromede/simulation/optimization.py +++ b/src/andromede/simulation/optimization.py @@ -15,33 +15,25 @@ into a mathematical optimization problem. """ +import itertools import math -from abc import ABC, abstractmethod from dataclasses import dataclass from enum import Enum from typing import Dict, Iterable, List, Optional import ortools.linear_solver.pywraplp as lp -from andromede.expression import ( - EvaluationVisitor, - ExpressionNode, - ParameterValueProvider, - ValueProvider, - resolve_parameters, - visit, -) +from andromede.expression import EvaluationVisitor, ExpressionNode, ValueProvider, visit from andromede.expression.context_adder import add_component_context from andromede.expression.indexing import IndexingStructureProvider, compute_indexation from andromede.expression.indexing_structure import IndexingStructure +from andromede.expression.operators_expansion import ProblemDimensions, expand_operators from andromede.expression.port_resolver import PortFieldKey, resolve_port -from andromede.expression.scenario_operator import Expectation -from andromede.expression.time_operator import TimeEvaluation, TimeShift, TimeSum from andromede.model.common import ValueType from andromede.model.constraint import Constraint from andromede.model.port import PortFieldId from andromede.simulation.linear_expression import LinearExpression, Term -from andromede.simulation.linearize import linearize_expression +from andromede.simulation.linearize import ParameterGetter, linearize_expression from andromede.simulation.strategy import ( MergedProblemStrategy, ModelSelectionStrategy, @@ -68,8 +60,8 @@ class TimestepComponentVariableKey: def _get_parameter_value( context: "OptimizationContext", - block_timestep: int, - scenario: int, + block_timestep: Optional[int], + scenario: Optional[int], component_id: str, name: str, ) -> float: @@ -78,21 +70,10 @@ def _get_parameter_value( return data.get_value(absolute_timestep, scenario, context.tree_node) -class TimestepValueProvider(ABC): - """ - Interface which provides numerical values for individual timesteps. - """ - - @abstractmethod - def get_value(self, block_timestep: int, scenario: int) -> float: - raise NotImplementedError() - - def _make_value_provider( context: "OptimizationContext", - block_timestep: int, - scenario: int, - component: Component, + block_timestep: Optional[int], + scenario: Optional[int], ) -> ValueProvider: """ Create a value provider which takes its values from @@ -101,7 +82,7 @@ def _make_value_provider( Cannot evaluate expressions which contain variables. """ - class Provider(ValueProvider): + class Impl(ValueProvider): def get_component_variable_value(self, component_id: str, name: str) -> float: raise NotImplementedError( "Cannot provide variable value at problem build time." @@ -122,139 +103,18 @@ def get_parameter_value(self, name: str) -> float: "Parameter must be associated to its component before resolution." ) - def parameter_is_constant_over_time(self, name: str) -> bool: - return not component.model.parameters[name].structure.time + return Impl() - return Provider() - -@dataclass(frozen=True) -class ExpressionTimestepValueProvider(TimestepValueProvider): - context: "OptimizationContext" - component: Component - expression: ExpressionNode - - # OptimizationContext has knowledge of the block, so that get_value only needs block_timestep and scenario to get the correct data value - - def get_value(self, block_timestep: int, scenario: int) -> float: - param_value_provider = _make_value_provider( - self.context, block_timestep, scenario, self.component - ) - visitor = EvaluationVisitor(param_value_provider) - return visit(self.expression, visitor) - - -def _make_parameter_value_provider( +def _compute_expression_value( + expression: ExpressionNode, context: "OptimizationContext", - block_timestep: int, - scenario: int, -) -> ParameterValueProvider: - """ - A value provider which takes its values from - the parameter values as defined in the network data. - - Cannot evaluate expressions which contain variables. - """ - - class Provider(ParameterValueProvider): - def get_component_parameter_value(self, component_id: str, name: str) -> float: - return _get_parameter_value( - context, block_timestep, scenario, component_id, name - ) - - def get_parameter_value(self, name: str) -> float: - raise ValueError( - "Parameters should have been associated with their component before resolution." - ) - - return Provider() - - -def _make_data_structure_provider( - network: Network, component: Component -) -> IndexingStructureProvider: - """ - Retrieve information in data structure (parameter and variable) from the model - """ - - class Provider(IndexingStructureProvider): - def get_component_variable_structure( - self, component_id: str, name: str - ) -> IndexingStructure: - return network.get_component(component_id).model.variables[name].structure - - def get_component_parameter_structure( - self, component_id: str, name: str - ) -> IndexingStructure: - return network.get_component(component_id).model.parameters[name].structure - - def get_parameter_structure(self, name: str) -> IndexingStructure: - return component.model.parameters[name].structure - - def get_variable_structure(self, name: str) -> IndexingStructure: - return component.model.variables[name].structure - - return Provider() - - -@dataclass(frozen=True) -class ComponentContext: - """ - Helper class to fill the optimization problem with component-related equations and variables. - """ - - opt_context: "OptimizationContext" - component: Component - - def get_values(self, expression: ExpressionNode) -> TimestepValueProvider: - """ - The returned value provider will evaluate the provided expression. - """ - return ExpressionTimestepValueProvider( - self.opt_context, self.component, expression - ) - - def add_variable( - self, - block_timestep: int, - scenario: int, - model_var_name: str, - variable: lp.Variable, - ) -> None: - self.opt_context.register_component_variable( - block_timestep, scenario, self.component.id, model_var_name, variable - ) - - def get_variable( - self, block_timestep: int, scenario: int, variable_name: str - ) -> lp.Variable: - return self.opt_context.get_component_variable( - block_timestep, - scenario, - self.component.id, - variable_name, - self.component.model.variables[variable_name].structure, - ) - - def linearize_expression( - self, - block_timestep: int, - scenario: int, - expression: ExpressionNode, - ) -> LinearExpression: - parameters_valued_provider = _make_parameter_value_provider( - self.opt_context, block_timestep, scenario - ) - evaluated_expr = resolve_parameters(expression, parameters_valued_provider) - - value_provider = _make_value_provider( - self.opt_context, block_timestep, scenario, self.component - ) - structure_provider = _make_data_structure_provider( - self.opt_context.network, self.component - ) - - return linearize_expression(evaluated_expr, structure_provider, value_provider) + block_timestep: Optional[int], + scenario: Optional[int], +) -> float: + value_provider = _make_value_provider(context, block_timestep, scenario) + visitor = EvaluationVisitor(value_provider) + return visit(expression, visitor) class BlockBorderManagement(Enum): @@ -282,11 +142,21 @@ class SolverVariableInfo: is_in_objective: bool +def float_to_int(value: float) -> int: + if isinstance(value, int) or value.is_integer(): + return int(value) + else: + raise ValueError(f"{value} is not an integer.") + + class OptimizationContext: """ Helper class to build the optimization problem. - Maintains some mappings between model and solver objects. - Also provides navigation method in the model (components by node ...). + + - Maintains mappings between model and solver objects, + - Maintains mappings between port fields and expressions, + - Provides implementations of interfaces required by various visitors + used to transform expressions (values providers ...). """ def __init__( @@ -317,6 +187,10 @@ def __init__( PortFieldKey, List[ExpressionNode] ] = {} + self._constant_value_provider = self._make_constant_value_provider() + self._indexing_structure_provider = self._make_data_structure_provider() + self._parameter_getter = self._make_parameter_getter() + @property def network(self) -> Network: return self._network @@ -349,8 +223,21 @@ def connection_fields_expressions(self) -> Dict[PortFieldKey, List[ExpressionNod return self._connection_fields_expressions # TODO: Need to think about data processing when creating blocks with varying or inequal time steps length (aggregation, sum ?, mean of data ?) - def block_timestep_to_absolute_timestep(self, block_timestep: int) -> int: - return self._block.timesteps[block_timestep] + def block_timestep_to_absolute_timestep( + self, block_timestep: Optional[int] + ) -> Optional[int]: + """ + Timestep may be None for parameters or variables that don't depend on time. + """ + if block_timestep is None: + return None + return self._block.timesteps[self.get_actual_block_timestep(block_timestep)] + + def get_actual_block_timestep(self, block_timestep: int) -> int: + if self._border_management == BlockBorderManagement.CYCLE: + return block_timestep % self.block_length() + else: + raise NotImplementedError() @property def database(self) -> DataBase: @@ -368,23 +255,15 @@ def get_time_indices(self, index_structure: IndexingStructure) -> Iterable[int]: def get_scenario_indices(self, index_structure: IndexingStructure) -> Iterable[int]: return range(self.scenarios) 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( self, - block_timestep: int, - scenario: int, + block_timestep: Optional[int], + scenario: Optional[int], component_id: str, variable_name: str, - variable_structure: IndexingStructure, ) -> lp.Variable: - block_timestep = self._manage_border_timesteps(block_timestep) - - # TODO: Improve design, variable_structure defines indexing - if variable_structure.time == False: - block_timestep = 0 - if variable_structure.scenario == False: - scenario = 0 - + if block_timestep is not None: + block_timestep = self._manage_border_timesteps(block_timestep) return self._component_variables[ TimestepComponentVariableKey( component_id, variable_name, block_timestep, scenario @@ -398,8 +277,8 @@ def get_all_component_variables( def register_component_variable( self, - block_timestep: int, - scenario: int, + block_timestep: Optional[int], + scenario: Optional[int], component_id: str, model_var_name: str, variable: lp.Variable, @@ -413,9 +292,6 @@ def register_component_variable( ) self._component_variables[key] = variable - def create_component_context(self, component: Component) -> ComponentContext: - return ComponentContext(self, component) - def register_connection_fields_expressions( self, component_id: str, @@ -428,25 +304,128 @@ def register_connection_fields_expressions( expression ) + def evaluate_time_bound(self, expression: ExpressionNode) -> int: + res = visit(expression, EvaluationVisitor(self._constant_value_provider)) + return float_to_int(res) -def _get_indexing( - constraint: Constraint, provider: IndexingStructureProvider -) -> IndexingStructure: - return ( - compute_indexation(constraint.expression, provider) - or compute_indexation(constraint.lower_bound, provider) - or compute_indexation(constraint.upper_bound, provider) - ) + def _make_data_structure_provider(self) -> IndexingStructureProvider: + """ + Retrieve information in data structure (parameter and variable) from the model + """ + network = self.network + + class Impl(IndexingStructureProvider): + def get_component_variable_structure( + self, component_id: str, name: str + ) -> IndexingStructure: + return ( + network.get_component(component_id).model.variables[name].structure + ) + + def get_component_parameter_structure( + self, component_id: str, name: str + ) -> IndexingStructure: + return ( + network.get_component(component_id).model.parameters[name].structure + ) + + def get_parameter_structure(self, name: str) -> IndexingStructure: + raise RuntimeError("Component context should have been initialized.") + + def get_variable_structure(self, name: str) -> IndexingStructure: + raise RuntimeError("Component context should have been initialized.") + + return Impl() + + def expand_operators(self, expression: ExpressionNode) -> ExpressionNode: + dimensions = ProblemDimensions(self.block_length(), self.scenarios) + time_bound_evaluator = self.evaluate_time_bound + return expand_operators( + expression, + dimensions, + time_bound_evaluator, + self._indexing_structure_provider, + ) + def _make_parameter_getter(self) -> ParameterGetter: + ctxt = self + + class Impl(ParameterGetter): + def get_parameter_value( + self, + component_id: str, + parameter_name: str, + timestep: Optional[int], + scenario: Optional[int], + ) -> float: + return _get_parameter_value( + ctxt, + timestep, + scenario, + component_id, + parameter_name, + ) + + return Impl() -def _compute_indexing_structure( - context: ComponentContext, constraint: Constraint + def linearize_expression( + self, + expanded: ExpressionNode, + timestep: Optional[int] = None, + scenario: Optional[int] = None, + ) -> LinearExpression: + return linearize_expression( + expanded, timestep, scenario, self._parameter_getter + ) + + def compute_indexing(self, expression: ExpressionNode) -> IndexingStructure: + return compute_indexation(expression, self._indexing_structure_provider) + + def _make_constant_value_provider(self) -> ValueProvider: + """ + Value provider which only provides values for constant parameters + """ + context = self + network = self.network + + class Impl(ValueProvider): + def get_component_variable_value( + self, component_id: str, name: str + ) -> float: + raise NotImplementedError( + "Cannot provide variable value at problem build time." + ) + + def get_component_parameter_value( + self, component_id: str, name: str + ) -> float: + model = network.get_component(component_id).model + structure = model.parameters[name].structure + if structure.time or structure.scenario: + raise ValueError(f"Parameter {name} is not constant.") + return _get_parameter_value(context, None, None, component_id, name) + + def get_variable_value(self, name: str) -> float: + raise NotImplementedError( + "Cannot provide variable value at problem build time." + ) + + def get_parameter_value(self, name: str) -> float: + raise ValueError( + "Parameter must be associated to its component before resolution." + ) + + return Impl() + + +def _compute_indexing( + context: OptimizationContext, constraint: Constraint ) -> IndexingStructure: - data_structure_provider = _make_data_structure_provider( - context.opt_context.network, context.component + return ( + context.compute_indexing(constraint.expression) + or context.compute_indexing(constraint.lower_bound) + or context.compute_indexing(constraint.upper_bound) ) - constraint_indexing = _get_indexing(constraint, data_structure_provider) - return constraint_indexing def _instantiate_model_expression( @@ -468,42 +447,44 @@ def _instantiate_model_expression( def _create_constraint( solver: lp.Solver, - context: ComponentContext, + context: OptimizationContext, constraint: Constraint, ) -> None: """ Adds a component-related constraint to the solver. """ - constraint_indexing = _compute_indexing_structure(context, constraint) - - # Perf: Perform linearization (tree traversing) without timesteps so that we can get the number of instances for the expression (from the time_ids of operators) - linear_expr = context.linearize_expression(0, 0, constraint.expression) - # Will there be cases where instances > 1 ? If not, maybe just a check that get_number_of_instances == 1 is sufficient ? Anyway, the function should be implemented - instances_per_time_step = linear_expr.number_of_instances() + expanded = context.expand_operators(constraint.expression) + constraint_indexing = _compute_indexing(context, constraint) - for block_timestep in context.opt_context.get_time_indices(constraint_indexing): - for scenario in context.opt_context.get_scenario_indices(constraint_indexing): + for block_timestep in context.get_time_indices(constraint_indexing): + for scenario in context.get_scenario_indices(constraint_indexing): linear_expr_at_t = context.linearize_expression( - block_timestep, scenario, constraint.expression + expanded, block_timestep, scenario ) + # What happens if there is some time_operator in the bounds ? constraint_data = ConstraintData( name=constraint.name, - lower_bound=context.get_values(constraint.lower_bound).get_value( - block_timestep, scenario + lower_bound=_compute_expression_value( + constraint.lower_bound, + context, + block_timestep, + scenario, ), - upper_bound=context.get_values(constraint.upper_bound).get_value( - block_timestep, scenario + upper_bound=_compute_expression_value( + constraint.upper_bound, + context, + block_timestep, + scenario, ), expression=linear_expr_at_t, ) make_constraint( solver, - context.opt_context, + context, block_timestep, scenario, constraint_data, - instances_per_time_step, ) @@ -511,40 +492,25 @@ def _create_objective( solver: lp.Solver, opt_context: OptimizationContext, component: Component, - component_context: ComponentContext, objective_contribution: ExpressionNode, ) -> None: instantiated_expr = _instantiate_model_expression( objective_contribution, component.id, opt_context ) - # We have already checked in the model creation that the objective contribution is neither indexed by time nor by scenario - linear_expr = component_context.linearize_expression(0, 0, instantiated_expr) + expanded = opt_context.expand_operators(instantiated_expr) + linear_expr = opt_context.linearize_expression(expanded) obj: lp.Objective = solver.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) - else: - weight = 1 - scenario_ids = range(1) - - for scenario in scenario_ids: - solver_vars = _get_solver_vars( - term, - opt_context, - 0, - scenario, - 0, - ) - - for solver_var in solver_vars: - opt_context._solver_variables[solver_var.name()].is_in_objective = True - obj.SetCoefficient( - solver_var, - obj.GetCoefficient(solver_var) + weight * term.coefficient, - ) + solver_var = _get_solver_var( + term, + opt_context, + ) + opt_context._solver_variables[solver_var.name()].is_in_objective = True + obj.SetCoefficient( + solver_var, + obj.GetCoefficient(solver_var) + term.coefficient, + ) # This should have no effect on the optimization obj.SetOffset(linear_expr.constant + obj.offset()) @@ -558,82 +524,16 @@ class ConstraintData: expression: LinearExpression -def _get_solver_vars( +def _get_solver_var( term: Term, context: OptimizationContext, - block_timestep: int, - scenario: int, - instance: int, -) -> List[lp.Variable]: - solver_vars = [] - if isinstance(term.time_aggregator, TimeSum): - if isinstance(term.time_operator, TimeShift): - for time_id in term.time_operator.time_ids: - solver_vars.append( - context.get_component_variable( - block_timestep + time_id, - scenario, - term.component_id, - term.variable_name, - term.structure, - ) - ) - elif isinstance(term.time_operator, TimeEvaluation): - for time_id in term.time_operator.time_ids: - solver_vars.append( - context.get_component_variable( - time_id, - scenario, - term.component_id, - term.variable_name, - term.structure, - ) - ) - else: # time_operator is None, retrieve variable for each time step of the block. What happens if we do x.sum() with x not being indexed by time ? Is there a check that it is a valid expression ? - for time_id in range(context.block_length()): - solver_vars.append( - context.get_component_variable( - block_timestep + time_id, - scenario, - term.component_id, - term.variable_name, - term.structure, - ) - ) - - else: # time_aggregator is None - if isinstance(term.time_operator, TimeShift): - solver_vars.append( - context.get_component_variable( - block_timestep + term.time_operator.time_ids[instance], - scenario, - term.component_id, - term.variable_name, - term.structure, - ) - ) - elif isinstance(term.time_operator, TimeEvaluation): - solver_vars.append( - context.get_component_variable( - term.time_operator.time_ids[instance], - scenario, - term.component_id, - term.variable_name, - term.structure, - ) - ) - else: # time_operator is None - # TODO: horrible tous ces if/else - solver_vars.append( - context.get_component_variable( - block_timestep, - scenario, - term.component_id, - term.variable_name, - term.structure, - ) - ) - return solver_vars +) -> lp.Variable: + return context.get_component_variable( + term.time_index, + term.scenario_index, + term.component_id, + term.variable_name, + ) def make_constraint( @@ -642,42 +542,26 @@ def make_constraint( block_timestep: int, scenario: int, data: ConstraintData, - instances: int, -) -> Dict[str, lp.Constraint]: +) -> None: """ Adds constraint to the solver. """ - solver_constraints = {} constraint_name = f"{data.name}_t{block_timestep}_s{scenario}" - for instance in range(instances): - if instances > 1: - constraint_name += f"_{instance}" - - solver_constraint: lp.Constraint = solver.Constraint(constraint_name) - constant: float = 0 - for term in data.expression.terms.values(): - solver_vars = _get_solver_vars( - term, - context, - block_timestep, - scenario, - instance, - ) - for solver_var in solver_vars: - coefficient = term.coefficient + solver_constraint.GetCoefficient( - solver_var - ) - solver_constraint.SetCoefficient(solver_var, coefficient) - # TODO: On pourrait aussi faire que l'objet Constraint n'ait pas de terme constant dans son expression et que les constantes soit déjà prises en compte dans les bornes, ça simplifierait le traitement ici - constant += data.expression.constant - solver_constraint.SetBounds( - data.lower_bound - constant, data.upper_bound - constant + solver_constraint: lp.Constraint = solver.Constraint(constraint_name) + constant: float = 0 + for term in data.expression.terms.values(): + solver_var = _get_solver_var( + term, + context, ) + coefficient = term.coefficient + solver_constraint.GetCoefficient(solver_var) + solver_constraint.SetCoefficient(solver_var, coefficient) - # TODO: this dictionary does not make sense, we override the content when there are multiple instances - solver_constraints[constraint_name] = solver_constraint - return solver_constraints + constant += data.expression.constant + solver_constraint.SetBounds( + data.lower_bound - constant, data.upper_bound - constant + ) class OptimizationProblem: @@ -728,9 +612,30 @@ def _register_connection_fields_definitions(self) -> None: expression=instantiated_expression, ) + def _solver_variable_name( + self, component_id: str, var_name: str, t: Optional[int], s: Optional[int] + ) -> str: + component_prefix = ( + f"{component_id}_" if (self.context.full_var_name and component_id) else "" + ) + tree_prefix = ( + f"{self.context.tree_node}_" + if (self.context.full_var_name and self.context.tree_node) + else "" + ) + scenario_suffix = f"_s{s}" if s is not None else "" + block_suffix = f"_t{t}" if t is not None else "" + + # Set solver var name + # Externally, for the Solver, this variable will have a full name + # Internally, it will be indexed by a structure that into account + # the component id, variable name, timestep and scenario separately + return ( + f"{tree_prefix}{component_prefix}{var_name}{block_suffix}{scenario_suffix}" + ) + def _create_variables(self) -> None: for component in self.context.network.all_components: - component_context = self.context.create_component_context(component) model = component.model for model_var in self.context.build_strategy.get_variables(model): @@ -747,81 +652,57 @@ def _create_variables(self) -> None: model_var.upper_bound, component.id, self.context ) - var_name: str = f"{model_var.name}" - component_prefix = ( - f"{component.id}_" - if (self.context.full_var_name and component.id) - else "" - ) - tree_prefix = ( - f"{self.context.tree_node}_" - if (self.context.full_var_name and self.context.tree_node) - else "" - ) + time_indices: Iterable[Optional[int]] = [None] + if var_indexing.time: + time_indices = self.context.get_time_indices(var_indexing) + scenario_indices: Iterable[Optional[int]] = [None] + if var_indexing.scenario: + scenario_indices = self.context.get_scenario_indices(var_indexing) + + for t, s in itertools.product(time_indices, scenario_indices): + lower_bound = -self.solver.infinity() + upper_bound = self.solver.infinity() + if instantiated_lb_expr: + lower_bound = _compute_expression_value( + instantiated_lb_expr, self.context, t, s + ) + if instantiated_ub_expr: + upper_bound = _compute_expression_value( + instantiated_ub_expr, self.context, t, s + ) - for block_timestep in self.context.get_time_indices(var_indexing): - block_suffix = ( - f"_t{block_timestep}" - if var_indexing.is_time_varying() - and (self.context.block_length() > 1) - else "" + solver_var_name = self._solver_variable_name( + component.id, model_var.name, t, s ) - for scenario in self.context.get_scenario_indices(var_indexing): - lower_bound = -self.solver.infinity() - upper_bound = self.solver.infinity() - if instantiated_lb_expr: - lower_bound = component_context.get_values( - instantiated_lb_expr - ).get_value(block_timestep, scenario) - if instantiated_ub_expr: - upper_bound = component_context.get_values( - instantiated_ub_expr - ).get_value(block_timestep, scenario) - - scenario_suffix = ( - f"_s{scenario}" - if var_indexing.is_scenario_varying() - and (self.context.scenarios > 1) - else "" + if math.isclose(lower_bound, upper_bound): + raise ValueError( + f"Upper and lower bounds of variable {solver_var_name} have the same value: {lower_bound}" + ) + elif lower_bound > upper_bound: + raise ValueError( + f"Upper bound ({upper_bound}) must be strictly greater than lower bound ({lower_bound}) for variable {solver_var_name}" ) - # Set solver var name - # Externally, for the Solver, this variable will have a full name - # Internally, it will be indexed by a structure that into account - # the component id, variable name, timestep and scenario separately - solver_var = None - solver_var_name = f"{tree_prefix}{component_prefix}{var_name}{block_suffix}{scenario_suffix}" - - if math.isclose(lower_bound, upper_bound): - raise ValueError( - f"Upper and lower bounds of variable {solver_var_name} have the same value: {lower_bound}" - ) - elif lower_bound > upper_bound: - raise ValueError( - f"Upper bound ({upper_bound}) must be strictly greater than lower bound ({lower_bound}) for variable {solver_var_name}" - ) - - if model_var.data_type == ValueType.BOOL: - solver_var = self.solver.BoolVar( - solver_var_name, - ) - elif model_var.data_type == ValueType.INTEGER: - solver_var = self.solver.IntVar( - lower_bound, - upper_bound, - solver_var_name, - ) - else: - solver_var = self.solver.NumVar( - lower_bound, - upper_bound, - solver_var_name, - ) - - component_context.add_variable( - block_timestep, scenario, model_var.name, solver_var + if model_var.data_type == ValueType.BOOL: + solver_var = self.solver.BoolVar( + solver_var_name, + ) + elif model_var.data_type == ValueType.INTEGER: + solver_var = self.solver.IntVar( + lower_bound, + upper_bound, + solver_var_name, ) + else: + solver_var = self.solver.NumVar( + lower_bound, + upper_bound, + solver_var_name, + ) + self.context.register_component_variable( + t, s, component.id, model_var.name, solver_var + ) def _create_constraints(self) -> None: for component in self.context.network.all_components: @@ -846,13 +727,12 @@ def _create_constraints(self) -> None: ) _create_constraint( self.solver, - self.context.create_component_context(component), + self.context, instantiated_constraint, ) def _create_objectives(self) -> None: for component in self.context.network.all_components: - component_context = self.context.create_component_context(component) model = component.model for objective in self.context.build_strategy.get_objectives(model): @@ -861,8 +741,7 @@ def _create_objectives(self) -> None: self.solver, self.context, component, - component_context, - self.context.risk_strategy(objective), + objective, ) def export_as_mps(self) -> str: @@ -880,7 +759,7 @@ def build_problem( *, problem_name: str = "optimization_problem", border_management: BlockBorderManagement = BlockBorderManagement.CYCLE, - solver_id: str = "GLOP", + solver_id: str = "SCIP", build_strategy: ModelSelectionStrategy = MergedProblemStrategy(), risk_strategy: RiskManagementStrategy = UniformRisk(), decision_tree_node: str = "", diff --git a/src/andromede/simulation/output_values.py b/src/andromede/simulation/output_values.py index cf94e7c0..c8321483 100644 --- a/src/andromede/simulation/output_values.py +++ b/src/andromede/simulation/output_values.py @@ -121,7 +121,11 @@ def value(self, values: Union[float, List[float], List[List[float]]]) -> None: self._size = (size_s, size_t) - def _set(self, timestep: int, scenario: int, value: float) -> None: + def _set( + self, timestep: Optional[int], scenario: Optional[int], value: float + ) -> None: + timestep = 0 if timestep is None else timestep + scenario = 0 if scenario is None else scenario key = TimeScenarioIndex(timestep, scenario) if key not in self._value: size_s = max(self._size[0], scenario + 1) @@ -200,9 +204,6 @@ def _build_components(self) -> None: return for key, value in self.problem.context.get_all_component_variables().items(): - if (key.block_timestep is None) or (key.scenario is None): - continue - ( self.component(key.component_id) .var(str(key.variable_name)) diff --git a/src/andromede/study/__init__.py b/src/andromede/study/__init__.py index 2a89893a..0f3ba478 100644 --- a/src/andromede/study/__init__.py +++ b/src/andromede/study/__init__.py @@ -15,6 +15,7 @@ DataBase, ScenarioIndex, ScenarioSeriesData, + Scenarization, TimeIndex, TimeScenarioIndex, TimeScenarioSeriesData, diff --git a/src/andromede/study/data.py b/src/andromede/study/data.py index c4822e1a..c3a1b197 100644 --- a/src/andromede/study/data.py +++ b/src/andromede/study/data.py @@ -38,12 +38,10 @@ class ScenarioIndex: @dataclass(frozen=True) class AbstractDataStructure(ABC): @abstractmethod - def get_value(self, timestep: int, scenario: int, node_id: str = "") -> float: - """ - Get the data value for a given timestep and scenario at a given node - Implement this method in subclasses as needed. - """ - pass + def get_value( + self, timestep: Optional[int], scenario: Optional[int], node_id: str = "" + ) -> float: + raise NotImplementedError() @abstractmethod def check_requirement(self, time: bool, scenario: bool) -> bool: @@ -58,7 +56,9 @@ def check_requirement(self, time: bool, scenario: bool) -> bool: class ConstantData(AbstractDataStructure): value: float - def get_value(self, timestep: int, scenario: int, node_id: str = "") -> float: + def get_value( + self, timestep: Optional[int], scenario: Optional[int], node_id: str = "" + ) -> float: return self.value # ConstantData can be used for time varying or constant models @@ -78,7 +78,11 @@ class TimeSeriesData(AbstractDataStructure): time_series: Mapping[TimeIndex, float] - def get_value(self, timestep: int, scenario: int, node_id: str = "") -> float: + def get_value( + self, timestep: Optional[int], scenario: Optional[int], node_id: str = "" + ) -> float: + if timestep is None: + raise KeyError("Time series data requires a time index.") return self.time_series[TimeIndex(timestep)] def check_requirement(self, time: bool, scenario: bool) -> bool: @@ -98,7 +102,11 @@ class ScenarioSeriesData(AbstractDataStructure): scenario_series: Mapping[ScenarioIndex, float] - def get_value(self, timestep: int, scenario: int, node_id: str = "") -> float: + def get_value( + self, timestep: Optional[int], scenario: Optional[int], node_id: str = "" + ) -> float: + if scenario is None: + raise KeyError("Scenario series data requires a scenario index.") return self.scenario_series[ScenarioIndex(scenario)] def check_requirement(self, time: bool, scenario: bool) -> bool: @@ -116,10 +124,26 @@ def load_ts_from_txt( ts_path = path_to_file / timeseries_with_extension try: return pd.read_csv(ts_path, header=None, sep=r"\s+") + + except FileNotFoundError: + raise FileNotFoundError(f"File '{timeseries_name}' does not exist") except Exception: raise Exception(f"An error has arrived when processing '{ts_path}'") +@dataclass(frozen=True) +class Scenarization: + _scenarization: Dict[int, int] + + def get_scenario_for_year(self, year: int) -> int: + return self._scenarization[year] + + def add_year(self, year: int, scenario: int) -> None: + if year in self._scenarization: + raise ValueError(f"the year {year} is already defined") + self._scenarization[year] = scenario + + @dataclass(frozen=True) class TimeScenarioSeriesData(AbstractDataStructure): """ @@ -129,8 +153,17 @@ class TimeScenarioSeriesData(AbstractDataStructure): """ time_scenario_series: pd.DataFrame - - def get_value(self, timestep: int, scenario: int, node_id: str = "") -> float: + scenarization: Optional[Scenarization] = None + + def get_value( + self, timestep: Optional[int], scenario: Optional[int], node_id: str = "" + ) -> float: + if timestep is None: + raise KeyError("Time scenario data requires a time index.") + if scenario is None: + raise KeyError("Time scenario data requires a scenario index.") + if self.scenarization: + scenario = self.scenarization.get_scenario_for_year(scenario) value = str(self.time_scenario_series.iloc[timestep, scenario]) return float(value) @@ -145,7 +178,9 @@ def check_requirement(self, time: bool, scenario: bool) -> bool: class TreeData(AbstractDataStructure): data: Mapping[str, AbstractDataStructure] - def get_value(self, timestep: int, scenario: int, node_id: str = "") -> float: + def get_value( + self, timestep: Optional[int], scenario: Optional[int], node_id: str = "" + ) -> float: return self.data[node_id].get_value(timestep, scenario) def check_requirement(self, time: bool, scenario: bool) -> bool: diff --git a/src/andromede/study/parsing.py b/src/andromede/study/parsing.py index 6f3723fe..e4a6d19a 100644 --- a/src/andromede/study/parsing.py +++ b/src/andromede/study/parsing.py @@ -9,18 +9,29 @@ # SPDX-License-Identifier: MPL-2.0 # # This file is part of the Antares project. -import typing -from typing import List, Optional +import argparse +import os +from dataclasses import dataclass +from pathlib import Path +from typing import List, Optional, TextIO + +import pandas as pd from pydantic import BaseModel, Field from yaml import safe_load -def parse_yaml_components(input_components: typing.TextIO) -> "InputComponents": +def parse_yaml_components(input_components: TextIO) -> "InputComponents": tree = safe_load(input_components) return InputComponents.model_validate(tree["study"]) +def parse_scenario_builder(file: Path) -> pd.DataFrame: + sb = pd.read_csv(file, names=("name", "year", "scenario")) + sb.rename(columns={0: "name", 1: "year", 2: "scenario"}) + return sb + + # Design note: actual parsing and validation is delegated to pydantic models def _to_kebab(snake: str) -> str: return snake.replace("_", "-") @@ -36,15 +47,23 @@ class InputPortConnections(BaseModel): class InputComponentParameter(BaseModel): name: str type: str + scenario_group: Optional[str] = None value: Optional[float] = None timeseries: Optional[str] = None + class Config: + alias_generator = _to_kebab + class InputComponent(BaseModel): id: str model: str + scenario_group: Optional[str] = None parameters: Optional[List[InputComponentParameter]] = None + class Config: + alias_generator = _to_kebab + class InputComponents(BaseModel): nodes: List[InputComponent] = Field(default_factory=list) @@ -53,3 +72,61 @@ class InputComponents(BaseModel): class Config: alias_generator = _to_kebab + + +@dataclass(frozen=True) +class ParsedArguments: + models_path: List[Path] + components_path: Path + timeseries_path: Path + duration: int + nb_scenarios: int + + +def parse_cli() -> ParsedArguments: + parser = argparse.ArgumentParser() + parser.add_argument( + "--study", type=Path, help="path to the root directory of the study" + ) + parser.add_argument( + "--models", nargs="+", type=Path, help="list of path to model file, *.yml" + ) + parser.add_argument( + "--component", type=Path, help="path to the component file, *.yml" + ) + parser.add_argument( + "--timeseries", type=Path, help="path to the timeseries directory" + ) + parser.add_argument( + "--duration", type=int, help="duration of the simulation", default=1 + ) + parser.add_argument( + "--scenario", type=int, help="number of scenario of the simulation", default=1 + ) + + args = parser.parse_args() + + if args.study: + if args.models or args.component or args.timeseries: + parser.error( + "--study flag can't be use with --models, --component and --timeseries" + ) + + components_path = args.study / "input" / "components" / "components.yml" + timeseries_dir = args.study / "input" / "components" / "series" + model_paths = [ + args.study / "input" / "models" / file + for file in os.listdir(args.study / "input" / "models") + ] + + else: + if not args.models or not args.component: + parser.error("--models and --component must be entered") + + components_path = args.component + timeseries_dir = args.timeseries + model_paths = args.models + + return ParsedArguments( + model_paths, components_path, timeseries_dir, args.duration, args.scenario + ) diff --git a/src/andromede/study/resolve_components.py b/src/andromede/study/resolve_components.py index d6650e78..9ed299d8 100644 --- a/src/andromede/study/resolve_components.py +++ b/src/andromede/study/resolve_components.py @@ -25,10 +25,10 @@ Node, PortRef, PortsConnection, + Scenarization, ) from andromede.study.data import ( AbstractDataStructure, - TimeScenarioIndex, TimeScenarioSeriesData, load_ts_from_txt, ) @@ -145,7 +145,9 @@ def build_data_base( input_comp: InputComponents, timeseries_dir: Optional[Path] ) -> DataBase: database = DataBase() + for comp in input_comp.components: + # This idiom allows mypy to 'ignore' the fact that comp.parameter can be None for param in comp.parameters or []: param_value = _evaluate_param_type( param.type, param.value, param.timeseries, timeseries_dir @@ -160,11 +162,51 @@ def _evaluate_param_type( param_value: Optional[float], timeseries_name: Optional[str], timeseries_dir: Optional[Path], + scenarization: Optional[Scenarization] = None, ) -> AbstractDataStructure: if param_type == "constant" and param_value is not None: return ConstantData(float(param_value)) elif param_type == "timeseries": - return TimeScenarioSeriesData(load_ts_from_txt(timeseries_name, timeseries_dir)) + return TimeScenarioSeriesData( + load_ts_from_txt(timeseries_name, timeseries_dir), scenarization + ) + + raise ValueError("Data should be either constant or timeseries ") + + +def _resolve_scenarization( + scenario_builder_data: pd.DataFrame, +) -> Dict[str, Scenarization]: + output: Dict[str, Scenarization] = {} + for i, row in scenario_builder_data.iterrows(): + if row["name"] in output: + output[row["name"]].add_year(row["year"], row["scenario"]) + else: + output[row["name"]] = Scenarization({row["year"]: row["scenario"]}) + return output + + +def build_scenarized_data_base( + input_comp: InputComponents, + scenario_builder_data: pd.DataFrame, + timeseries_dir: Optional[Path], +) -> DataBase: + database = DataBase() + scenarizations = _resolve_scenarization(scenario_builder_data) - raise ValueError(f"Data should be either constant or timeseries ") + for comp in input_comp.components: + scenarization = None + if comp.scenario_group: + scenarization = scenarizations[comp.scenario_group] + + # This idiom allows mypy to 'ignore' the fact that comp.parameter can be None + for param in comp.parameters or []: + if param.scenario_group: + scenarization = scenarizations[param.scenario_group] + param_value = _evaluate_param_type( + param.type, param.value, param.timeseries, timeseries_dir, scenarization + ) + database.add_data(comp.id, param.name, param_value) + + return database diff --git a/tests/functional/conftest.py b/tests/functional/conftest.py index 9d443ee1..85568a7b 100644 --- a/tests/functional/conftest.py +++ b/tests/functional/conftest.py @@ -14,7 +14,7 @@ import pytest from andromede.model.parsing import parse_yaml_library -from andromede.model.resolve_library import resolve_library +from andromede.model.resolve_library import Library, resolve_library @pytest.fixture(scope="session") @@ -23,11 +23,11 @@ def libs_dir() -> Path: @pytest.fixture(scope="session") -def lib(libs_dir: Path): +def lib(libs_dir: Path) -> Library: lib_file = libs_dir / "lib.yml" with lib_file.open() as f: input_lib = parse_yaml_library(f) - lib = resolve_library(input_lib) + lib = resolve_library([input_lib]) return lib diff --git a/tests/functional/input/components/components.yml b/tests/functional/input/components/components.yml new file mode 100644 index 00000000..c0307b7e --- /dev/null +++ b/tests/functional/input/components/components.yml @@ -0,0 +1,84 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + + +#test file for command line execution +#run : +#$ python src/andromede/main/main.py --models tests/functional/input/models/lib_1.yml tests/functional/input/models/lib_2.yml --component tests/functional/input/components/components.yml --duration 3 --timeseries tests/functional/input/components/series/ +# or +#$ python src/andromede/main/main.py --study tests/functional/ --duration 3 +#expected value : +#> 0 +#> 72000 + +study: + nodes: + - id: N + model: node + + components: + - id: G + model: thermal-cluster-dhd + parameters: + - name: cost + type: constant + value: 100 + - name: p_min + type: constant + value: 100 + - name: p_max + type: constant + value: 500 + - name: d_min_up + type: constant + value: 3 + - name: d_min_down + type: constant + value: 3 + - name: nb_units_max + type: constant + value: 1 + - name: nb_failures + type: constant + value: 0 + - id: D + model: demand + parameters: + - name: demand + scenario-group: g1 + type: timeseries + timeseries: demand-ts + - id: S + model: spillage + parameters: + - name: cost + type: constant + value: 10 + + connections: + - component1: N + port_1: injection_port + component2: D + port_2: injection_port + + - component1: N + port_1: injection_port + component2: G + port_2: injection_port + + - component1: N + port_1: injection_port + component2: S + port_2: injection_port + + + diff --git a/tests/functional/input/components/series/demand-ts.txt b/tests/functional/input/components/series/demand-ts.txt new file mode 100644 index 00000000..7f6e5279 --- /dev/null +++ b/tests/functional/input/components/series/demand-ts.txt @@ -0,0 +1,3 @@ +500 100 +0 100 +0 100 \ No newline at end of file diff --git a/tests/functional/input/models/lib_1.yml b/tests/functional/input/models/lib_1.yml new file mode 100644 index 00000000..b57e6da7 --- /dev/null +++ b/tests/functional/input/models/lib_1.yml @@ -0,0 +1,67 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. +library: + id: basic + description: Basic library + + port-types: + - id: flow + description: A port which transfers power flow + fields: + - name: flow + + models: + + - id: thermal-cluster-dhd + parameters: + - name: p_max + - name: p_min + - name: cost + - name: d_min_up + - name: d_min_down + - name: nb_units_max + - name: nb_failures + variables: + - name: nb_units_on + lower-bound: 0 + upper-bound: nb_units_max + variable-type: integer + - name: nb_starting + lower-bound: 0 + upper-bound: nb_units_max + variable-type: integer + - name: nb_stoping + lower-bound: 0 + upper-bound: nb_units_max + variable-type: integer + - name: production + lower-bound: 0 + upper-bound: nb_units_max * p_max + ports: + - name: injection_port + type: flow + port-field-definitions: + - port: injection_port + field: flow + definition: production + constraints: + - name: max production + expression: production <= nb_units_on * p_max + - name: min production + expression: production >= nb_units_on * p_min + - name: on units variation + expression: nb_units_on = nb_units_on[-1] + nb_starting - nb_stoping + - name: starting time + expression: sum(nb_starting[-d_min_up + 1 .. 0]) <= nb_units_on + - name: stoping time + expression: sum(nb_stoping[-d_min_down + 1 .. 0]) <= nb_units_max - nb_units_on + objective: expec(sum(cost * production)) \ No newline at end of file diff --git a/tests/functional/input/models/lib_2.yml b/tests/functional/input/models/lib_2.yml new file mode 100644 index 00000000..055110fb --- /dev/null +++ b/tests/functional/input/models/lib_2.yml @@ -0,0 +1,50 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. +library: + id: basic 2 + dependencies: + - basic + description: Basic library + + models: + + - id: spillage + description: A basic spillage model + parameters: + - name: cost + time-dependent: false + scenario-dependent: false + variables: + - name: spillage + lower-bound: 0 + ports: + - name: injection_port + type: flow + port-field-definitions: + - port: injection_port + field: flow + definition: -spillage + objective: expec(sum(cost * spillage)) + + - id: demand + description: A basic fixed demand model + parameters: + - name: demand + time-dependent: true + scenario-dependent: true + ports: + - name: injection_port + type: flow + port-field-definitions: + - port: injection_port + field: flow + definition: -demand \ No newline at end of file diff --git a/tests/functional/input/models/lib_3.yml b/tests/functional/input/models/lib_3.yml new file mode 100644 index 00000000..ada9ca5c --- /dev/null +++ b/tests/functional/input/models/lib_3.yml @@ -0,0 +1,26 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. +library: + id: basic 3 + dependencies: + - basic 2 + - basic + description: Basic library + + models: + - id: node + ports: + - name: injection_port + type: flow + binding-constraints: + - name: balance + expression: sum_connections(injection_port.flow) = 0 \ No newline at end of file diff --git a/tests/functional/libs/lib.yml b/tests/functional/libs/lib.yml index 27a8b47e..77e167ca 100644 --- a/tests/functional/libs/lib.yml +++ b/tests/functional/libs/lib.yml @@ -182,7 +182,7 @@ library: - name: on units variation expression: nb_units_on = nb_units_on[t-1] + nb_starting - nb_stoping - name: starting time - expression: sum(nb_starting[t-d_min_up + 1 .. t]) <= nb_units_on + expression: sum(t-d_min_up + 1 .. t, nb_starting) <= nb_units_on - name: stoping time - expression: sum(nb_stoping[t-d_min_down + 1 .. t]) <= nb_units_max - nb_units_on + expression: sum(t-d_min_down + 1 .. t, nb_stoping) <= nb_units_max - nb_units_on objective: expec(sum(cost * production)) \ No newline at end of file diff --git a/tests/functional/test_andromede.py b/tests/functional/test_andromede.py index 7820c130..e778bb8c 100644 --- a/tests/functional/test_andromede.py +++ b/tests/functional/test_andromede.py @@ -19,22 +19,14 @@ BALANCE_PORT_TYPE, DEMAND_MODEL, GENERATOR_MODEL, - GENERATOR_MODEL_WITH_PMIN, - LINK_MODEL, NODE_BALANCE_MODEL, SHORT_TERM_STORAGE_SIMPLE, SPILLAGE_MODEL, - THERMAL_CLUSTER_MODEL_HD, UNSUPPLIED_ENERGY_MODEL, ) from andromede.model import Model, ModelPort, float_parameter, float_variable, model from andromede.model.port import PortFieldDefinition, PortFieldId -from andromede.simulation import ( - BlockBorderManagement, - OutputValues, - TimeBlock, - build_problem, -) +from andromede.simulation import BlockBorderManagement, TimeBlock, build_problem from andromede.study import ( ConstantData, DataBase, @@ -47,6 +39,43 @@ ) +def test_basic_balance() -> None: + """ + Balance on one node with one fixed demand and one generation, on 1 timestep. + """ + + database = DataBase() + database.add_data("D", "demand", ConstantData(100)) + + database.add_data("G", "p_max", ConstantData(100)) + database.add_data("G", "cost", ConstantData(30)) + + node = Node(model=NODE_BALANCE_MODEL, id="N") + demand = create_component( + model=DEMAND_MODEL, + id="D", + ) + + gen = create_component( + model=GENERATOR_MODEL, + id="G", + ) + + network = Network("test") + network.add_node(node) + network.add_component(demand) + network.add_component(gen) + network.connect(PortRef(demand, "balance_port"), PortRef(node, "balance_port")) + network.connect(PortRef(gen, "balance_port"), PortRef(node, "balance_port")) + + scenarios = 1 + problem = build_problem(network, database, TimeBlock(1, [0]), scenarios) + status = problem.solver.Solve() + + assert status == problem.solver.OPTIMAL + assert problem.solver.Objective().Value() == 3000 + + def test_timeseries() -> None: """ Basic case with 2 timesteps, where the demand is 100 on first timestep and 50 on second timestep. @@ -155,7 +184,7 @@ def test_variable_bound() -> None: ) ], objective_operational_contribution=(param("cost") * var("generation")) - .sum() + .time_sum() .expec(), ) @@ -197,7 +226,7 @@ def generate_data( for scenario in range(scenarios): for absolute_timestep in range(horizon): if absolute_timestep == 0: - data[TimeScenarioIndex(absolute_timestep, scenario)] = -18 + data[TimeScenarioIndex(absolute_timestep, scenario)] = -18.0 else: data[TimeScenarioIndex(absolute_timestep, scenario)] = 2 * efficiency diff --git a/tests/functional/test_andromede_yml.py b/tests/functional/test_andromede_yml.py index 07a2d1dd..36dc2e13 100644 --- a/tests/functional/test_andromede_yml.py +++ b/tests/functional/test_andromede_yml.py @@ -1,6 +1,7 @@ import pandas as pd import pytest +from andromede.model.library import Library from andromede.simulation import ( BlockBorderManagement, OutputValues, @@ -18,7 +19,7 @@ ) -def test_network(lib) -> None: +def test_network(lib: Library) -> None: network = Network("test") assert network.id == "test" assert list(network.nodes) == [] @@ -42,7 +43,7 @@ def test_network(lib) -> None: network.get_component("unknown") -def test_basic_balance(lib) -> None: +def test_basic_balance(lib: Library) -> None: """ Balance on one node with one fixed demand and one generation, on 1 timestep. """ @@ -83,7 +84,7 @@ def test_basic_balance(lib) -> None: assert problem.solver.Objective().Value() == 3000 -def test_link(lib) -> None: +def test_link(lib: Library) -> None: """ Balance on one node with one fixed demand and one generation, on 1 timestep. """ @@ -141,7 +142,7 @@ def test_link(lib) -> None: assert variable.solution_value() == -100 -def test_stacking_generation(lib) -> None: +def test_stacking_generation(lib: Library) -> None: """ Balance on one node with one fixed demand and 2 generations with different costs, on 1 timestep. """ @@ -193,7 +194,7 @@ def test_stacking_generation(lib) -> None: assert problem.solver.Objective().Value() == 30 * 100 + 50 * 50 -def test_spillage(lib) -> None: +def test_spillage(lib: Library) -> None: """ Balance on one node with one fixed demand and 1 generation higher than demand and 1 timestep . """ @@ -233,7 +234,7 @@ def test_spillage(lib) -> None: assert problem.solver.Objective().Value() == 30 * 200 + 50 * 10 -def test_min_up_down_times(lib) -> None: +def test_min_up_down_times(lib: Library) -> None: """ Model on 3 time steps with one thermal generation and one demand on a single node. - Demand is the following time series : [500 MW, 0, 0] @@ -327,7 +328,7 @@ def test_min_up_down_times(lib) -> None: assert problem.solver.Objective().Value() == pytest.approx(72000, abs=0.01) -def test_changing_demand(lib) -> None: +def test_changing_demand(lib: Library) -> None: """ Model on 3 time steps simple production, demand - P_max = 500 MW @@ -383,7 +384,7 @@ def test_changing_demand(lib) -> None: assert problem.solver.Objective().Value() == 40000 -def test_min_up_down_times_2(lib) -> None: +def test_min_up_down_times_2(lib: Library) -> None: """ Model on 3 time steps with one thermal generation and one demand on a single node. - Demand is the following time series : [500 MW, 0, 0] diff --git a/tests/functional/test_performance.py b/tests/functional/test_performance.py index 1c50af1c..7ecf7b12 100644 --- a/tests/functional/test_performance.py +++ b/tests/functional/test_performance.py @@ -9,15 +9,13 @@ # SPDX-License-Identifier: MPL-2.0 # # This file is part of the Antares project. - +import cProfile +from pstats import SortKey from typing import cast -import pytest - from andromede.expression.expression import ExpressionNode, literal, param, var from andromede.expression.indexing_structure import IndexingStructure from andromede.libs.standard import ( - BALANCE_PORT_TYPE, DEMAND_MODEL, GENERATOR_MODEL, GENERATOR_MODEL_WITH_STORAGE, @@ -40,9 +38,6 @@ def test_large_sum_inside_model_with_loop() -> None: """ Test performance when the problem involves an expression with a high number of terms. Here the objective function is the sum over nb_terms terms on a for-loop inside the model - - This test pass with 476 terms but fails with 477 locally due to recursion depth, - and even less terms are possible with Jenkins... """ nb_terms = 500 @@ -53,30 +48,28 @@ def test_large_sum_inside_model_with_loop() -> None: for i in range(1, nb_terms): database.add_data("simple_cost", f"cost_{i}", ConstantData(1 / i)) - with pytest.raises(RecursionError, match="maximum recursion depth exceeded"): - SIMPLE_COST_MODEL = model( - id="SIMPLE_COST", - parameters=[ - float_parameter(f"cost_{i}", IndexingStructure(False, False)) - for i in range(1, nb_terms) - ], - objective_operational_contribution=cast( - ExpressionNode, sum(param(f"cost_{i}") for i in range(1, nb_terms)) - ), - ) + SIMPLE_COST_MODEL = model( + id="SIMPLE_COST", + parameters=[ + float_parameter(f"cost_{i}", IndexingStructure(False, False)) + for i in range(1, nb_terms) + ], + objective_operational_contribution=cast( + ExpressionNode, sum(param(f"cost_{i}") for i in range(1, nb_terms)) + ), + ) - # Won't run because last statement will raise the error - network = Network("test") - cost_model = create_component(model=SIMPLE_COST_MODEL, id="simple_cost") - network.add_component(cost_model) + network = Network("test") + 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) - status = problem.solver.Solve() + problem = build_problem(network, database, time_blocks[0], scenarios) + status = problem.solver.Solve() - assert status == problem.solver.OPTIMAL - assert problem.solver.Objective().Value() == sum( - [1 / i for i in range(1, nb_terms)] - ) + assert status == problem.solver.OPTIMAL + assert problem.solver.Objective().Value() == sum( + [1 / i for i in range(1, nb_terms)] + ) def test_large_sum_outside_model_with_loop() -> None: @@ -113,12 +106,13 @@ def test_large_sum_outside_model_with_loop() -> None: assert problem.solver.Objective().Value() == obj_coeff +# Takes 3 minutes with current implementation !! def test_large_sum_inside_model_with_sum_operator() -> None: """ Test performance when the problem involves an expression with a high number of terms. Here the objective function is the sum over nb_terms terms with the sum() operator inside the model """ - nb_terms = 10_000 + nb_terms = 3000 scenarios = 1 time_blocks = [TimeBlock(0, list(range(nb_terms)))] @@ -143,7 +137,7 @@ def test_large_sum_inside_model_with_sum_operator() -> None: structure=IndexingStructure(True, False), ), ], - objective_operational_contribution=(param("cost") * var("var")).sum(), + objective_operational_contribution=(param("cost") * var("var")).time_sum(), ) network = Network("test") @@ -161,9 +155,6 @@ def test_large_sum_inside_model_with_sum_operator() -> None: def test_large_sum_of_port_connections() -> None: """ Test performance when the problem involves a model where several generators are connected to a node. - - This test pass with 470 terms but fails with 471 locally due to recursion depth, - and possibly even less terms are possible with Jenkins... """ nb_generators = 500 @@ -196,14 +187,11 @@ def test_large_sum_of_port_connections() -> None: PortRef(generators[gen_id], "balance_port"), PortRef(node, "balance_port") ) - with pytest.raises(RecursionError, match="maximum recursion depth exceeded"): - problem = build_problem(network, database, time_block, scenarios) - - # Won't run because last statement will raise the error - status = problem.solver.Solve() + problem = build_problem(network, database, time_block, scenarios) + status = problem.solver.Solve() - assert status == problem.solver.OPTIMAL - assert problem.solver.Objective().Value() == 5 * nb_generators + assert status == problem.solver.OPTIMAL + assert problem.solver.Objective().Value() == 5 * nb_generators def test_basic_balance_on_whole_year() -> None: @@ -212,7 +200,7 @@ def test_basic_balance_on_whole_year() -> None: """ scenarios = 1 - horizon = 8760 + horizon = 10000 time_block = TimeBlock(1, list(range(horizon))) database = DataBase() @@ -235,7 +223,9 @@ 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) + with cProfile.Profile() as pr: + problem = build_problem(network, database, time_block, scenarios) + pr.print_stats(sort=SortKey.CUMULATIVE) status = problem.solver.Solve() assert status == problem.solver.OPTIMAL diff --git a/tests/functional/test_xpansion.py b/tests/functional/test_xpansion.py index 164671cf..309c17e4 100644 --- a/tests/functional/test_xpansion.py +++ b/tests/functional/test_xpansion.py @@ -21,7 +21,6 @@ DEMAND_MODEL, GENERATOR_MODEL, NODE_BALANCE_MODEL, - THERMAL_CANDIDATE, ) from andromede.model import ( Constraint, @@ -51,7 +50,6 @@ create_component, ) -CONSTANT = IndexingStructure(False, False) FREE = IndexingStructure(True, True) INVESTMENT = ProblemContext.INVESTMENT @@ -59,6 +57,44 @@ COUPLING = ProblemContext.COUPLING +@pytest.fixture +def thermal_candidate() -> Model: + THERMAL_CANDIDATE = model( + id="GEN", + parameters=[ + float_parameter("op_cost", CONSTANT), + float_parameter("invest_cost", CONSTANT), + ], + variables=[ + float_variable("generation", lower_bound=literal(0)), + float_variable( + "p_max", + lower_bound=literal(0), + upper_bound=literal(1000), + structure=CONSTANT, + context=COUPLING, + ), + ], + ports=[ModelPort(port_type=BALANCE_PORT_TYPE, port_name="balance_port")], + port_fields_definitions=[ + PortFieldDefinition( + port_field=PortFieldId("balance_port", "flow"), + definition=var("generation"), + ) + ], + constraints=[ + Constraint( + name="Max generation", expression=var("generation") <= var("p_max") + ) + ], + objective_operational_contribution=(param("op_cost") * var("generation")) + .time_sum() + .expec(), + objective_investment_contribution=param("invest_cost") * var("p_max"), + ) + return THERMAL_CANDIDATE + + @pytest.fixture def discrete_candidate() -> Model: DISCRETE_CANDIDATE = model( @@ -102,7 +138,7 @@ def discrete_candidate() -> Model: ), ], objective_operational_contribution=(param("op_cost") * var("generation")) - .sum() + .time_sum() .expec(), objective_investment_contribution=param("invest_cost") * var("p_max"), ) @@ -119,8 +155,8 @@ def generator() -> Component: @pytest.fixture -def candidate() -> Component: - candidate = create_component(model=THERMAL_CANDIDATE, id="CAND") +def candidate(thermal_candidate: Model) -> Component: + candidate = create_component(model=thermal_candidate, id="CAND") return candidate @@ -352,8 +388,14 @@ def test_generation_xpansion_two_time_steps_two_scenarios( output = OutputValues(problem) expected_output = OutputValues() - expected_output.component("G1").var("generation").value = [[0, 200], [0, 100]] - expected_output.component("CAND").var("generation").value = [[300, 300], [200, 300]] + expected_output.component("G1").var("generation").value = [ + [0.0, 200.0], + [0.0, 100.0], + ] + expected_output.component("CAND").var("generation").value = [ + [300.0, 300.0], + [200.0, 300.0], + ] expected_output.component("CAND").var("p_max").value = 300.0 assert output == expected_output, f"Output differs from expected: {output}" diff --git a/tests/integration/test_benders_decomposed.py b/tests/integration/test_benders_decomposed.py index bd6d79a8..3bca1b2a 100644 --- a/tests/integration/test_benders_decomposed.py +++ b/tests/integration/test_benders_decomposed.py @@ -57,8 +57,6 @@ create_component, ) -CONSTANT = IndexingStructure(False, False) - INVESTMENT = ProblemContext.INVESTMENT OPERATIONAL = ProblemContext.OPERATIONAL COUPLING = ProblemContext.COUPLING @@ -95,7 +93,7 @@ def thermal_candidate() -> Model: ) ], objective_operational_contribution=(param("op_cost") * var("generation")) - .sum() + .time_sum() .expec(), objective_investment_contribution=param("invest_cost") * var("p_max"), ) @@ -146,7 +144,7 @@ def discrete_candidate() -> Model: ), ], objective_operational_contribution=(param("op_cost") * var("generation")) - .sum() + .time_sum() .expec(), objective_investment_contribution=param("invest_cost") * var("p_max"), ) @@ -296,8 +294,8 @@ def test_benders_decomposed_multi_time_block_single_scenario( """ data = {} - data[TimeIndex(0)] = 200 - data[TimeIndex(1)] = 300 + data[TimeIndex(0)] = 200.0 + data[TimeIndex(1)] = 300.0 demand_data = TimeSeriesData(time_series=data) @@ -336,7 +334,7 @@ def test_benders_decomposed_multi_time_block_single_scenario( xpansion = build_benders_decomposed_problem(decision_tree_root, database) - data = { + data_output = { "solution": { "overall_cost": 62_000, "values": { @@ -344,7 +342,7 @@ def test_benders_decomposed_multi_time_block_single_scenario( }, } } - solution = BendersSolution(data) + solution = BendersSolution(data_output) assert xpansion.run() decomposed_solution = xpansion.solution @@ -423,7 +421,7 @@ def test_benders_decomposed_single_time_block_multi_scenario( xpansion = build_benders_decomposed_problem(decision_tree_root, database) - data = { + data_output = { "solution": { "overall_cost": 55_000, "values": { @@ -431,7 +429,7 @@ def test_benders_decomposed_single_time_block_multi_scenario( }, } } - solution = BendersSolution(data) + solution = BendersSolution(data_output) assert xpansion.run() decomposed_solution = xpansion.solution @@ -517,7 +515,7 @@ def test_benders_decomposed_multi_time_block_multi_scenario( xpansion = build_benders_decomposed_problem(decision_tree_root, database) - data = { + data_output = { "solution": { "overall_cost": 58_000, "values": { @@ -525,7 +523,7 @@ def test_benders_decomposed_multi_time_block_multi_scenario( }, } } - solution = BendersSolution(data) + solution = BendersSolution(data_output) assert xpansion.run() decomposed_solution = xpansion.solution diff --git a/tests/models/conftest.py b/tests/models/conftest.py index ee330ed3..a559b8d5 100644 --- a/tests/models/conftest.py +++ b/tests/models/conftest.py @@ -14,7 +14,7 @@ import pytest from andromede.model.parsing import parse_yaml_library -from andromede.model.resolve_library import resolve_library +from andromede.model.resolve_library import Library, resolve_library @pytest.fixture(scope="session") @@ -28,23 +28,23 @@ def data_dir() -> Path: @pytest.fixture(scope="session") -def lib(data_dir: Path): +def lib(data_dir: Path) -> Library: lib_file = data_dir / "lib.yml" with lib_file.open() as f: input_lib = parse_yaml_library(f) - lib = resolve_library(input_lib) + lib = resolve_library([input_lib]) return lib @pytest.fixture(scope="session") -def lib_sc(): +def lib_sc() -> Library: libs_path = Path(__file__).parents[2] / "src/andromede/libs/" lib_sc_file = libs_path / "standard_sc.yml" with lib_sc_file.open() as f: input_lib_sc = parse_yaml_library(f) - lib_sc = resolve_library(input_lib_sc) + lib_sc = resolve_library([input_lib_sc]) return lib_sc diff --git a/tests/models/test_ac_link.py b/tests/models/test_ac_link.py index b94bab02..39d4d52f 100644 --- a/tests/models/test_ac_link.py +++ b/tests/models/test_ac_link.py @@ -40,10 +40,10 @@ def ac_lib(libs_dir: Path, std_lib: Library) -> Library: lib_file = libs_dir / "ac.yml" with lib_file.open() as f: input_lib = parse_yaml_library(f) - return resolve_library(input_lib, preloaded_libraries=[std_lib]) + return resolve_library([input_lib], preloaded_libs=[std_lib]) -def test_ac_network_no_links(ac_lib: Library): +def test_ac_network_no_links(ac_lib: Library) -> None: """ The network only has one AC node where a generator and a demand are connected. @@ -87,7 +87,7 @@ def test_ac_network_no_links(ac_lib: Library): assert problem.solver.Objective().Value() == pytest.approx(3000, abs=0.01) -def test_ac_network(ac_lib: Library): +def test_ac_network(ac_lib: Library) -> None: """ The network only has 2 AC nodes connected by 1 AC link. @@ -148,7 +148,7 @@ def test_ac_network(ac_lib: Library): ) -def test_parallel_ac_links(ac_lib: Library): +def test_parallel_ac_links(ac_lib: Library) -> None: """ The network has 2 AC nodes connected by 2 parallel links, where reactance is 1 for line L1, and 2 for line L2. @@ -220,7 +220,7 @@ def test_parallel_ac_links(ac_lib: Library): ) -def test_parallel_ac_links_with_pst(ac_lib: Library): +def test_parallel_ac_links_with_pst(ac_lib: Library) -> None: """ Same case as in parallel_ac_links but: - flow is restricted to 50 MW on line L1, so it cannot diff --git a/tests/models/test_electrolyzer.py b/tests/models/test_electrolyzer.py index fedaf7d9..b85885a7 100644 --- a/tests/models/test_electrolyzer.py +++ b/tests/models/test_electrolyzer.py @@ -64,7 +64,7 @@ ) ], objective_operational_contribution=(param("cost") * var("generation")) - .sum() + .time_sum() .expec(), ) @@ -128,7 +128,7 @@ ) -def test_electrolyzer(): +def test_electrolyzer() -> None: elec_node = Node(model=ELECTRICAL_NODE_MODEL, id="1") h2_node = Node(model=H2_NODE_MODEL, id="2") diff --git a/tests/models/test_electrolyzer_n_inputs.py b/tests/models/test_electrolyzer_n_inputs.py index 6c6f5be5..53f3f3bc 100644 --- a/tests/models/test_electrolyzer_n_inputs.py +++ b/tests/models/test_electrolyzer_n_inputs.py @@ -50,7 +50,7 @@ """ -def test_electrolyzer_n_inputs_1(): +def test_electrolyzer_n_inputs_1() -> None: """ Test with an electrolyzer for each input @@ -133,15 +133,15 @@ def test_electrolyzer_n_inputs_1(): print(ep2_gen) print(gp_gen) - assert math.isclose(ep1_gen, 70) - assert math.isclose(ep2_gen, 42) - assert math.isclose(gp_gen, 30) + assert math.isclose(ep1_gen, 70) # type:ignore + assert math.isclose(ep2_gen, 42) # type:ignore + assert math.isclose(gp_gen, 30) # type:ignore assert status == problem.solver.OPTIMAL assert math.isclose(problem.solver.Objective().Value(), 1990) -def test_electrolyzer_n_inputs_2(): +def test_electrolyzer_n_inputs_2() -> None: """ Test with one electrolyzer that has two inputs @@ -222,15 +222,15 @@ def test_electrolyzer_n_inputs_2(): print(ep2_gen) print(gp_gen) - assert math.isclose(ep1_gen, 70) - assert math.isclose(ep2_gen, 42) - assert math.isclose(gp_gen, 30) + assert math.isclose(ep1_gen, 70) # type:ignore + assert math.isclose(ep2_gen, 42) # type:ignore + assert math.isclose(gp_gen, 30) # type:ignore assert status == problem.solver.OPTIMAL assert math.isclose(problem.solver.Objective().Value(), 1990) -def test_electrolyzer_n_inputs_3(): +def test_electrolyzer_n_inputs_3() -> None: """ Test with a consumption_electrolyzer with two inputs @@ -317,15 +317,15 @@ def test_electrolyzer_n_inputs_3(): ep2_gen = output.component("ep2").var("generation").value gp_gen = output.component("gp").var("generation").value - assert math.isclose(ep1_gen, 70) - assert math.isclose(ep2_gen, 30) - assert math.isclose(gp_gen, 30) + assert math.isclose(ep1_gen, 70) # type:ignore + assert math.isclose(ep2_gen, 30) # type:ignore + assert math.isclose(gp_gen, 30) # type:ignore assert status == problem.solver.OPTIMAL assert math.isclose(problem.solver.Objective().Value(), 1750) -def test_electrolyzer_n_inputs_4(): +def test_electrolyzer_n_inputs_4() -> None: """ Test with one electrolyzer with one input that takes every inputs @@ -405,9 +405,9 @@ def test_electrolyzer_n_inputs_4(): ep2_gen = output.component("ep2").var("generation").value gp_gen = output.component("gp").var("generation").value - assert math.isclose(ep1_gen, 70) - assert math.isclose(ep2_gen, 30) - assert math.isclose(gp_gen, 30) + assert math.isclose(ep1_gen, 70) # type:ignore + assert math.isclose(ep2_gen, 30) # type:ignore + assert math.isclose(gp_gen, 30) # type:ignore assert status == problem.solver.OPTIMAL assert math.isclose(problem.solver.Objective().Value(), 1750) diff --git a/tests/models/test_electrolyzer_n_inputs_yaml.py b/tests/models/test_electrolyzer_n_inputs_yaml.py index ac6e8a25..f7bfb8f0 100644 --- a/tests/models/test_electrolyzer_n_inputs_yaml.py +++ b/tests/models/test_electrolyzer_n_inputs_yaml.py @@ -13,6 +13,7 @@ import math from pathlib import Path +from andromede.model.library import Library from andromede.simulation import OutputValues, TimeBlock, build_problem from andromede.study import ( ConstantData, @@ -43,7 +44,7 @@ """ -def test_electrolyzer_n_inputs_1(data_dir: Path, lib: Path, lib_sc: Path): +def test_electrolyzer_n_inputs_1(data_dir: Path, lib: Library, lib_sc: Library) -> None: """ Test with an electrolyzer for each input @@ -138,15 +139,15 @@ def test_electrolyzer_n_inputs_1(data_dir: Path, lib: Path, lib_sc: Path): print(ep2_gen) print(gp_gen) - assert math.isclose(ep1_gen, 70) - assert math.isclose(ep2_gen, 42) - assert math.isclose(gp_gen, 30) + assert math.isclose(ep1_gen, 70) # type:ignore + assert math.isclose(ep2_gen, 42) # type:ignore + assert math.isclose(gp_gen, 30) # type:ignore assert status == problem.solver.OPTIMAL assert math.isclose(problem.solver.Objective().Value(), 1990) -def test_electrolyzer_n_inputs_2(data_dir: Path, lib: Path, lib_sc: Path): +def test_electrolyzer_n_inputs_2(data_dir: Path, lib: Library, lib_sc: Library) -> None: """ Test with one electrolyzer that has two inputs @@ -236,15 +237,15 @@ def test_electrolyzer_n_inputs_2(data_dir: Path, lib: Path, lib_sc: Path): print(ep2_gen) print(gp_gen) - assert math.isclose(ep1_gen, 70) - assert math.isclose(ep2_gen, 42) - assert math.isclose(gp_gen, 30) + assert math.isclose(ep1_gen, 70) # type:ignore + assert math.isclose(ep2_gen, 42) # type:ignore + assert math.isclose(gp_gen, 30) # type:ignore assert status == problem.solver.OPTIMAL assert math.isclose(problem.solver.Objective().Value(), 1990) -def test_electrolyzer_n_inputs_3(data_dir: Path, lib: Path, lib_sc: Path): +def test_electrolyzer_n_inputs_3(data_dir: Path, lib: Library, lib_sc: Library) -> None: """ Test with a consumption_electrolyzer with two inputs @@ -341,15 +342,15 @@ def test_electrolyzer_n_inputs_3(data_dir: Path, lib: Path, lib_sc: Path): ep2_gen = output.component("ep2").var("generation").value gp_gen = output.component("gp").var("generation").value - assert math.isclose(ep1_gen, 70) - assert math.isclose(ep2_gen, 30) - assert math.isclose(gp_gen, 30) + assert math.isclose(ep1_gen, 70) # type:ignore + assert math.isclose(ep2_gen, 30) # type:ignore + assert math.isclose(gp_gen, 30) # type:ignore assert status == problem.solver.OPTIMAL assert math.isclose(problem.solver.Objective().Value(), 1750) -def test_electrolyzer_n_inputs_4(data_dir: Path, lib: Path, lib_sc: Path): +def test_electrolyzer_n_inputs_4(data_dir: Path, lib: Library, lib_sc: Library) -> None: """ Test with one electrolyzer with one input that takes every inputs @@ -440,9 +441,9 @@ def test_electrolyzer_n_inputs_4(data_dir: Path, lib: Path, lib_sc: Path): ep2_gen = output.component("ep2").var("generation").value gp_gen = output.component("gp").var("generation").value - assert math.isclose(ep1_gen, 70) - assert math.isclose(ep2_gen, 30) - assert math.isclose(gp_gen, 30) + assert math.isclose(ep1_gen, 70) # type:ignore + assert math.isclose(ep2_gen, 30) # type:ignore + assert math.isclose(gp_gen, 30) # type:ignore assert status == problem.solver.OPTIMAL assert math.isclose(problem.solver.Objective().Value(), 1750) diff --git a/tests/models/test_quota_co2.py b/tests/models/test_quota_co2.py index 07f50e58..5888b81b 100644 --- a/tests/models/test_quota_co2.py +++ b/tests/models/test_quota_co2.py @@ -39,7 +39,7 @@ """ Test of a generation of energy and co2 with a quota to limit the emission""" -def test_quota_co2(): +def test_quota_co2() -> None: n1 = Node(model=NODE_BALANCE_MODEL, id="N1") n2 = Node(model=NODE_BALANCE_MODEL, id="N2") oil1 = create_component(model=C02_POWER_MODEL, id="Oil1") @@ -89,6 +89,6 @@ def test_quota_co2(): assert status == problem.solver.OPTIMAL assert math.isclose(problem.solver.Objective().Value(), 5500) - assert math.isclose(oil1_p, 50) - assert math.isclose(coal1_p, 50) - assert math.isclose(l12_flow, -50) + assert math.isclose(oil1_p, 50) # type:ignore + assert math.isclose(coal1_p, 50) # type:ignore + assert math.isclose(l12_flow, -50) # type:ignore diff --git a/tests/models/test_quota_co2_yaml.py b/tests/models/test_quota_co2_yaml.py index f11a2369..1c59494e 100644 --- a/tests/models/test_quota_co2_yaml.py +++ b/tests/models/test_quota_co2_yaml.py @@ -13,6 +13,7 @@ import math from pathlib import Path +from andromede.model.library import Library from andromede.simulation import OutputValues, TimeBlock, build_problem from andromede.study import ( ConstantData, @@ -38,7 +39,7 @@ """ Test of a generation of energy and co2 with a quota to limit the emission""" -def test_quota_co2(data_dir: Path, lib: Path, lib_sc: Path): +def test_quota_co2(data_dir: Path, lib: Library, lib_sc: Library) -> None: gen_model = lib_sc.models["generator_with_co2"] node_model = lib.models["node"] quota_co2_model = lib_sc.models["quota_co2"] @@ -94,6 +95,6 @@ def test_quota_co2(data_dir: Path, lib: Path, lib_sc: Path): assert status == problem.solver.OPTIMAL assert math.isclose(problem.solver.Objective().Value(), 5500) - assert math.isclose(oil1_p, 50) - assert math.isclose(coal1_p, 50) - assert math.isclose(l12_flow, -50) + assert math.isclose(oil1_p, 50) # type:ignore + assert math.isclose(coal1_p, 50) # type:ignore + assert math.isclose(l12_flow, -50) # type:ignore diff --git a/tests/models/test_short_term_storage_complex.py b/tests/models/test_short_term_storage_complex.py index 0d23c050..0d4b6d9c 100644 --- a/tests/models/test_short_term_storage_complex.py +++ b/tests/models/test_short_term_storage_complex.py @@ -3,14 +3,9 @@ import pandas as pd from andromede.libs.standard import ( - BALANCE_PORT_TYPE, DEMAND_MODEL, - GENERATOR_MODEL, - GENERATOR_MODEL_WITH_PMIN, - LINK_MODEL, NODE_BALANCE_MODEL, SPILLAGE_MODEL, - THERMAL_CLUSTER_MODEL_HD, UNSUPPLIED_ENERGY_MODEL, ) from andromede.libs.standard_sc import SHORT_TERM_STORAGE_COMPLEX @@ -21,7 +16,6 @@ Network, Node, PortRef, - TimeScenarioIndex, TimeScenarioSeriesData, create_component, ) diff --git a/tests/unittests/data/components_for_scenarization_test.yml b/tests/unittests/data/components_for_scenarization_test.yml new file mode 100644 index 00000000..98cdffc2 --- /dev/null +++ b/tests/unittests/data/components_for_scenarization_test.yml @@ -0,0 +1,47 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. +study: + nodes: + - id: N + model: node + + components: + - id: G + model: generator + parameters: + - name: cost + type: constant + value: 100 + - name: p_max + type: constant + value: 100 + - id: D + model: demand + scenario-group: load + parameters: + - name: demand + type: timeseries + timeseries: loads + + connections: + - component1: N + port_1: injection_port + component2: D + port_2: injection_port + + - component1: N + port_1: injection_port + component2: G + port_2: injection_port + + + diff --git a/tests/unittests/data/lib.yml b/tests/unittests/data/lib.yml index f42df312..ffdeedb1 100644 --- a/tests/unittests/data/lib.yml +++ b/tests/unittests/data/lib.yml @@ -84,7 +84,6 @@ library: field: flow definition: unsupplied_energy - - id: demand description: A basic fixed demand model parameters: @@ -175,9 +174,9 @@ library: - name: Min generation expression: generation >= nb_on * p_min - name: Number of units variation - expression: nb_on = nb_on[-1] + nb_start - nb_stop + expression: nb_on = nb_on[t-1] + nb_start - nb_stop - name: Min up time - expression: sum(nb_start[-d_min_up + 1 .. 0]) <= nb_on + expression: sum(t-d_min_up + 1 .. t, nb_start) <= nb_on - name: Min down time - expression: sum(nb_stop[-d_min_down + 1 .. 0]) <= nb_units_max[-d_min_down] - nb_on + expression: sum(t-d_min_down + 1 .. t, nb_stop) <= nb_units_max[t-d_min_down] - nb_on objective: expec(sum(cost * generation)) \ No newline at end of file diff --git a/tests/unittests/data/lib_for_resolving_test/CO2_port.yml b/tests/unittests/data/lib_for_resolving_test/CO2_port.yml new file mode 100644 index 00000000..7cdf190c --- /dev/null +++ b/tests/unittests/data/lib_for_resolving_test/CO2_port.yml @@ -0,0 +1,21 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + +library: + id: CO2_port + description: definition of CO2 port + + port-types: + - id: emission + description: A port which transfers CO2 emission + fields: + - name: CO2 diff --git a/tests/unittests/data/lib_for_resolving_test/basic_lib.yml b/tests/unittests/data/lib_for_resolving_test/basic_lib.yml new file mode 100644 index 00000000..5cd9c98c --- /dev/null +++ b/tests/unittests/data/lib_for_resolving_test/basic_lib.yml @@ -0,0 +1,31 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + +library: + id: basic + description: Basic library + + port-types: + - id: flow + description: A port which transfers power flow + fields: + - name: flow + + models: + - id: node + description: A basic balancing node model + ports: + - name: injection_port + type: flow + binding-constraints: + - name: balance + expression: sum_connections(injection_port.flow) = 0 diff --git a/tests/unittests/data/lib_for_resolving_test/demand.yml b/tests/unittests/data/lib_for_resolving_test/demand.yml new file mode 100644 index 00000000..fd8c081a --- /dev/null +++ b/tests/unittests/data/lib_for_resolving_test/demand.yml @@ -0,0 +1,32 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + +library: + id: demand + description: Library with only demand + dependencies: + - basic + + models: + - id: demand + description: A basic fixed demand model + parameters: + - name: demand + time-dependent: true + scenario-dependent: true + ports: + - name: injection_port + type: flow + port-field-definitions: + - port: injection_port + field: flow + definition: -demand diff --git a/tests/unittests/data/lib_for_resolving_test/looping_lib_1.yml b/tests/unittests/data/lib_for_resolving_test/looping_lib_1.yml new file mode 100644 index 00000000..e5e7d231 --- /dev/null +++ b/tests/unittests/data/lib_for_resolving_test/looping_lib_1.yml @@ -0,0 +1,17 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + +library: + id: loop_1 + description: Basic library + dependencies: + - loop_2 diff --git a/tests/unittests/data/lib_for_resolving_test/looping_lib_2.yml b/tests/unittests/data/lib_for_resolving_test/looping_lib_2.yml new file mode 100644 index 00000000..ca4d2065 --- /dev/null +++ b/tests/unittests/data/lib_for_resolving_test/looping_lib_2.yml @@ -0,0 +1,17 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + +library: + id: loop_2 + description: Basic library + dependencies: + - loop_1 diff --git a/tests/unittests/data/lib_for_resolving_test/port_redefinition.yml b/tests/unittests/data/lib_for_resolving_test/port_redefinition.yml new file mode 100644 index 00000000..554a525a --- /dev/null +++ b/tests/unittests/data/lib_for_resolving_test/port_redefinition.yml @@ -0,0 +1,38 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + +library: + id: demand + description: Library with only demand + dependencies: + - basic + + port-types: + - id: flow + description: A port which transfers power flow + fields: + - name: flow + + models: + - id: demand + description: A basic fixed demand model + parameters: + - name: demand + time-dependent: true + scenario-dependent: true + ports: + - name: injection_port + type: flow + port-field-definitions: + - port: injection_port + field: flow + definition: -demand diff --git a/tests/unittests/data/lib_for_resolving_test/production.yml b/tests/unittests/data/lib_for_resolving_test/production.yml new file mode 100644 index 00000000..b898611f --- /dev/null +++ b/tests/unittests/data/lib_for_resolving_test/production.yml @@ -0,0 +1,40 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + +library: + id: production + description: Library with only production + dependencies: + - basic + + models: + - id: generator + description: A basic generator model + parameters: + - name: cost + time-dependent: false + scenario-dependent: false + - name: p_max + time-dependent: false + scenario-dependent: false + variables: + - name: generation + lower-bound: 0 + upper-bound: p_max + ports: + - name: injection_port + type: flow + port-field-definitions: + - port: injection_port + field: flow + definition: generation + objective: expec(sum(cost * generation)) \ No newline at end of file diff --git a/tests/unittests/data/lib_for_resolving_test/production_with_CO2.yml b/tests/unittests/data/lib_for_resolving_test/production_with_CO2.yml new file mode 100644 index 00000000..fd3c677d --- /dev/null +++ b/tests/unittests/data/lib_for_resolving_test/production_with_CO2.yml @@ -0,0 +1,49 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + +library: + id: production_CO2 + description: Library with only production + dependencies: + - basic + - CO2_port + + models: + - id: generator + description: A basic generator model + parameters: + - name: cost + time-dependent: false + scenario-dependent: false + - name: emission + time-dependent: false + scenario-dependent: false + - name: p_max + time-dependent: false + scenario-dependent: false + variables: + - name: generation + lower-bound: 0 + upper-bound: p_max + ports: + - name: injection_port + type: flow + - name: emission_port + type: emission + port-field-definitions: + - port: injection_port + field: flow + definition: generation + - port: emission_port + field: CO2 + definition: generation * emission + objective: expec(sum(cost * generation)) \ No newline at end of file diff --git a/tests/unittests/data/loads.txt b/tests/unittests/data/loads.txt new file mode 100644 index 00000000..51f4c8f2 --- /dev/null +++ b/tests/unittests/data/loads.txt @@ -0,0 +1,2 @@ +50 100 +50 100 \ No newline at end of file diff --git a/tests/unittests/data/scenario_builder.csv b/tests/unittests/data/scenario_builder.csv new file mode 100644 index 00000000..c7e948ca --- /dev/null +++ b/tests/unittests/data/scenario_builder.csv @@ -0,0 +1,8 @@ +load, 0, 0 +load, 1, 1 +load, 2, 0 +load, 3, 1 +cost-group, 0, 0 +cost-group, 1, 0 +cost-group, 2, 1 +cost-group, 3, 1 \ No newline at end of file diff --git a/tests/unittests/expressions/parsing/test_expression_parsing.py b/tests/unittests/expressions/parsing/test_expression_parsing.py index 13fb04a3..ba540e9e 100644 --- a/tests/unittests/expressions/parsing/test_expression_parsing.py +++ b/tests/unittests/expressions/parsing/test_expression_parsing.py @@ -15,7 +15,7 @@ from andromede.expression import ExpressionNode, literal, param, print_expr, var from andromede.expression.equality import expressions_equal -from andromede.expression.expression import ExpressionRange, port_field +from andromede.expression.expression import port_field from andromede.expression.parsing.parse_expression import ( AntaresParseException, ModelIdentifiers, @@ -41,22 +41,10 @@ "port.f <= 0", port_field("port", "f") <= 0, ), - ({"x"}, {}, "sum(x)", var("x").sum()), + ({"x"}, {}, "sum(x)", var("x").time_sum()), ({"x"}, {}, "x[-1]", var("x").eval(-literal(1))), - ( - {"x"}, - {}, - "x[-1..5]", - var("x").eval(ExpressionRange(-literal(1), literal(5))), - ), ({"x"}, {}, "x[1]", var("x").eval(1)), ({"x"}, {}, "x[t-1]", var("x").shift(-literal(1))), - ( - {"x"}, - {}, - "x[t-1, t+4]", - var("x").shift([-literal(1), literal(4)]), - ), ( {"x"}, {}, @@ -90,35 +78,23 @@ ( {"x"}, {}, - "x[t-1, t, t+4]", - var("x").shift([-literal(1), literal(0), literal(4)]), + "sum(t-1..t+5, x)", + var("x").time_sum(-literal(1), literal(5)), ), ( {"x"}, {}, - "x[t-1..t+5]", - var("x").shift(ExpressionRange(-literal(1), literal(5))), + "sum(t-1..t, x)", + var("x").time_sum(-literal(1), literal(0)), ), ( {"x"}, {}, - "x[t-1..t]", - var("x").shift(ExpressionRange(-literal(1), literal(0))), - ), - ( - {"x"}, - {}, - "x[t..t+5]", - var("x").shift(ExpressionRange(literal(0), literal(5))), + "sum(t..t+5, x)", + var("x").time_sum(literal(0), literal(5)), ), ({"x"}, {}, "x[t]", var("x")), ({"x"}, {"p"}, "x[t+p]", var("x").shift(param("p"))), - ( - {"x"}, - {}, - "sum(x[-1..5])", - var("x").eval(ExpressionRange(-literal(1), literal(5))).sum(), - ), ({}, {}, "sum_connections(port.f)", port_field("port", "f").sum_connections()), ( {"level", "injection", "withdrawal"}, @@ -133,17 +109,15 @@ ( {"nb_start", "nb_on"}, {"d_min_up"}, - "sum(nb_start[-d_min_up + 1 .. 0]) <= nb_on", - var("nb_start") - .eval(ExpressionRange(-param("d_min_up") + 1, literal(0))) - .sum() + "sum(t - d_min_up + 1 .. t, nb_start) <= nb_on", + var("nb_start").time_sum(-param("d_min_up") + 1, literal(0)) <= var("nb_on"), ), ( {"generation"}, {"cost"}, "expec(sum(cost * generation))", - (param("cost") * var("generation")).sum().expec(), + (param("cost") * var("generation")).time_sum().expec(), ), ], ) @@ -152,7 +126,7 @@ def test_parsing_visitor( parameters: Set[str], expression_str: str, expected: ExpressionNode, -): +) -> None: identifiers = ModelIdentifiers(variables, parameters) expr = parse_expression(expression_str, identifiers) print() @@ -170,7 +144,7 @@ def test_parsing_visitor( "x[t 4]", ], ) -def test_parse_cancellation_should_throw(expression_str: str): +def test_parse_cancellation_should_throw(expression_str: str) -> None: # Console log error is displayed ! identifiers = ModelIdentifiers( variables={"x"}, diff --git a/tests/unittests/expressions/test_equality.py b/tests/unittests/expressions/test_equality.py index c042fdde..f949fe7f 100644 --- a/tests/unittests/expressions/test_equality.py +++ b/tests/unittests/expressions/test_equality.py @@ -10,21 +10,10 @@ # # This file is part of the Antares project. -import math - import pytest from andromede.expression import ExpressionNode, copy_expression, literal, param, var from andromede.expression.equality import expressions_equal -from andromede.expression.expression import ( - ExpressionRange, - TimeAggregatorNode, - expression_range, -) - - -def shifted_x(): - return var("x").shift(expression_range(0, 2)) @pytest.mark.parametrize( @@ -36,10 +25,9 @@ def shifted_x(): var("x") - 1, var("x") / 2, var("x") * 3, - var("x").shift(expression_range(1, 10, 2)).sum(), - var("x").shift(expression_range(1, param("p"))).sum(), - TimeAggregatorNode(shifted_x(), name="TimeSum", stay_roll=True), - TimeAggregatorNode(shifted_x(), name="TimeAggregator", stay_roll=True), + var("x").time_sum(1, 10), + var("x").time_sum(1, param("p")), + var("x").time_sum(), var("x") + 5 <= 2, var("x").expec(), ], @@ -56,20 +44,12 @@ def test_equals(expr: ExpressionNode) -> None: (literal(1), literal(2)), (var("x") + 1, var("x")), ( - var("x").shift(expression_range(1, param("p"))).sum(), - var("x").shift(expression_range(1, param("q"))).sum(), - ), - ( - var("x").shift(expression_range(1, 10, 2)).sum(), - var("x").shift(expression_range(1, 10, 3)).sum(), - ), - ( - TimeAggregatorNode(shifted_x(), name="TimeSum", stay_roll=True), - TimeAggregatorNode(shifted_x(), name="TimeSum", stay_roll=False), + var("x").time_sum(1, param("p")), + var("x").time_sum(1, param("q")), ), ( - TimeAggregatorNode(shifted_x(), name="TimeSum", stay_roll=True), - TimeAggregatorNode(shifted_x(), name="TimeAggregator", stay_roll=True), + var("x").time_sum(2, 10), + var("x").time_sum(1, 10), ), (var("x").expec(), var("y").expec()), ], @@ -78,7 +58,7 @@ def test_not_equals(lhs: ExpressionNode, rhs: ExpressionNode) -> None: assert not expressions_equal(lhs, rhs) -def test_tolerance(): +def test_tolerance() -> None: assert expressions_equal(literal(10), literal(10.09), abs_tol=0.1) assert not expressions_equal(literal(10), literal(10.11), abs_tol=0.1) assert expressions_equal(literal(10), literal(10.9), rel_tol=0.1) diff --git a/tests/unittests/expressions/test_expressions.py b/tests/unittests/expressions/test_expressions.py index 922dbcb3..f8d644ab 100644 --- a/tests/unittests/expressions/test_expressions.py +++ b/tests/unittests/expressions/test_expressions.py @@ -39,16 +39,9 @@ from andromede.expression.expression import ( ComponentParameterNode, ComponentVariableNode, - ExpressionRange, - Instances, - comp_param, - comp_var, - port_field, ) from andromede.expression.indexing import IndexingStructureProvider, compute_indexation from andromede.expression.indexing_structure import IndexingStructure -from andromede.simulation.linear_expression import LinearExpression, Term -from andromede.simulation.linearize import linearize_expression @dataclass(frozen=True) @@ -83,12 +76,9 @@ def get_component_variable_value(self, component_id: str, name: str) -> float: def get_component_parameter_value(self, component_id: str, name: str) -> float: return self.parameters[comp_key(component_id, name)] - def parameter_is_constant_over_time(self, name: str) -> bool: - raise NotImplementedError() - def test_comp_parameter() -> None: - add_node = AdditionNode(LiteralNode(1), ComponentVariableNode("comp1", "x")) + add_node = AdditionNode([LiteralNode(1), ComponentVariableNode("comp1", "x")]) expr = DivisionNode(add_node, ComponentParameterNode("comp1", "p")) assert visit(expr, PrinterVisitor()) == "((1 + comp1.x) / comp1.p)" @@ -100,7 +90,7 @@ def test_comp_parameter() -> None: def test_ast() -> None: - add_node = AdditionNode(LiteralNode(1), VariableNode("x")) + add_node = AdditionNode([LiteralNode(1), VariableNode("x")]) expr = DivisionNode(add_node, ParameterNode("p")) assert visit(expr, PrinterVisitor()) == "((1 + x) / p)" @@ -157,32 +147,6 @@ def get_parameter_value(self, name: str) -> float: assert resolve_parameters(expr, TestParamProvider()) == (5 * x + 3) / 2 -def test_linearization() -> None: - x = comp_var("c", "x") - expr = (5 * x + 3) / 2 - provider = StructureProvider() - - assert linearize_expression(expr, provider) == LinearExpression( - [Term(2.5, "c", "x")], 1.5 - ) - - with pytest.raises(ValueError): - linearize_expression(param("p") * x, provider) - - -def test_linearization_of_non_linear_expressions_should_raise_value_error() -> None: - x = var("x") - expr = x.variance() - - provider = StructureProvider() - with pytest.raises(ValueError) as exc: - linearize_expression(expr, provider) - assert ( - str(exc.value) - == "Cannot linearize expression with a non-linear operator: Variance" - ) - - def test_comparison() -> None: x = var("x") p = param("p") @@ -211,54 +175,26 @@ def get_variable_structure(self, name: str) -> IndexingStructure: def test_shift() -> None: x = var("x") - expr = x.shift(ExpressionRange(1, 4)) + expr = x.shift(1) provider = StructureProvider() - assert compute_indexation(expr, provider) == IndexingStructure(True, True) - assert expr.instances == Instances.MULTIPLE -def test_shifting_sum() -> None: +def test_time_sum() -> None: x = var("x") - expr = x.shift(ExpressionRange(1, 4)).sum() + expr = x.time_sum(1, 4) provider = StructureProvider() assert compute_indexation(expr, provider) == IndexingStructure(True, True) - assert expr.instances == Instances.SIMPLE - - -def test_eval() -> None: - x = var("x") - expr = x.eval(ExpressionRange(1, 4)) - provider = StructureProvider() - - assert compute_indexation(expr, provider) == IndexingStructure(False, True) - assert expr.instances == Instances.MULTIPLE - - -def test_eval_sum() -> None: - x = var("x") - expr = x.eval(ExpressionRange(1, 4)).sum() - provider = StructureProvider() - - assert compute_indexation(expr, provider) == IndexingStructure(False, True) - assert expr.instances == Instances.SIMPLE def test_sum_over_whole_block() -> None: x = var("x") - expr = x.sum() + expr = x.time_sum() provider = StructureProvider() assert compute_indexation(expr, provider) == IndexingStructure(False, True) - assert expr.instances == Instances.SIMPLE - - -def test_forbidden_composition_should_raise_value_error() -> None: - x = var("x") - with pytest.raises(ValueError): - _ = x.shift(ExpressionRange(1, 4)) + var("y") def test_expectation() -> None: @@ -267,7 +203,6 @@ def test_expectation() -> None: provider = StructureProvider() assert compute_indexation(expr, provider) == IndexingStructure(True, False) - assert expr.instances == Instances.SIMPLE def test_indexing_structure_comparison() -> None: diff --git a/tests/unittests/expressions/test_linear_expressions.py b/tests/unittests/expressions/test_linear_expressions.py index 54a11e95..519f4ece 100644 --- a/tests/unittests/expressions/test_linear_expressions.py +++ b/tests/unittests/expressions/test_linear_expressions.py @@ -14,40 +14,15 @@ import pytest -from andromede.expression.scenario_operator import Expectation -from andromede.expression.time_operator import TimeShift, TimeSum -from andromede.simulation.linear_expression import LinearExpression, Term +from andromede.simulation.linear_expression import LinearExpression, Term, TermKey @pytest.mark.parametrize( "term, expected", [ - (Term(1, "c", "x"), "+x"), - (Term(-1, "c", "x"), "-x"), - (Term(2.50, "c", "x"), "+2.5x"), - (Term(-3, "c", "x"), "-3x"), - (Term(-3, "c", "x", time_operator=TimeShift(-1)), "-3x.shift([-1])"), - (Term(-3, "c", "x", time_aggregator=TimeSum(True)), "-3x.sum(True)"), ( - Term( - -3, - "c", - "x", - time_operator=TimeShift([2, 3]), - time_aggregator=TimeSum(False), - ), - "-3x.shift([2, 3]).sum(False)", - ), - (Term(-3, "c", "x", scenario_operator=Expectation()), "-3x.expec()"), - ( - Term( - -3, - "c", - "x", - time_aggregator=TimeSum(True), - scenario_operator=Expectation(), - ), - "-3x.sum(True).expec()", + Term(-2.50, "c", "x", time_index=10, scenario_index=15), + "-2.5c.x[10,15]", ), ], ) @@ -59,16 +34,16 @@ def test_printing_term(term: Term, expected: str) -> None: "coeff, var_name, constant, expec_str", [ (0, "x", 0, "0"), - (1, "x", 0, "+x"), - (1, "x", 1, "+x+1"), - (3.7, "x", 1, "+3.7x+1"), + (1, "x", 0, "+c.x[0,0]"), + (1, "x", 1, "+c.x[0,0]+1"), + (3.7, "x", 1, "+3.7c.x[0,0]+1"), (0, "x", 1, "+1"), ], ) def test_affine_expression_printing_should_reflect_required_formatting( coeff: float, var_name: str, constant: float, expec_str: str ) -> None: - expr = LinearExpression([Term(coeff, "c", var_name)], constant) + expr = LinearExpression([Term(coeff, "c", var_name, 0, 0)], constant) assert str(expr) == expec_str @@ -88,12 +63,12 @@ def test_constant_expressions(lhs: LinearExpression, rhs: LinearExpression) -> N @pytest.mark.parametrize( "terms_dict, constant, exp_terms, exp_constant", [ - ({"x": Term(0, "c", "x")}, 1, {}, 1), - ({"x": Term(1, "c", "x")}, 1, {"x": Term(1, "c", "x")}, 1), + ({"x": Term(0, "c", "x", 0, 0)}, 1, {}, 1), + ({"x": Term(1, "c", "x", 0, 0)}, 1, {"x": Term(1, "c", "x", 0, 0)}, 1), ], ) def test_instantiate_linear_expression_from_dict( - terms_dict: Dict[str, Term], + terms_dict: Dict[TermKey, Term], constant: float, exp_terms: Dict[str, Term], exp_constant: float, @@ -107,66 +82,29 @@ def test_instantiate_linear_expression_from_dict( "e1, e2, expected", [ ( - LinearExpression([Term(10, "c", "x")], 1), - LinearExpression([Term(5, "c", "x")], 2), - LinearExpression([Term(15, "c", "x")], 3), + LinearExpression([Term(10, "c", "x", 0, 0)], 1), + LinearExpression([Term(5, "c", "x", 0, 0)], 2), + LinearExpression([Term(15, "c", "x", 0, 0)], 3), ), ( - LinearExpression([Term(10, "c1", "x")], 1), - LinearExpression([Term(5, "c2", "x")], 2), - LinearExpression([Term(10, "c1", "x"), Term(5, "c2", "x")], 3), + LinearExpression([Term(10, "c1", "x", 0, 0)], 1), + LinearExpression([Term(5, "c2", "x", 0, 0)], 2), + LinearExpression([Term(10, "c1", "x", 0, 0), Term(5, "c2", "x", 0, 0)], 3), ), ( - LinearExpression([Term(10, "c", "x")], 0), - LinearExpression([Term(5, "c", "y")], 0), - LinearExpression([Term(10, "c", "x"), Term(5, "c", "y")], 0), + LinearExpression([Term(10, "c", "x", 0, 0)], 0), + LinearExpression([Term(5, "c", "y", 0, 0)], 0), + LinearExpression([Term(10, "c", "x", 0, 0), Term(5, "c", "y", 0, 0)], 0), ), ( - LinearExpression(), - LinearExpression([Term(10, "c", "x", TimeShift(-1))]), - LinearExpression([Term(10, "c", "x", TimeShift(-1))]), + LinearExpression([Term(10, "c", "x", 0, 0)], 0), + LinearExpression([Term(5, "c", "x", 1, 0)], 0), + LinearExpression([Term(10, "c", "x", 0, 0), Term(5, "c", "x", 1, 0)], 0), ), ( - LinearExpression(), - LinearExpression( - [Term(10, "c", "x", time_aggregator=TimeSum(stay_roll=True))] - ), - LinearExpression( - [Term(10, "c", "x", time_aggregator=TimeSum(stay_roll=True))] - ), - ), - ( - LinearExpression([Term(10, "c", "x")]), - LinearExpression([Term(10, "c", "x", time_operator=TimeShift(-1))]), - LinearExpression( - [Term(10, "c", "x"), Term(10, "c", "x", time_operator=TimeShift(-1))] - ), - ), - ( - LinearExpression([Term(10, "c", "x")]), - LinearExpression( - [ - Term( - 10, - "c", - "x", - time_operator=TimeShift(-1), - scenario_operator=Expectation(), - ) - ] - ), - LinearExpression( - [ - Term(10, "c", "x"), - Term( - 10, - "c", - "x", - time_operator=TimeShift(-1), - scenario_operator=Expectation(), - ), - ] - ), + LinearExpression([Term(10, "c", "x", 0, 0)], 0), + LinearExpression([Term(5, "c", "x", 0, 1)], 0), + LinearExpression([Term(10, "c", "x", 0, 0), Term(5, "c", "x", 0, 1)], 0), ), ], ) @@ -185,8 +123,8 @@ def test_addition_of_linear_expressions_with_different_number_of_instances_shoul def test_operation_that_leads_to_term_with_zero_coefficient_should_be_removed_from_terms() -> ( None ): - e1 = LinearExpression([Term(10, "c", "x")], 1) - e2 = LinearExpression([Term(10, "c", "x")], 2) + e1 = LinearExpression([Term(10, "c", "x", 0, 0)], 1) + e2 = LinearExpression([Term(10, "c", "x", 0, 0)], 2) e3 = e2 - e1 assert e3.terms == {} @@ -195,47 +133,20 @@ def test_operation_that_leads_to_term_with_zero_coefficient_should_be_removed_fr "e1, e2, expected", [ ( - LinearExpression([Term(10, "c", "x")], 3), + LinearExpression([Term(10, "c", "x", 1, 0)], 3), LinearExpression([], 2), - LinearExpression([Term(20, "c", "x")], 6), + LinearExpression([Term(20, "c", "x", 1, 0)], 6), ), ( - LinearExpression([Term(10, "c", "x")], 3), + LinearExpression([Term(10, "c", "x", 0, 1)], 3), LinearExpression([], 1), - LinearExpression([Term(10, "c", "x")], 3), + LinearExpression([Term(10, "c", "x", 0, 1)], 3), ), ( - LinearExpression([Term(10, "c", "x")], 3), + LinearExpression([Term(10, "c", "x", 0, 0)], 3), LinearExpression(), LinearExpression(), ), - ( - LinearExpression( - [ - Term( - 10, - "c", - "x", - time_operator=TimeShift(-1), - scenario_operator=Expectation(), - ) - ], - 3, - ), - LinearExpression([], 2), - LinearExpression( - [ - Term( - 20, - "c", - "x", - time_operator=TimeShift(-1), - scenario_operator=Expectation(), - ) - ], - 6, - ), - ), ], ) def test_multiplication( @@ -246,8 +157,8 @@ def test_multiplication( def test_multiplication_of_two_non_constant_terms_should_raise_value_error() -> None: - e1 = LinearExpression([Term(10, "c", "x")], 0) - e2 = LinearExpression([Term(5, "c", "x")], 0) + e1 = LinearExpression([Term(10, "c", "x", 0, 0)], 0) + e2 = LinearExpression([Term(5, "c", "x", 0, 0)], 0) with pytest.raises(ValueError) as exc: _ = e1 * e2 assert str(exc.value) == "Cannot multiply two non constant expression" @@ -257,36 +168,8 @@ def test_multiplication_of_two_non_constant_terms_should_raise_value_error() -> "e1, expected", [ ( - LinearExpression([Term(10, "c", "x")], 5), - LinearExpression([Term(-10, "c", "x")], -5), - ), - ( - LinearExpression( - [ - Term( - 10, - "c", - "x", - time_operator=TimeShift(-1), - time_aggregator=TimeSum(False), - scenario_operator=Expectation(), - ) - ], - 5, - ), - LinearExpression( - [ - Term( - -10, - "c", - "x", - time_operator=TimeShift(-1), - time_aggregator=TimeSum(False), - scenario_operator=Expectation(), - ) - ], - -5, - ), + LinearExpression([Term(10, "c", "x", 1, 2)], 5), + LinearExpression([Term(-10, "c", "x", 1, 2)], -5), ), ], ) @@ -298,68 +181,31 @@ def test_negation(e1: LinearExpression, expected: LinearExpression) -> None: "e1, e2, expected", [ ( - LinearExpression([Term(10, "c", "x")], 1), - LinearExpression([Term(5, "c", "x")], 2), - LinearExpression([Term(5, "c", "x")], -1), - ), - ( - LinearExpression([Term(10, "c1", "x")], 1), - LinearExpression([Term(5, "c2", "x")], 2), - LinearExpression([Term(10, "c1", "x"), Term(-5, "c2", "x")], -1), - ), - ( - LinearExpression([Term(10, "c", "x")], 0), - LinearExpression([Term(5, "c", "y")], 0), - LinearExpression([Term(10, "c", "x"), Term(-5, "c", "y")], 0), + LinearExpression([Term(10, "c", "x", 0, 0)], 1), + LinearExpression([Term(5, "c", "x", 0, 0)], 2), + LinearExpression([Term(5, "c", "x", 0, 0)], -1), ), ( - LinearExpression(), - LinearExpression([Term(10, "c", "x", time_operator=TimeShift(-1))]), - LinearExpression([Term(-10, "c", "x", time_operator=TimeShift(-1))]), - ), - ( - LinearExpression(), + LinearExpression([Term(10, "c1", "x", 0, 0)], 1), + LinearExpression([Term(5, "c2", "x", 0, 0)], 2), LinearExpression( - [Term(10, "c", "x", time_aggregator=TimeSum(stay_roll=True))] - ), - LinearExpression( - [Term(-10, "c", "x", time_aggregator=TimeSum(stay_roll=True))] + [Term(10, "c1", "x", 0, 0), Term(-5, "c2", "x", 0, 0)], -1 ), ), ( - LinearExpression([Term(10, "c", "x")]), - LinearExpression([Term(10, "c", "x", time_operator=TimeShift(-1))]), - LinearExpression( - [Term(10, "c", "x"), Term(-10, "c", "x", time_operator=TimeShift(-1))] - ), + LinearExpression([Term(10, "c", "x", 0, 0)], 0), + LinearExpression([Term(5, "c", "y", 0, 0)], 0), + LinearExpression([Term(10, "c", "x", 0, 0), Term(-5, "c", "y", 0, 0)], 0), ), ( - LinearExpression([Term(10, "c", "x")]), - LinearExpression( - [ - Term( - 10, - "c", - "x", - time_operator=TimeShift(-1), - time_aggregator=TimeSum(False), - scenario_operator=Expectation(), - ) - ] - ), - LinearExpression( - [ - Term(10, "c", "x"), - Term( - -10, - "c", - "x", - time_operator=TimeShift(-1), - time_aggregator=TimeSum(False), - scenario_operator=Expectation(), - ), - ] - ), + LinearExpression([Term(10, "c", "x", 0, 0)], 0), + LinearExpression([Term(5, "c", "x", 1, 0)], 0), + LinearExpression([Term(10, "c", "x", 0, 0), Term(-5, "c", "x", 1, 0)], 0), + ), + ( + LinearExpression([Term(10, "c", "x", 0, 0)], 0), + LinearExpression([Term(5, "c", "x", 0, 1)], 0), + LinearExpression([Term(10, "c", "x", 0, 0), Term(-5, "c", "x", 0, 1)], 0), ), ], ) @@ -373,43 +219,14 @@ def test_substraction( "e1, e2, expected", [ ( - LinearExpression([Term(10, "c", "x")], 15), + LinearExpression([Term(10, "c", "x", 0, 1)], 15), LinearExpression([], 5), - LinearExpression([Term(2, "c", "x")], 3), + LinearExpression([Term(2, "c", "x", 0, 1)], 3), ), ( - LinearExpression([Term(10, "c", "x")], 15), + LinearExpression([Term(10, "c", "x", 1, 0)], 15), LinearExpression([], 1), - LinearExpression([Term(10, "c", "x")], 15), - ), - ( - LinearExpression( - [ - Term( - 10, - "c", - "x", - time_operator=TimeShift(-1), - time_aggregator=TimeSum(False), - scenario_operator=Expectation(), - ) - ], - 15, - ), - LinearExpression([], 5), - LinearExpression( - [ - Term( - 2, - "c", - "x", - time_operator=TimeShift(-1), - time_aggregator=TimeSum(False), - scenario_operator=Expectation(), - ) - ], - 3, - ), + LinearExpression([Term(10, "c", "x", 1, 0)], 15), ), ], ) @@ -420,7 +237,7 @@ def test_division( def test_division_by_zero_sould_raise_zero_division_error() -> None: - e1 = LinearExpression([Term(10, "c", "x")], 15) + e1 = LinearExpression([Term(10, "c", "x", 0, 0)], 15) e2 = LinearExpression() with pytest.raises(ZeroDivisionError) as exc: _ = e1 / e2 @@ -428,7 +245,7 @@ def test_division_by_zero_sould_raise_zero_division_error() -> None: def test_division_by_non_constant_expr_sould_raise_value_error() -> None: - e1 = LinearExpression([Term(10, "c", "x")], 15) + e1 = LinearExpression([Term(10, "c", "x", 0, 0)], 15) e2 = LinearExpression() with pytest.raises(ValueError) as exc: _ = e2 / e1 diff --git a/tests/unittests/expressions/test_linearization.py b/tests/unittests/expressions/test_linearization.py new file mode 100644 index 00000000..e7626b67 --- /dev/null +++ b/tests/unittests/expressions/test_linearization.py @@ -0,0 +1,153 @@ +from unittest.mock import Mock + +import pytest + +from andromede.expression import ExpressionNode, LiteralNode, literal, var +from andromede.expression.expression import ( + ComponentVariableNode, + CurrentScenarioIndex, + NoScenarioIndex, + TimeShift, + comp_param, + comp_var, + problem_var, +) +from andromede.expression.indexing import IndexingStructureProvider +from andromede.expression.indexing_structure import IndexingStructure +from andromede.expression.operators_expansion import ( + ProblemDimensions, + ProblemIndex, + expand_operators, +) +from andromede.simulation.linear_expression import LinearExpression, Term +from andromede.simulation.linearize import ParameterGetter, linearize_expression + +from .test_expressions import StructureProvider + +P = comp_param("c", "p") +X = comp_var("c", "x") +Y = comp_var("c", "y") + + +def var_at(var: ComponentVariableNode, timestep, scenario) -> LinearExpression: + return LinearExpression( + terms=[ + Term( + 1, + var.component_id, + var.name, + time_index=timestep, + scenario_index=scenario, + ) + ], + constant=0, + ) + + +def X_at(t: int = 0, s: int = 0) -> LinearExpression: + return var_at(X, timestep=t, scenario=s) + + +def Y_at(t: int = 0, s: int = 0) -> LinearExpression: + return var_at(Y, timestep=t, scenario=s) + + +def constant(c: float) -> LinearExpression: + return LinearExpression([], c) + + +def evaluate_literal(node: ExpressionNode) -> int: + if isinstance(node, LiteralNode): + return int(node.value) + raise NotImplementedError("Can only evaluate literal nodes.") + + +def test_linearization_before_operator_substitution_raises_an_error() -> None: + x = var("x") + expr = x.variance() + + provider = StructureProvider() + with pytest.raises( + ValueError, match="Scenario operators need to be expanded before linearization" + ): + linearize_expression(expr, timestep=0, scenario=0) + + +class AllTimeScenarioDependent(IndexingStructureProvider): + def get_parameter_structure(self, name: str) -> IndexingStructure: + return IndexingStructure(True, True) + + def get_variable_structure(self, name: str) -> IndexingStructure: + return IndexingStructure(True, True) + + def get_component_variable_structure( + self, component_id: str, name: str + ) -> IndexingStructure: + return IndexingStructure(True, True) + + def get_component_parameter_structure( + self, component_id: str, name: str + ) -> IndexingStructure: + return IndexingStructure(True, True) + + +def _expand_and_linearize( + expr: ExpressionNode, + dimensions: ProblemDimensions, + index: ProblemIndex, + parameter_value_provider: ParameterGetter, +) -> LinearExpression: + expanded = expand_operators( + expr, dimensions, evaluate_literal, AllTimeScenarioDependent() + ) + return linearize_expression( + expanded, index.timestep, index.scenario, parameter_value_provider + ) + + +@pytest.mark.parametrize( + "expr,expected", + [ + ((5 * X + 3) / 2, constant(2.5) * X_at(t=0) + constant(1.5)), + ((X + Y).time_sum(), X_at(t=0) + Y_at(t=0) + X_at(t=1) + Y_at(t=1)), + (X.shift(-1).shift(+1), X_at(t=0)), + (X.shift(-1).time_sum(), X_at(t=-1) + X_at(t=0)), + (X.shift(-1).time_sum(-1, +1), X_at(t=-2) + X_at(t=-1) + X_at(t=0)), + (X.time_sum().shift(-1), X_at(t=-1) + X_at(t=0)), + (X.time_sum(-1, +1).shift(-1), X_at(t=-2) + X_at(t=-1) + X_at(t=0)), + (X.eval(2).time_sum(), X_at(t=2) + X_at(t=2)), + ((X + 2).time_sum(), X_at(t=0) + X_at(t=1) + constant(4)), + ((X + 2).time_sum(-1, 0), X_at(t=-1) + X_at(t=0) + constant(4)), + ((X + 2).time_sum(-1, 0), X_at(t=-1) + X_at(t=0) + constant(4)), + ], +) +def test_linearization_of_nested_time_operations( + expr: ExpressionNode, expected: LinearExpression +) -> None: + dimensions = ProblemDimensions(timesteps_count=2, scenarios_count=1) + index = ProblemIndex(timestep=0, scenario=0) + params = Mock(spec=ParameterGetter) + + assert _expand_and_linearize(expr, dimensions, index, params) == expected + + +def test_invalid_multiplication() -> None: + params = Mock(spec=ParameterGetter) + + x = problem_var( + "c", "x", time_index=TimeShift(0), scenario_index=CurrentScenarioIndex() + ) + expression = x * x + with pytest.raises(ValueError, match="constant"): + linearize_expression(expression, 0, 0, params) + + +def test_invalid_division() -> None: + params = Mock(spec=ParameterGetter) + + x = problem_var( + "c", "x", time_index=TimeShift(0), scenario_index=CurrentScenarioIndex() + ) + expression = literal(1) / x + with pytest.raises(ValueError, match="constant"): + linearize_expression(expression, 0, 0, params) diff --git a/tests/unittests/expressions/test_operators_expansion.py b/tests/unittests/expressions/test_operators_expansion.py new file mode 100644 index 00000000..05fff221 --- /dev/null +++ b/tests/unittests/expressions/test_operators_expansion.py @@ -0,0 +1,131 @@ +from dataclasses import dataclass +from typing import Dict + +import pytest + +from andromede.expression import ExpressionNode, LiteralNode +from andromede.expression.equality import expressions_equal +from andromede.expression.expression import ( + CurrentScenarioIndex, + NoScenarioIndex, + NoTimeIndex, + ProblemParameterNode, + ProblemVariableNode, + TimeShift, + TimeStep, + comp_param, + comp_var, + problem_param, + problem_var, +) +from andromede.expression.indexing import IndexingStructureProvider +from andromede.expression.indexing_structure import IndexingStructure +from andromede.expression.operators_expansion import ProblemDimensions, expand_operators + +P = comp_param("c", "p") +X = comp_var("c", "x") +CONST = comp_var("c", "const") + + +def shifted_P(t: int = 0) -> ProblemParameterNode: + return problem_param("c", "p", TimeShift(t), CurrentScenarioIndex()) + + +def P_at(t: int = 0) -> ProblemParameterNode: + return problem_param("c", "p", TimeStep(t), CurrentScenarioIndex()) + + +def X_at(t: int = 0) -> ProblemVariableNode: + return problem_var("c", "x", TimeStep(t), CurrentScenarioIndex()) + + +def shifted_X(t: int = 0) -> ProblemVariableNode: + return problem_var("c", "x", TimeShift(t), CurrentScenarioIndex()) + + +def const() -> ProblemVariableNode: + return problem_var("c", "x", NoTimeIndex(), NoScenarioIndex()) + + +def evaluate_literal(node: ExpressionNode) -> int: + if isinstance(node, LiteralNode): + return int(node.value) + raise NotImplementedError("Can only evaluate literal nodes.") + + +@dataclass(frozen=True) +class AllTimeScenarioDependent(IndexingStructureProvider): + def get_parameter_structure(self, name: str) -> IndexingStructure: + return IndexingStructure(True, True) + + def get_variable_structure(self, name: str) -> IndexingStructure: + return IndexingStructure(True, True) + + def get_component_variable_structure( + self, component_id: str, name: str + ) -> IndexingStructure: + return IndexingStructure(True, True) + + def get_component_parameter_structure( + self, component_id: str, name: str + ) -> IndexingStructure: + return IndexingStructure(True, True) + + +@dataclass(frozen=True) +class StructureProviderDict(IndexingStructureProvider): + """ + Defines indexing structure through dictionaries. Default is time-scenario dependent. + """ + + variables: Dict[str, IndexingStructure] + parameters: Dict[str, IndexingStructure] + + def get_parameter_structure(self, name: str) -> IndexingStructure: + return self.parameters.get(name, IndexingStructure(True, True)) + + def get_variable_structure(self, name: str) -> IndexingStructure: + return self.variables.get(name, IndexingStructure(True, True)) + + def get_component_variable_structure( + self, component_id: str, name: str + ) -> IndexingStructure: + return self.variables.get(name, IndexingStructure(True, True)) + + def get_component_parameter_structure( + self, component_id: str, name: str + ) -> IndexingStructure: + return self.parameters.get(name, IndexingStructure(True, True)) + + +@pytest.mark.parametrize( + "expr,expected", + [ + (X.time_sum(), X_at(0) + X_at(1)), + (X.shift(-1), shifted_X(-1)), + (X.time_sum(-2, 0), shifted_X(-2) + (shifted_X(-1) + shifted_X(0))), + ((P * X).shift(-1), shifted_P(-1) * shifted_X(-1)), + (X.shift(-1).shift(+1), shifted_X(0)), + ( + P * (P * X).time_sum(0, 1), + shifted_P(0) * (shifted_P(0) * shifted_X(0) + shifted_P(1) * shifted_X(1)), + ), + (X.eval(2).time_sum(), X_at(2) + X_at(2)), + ], +) +def test_operators_expansion(expr: ExpressionNode, expected: ExpressionNode) -> None: + expanded = expand_operators( + expr, ProblemDimensions(2, 1), evaluate_literal, AllTimeScenarioDependent() + ) + assert expressions_equal(expanded, expected) + + +def test_time_scenario_independent_var_has_no_time_or_scenario_index(): + structure_provider = StructureProviderDict( + parameters={}, variables={"const": IndexingStructure(False, False)} + ) + expr = (X + CONST).time_sum() + expanded = expand_operators( + expr, ProblemDimensions(2, 1), evaluate_literal, structure_provider + ) + assert expanded == X_at(0) + const() + X_at(1) + const() diff --git a/tests/unittests/expressions/test_port_resolver.py b/tests/unittests/expressions/test_port_resolver.py index ca94e20a..afa5db4d 100644 --- a/tests/unittests/expressions/test_port_resolver.py +++ b/tests/unittests/expressions/test_port_resolver.py @@ -19,7 +19,7 @@ from andromede.model.port import PortFieldId -def test_port_field_resolution(): +def test_port_field_resolution() -> None: ports_expressions: Dict[PortFieldKey, List[ExpressionNode]] = {} key = PortFieldKey("com_id", PortFieldId(field_name="field", port_name="port")) @@ -34,7 +34,7 @@ def test_port_field_resolution(): ) -def test_port_field_resolution_sum(): +def test_port_field_resolution_sum() -> None: ports_expressions: Dict[PortFieldKey, List[ExpressionNode]] = {} key = PortFieldKey("com_id", PortFieldId(field_name="field", port_name="port")) diff --git a/tests/unittests/model/test_model_parsing.py b/tests/unittests/model/test_model_parsing.py index dc34a2fd..599abee9 100644 --- a/tests/unittests/model/test_model_parsing.py +++ b/tests/unittests/model/test_model_parsing.py @@ -9,7 +9,6 @@ # SPDX-License-Identifier: MPL-2.0 # # This file is part of the Antares project. -import io from pathlib import Path import pytest @@ -27,12 +26,12 @@ float_variable, model, ) +from andromede.model.model import PortFieldDefinition, PortFieldId from andromede.model.parsing import parse_yaml_library -from andromede.model.port import PortFieldDefinition, PortFieldId from andromede.model.resolve_library import resolve_library -def test_library_parsing(data_dir: Path): +def test_library_parsing(data_dir: Path) -> None: lib_file = data_dir / "lib.yml" with lib_file.open() as f: @@ -41,7 +40,7 @@ def test_library_parsing(data_dir: Path): assert len(input_lib.models) == 7 assert len(input_lib.port_types) == 1 - lib = resolve_library(input_lib) + lib = resolve_library([input_lib]) assert len(lib.models) == 7 assert len(lib.port_types) == 1 port_type = lib.port_types["flow"] @@ -66,7 +65,7 @@ def test_library_parsing(data_dir: Path): ) ], objective_operational_contribution=(param("cost") * var("generation")) - .sum() + .time_sum() .expec(), ) short_term_storage = lib.models["short-term-storage"] @@ -117,7 +116,7 @@ def test_library_parsing(data_dir: Path): ) -def test_library_error_parsing(data_dir: Path): +def test_library_error_parsing(data_dir: Path) -> None: lib_file = data_dir / "model_port_definition_ko.yml" with lib_file.open() as f: @@ -127,17 +126,17 @@ def test_library_error_parsing(data_dir: Path): AntaresParseException, match=r"An error occurred during parsing: ParseCancellationException", ): - resolve_library(input_lib) + resolve_library([input_lib]) -def test_library_port_model_ok_parsing(data_dir: Path): +def test_library_port_model_ok_parsing(data_dir: Path) -> None: lib_file = data_dir / "model_port_definition_ok.yml" with lib_file.open() as f: input_lib = parse_yaml_library(f) assert input_lib.id == "basic" - lib = resolve_library(input_lib) + lib = resolve_library([input_lib]) port_type = lib.port_types["flow"] assert port_type == PortType(id="flow", fields=[PortField(name="flow")]) short_term_storage = lib.models["short-term-storage-2"] diff --git a/tests/unittests/model/test_multiple_model_parsing.py b/tests/unittests/model/test_multiple_model_parsing.py new file mode 100644 index 00000000..533dadfa --- /dev/null +++ b/tests/unittests/model/test_multiple_model_parsing.py @@ -0,0 +1,161 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. +import re +from pathlib import Path + +import pytest + +from andromede.expression import literal, param, var +from andromede.expression.expression import port_field +from andromede.expression.parsing.parse_expression import AntaresParseException +from andromede.libs.standard import CONSTANT +from andromede.model import ( + Constraint, + ModelPort, + PortField, + PortType, + float_parameter, + float_variable, + model, +) +from andromede.model.model import PortFieldDefinition, PortFieldId +from andromede.model.parsing import parse_yaml_library +from andromede.model.resolve_library import resolve_library + + +@pytest.fixture(scope="session") +def lib_dir(data_dir: Path) -> Path: + return data_dir / "lib_for_resolving_test" + + +# in following tests "lib_A -> lib_B" means lib_A must be resolved before lib_B + + +def test_simple_dependency_tree(lib_dir: Path) -> None: + """basic_lib + | | + V V + demand production + """ + lib_files = [ + lib_dir / "basic_lib.yml", + lib_dir / "demand.yml", + lib_dir / "production.yml", + ] + + input_libs = [] + for lib_file in lib_files: + with lib_file.open() as f: + input_libs.append(parse_yaml_library(f)) + + lib = resolve_library(input_libs) + assert len(lib.models) == 3 + assert len(lib.port_types) == 1 + + # changing order in lib_files + lib_files = [ + lib_dir / "demand.yml", + lib_dir / "production.yml", + lib_dir / "basic_lib.yml", + ] + + input_libs = [] + for lib_file in lib_files: + with lib_file.open() as f: + input_libs.append(parse_yaml_library(f)) + + lib = resolve_library(input_libs) + assert len(lib.models) == 3 + assert len(lib.port_types) == 1 + + +def test_multiple_dependencies_tree(lib_dir: Path) -> None: + """basic_lib CO2_port + | | | + V V V + demand production_with_CO2 + """ + lib_files = [ + lib_dir / "basic_lib.yml", + lib_dir / "CO2_port.yml", + lib_dir / "demand.yml", + lib_dir / "production_with_CO2.yml", + ] + + input_libs = [] + for lib_file in lib_files: + with lib_file.open() as f: + input_libs.append(parse_yaml_library(f)) + + lib = resolve_library(input_libs) + assert len(lib.models) == 3 + assert len(lib.port_types) == 2 + + +def test_looping_dependency(lib_dir: Path) -> None: + """looping_lib_1 -> looping_lib_2 + <- + """ + lib_files = [ + lib_dir / "looping_lib_1.yml", + lib_dir / "looping_lib_2.yml", + ] + + input_libs = [] + for lib_file in lib_files: + with lib_file.open() as f: + input_libs.append(parse_yaml_library(f)) + + with pytest.raises(Exception, match=r"Circular import in yaml libraries"): + lib = resolve_library(input_libs) + + +def test_model_redefinition(lib_dir: Path) -> None: + """basic_lib CO2_port + | | | + V V V + production production_with_CO2 + """ + lib_files = [ + lib_dir / "basic_lib.yml", + lib_dir / "CO2_port.yml", + lib_dir / "production.yml", + lib_dir / "production_with_CO2.yml", + ] + + input_libs = [] + for lib_file in lib_files: + with lib_file.open() as f: + input_libs.append(parse_yaml_library(f)) + + with pytest.raises( + Exception, match=re.escape("Model(s) : {'generator'} is(are) defined twice") + ): + lib = resolve_library(input_libs) + + +def test_port_redefinition(lib_dir: Path) -> None: + """basic_lib -> port_redefinition""" + lib_files = [ + lib_dir / "basic_lib.yml", + lib_dir / "port_redefinition.yml", + ] + + input_libs = [] + for lib_file in lib_files: + with lib_file.open() as f: + input_libs.append(parse_yaml_library(f)) + + with pytest.raises( + Exception, match=re.escape("Port(s) : {'flow'} is(are) defined twice") + ): + lib = resolve_library(input_libs) diff --git a/tests/unittests/study/test_components_parsing.py b/tests/unittests/study/test_components_parsing.py index ddae9cbd..74832d10 100644 --- a/tests/unittests/study/test_components_parsing.py +++ b/tests/unittests/study/test_components_parsing.py @@ -36,11 +36,13 @@ def input_library( return parse_yaml_library(lib) -def test_parsing_components_ok(input_component, input_library): +def test_parsing_components_ok( + input_component: InputComponents, input_library: InputLibrary +) -> None: assert len(input_component.components) == 2 assert len(input_component.nodes) == 1 assert len(input_component.connections) == 2 - lib = resolve_library(input_library) + lib = resolve_library([input_library]) result = resolve_components_and_cnx(input_component, lib) assert len(result.components) == 2 @@ -48,14 +50,18 @@ def test_parsing_components_ok(input_component, input_library): assert len(result.connections) == 2 -def test_consistency_check_ok(input_component, input_library): - result_lib = resolve_library(input_library) +def test_consistency_check_ok( + input_component: InputComponents, input_library: InputLibrary +) -> None: + result_lib = resolve_library([input_library]) result_comp = resolve_components_and_cnx(input_component, result_lib) consistency_check(result_comp.components, result_lib.models) -def test_consistency_check_ko(input_component, input_library): - result_lib = resolve_library(input_library) +def test_consistency_check_ko( + input_component: InputComponents, input_library: InputLibrary +) -> None: + result_lib = resolve_library([input_library]) result_comp = resolve_components_and_cnx(input_component, result_lib) result_lib.models.pop("generator") with pytest.raises( @@ -65,8 +71,10 @@ def test_consistency_check_ko(input_component, input_library): consistency_check(result_comp.components, result_lib.models) -def test_basic_balance_using_yaml(input_component, input_library) -> None: - result_lib = resolve_library(input_library) +def test_basic_balance_using_yaml( + input_component: InputComponents, input_library: InputLibrary +) -> None: + result_lib = resolve_library([input_library]) components_input = resolve_components_and_cnx(input_component, result_lib) consistency_check(components_input.components, result_lib.models) @@ -87,7 +95,7 @@ def generate_data_for_short_term_storage_test(scenarios: int) -> TimeScenarioSer for scenario in range(scenarios): for absolute_timestep in range(10): if absolute_timestep == 0: - data[TimeScenarioIndex(absolute_timestep, scenario)] = -18 + data[TimeScenarioIndex(absolute_timestep, scenario)] = -18.0 else: data[TimeScenarioIndex(absolute_timestep, scenario)] = 2 * efficiency @@ -104,7 +112,7 @@ def test_short_term_storage_base_with_yaml(data_dir: Path) -> None: with compo_file.open() as c: components_file = parse_yaml_components(c) - library = resolve_library(input_library) + library = resolve_library([input_library]) components_input = resolve_components_and_cnx(components_file, library) # 18 produced in the 1st time-step, then consumed 2 * efficiency in the rest scenarios = 1 diff --git a/tests/unittests/test_data.py b/tests/unittests/test_data.py index f30c3029..026f3eb6 100644 --- a/tests/unittests/test_data.py +++ b/tests/unittests/test_data.py @@ -89,7 +89,7 @@ def mock_generator_with_fixed_scenario_time_varying_param() -> Model: ) ], objective_operational_contribution=(param("cost") * var("generation")) - .sum() + .time_sum() .expec(), ) return fixed_scenario_time_varying_param_generator @@ -117,7 +117,7 @@ def mock_generator_with_scenario_varying_fixed_time_param() -> Model: ) ], objective_operational_contribution=(param("cost") * var("generation")) - .sum() + .time_sum() .expec(), ) return scenario_varying_fixed_time_generator @@ -333,7 +333,7 @@ def test_requirements_consistency_scenario_varying_parameter_with_correct_data_p database.requirements_consistency(network) -def test_load_data_from_txt(data_dir: Path): +def test_load_data_from_txt(data_dir: Path) -> None: txt_file = "gen-costs" gen_costs = load_ts_from_txt(txt_file, data_dir) diff --git a/tests/unittests/test_model.py b/tests/unittests/test_model.py index 04d2bc6f..1ea6c182 100644 --- a/tests/unittests/test_model.py +++ b/tests/unittests/test_model.py @@ -14,7 +14,6 @@ from andromede.expression.expression import ( ExpressionNode, - ExpressionRange, comp_param, comp_var, literal, @@ -172,7 +171,7 @@ def test_writing_min_up_constraint_should_represent_all_expected_constraints() - _ = Constraint( "min_up_time", - off_on <= on.shift(ExpressionRange(literal(1), d_min_up)).sum(), + off_on <= on.time_sum(literal(1), d_min_up), ) # Later on, the goal is to assert that when this constraint is sent to the solver, it correctly builds: for all t, for all t' in [t+1, t+d_min_up], off_on[k,t,w] <= on[k,t',w] @@ -208,7 +207,7 @@ def test_invalid_port_field_definition_should_raise(expression: ExpressionNode) port_field_def(port_name="p", field_name="f", definition=expression) -def test_constraint_equals(): +def test_constraint_equals() -> None: # checks in particular that expressions are correctly compared assert Constraint(name="c", expression=var("x") <= param("p")) == Constraint( name="c", expression=var("x") <= param("p") diff --git a/tests/unittests/test_port.py b/tests/unittests/test_port.py index 2ff4547f..36bfa4a1 100644 --- a/tests/unittests/test_port.py +++ b/tests/unittests/test_port.py @@ -28,7 +28,7 @@ def test_port_type_compatibility_ko() -> None: constraints=[ Constraint( name="Balance", - expression=port_field("balance_port", "flow").sum() == literal(0), + expression=port_field("balance_port", "flow").time_sum() == literal(0), ) ], ) diff --git a/tests/unittests/test_scenario_builder.py b/tests/unittests/test_scenario_builder.py new file mode 100644 index 00000000..a793160c --- /dev/null +++ b/tests/unittests/test_scenario_builder.py @@ -0,0 +1,99 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + +from pathlib import Path + +import pandas as pd +import pytest + +from andromede.model.parsing import parse_yaml_library +from andromede.model.resolve_library import resolve_library +from andromede.simulation import TimeBlock, build_problem +from andromede.study import DataBase +from andromede.study.data import ComponentParameterIndex +from andromede.study.parsing import parse_scenario_builder, parse_yaml_components +from andromede.study.resolve_components import ( + build_network, + build_scenarized_data_base, + consistency_check, + resolve_components_and_cnx, +) + + +@pytest.fixture +def scenario_builder(data_dir: Path) -> pd.DataFrame: + buider_path = data_dir / "scenario_builder.csv" + return parse_scenario_builder(buider_path) + + +@pytest.fixture +def database(data_dir: Path, scenario_builder: pd.DataFrame) -> DataBase: + components_path = data_dir / "components_for_scenarization_test.yml" + ts_path = data_dir + with components_path.open() as components: + return build_scenarized_data_base( + parse_yaml_components(components), scenario_builder, ts_path + ) + + +def test_parser(scenario_builder: pd.DataFrame) -> None: + builder = pd.DataFrame( + { + "name": [ + "load", + "load", + "load", + "load", + "cost-group", + "cost-group", + "cost-group", + "cost-group", + ], + "year": [0, 1, 2, 3, 0, 1, 2, 3], + "scenario": [0, 1, 0, 1, 0, 0, 1, 1], + } + ) + + assert builder.equals(scenario_builder) + + +# cost-group group isnt use in following test because sum can't take time dependant parameters +def test_scenarized_data_base(database: DataBase) -> None: + load_index = ComponentParameterIndex("D", "demand") + assert database.get_value(load_index, 0, 0) == 50 + assert database.get_value(load_index, 0, 1) == 100 + assert database.get_value(load_index, 0, 2) == 50 + assert database.get_value(load_index, 0, 3) == 100 + + +def test_solving(data_dir: Path, database: DataBase) -> None: + library_path = data_dir / "lib.yml" + with library_path.open("r") as file: + yaml_lib = parse_yaml_library(file) + models = resolve_library([yaml_lib]) + + components_path = data_dir / "components_for_scenarization_test.yml" + with components_path.open("r") as file: + yaml_comp = parse_yaml_components(file) + components = resolve_components_and_cnx(yaml_comp, models) + + consistency_check(components.components, models.models) + network = build_network(components) + + timeblock = TimeBlock(1, list(range(2))) + problem = build_problem(network, database, timeblock, 3) + + status = problem.solver.Solve() + cost = problem.solver.Objective().Value() + + assert status == 0 + assert cost == pytest.approx(40000 / 3, abs=0.001) diff --git a/tests/unittests/test_utils.py b/tests/unittests/test_utils.py index f88477eb..44d4da5c 100644 --- a/tests/unittests/test_utils.py +++ b/tests/unittests/test_utils.py @@ -27,7 +27,7 @@ def test_get_or_add_should_evaluate_lazily() -> None: d = {"key1": "value1"} - def raise_factory() -> None: + def raise_factory() -> str: raise AssertionError("No value should be created") assert get_or_add(d, "key1", raise_factory) == "value1"