Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/gems/expression/copy.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
AllTimeSumNode,
CeilNode,
ComparisonNode,
DualNode,
ExpressionNode,
FloorNode,
LiteralNode,
Expand All @@ -26,6 +27,7 @@
ParameterNode,
PortFieldAggregatorNode,
PortFieldNode,
ReducedCostNode,
ScenarioOperatorNode,
TimeEvalNode,
TimeShiftNode,
Expand Down Expand Up @@ -95,6 +97,12 @@ def maximum(self, node: MaxNode) -> ExpressionNode:
def minimum(self, node: MinNode) -> ExpressionNode:
return MinNode([visit(op, self) for op in node.operands])

def dual(self, node: DualNode) -> ExpressionNode:
return DualNode(node.constraint_id)

def reduced_cost(self, node: ReducedCostNode) -> ExpressionNode:
return ReducedCostNode(node.variable_id)


def copy_expression(expression: ExpressionNode) -> ExpressionNode:
return visit(expression, CopyVisitor())
25 changes: 24 additions & 1 deletion src/gems/expression/degree.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,24 @@

import gems.expression.scenario_operator
from gems.expression.expression import (
AdditionNode,
AllTimeSumNode,
BinaryOperatorNode,
CeilNode,
DualNode,
FloorNode,
MaxNode,
MinNode,
PortFieldAggregatorNode,
PortFieldNode,
ReducedCostNode,
TimeEvalNode,
TimeShiftNode,
TimeSumNode,
UnaryOperatorNode,
)

