Skip to content

Commit

Permalink
Added and tested support for scenario-based problems.
Browse files Browse the repository at this point in the history
Objectives, constraints, extra functions, and scalarization functions
now have an optional "scenario_keys" field, which indicated the
scenario(s) they belong to. The Problem model has also a new method
"scenario" that when called with a scenario key, will return a new
problem with just that scenario's functions. The Problem has also the
field "scenario_keys" which lists all the defined scenarios. Implemented
also a simple scenario-based test problem and tested the new
functionalities.
  • Loading branch information
gialmisi committed Apr 23, 2024
1 parent c1044a1 commit 1b5a300
Show file tree
Hide file tree
Showing 4 changed files with 361 additions and 4 deletions.
2 changes: 2 additions & 0 deletions desdeo/problem/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"simple_data_problem",
"simple_knapsack",
"simple_linear_test_problem",
"simple_scenario_test_problem",
"simple_test_problem",
"ScalarizationFunction",
"Variable",
Expand Down Expand Up @@ -71,6 +72,7 @@
simple_data_problem,
simple_knapsack,
simple_linear_test_problem,
simple_scenario_test_problem,
simple_test_problem,
zdt1,
)
Expand Down
133 changes: 131 additions & 2 deletions desdeo/problem/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,34 @@ def parse_infix_to_func(cls, v: str | list) -> list:
return v

# Raise an error if v is neither a string nor a list
msg = f"func must be a string (infix expression) or a list, got {type(v)}"
msg = f"The function expressions must be a string (infix expression) or a list. Got {type(v)}."
raise ValueError(msg)


def parse_scenario_key_singleton_to_list(cls, v: str | list[str]) -> list[str]:
"""Validator that checks the type of a scenario key.
If the type is a list, it will be returned as it is. If it is a string,
then a list with the single string is returned. Else, a ValueError is raised.
Args:
cls: the class fo the pydantic model the validtor is applied to.
v (str | list[str]): the scenario key, or keys, to be validted.
Raises:
ValueError: raised when `v` it neither a string or a list.
Returns:
list[str]: a list with scenario keys.
"""
if v is None:
return v
if isinstance(v, str):
return [v]
if isinstance(v, list):
return v

msg = f"The scenario keys must be either a list of strings, or a single string. Got {type(v)}."
raise ValueError(msg)


Expand Down Expand Up @@ -202,8 +229,15 @@ class ExtraFunction(BaseModel):
description="Whether the function expression is twice differentiable or not. Defaults to `False`", default=False
)
"""Whether the function expression is twice differentiable or not. Defaults to `False`"""
scenario_keys: list[str] | None = Field(
description="Optional. The keys of the scenario the extra functions belongs to.", default=None
)
"""Optional. The keys of the scenarios the extra functions belongs to."""

_parse_infix_to_func = field_validator("func", mode="before")(parse_infix_to_func)
_parse_scenario_key_singleton_to_list = field_validator("scenario_keys", mode="before")(
parse_scenario_key_singleton_to_list
)


class ScalarizationFunction(BaseModel):
Expand Down Expand Up @@ -249,8 +283,15 @@ class ScalarizationFunction(BaseModel):
frozen=True,
)
"""Whether the function expression is twice differentiable or not. Defaults to `False`"""
scenario_keys: list[str] = Field(
description="Optional. The keys of the scenarios the scalarization function belongs to.", default=False
)
"""Optional. The keys of the scenarios the scalarization function belongs to."""

_parse_infix_to_func = field_validator("func", mode="before")(parse_infix_to_func)
_parse_scenario_key_singleton_to_list = field_validator("scenario_keys", mode="before")(
parse_scenario_key_singleton_to_list
)


class Objective(BaseModel):
Expand Down Expand Up @@ -328,8 +369,15 @@ class Objective(BaseModel):
description="Whether the function expression is twice differentiable or not. Defaults to `False`", default=False
)
"""Whether the function expression is twice differentiable or not. Defaults to `False`"""
scenario_keys: list[str] | None = Field(
description="Optional. The keys of the scenarios the objective function belongs to.", default=None
)
"""Optional. The keys of the scenarios the objective function belongs to."""

