-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature/ant 958 #12
Feature/ant 958 #12
Conversation
class SolverVariableInfo: | ||
""" | ||
Helper class for constructing the structure file | ||
for Bender solver. It keeps track of the corresponding |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Benders (name of a mathematician)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
""" | ||
|
||
name: str | ||
column: int |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe column_id
or col_id
is more explicit ?
@@ -370,6 +390,10 @@ def register_component_variable( | |||
key = TimestepComponentVariableKey( | |||
component_id, variable_name, block_timestep, scenario | |||
) | |||
if key not in self._component_variables: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you use get_or_add
defined in utils.py
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not quite. It checks a first dictionary and, doing so, it checks implicitly the second adding a new key to the second
expression=instantiated_expression, | ||
) | ||
simulator = 0 | ||
xpansion_merged = 1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the difference between simulator
and xpansion_merged
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For now, none. I can remove it for better understanding, but the idea was to eventually differentiate both types of problem if needed
src/andromede/simulation/xpansion.py
Outdated
|
||
""" | ||
The xpansion module extends the optimization module | ||
with Bender solver related functions |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Benders
src/andromede/simulation/xpansion.py
Outdated
root_dir = os.getcwd() | ||
|
||
if not os.path.isfile(root_dir + "/bin/benders"): | ||
# TODO Maybe a more robust check and/or return value? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For now add an error message to explain the expected binary location
|
||
output = OutputValues(problem) | ||
expected_output = OutputValues() | ||
expected_output.component("G1").var("generation").value = 200.0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Check equality with float values with pytest.approx
(update everywhere, even in previous unchanged tests)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm working on it on another branch with other modifications to the OutputValues class. Stay tuned :)
tests/andromede/test_xpansion.py
Outdated
status = problem.solver.Solve() | ||
|
||
assert status == problem.solver.OPTIMAL | ||
assert problem.solver.Objective().Value() == (45 * 200) + (490 * 100 + 10 * 100) + ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Check equality with float values with pytest.approx
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
tests/andromede/test_xpansion.py
Outdated
) -> None: | ||
""" | ||
Same test as before but this time we separate master/subproblem and | ||
export the problems in MPS format to be solved by the Bender solver in Xpansion |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Benders
debb29a
to
3d6990c
Compare
src/andromede/simulation/xpansion.py
Outdated
@@ -155,6 +155,7 @@ def run( | |||
# TODO Maybe a more robust check and/or return value? | |||
# For now, it won't look anywhere else because a new | |||
# architecture should be discussed | |||
print(root_dir + "/bin/benders executable not found. Returning True") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the future you can use f-strings f"{root_dir}/bin/benders executable ..."
that is easier to use and more readable when you have more complex strings
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed a f-string would be nicer. Since It was a simple string, I didn't bother using one but you are right :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
instantiated_constraint, | ||
) | ||
for model_var in model.variables.values(): | ||
if not _should_keep_in_model(self.problem_type, model_var.context): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Much clearer but I still find it even clearer to do if _should_keep_in_model():
even if it makes one more indentation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🙃
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be clarer, from the design point of view, that the choice of "what should be included in the problem" is not hidden deep in the problem building code.
Instead, we could have a small interface that makes this choice, as an entry of build_problem
, like:
class ModelSelectionStrategy(ABC):
@abstractmethod
def get_variables(model: Model) -> List[Variable]:
...
@abstractmethod
def get_objective(model: Model) -> ExpressionNode:
...
This would have 2 implementations, one for investment, one for operation.
Then you won't need the problem_type
argument to build_problem, just this strategy:
master = build_problem(
network,
database,
block,
scenarios,
problem_name="master",
border_management=border_management,
solver_id=solver_id,
strategy=InvestmentProblemStrategy(),
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
tests/andromede/test_xpansion.py
Outdated
) | ||
return THERMAL_CANDIDATE | ||
|
||
|
||
@pytest.fixture | ||
def wind_cluster_candidate() -> Model: | ||
WIND_CLUSTER_CANDIDATE = model( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rename this candidate as DISCRETE_CANDIDATE
which is more explicit regarding its behaviour and also because usually wind is considered a continuous candidate (whereas gas plant are not for instance)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few technical comment + proposition to isolate the particularities of operational and investment problems outside from the build_problem
logic, by moving it to a dedicated interface.
This would isolate this logic better.
src/andromede/model/common.py
Outdated
|
||
|
||
class ProblemContext(Enum): | ||
operational = 0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Upper case for consistency sake with other parts of the code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
|
||
|
||
def int_variable( | ||
name: str, | ||
lower_bound: Optional[ExpressionNode] = None, | ||
upper_bound: Optional[ExpressionNode] = None, | ||
structural_type: Optional[IndexingStructure] = None, | ||
structure: IndexingStructure = IndexingStructure(True, True), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that it's a little dangerous to use such objects as default in python, because this object will be shared by all calls:
it means that if it's modified at some point, it will be modified for all calls.
However here it's OK because IndexingStructure is immutable (frozen).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I hadn't take it into account, thanks for the heads up!
In this case then you are ok with leaving it like this?
if model_var.lower_bound: | ||
instantiated_lb_expr = _instantiate_model_expression( | ||
model_var.lower_bound, component.id, opt_context | ||
merged = 0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
upper case for consistency
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually this enum could be removed by using an interface for selecting what to include in the problem, see other comment.
It will make this part of the code agnostic from xpansion, which will be clearer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure to understand what you meant
master = 1 | ||
subproblem = 2 | ||
|
||
name: str |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we don't use dataclass
annotation, those fields define class attributes, not instance attributes --> they should be removed since they are actually not used
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I created a small snippet to check and it seems to work as I intended
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed thanks for sharing the knowledge :)
|
||
|
||
def build_problem( | ||
network: Network, | ||
database: DataBase, | ||
block: TimeBlock, | ||
scenarios: int, | ||
*, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't know this syntax!
It forces to use following arguments as keyword arguments (as opposed to positional arguments).
https://peps.python.org/pep-3102/#specification
src/andromede/utils.py
Outdated
@@ -41,3 +42,20 @@ def get_or_add(dictionary: Dict[K, V], key: K, default_factory: Supplier[V]) -> | |||
value = default_factory() | |||
dictionary[key] = value | |||
return value | |||
|
|||
|
|||
def serialize(filename: str, message: str, path: str = "outputs") -> bool: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we can use pathlib.Path
for paths, for example it allows to use /
operator to have OS-agnostic construction of paths:
file = open(path / filename, "w")
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree, but since it was just for the PoC, I didn't think we needed it, but I can change it :)
src/andromede/utils.py
Outdated
os.makedirs(path, exist_ok=True) | ||
file = open(f"{path}/{filename}", "w") | ||
|
||
except os.error: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not let the exception pass through ? Do we really want to ignore this error sometimes?
Most of the time it will be better to get an exception instead of ignoring this, otherwise we can get unexpected behaviour afterwards (like xpansion not having one of its input files).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Initially the idea was to catch the return False, but I am not using it this way anyway. I agree that an exception would be better now
instantiated_constraint, | ||
) | ||
for model_var in model.variables.values(): | ||
if not _should_keep_in_model(self.problem_type, model_var.context): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be clarer, from the design point of view, that the choice of "what should be included in the problem" is not hidden deep in the problem building code.
Instead, we could have a small interface that makes this choice, as an entry of build_problem
, like:
class ModelSelectionStrategy(ABC):
@abstractmethod
def get_variables(model: Model) -> List[Variable]:
...
@abstractmethod
def get_objective(model: Model) -> ExpressionNode:
...
This would have 2 implementations, one for investment, one for operation.
Then you won't need the problem_type
argument to build_problem, just this strategy:
master = build_problem(
network,
database,
block,
scenarios,
problem_name="master",
border_management=border_management,
solver_id=solver_id,
strategy=InvestmentProblemStrategy(),
)
scenarios = 1 | ||
|
||
xpansion = build_xpansion_problem(network, database, TimeBlock(1, [0]), scenarios) | ||
assert xpansion.run() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I understand correctly, it's a little hacky that this passes the CI :)
It would be great to adapt the CI workflow to download the asset from xpansionn, so that it actually runs:
https://github.com/AntaresSimulatorTeam/antares-xpansion/releases/tag/v1.2.1
For example that's what xpansion does for its CI, it downloads antares-solver with a small script step.
We could do that in another PR if you prefer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You understood it perfectly 😅
I prefer doing it in another PR indeed.
When we create such PR, we could allow it to throw an error if the Benders solvers is not found (then again the question will arise for the other developers that doesn't have it installed locally )
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed, maybe we'll need to separate integration tests and unit tests, so that developers don't have to run this one.
src/andromede/simulation/xpansion.py
Outdated
from andromede.utils import serialize | ||
|
||
|
||
class XpansionProblem: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about naming it InvestmentProblem
, to make reference to the actual feature instead of the existing tool ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree that we could rename it to something not tool related, but since we can solve a investment problem doing a frontal LP, I don't think this name would be very clear.
The main idea here is to separate both master and sub-problem to use the Benders solver. In that case, BendersProblem would be better, no ?
What do you think @tbittar ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes exactly I suggest BendersProblem
or DecomposedProblem
or BendersDecomposedProblem
as we do not want to have any operational knowledge at problem / solver level
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The idea here is really that we provide a decomposed form of the big initial problem (and more specially a decomposition suited for a resolution with a Benders algorithm)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done. I went for BendersDecomposedProblem
name: str | ||
solver: lp.Solver | ||
context: OptimizationContext | ||
problem_type: Type | ||
strategy: Type[ModelSelectionStrategy] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ModelSelectionStrategy
is not enough for typing ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately no, because here we are not dealing with instances of the class but the class itself so ModelSelectionStrategy
wouldn't pass for MyPy
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay !
|
||
def __init__( | ||
self, | ||
name: str, | ||
solver: lp.Solver, | ||
opt_context: OptimizationContext, | ||
opt_type: Type = Type.merged, | ||
opt_strategy: Type[ModelSelectionStrategy] = MergedProblemStrategy, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
build_strategy
maybe ?
|
||
except os.error: | ||
return False | ||
Will throw an exception if it fails to create dir or ro open file |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ro
is a typo ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it is :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, 2 more minor design comments that I'd prefer handled, but not a must-have.
A first working dev for the integration of Benders to the PoC