from .expression import (
AdditionNode,
ComparisonNode,
DivisionNode,
ExpressionNode,
Expand Down Expand Up @@ -112,6 +116,12 @@ def maximum(self, node: MaxNode) -> int | float:
def minimum(self, node: MinNode) -> int | float:
return 0 if all(visit(op, self) == 0 for op in node.operands) else math.inf

def dual(self, node: DualNode) -> int | float:
return math.inf

def reduced_cost(self, node: ReducedCostNode) -> int | float:
return math.inf


def compute_degree(expression: ExpressionNode) -> int | float:
return visit(expression, ExpressionDegreeVisitor())
Expand All @@ -129,3 +139,16 @@ def is_linear(expr: ExpressionNode) -> bool:
True if the expression is linear with respect to variables.
"""
return compute_degree(expr) <= 1


def contains_dual_or_reduced_cost(expr: ExpressionNode) -> bool:
"""Return True if expr contains any DualNode or ReducedCostNode."""
if isinstance(expr, (DualNode, ReducedCostNode)):
return True
if isinstance(expr, (AdditionNode, MaxNode, MinNode)):
return any(contains_dual_or_reduced_cost(o) for o in expr.operands)
if isinstance(expr, BinaryOperatorNode):
return contains_dual_or_reduced_cost(expr.left) or contains_dual_or_reduced_cost(expr.right)
if isinstance(expr, UnaryOperatorNode):
return contains_dual_or_reduced_cost(expr.operand)
return False
12 changes: 12 additions & 0 deletions src/gems/expression/equality.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,13 @@
AllTimeSumNode,
BinaryOperatorNode,
CeilNode,
DualNode,
FloorNode,
MaxNode,
MinNode,
PortFieldAggregatorNode,
PortFieldNode,
ReducedCostNode,
ScenarioOperatorNode,
TimeEvalNode,
TimeShiftNode,
Expand Down Expand Up @@ -103,6 +105,10 @@ def visit(self, left: ExpressionNode, right: ExpressionNode) -> bool:
return self.maximum(left, right)
if isinstance(left, MinNode) and isinstance(right, MinNode):
return self.minimum(left, right)
if isinstance(left, DualNode) and isinstance(right, DualNode):
return self.dual(left, right)
if isinstance(left, ReducedCostNode) and isinstance(right, ReducedCostNode):
return self.reduced_cost(left, right)
raise NotImplementedError(f"Equality not implemented for {left.__class__}")

def literal(self, left: LiteralNode, right: LiteralNode) -> bool:
Expand Down Expand Up @@ -193,6 +199,12 @@ def minimum(self, left: MinNode, right: MinNode) -> bool:
self.visit(l, r) for l, r in zip(left.operands, right.operands)
)

def dual(self, left: DualNode, right: DualNode) -> bool:
return left.constraint_id == right.constraint_id

def reduced_cost(self, left: ReducedCostNode, right: ReducedCostNode) -> bool:
return left.variable_id == right.variable_id


def expressions_equal(
left: ExpressionNode, right: ExpressionNode, abs_tol: float = 0, rel_tol: float = 0
Expand Down
8 changes: 8 additions & 0 deletions src/gems/expression/evaluate.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@
from gems.expression.expression import (
AllTimeSumNode,
CeilNode,
DualNode,
FloorNode,
MaxNode,
MinNode,
PortFieldAggregatorNode,
PortFieldNode,
ReducedCostNode,
TimeEvalNode,
TimeShiftNode,
TimeSumNode,
Expand Down Expand Up @@ -124,6 +126,12 @@ def maximum(self, node: MaxNode) -> float:
def minimum(self, node: MinNode) -> float:
return min(visit(op, self) for op in node.operands)

def dual(self, node: DualNode) -> float:
raise NotImplementedError("dual() is not statically evaluable.")

def reduced_cost(self, node: ReducedCostNode) -> float:
raise NotImplementedError("reduced_cost() is not statically evaluable.")


def evaluate(expression: ExpressionNode, value_provider: ValueProvider) -> float:
return visit(expression, EvaluationVisitor(value_provider))
Expand Down
10 changes: 10 additions & 0 deletions src/gems/expression/expression.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,16 @@ def __post_init__(self) -> None:
)


@dataclass(frozen=True, eq=False)
class DualNode(ExpressionNode):
constraint_id: str


@dataclass(frozen=True, eq=False)
class ReducedCostNode(ExpressionNode):
variable_id: str


def sum_expressions(expressions: Sequence[ExpressionNode]) -> ExpressionNode:
if len(expressions) == 0:
return LiteralNode(0)
Expand Down
11 changes: 11 additions & 0 deletions src/gems/expression/indexing.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
CeilNode,
ComparisonNode,
DivisionNode,
DualNode,
ExpressionNode,
FloorNode,
LiteralNode,
Expand All @@ -32,6 +33,7 @@
ParameterNode,
PortFieldAggregatorNode,
PortFieldNode,
ReducedCostNode,
ScenarioOperatorNode,
TimeEvalNode,
TimeShiftNode,
Expand All @@ -48,6 +50,9 @@ def get_parameter_structure(self, name: str) -> IndexingStructure: ...
@abstractmethod
def get_variable_structure(self, name: str) -> IndexingStructure: ...

@abstractmethod
def get_constraint_structure(self, name: str) -> IndexingStructure: ...


@dataclass(frozen=True)
class TimeScenarioIndexingVisitor(ExpressionVisitor[IndexingStructure]):
Expand Down Expand Up @@ -137,6 +142,12 @@ def maximum(self, node: MaxNode) -> IndexingStructure:
def minimum(self, node: MinNode) -> IndexingStructure:
return self._combine(node.operands)

def dual(self, node: DualNode) -> IndexingStructure:
return self.context.get_constraint_structure(node.constraint_id)

def reduced_cost(self, node: ReducedCostNode) -> IndexingStructure:
return self.context.get_variable_structure(node.variable_id)


def compute_indexation(
expression: ExpressionNode, provider: IndexingStructureProvider
Expand Down
32 changes: 29 additions & 3 deletions src/gems/expression/parsing/parse_expression.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
# SPDX-License-Identifier: MPL-2.0
#
# This file is part of the Antares project.
from dataclasses import dataclass
from dataclasses import dataclass, field
from typing import Set

from antlr4 import CommonTokenStream, InputStream
Expand All @@ -20,8 +20,10 @@
from gems.expression.expression import (
Comparator,
ComparisonNode,
DualNode,
PortFieldAggregatorNode,
PortFieldNode,
ReducedCostNode,
maximum,
minimum,
)
Expand All @@ -33,11 +35,12 @@
@dataclass(frozen=True)
class ModelIdentifiers:
"""
Allows to distinguish between parameters and variables.
Allows to distinguish between parameters, variables, and constraints.
"""

variables: Set[str]
parameters: Set[str]
constraints: Set[str] = field(default_factory=set)

def is_variable(self, identifier: str) -> bool:
return identifier in self.variables
Expand Down Expand Up @@ -175,12 +178,35 @@ def visitAllTimeSum(self, ctx: ExprParser.AllTimeSumContext) -> ExpressionNode:
shifted_expr = ctx.expr().accept(self) # type: ignore
return shifted_expr.time_sum()

def _visit_dual(self, arg_exprs: list) -> ExpressionNode:
if len(arg_exprs) != 1:
raise ValueError("dual() requires exactly 1 argument.")
cid = arg_exprs[0].getText() # type: ignore
if cid not in self.identifiers.constraints:
raise ValueError(f"'{cid}' is not a constraint of the model.")
return DualNode(cid)

def _visit_reduced_cost(self, arg_exprs: list) -> ExpressionNode:
if len(arg_exprs) != 1:
raise ValueError("reduced_cost() requires exactly 1 argument.")
vid = arg_exprs[0].getText() # type: ignore
if vid not in self.identifiers.variables:
raise ValueError(f"'{vid}' is not a variable of the model.")
return ReducedCostNode(vid)

# Visit a parse tree produced by ExprParser#function.
def visitFunction(self, ctx: ExprParser.FunctionContext) -> ExpressionNode:
function_name: str = ctx.IDENTIFIER().getText() # type: ignore
arg_list = ctx.argList() # type: ignore
arg_exprs = arg_list.expr() if arg_list is not None else [] # type: ignore

if function_name == "dual":
return self._visit_dual(arg_exprs)
if function_name == "reduced_cost":
return self._visit_reduced_cost(arg_exprs)

args: list[ExpressionNode] = (
[expr.accept(self) for expr in arg_list.expr()] # type: ignore
[expr.accept(self) for expr in arg_exprs] # type: ignore
if arg_list is not None
else []
)
Expand Down
8 changes: 8 additions & 0 deletions src/gems/expression/print.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@
from gems.expression.expression import (
AllTimeSumNode,
CeilNode,
DualNode,
ExpressionNode,
FloorNode,
MaxNode,
MinNode,
PortFieldAggregatorNode,
PortFieldNode,
ReducedCostNode,
TimeEvalNode,
TimeShiftNode,
TimeSumNode,
Expand Down Expand Up @@ -128,6 +130,12 @@ def maximum(self, node: MaxNode) -> str:
def minimum(self, node: MinNode) -> str:
return "min(" + ", ".join(visit(op, self) for op in node.operands) + ")"

def dual(self, node: DualNode) -> str:
return f"dual({node.constraint_id})"

def reduced_cost(self, node: ReducedCostNode) -> str:
return f"reduced_cost({node.variable_id})"


def print_expr(expression: ExpressionNode) -> str:
return visit(expression, PrinterVisitor())
12 changes: 12 additions & 0 deletions src/gems/expression/visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
CeilNode,
ComparisonNode,
DivisionNode,
DualNode,
ExpressionNode,
FloorNode,
LiteralNode,
Expand All @@ -34,6 +35,7 @@
ParameterNode,
PortFieldAggregatorNode,
PortFieldNode,
ReducedCostNode,
ScenarioOperatorNode,
TimeEvalNode,
TimeShiftNode,
Expand Down Expand Up @@ -110,6 +112,12 @@ def maximum(self, node: MaxNode) -> T: ...
@abstractmethod
def minimum(self, node: MinNode) -> T: ...

@abstractmethod
def dual(self, node: DualNode) -> T: ...

@abstractmethod
def reduced_cost(self, node: ReducedCostNode) -> T: ...


def visit(root: ExpressionNode, visitor: ExpressionVisitor[T]) -> T:
"""
Expand Down Expand Up @@ -153,6 +161,10 @@ def visit(root: ExpressionNode, visitor: ExpressionVisitor[T]) -> T:
return visitor.maximum(root)
elif isinstance(root, MinNode):
return visitor.minimum(root)
elif isinstance(root, DualNode):
return visitor.dual(root)
elif isinstance(root, ReducedCostNode):
return visitor.reduced_cost(root)
raise ValueError(f"Unknown expression node type {root.__class__}")


Expand Down
Loading
Loading