_parse_infix_to_func = field_validator("func", mode="before")(parse_infix_to_func)
_parse_scenario_key_singleton_to_list = field_validator("scenario_keys", mode="before")(
parse_scenario_key_singleton_to_list
)


class Constraint(BaseModel):
Expand Down Expand Up @@ -392,8 +440,15 @@ class Constraint(BaseModel):
description="Whether the function expression is twice differentiable or not. Defaults to `False`", default=False
)
"""Whether the function expression is twice differentiable or not. Defaults to `False`"""
scenario_keys: list[str] | None = Field(
description="Optional. The keys of the scenarios the constraint belongs to.", default=None
)
"""Optional. The keys of the scenarios the constraint belongs to."""

_parse_infix_to_func = field_validator("func", mode="before")(parse_infix_to_func)
_parse_scenario_key_singleton_to_list = field_validator("scenario_keys", mode="before")(
parse_scenario_key_singleton_to_list
)


class DiscreteRepresentation(BaseModel):
Expand Down Expand Up @@ -815,6 +870,68 @@ def is_twice_differentiable(self) -> bool:

return all(is_diff_values)

def scenario(self, scenario_key: str) -> "Problem":
"""Returns a new Problem with fields belonging to a specified scenario.
The new problem will have the fields `objectives`, `constraints`, `extra_funcs`,
and `scalarization_funcs` with only the entries that belong to the specified
scenario. The other entries will remain unchanged.
Note:
Fields with their `scenario_key` being `None` are assumed to belong to all scenarios,
and are thus always included in each scenario.
Args:
scenario_key (str): the key of the scenario we wish to get.
Raises:
ValueError: the given `scenario_key` has not been defined to be a scenario.
Returns:
Problem: a new problem with only the field that belong to the specified scenario.
"""
if self.scenario_keys is None or scenario_key not in self.scenario_keys:
# invalid scenario
msg = (
f"The scenario '{scenario_key} has not been defined to be a valid scenario, or the problem has no "
"scenarios defined."
)
raise ValueError(msg)

# add the fields if the field has the given scenario_key in its scenario_keys, or if the
# scenario_keys is None
scenario_objectives = [
obj for obj in self.objectives if obj.scenario_keys is None or scenario_key in obj.scenario_keys
]
scenario_constraints = (
[cons for cons in self.constraints if cons.scenario_keys is None or scenario_key in cons.scenario_keys]
if self.constraints is not None
else None
)
scenario_extras = (
[extra for extra in self.extra_funcs if extra.scenario_keys is None or scenario_key in extra.scenario_keys]
if self.extra_funcs is not None
else None
)
scenario_scals = (
[
scal
for scal in self.scalarization_funcs
if scal.scenario_keys is None or scenario_key in scal.scenario_keys
]
if self.scalarization_funcs is not None
else None
)

return self.model_copy(
update={
"objectives": scenario_objectives,
"constraints": scenario_constraints,
"extra_funcs": scenario_extras,
"scalarization_funcs": scenario_scals,
}
)

name: str = Field(
description="Name of the problem.",
)
Expand Down Expand Up @@ -857,11 +974,23 @@ def is_twice_differentiable(self) -> bool:
),
default=None,
)
""" Optional. Required when there are one or more 'data_based' Objectives.
"""Optional. Required when there are one or more 'data_based' Objectives.
The corresponding values of the 'data_based' objective function will be
fetched from this with the given variable values. Is also utilized for
methods which require both an analytical and discrete representation of a
problem. Defaults to `None`."""
scenario_keys: list[str] | None = Field(
description=(
"Optional. The scenario keys defined for the problem. Each key will point to a subset of objectives, "
"constraints, extra functions, and scalarization functions that have the same scenario key defined to them."
"If None, then the problem is assumed to not contain scenarios."
),
default=None,
)
"""Optional. The scenario keys defined for the problem. Each key will point
to a subset of objectives, " "constraints, extra functions, and
scalarization functions that have the same scenario key defined to them."
"If None, then the problem is assumed to not contain scenarios."""


if __name__ == "__main__":
Expand Down
165 changes: 164 additions & 1 deletion desdeo/problem/testproblems.py
Original file line number Diff line number Diff line change
Expand Up @@ -851,6 +851,169 @@ def simple_knapsack() -> Problem:
)


def simple_scenario_test_problem():
"""Returns a simple, scenario-based multiobjective optimization test problem."""
constants = [Constant(name="c_1", symbol="c_1", value=3)]
variables = [
Variable(
name="x_1",
symbol="x_1",
lowerbound=-5.1,
upperbound=6.2,
initial_value=0,
variable_type=VariableTypeEnum.real,
),
Variable(
name="x_2",
symbol="x_2",
lowerbound=-5.2,
upperbound=6.1,
initial_value=0,
variable_type=VariableTypeEnum.real,
),
]

constraints = [
Constraint(
name="con_1",
symbol="con_1",
cons_type=ConstraintTypeEnum.LTE,
func="x_1 + x_2 - 15",
is_linear=True,
is_convex=True,
is_twice_differentiable=True,
scenario_keys="s_1",
),
Constraint(
name="con_2",
symbol="con_2",
cons_type=ConstraintTypeEnum.LTE,
func="x_1 + x_2 - 65",
is_linear=True,
is_convex=True,
is_twice_differentiable=True,
scenario_keys="s_2",
),
Constraint(
name="con_3",
symbol="con_3",
cons_type=ConstraintTypeEnum.LTE,
func="x_2 - 50",
is_linear=True,
is_convex=True,
is_twice_differentiable=True,
scenario_keys=None,
),
Constraint(
name="con_4",
symbol="con_4",
cons_type=ConstraintTypeEnum.LTE,
func="x_1 - 5",
is_linear=True,
is_convex=True,
is_twice_differentiable=True,
scenario_keys=["s_1", "s_2"],
),
]

expr_1 = "x_1 + x_2"
expr_2 = "x_1 - x_2"
expr_3 = "(x_1 - 3)**2 + x_2"
expr_4 = "c_1 + x_2**2 - x_1"
expr_5 = "-x_1 - x_2"

objectives = [
Objective(
name="f_1",
symbol="f_1",
func=expr_1,
maximize=False,
ideal=-100,
nadir=100,
objective_type=ObjectiveTypeEnum.analytical,
is_linear=True,
is_convex=True,
is_twice_differentiable=True,
scenario_keys="s_1",
),
Objective(
name="f_2",
symbol="f_2",
func=expr_2,
maximize=False,
ideal=-100,
nadir=100,
objective_type=ObjectiveTypeEnum.analytical,
is_linear=True,
is_convex=True,
is_twice_differentiable=True,
scenario_keys=["s_1", "s_2"],
),
Objective(
name="f_3",
symbol="f_3",
func=expr_3,
maximize=False,
ideal=-100,
nadir=100,
objective_type=ObjectiveTypeEnum.analytical,
is_linear=True,
is_convex=True,
is_twice_differentiable=True,
scenario_keys=None,
),
Objective(
name="f_4",
symbol="f_4",
func=expr_4,
maximize=False,
ideal=-100,
nadir=100,
objective_type=ObjectiveTypeEnum.analytical,
is_linear=True,
is_convex=True,
is_twice_differentiable=True,
scenario_keys="s_2",
),
Objective(
name="f_5",
symbol="f_5",
func=expr_5,
maximize=False,
ideal=-100,
nadir=100,
objective_type=ObjectiveTypeEnum.analytical,
is_linear=True,
is_convex=True,
is_twice_differentiable=True,
scenario_keys="s_2",
),
]

extra_funcs = [
ExtraFunction(
name="extra_1",
symbol="extra_1",
func="5*x_1",
is_linear=True,
is_convex=True,
is_twice_differentiable=True,
scenario_keys="s_2",
)
]

return Problem(
name="Simple scenario test problem",
description="For testing the implementation of scenario-based problems.",
variables=variables,
constants=constants,
constraints=constraints,
objectives=objectives,
extra_funcs=extra_funcs,
scenario_keys=["s_1", "s_2"],
)


if __name__ == "__main__":
problem = simple_knapsack()
problem = simple_scenario_test_problem()
print(problem.model_dump_json(indent=2))

0 comments on commit 1b5a300

Please sign in to comment.