From 588698760787921ee5ab5fde3e814b1e62b9102e Mon Sep 17 00:00:00 2001 From: Tobias Becher Date: Thu, 20 Feb 2025 09:31:36 +0100 Subject: [PATCH 01/10] upload templates --- .../optimization/new_templates/objectives.py | 21 ++++++++ .../new_templates/planning_problem_sketch.py | 50 +++++++++++++++++++ .../new_templates/scalarization_method.py | 41 +++++++++++++++ .../tradeoff_exploration_method.py | 8 +++ 4 files changed, 120 insertions(+) create mode 100644 pyRadPlan/optimization/new_templates/objectives.py create mode 100644 pyRadPlan/optimization/new_templates/planning_problem_sketch.py create mode 100644 pyRadPlan/optimization/new_templates/scalarization_method.py create mode 100644 pyRadPlan/optimization/new_templates/tradeoff_exploration_method.py diff --git a/pyRadPlan/optimization/new_templates/objectives.py b/pyRadPlan/optimization/new_templates/objectives.py new file mode 100644 index 0000000..09b3579 --- /dev/null +++ b/pyRadPlan/optimization/new_templates/objectives.py @@ -0,0 +1,21 @@ +import numpy as np + + +class abstract_objective(): + def __init__(self): + pass + + def evaluate(x: np.ndarray[float]) -> float: + pass + + def evaluate_gradient(x: np.ndarray[float]) -> np.ndarray[float]: + pass + + def evaluate_hessian(x: np.ndarray[float]) -> np.ndarray[float]: + pass + + def is_linear() -> bool: + pass + + def is_convex() -> bool: + pass diff --git a/pyRadPlan/optimization/new_templates/planning_problem_sketch.py b/pyRadPlan/optimization/new_templates/planning_problem_sketch.py new file mode 100644 index 0000000..3996e99 --- /dev/null +++ b/pyRadPlan/optimization/new_templates/planning_problem_sketch.py @@ -0,0 +1,50 @@ +import numpy as np +from enum import Enum + +class PlanningProblem(): + def __init__(self, + scalarization_method: str | Enum, + tradeoff_exploration_method: str | Enum | None, + method_parameters: dict): + pass + + def evaluate_objectives(x: np.ndarray[float]) -> list[np.ndarray[float]]: + pass + + def evaluate_objective_gradients(x: np.ndarray[float]) -> list[np.ndarray[float]]: + # returns list of 2d arrays + pass + + def evaluate_objective_hessian(x: np.ndarray[float]) -> list[np.ndarray[float]]: + # returns list of 3d arrays + pass + + def evaluate_constraints(x: np.ndarray[float]) -> list[np.ndarray[float]]: + pass + + def evaluate_constraints_gradients(x: np.ndarray[float]) -> list[np.ndarray[float]]: + # returns list of 2d arrays + pass + + def evaluate_constraints_hessian(x: np.ndarray[float]) -> list[np.ndarray[float]]: + # returns list of 3d arrays + pass + + def are_objectives_convex() -> bool: + pass + + def are_objectives_linear() -> bool: + pass + + def solve(y: np.ndarray[float]) -> list[np.ndarray[float]]: + pass + + def make_tradeoff_exploration_method_instance( + evaluate_objectives: callable[np.ndarray[float],list[np.ndarray[float]]], + evaluate_constraints: callable[np.ndarray[float],list[np.ndarray[float]]], + evaluate_x_gradients, + etc, + scalarization_method: str, + method_parameters: dict, + ) -> TradeoffExplorationMethod: + pass diff --git a/pyRadPlan/optimization/new_templates/scalarization_method.py b/pyRadPlan/optimization/new_templates/scalarization_method.py new file mode 100644 index 0000000..ed7ecdc --- /dev/null +++ b/pyRadPlan/optimization/new_templates/scalarization_method.py @@ -0,0 +1,41 @@ +import numpy as np + + +class ScalarizationMethod(): + def __init__(self, + evaluate_objectives: callable[np.ndarray[float],list[np.ndarray[float]]], + evaluate_constraints: callable[np.ndarray[float],list[np.ndarray[float]]], + evaluate_x_gradients, + etc, + parameters: dict + ): + # Implementations of class should also manage the concatenating the additional variables and separating them + pass + + def variable_upper_bounds() -> np.ndarray[float]: + pass + + def variable_lower_bounds() -> np.ndarray[float]: + pass + + def get_linear_constrains(self) -> dict[Index, LinearConstraint]: + pass + + def get_nonlinear_constraints(self) -> dict[Index, NonlinearConstraint]: + pass + + def evaluate_objective(x: np.ndarray) -> float: + print("This is not the same as evaluating objectives. E.g. make weighted sum") + + def evaluate_constraints(x: np.ndarray) -> np.ndarray: + print("Most of the time this will be the objective constraints and the constraints from the scalarization method") + + def solve(self, x: np.ndarray[float]) -> np.ndarray[float]: + print("Do something") + self._call_solver_interface(self.solver, self.solver_params) + + def is_objective_convex() -> bool: + pass + + def _call_solver_interface(solver: str, params: dict) -> np.ndarray[float]: + pass diff --git a/pyRadPlan/optimization/new_templates/tradeoff_exploration_method.py b/pyRadPlan/optimization/new_templates/tradeoff_exploration_method.py new file mode 100644 index 0000000..5b70364 --- /dev/null +++ b/pyRadPlan/optimization/new_templates/tradeoff_exploration_method.py @@ -0,0 +1,8 @@ +import numpy as np + +class TradeoffExplorationMethod(): + def __init__(self, params: dict): + pass + + def solve(x: np.ndarray[float]) -> list[np.ndarray[float]]: + pass From 1c7886a802136173e433add1346aa8850abb429a Mon Sep 17 00:00:00 2001 From: Tobias Becher Date: Thu, 20 Feb 2025 15:28:09 +0100 Subject: [PATCH 02/10] Base implementations for tradeoff and scalarization strategies --- pyRadPlan/optimization/problems/_optiprob.py | 80 +++++++++++++++++-- .../_base_scalarization_strategies.py | 73 +++++++++++++++++ .../scalarization_strategies/_factory.py | 71 ++++++++++++++++ .../_lexicographic.py | 0 .../scalarization_strategies/_weighted_sum.py | 1 + .../_base_tradeoff_strategies.py | 25 ++++++ .../tradeoff_strategies/_factory.py | 71 ++++++++++++++++ .../tradeoff_strategies/_single_plan.py | 0 8 files changed, 313 insertions(+), 8 deletions(-) create mode 100644 pyRadPlan/optimization/strategies/scalarization_strategies/_base_scalarization_strategies.py create mode 100644 pyRadPlan/optimization/strategies/scalarization_strategies/_factory.py create mode 100644 pyRadPlan/optimization/strategies/scalarization_strategies/_lexicographic.py create mode 100644 pyRadPlan/optimization/strategies/scalarization_strategies/_weighted_sum.py create mode 100644 pyRadPlan/optimization/strategies/tradeoff_strategies/_base_tradeoff_strategies.py create mode 100644 pyRadPlan/optimization/strategies/tradeoff_strategies/_factory.py create mode 100644 pyRadPlan/optimization/strategies/tradeoff_strategies/_single_plan.py diff --git a/pyRadPlan/optimization/problems/_optiprob.py b/pyRadPlan/optimization/problems/_optiprob.py index d3ab4ae..a3d3354 100644 --- a/pyRadPlan/optimization/problems/_optiprob.py +++ b/pyRadPlan/optimization/problems/_optiprob.py @@ -17,6 +17,8 @@ from ..objectives import get_objective from ..solvers import get_available_solvers, get_solver, SolverBase +from ..strategies.scalarization_strategies import get_available_scalarization_strategies, get_scalarization_strategy, ScalarizationStrategyBase #TODO: Name to change +from ..strategies.tradeoff_strategies import get_available_tradeoff_strategies, get_tradeoff_strategy, TradeoffStrategyBase #TODO: Name to change logger = logging.getLogger(__name__) @@ -50,6 +52,8 @@ class PlanningProblem(ABC): apply_overlap: bool solver: Union[str, dict, SolverBase] + tradeoff_strategy: TradeoffStrategyBase #TODO: Name to change + scalarization_strategy: ScalarizationStrategyBase # TODO: Name to change # Private properties _ct: CT @@ -67,7 +71,8 @@ class PlanningProblem(ABC): def __init__(self, pln: Union[Plan, dict] = None): self._scenario_model = None - + self.scalarization_strategy = 'weighted_sum' #TODO: Name to change + self.tradeoff_strategy = 'single_plan' #TODO: Name to change self.solver = "ipopt" self.apply_overlap = True @@ -89,6 +94,28 @@ def __init__(self, pln: Union[Plan, dict] = None): self.solver = solver_names[0] + #check tradeoff strategy + tradeoff_strategies = get_available_tradeoff_strategies() + if self.tradeoff_strategy not in tradeoff_strategies: + warnings.warn( + f"Tradeoff strategy {self.tradeoff_strategy} not available. Choose from {tradeoff_strategies}" + ", and we will choose the first available one for you!" + ) + + self.tradeoff_strategy = tradeoff_strategies[0] + + + #check scalarization strategy + scalarization_strategies = get_available_scalarization_strategies() + if self.scalarization_strategy not in scalarization_strategies: + warnings.warn( + f"Scalarization strategy {self.scalarization_strategy} not available. Choose from {scalarization_strategies}" + ", and we will choose the first available one for you!" + ) + + self.scalarization_strategy = scalarization_strategies[0] + + def assign_properties_from_pln(self, pln: Plan, warn_when_property_changed: bool = False): """ Assign properties from a Plan object to the Planning Problem. @@ -223,6 +250,12 @@ def _initialize(self): # set solver options self.solver = get_solver(self.solver) + #set tradeoff strategy (options?) + self.tradeoff_strategy = get_tradeoff_strategy(self.tradeoff_strategy) + + #set scalarization method (options?) + self.scalarization_strategy = get_scalarization_strategy(self.scalarization_strategy) + # initial point def solve( @@ -263,33 +296,64 @@ def solve( return self._solve() +class LinearPlanningProblem(PlanningProblem): + """Abstract Class for all Treatment Planning Problems.""" + + @abstractmethod + def _evalulate_objective_functions(self, x: np.ndarray) -> np.ndarray: + """Define the objective functions.""" + + @abstractmethod + def _evalulate_objective_jacobian(self, x: np.ndarray) -> np.ndarray: + """Define the objective jacobian.""" + + def _evalulate_objective_hessian(self, x: np.ndarray) -> np.ndarray: + """Define the objective hessian.""" + return {} + + def _evalulate_constraint_functions(self, x: np.ndarray) -> np.ndarray: + """Define the constraint functions.""" + return None + + def _evalulate_constraint_jacobian(self, x: np.ndarray) -> np.ndarray: + """Define the constraint jacobian.""" + return None + + def _get_constraint_jacobian_structure(self) -> np.ndarray: + """Define the constraint jacobian structure.""" + return None + + def _get_variable_bounds(self, x: np.ndarray) -> np.ndarray: + """Define the variable bounds.""" + return np.array([0.0, np.inf], dtype=np.float64) + class NonLinearPlanningProblem(PlanningProblem): """Abstract Class for all Treatment Planning Problems.""" @abstractmethod - def _objective_functions(self, x: np.ndarray) -> np.ndarray: + def _evalulate_objective_functions(self, x: np.ndarray) -> np.ndarray: """Define the objective functions.""" @abstractmethod - def _objective_jacobian(self, x: np.ndarray) -> np.ndarray: + def _evalulate_objective_jacobian(self, x: np.ndarray) -> np.ndarray: """Define the objective jacobian.""" - def _objective_hessian(self, x: np.ndarray) -> np.ndarray: + def _evalulate_objective_hessian(self, x: np.ndarray) -> np.ndarray: """Define the objective hessian.""" return {} - def _constraint_functions(self, x: np.ndarray) -> np.ndarray: + def _evalulate_constraint_functions(self, x: np.ndarray) -> np.ndarray: """Define the constraint functions.""" return None - def _constraint_jacobian(self, x: np.ndarray) -> np.ndarray: + def _evalulate_constraint_jacobian(self, x: np.ndarray) -> np.ndarray: """Define the constraint jacobian.""" return None - def _constraint_jacobian_structure(self) -> np.ndarray: + def _get_constraint_jacobian_structure(self) -> np.ndarray: """Define the constraint jacobian structure.""" return None - def _variable_bounds(self, x: np.ndarray) -> np.ndarray: + def _get_variable_bounds(self, x: np.ndarray) -> np.ndarray: """Define the variable bounds.""" return np.array([0.0, np.inf], dtype=np.float64) diff --git a/pyRadPlan/optimization/strategies/scalarization_strategies/_base_scalarization_strategies.py b/pyRadPlan/optimization/strategies/scalarization_strategies/_base_scalarization_strategies.py new file mode 100644 index 0000000..0c84e94 --- /dev/null +++ b/pyRadPlan/optimization/strategies/scalarization_strategies/_base_scalarization_strategies.py @@ -0,0 +1,73 @@ +"""Scalarization Strategy Base Classes for Planning Problems.""" + +from typing import ClassVar, Callable +from abc import ABC, abstractmethod +import numpy as np + +class ScalarizationStrategyBase(ABC): + """ + Abstract Base Class for Scalarization Strategy Implementations / Interfaces. + + Attributes + ---------- + name : ClassVar[str] + Full name of the scalarization strategy + short_name : ClassVar[str] + Short name of the scalarization strategy + """ + + name = ClassVar[str] + short_name = ClassVar[str] + + # properties + # TODO + + def __init__(self, + evaluate_objectives: callable[np.ndarray[float],list[np.ndarray[float]]], + evaluate_constraints: callable[np.ndarray[float],list[np.ndarray[float]]], + evaluate_x_gradients, + evaluate__jacobian, + etc, + parameters: dict + ): + # Implementations of class should also manage the concatenating the additional variables and separating them + pass + + def __repr__(self) -> str: + return f"Scalarization Strategy {self.name} ({self.short_name})" + + + + @abstractmethod + def variable_upper_bounds() -> np.ndarray[float]: + pass + + @abstractmethod + def variable_lower_bounds() -> np.ndarray[float]: + pass + + @abstractmethod + def get_linear_constraints(self) -> dict[Index, LinearConstraint]: + pass + + @abstractmethod + def get_nonlinear_constraints(self) -> dict[Index, NonlinearConstraint]: + pass + + @abstractmethod + def evaluate_objective(x: np.ndarray) -> float: + print("This is not the same as evaluating objectives. E.g. make weighted sum") + + @abstractmethod + def evaluate_constraints(x: np.ndarray) -> np.ndarray: + print("Most of the time this will be the objective constraints and the constraints from the scalarization method") + + def solve(self, x: np.ndarray[float]) -> np.ndarray[float]: + print("Do something") + self._call_solver_interface(self.solver, self.solver_params) + + def is_objective_convex() -> bool: + pass + + def _call_solver_interface(solver: str, params: dict) -> np.ndarray[float]: + pass \ No newline at end of file diff --git a/pyRadPlan/optimization/strategies/scalarization_strategies/_factory.py b/pyRadPlan/optimization/strategies/scalarization_strategies/_factory.py new file mode 100644 index 0000000..8684650 --- /dev/null +++ b/pyRadPlan/optimization/strategies/scalarization_strategies/_factory.py @@ -0,0 +1,71 @@ +"""Factory methods to manage available scalarization strategy implementations.""" + +import warnings +import logging +from typing import Union, Type +from ._base_scalarization_strategies import ScalarizationStrategyBase + +SCALARIZATIONSTRATEGIES = {} + +logger = logging.getLogger(__name__) + + +def register_scalarization_strategy(scalarization_cls: Type[ScalarizationStrategyBase]) -> None: + """ + Register a new scalarization strategy. + + Parameters + ---------- + scalarization_cls : type + A Scalarization Strategy class. + """ + if not issubclass(scalarization_cls, ScalarizationStrategyBase): + raise ValueError("Scalarization strategy must be a subclass of ScalarizationStrategyBase.") + + if scalarization_cls.short_name is None: + raise ValueError("Scalarization strategy must have a 'short_name' attribute.") + + if scalarization_cls.name is None: + raise ValueError("Scalarization strategy must have a 'name' attribute.") + + scalarization_name = scalarization_cls.short_name + if scalarization_name in SCALARIZATIONSTRATEGIES: + warnings.warn(f"Scalarization strategy '{scalarization_name}' is already registered.") + else: + SCALARIZATIONSTRATEGIES[scalarization_name] = scalarization_cls + + +def get_available_scalarization_strategies() -> dict[str]: + """ + Get a list of available scalarization strategies based on the plan. + + Returns + ------- + list + A list of available scalarization strategies. + """ + return SCALARIZATIONSTRATEGIES + + +def get_scalarization_strategy(scalarization_desc: Union[str, dict]): + """ + Returns a scalarization strategy instance based on a descriptive parameter. + + Parameters + ---------- + scalarization_desc : Union[str, dict] + A string with the strategy name, or a dictionary with the strategy configuration + + Returns + ------- + ScalarizationStrategyBase + A strategy instance + """ + if isinstance(scalarization_desc, str): + strategy = SCALARIZATIONSTRATEGIES[scalarization_desc]() + elif isinstance(scalarization_desc, dict): + raise NotImplementedError("Scalarization strategy configuration from dictionary not implemented yet.") + else: + raise ValueError(f"Invalid scalarization strategy description: {scalarization_desc}") + + return strategy diff --git a/pyRadPlan/optimization/strategies/scalarization_strategies/_lexicographic.py b/pyRadPlan/optimization/strategies/scalarization_strategies/_lexicographic.py new file mode 100644 index 0000000..e69de29 diff --git a/pyRadPlan/optimization/strategies/scalarization_strategies/_weighted_sum.py b/pyRadPlan/optimization/strategies/scalarization_strategies/_weighted_sum.py new file mode 100644 index 0000000..1a75b47 --- /dev/null +++ b/pyRadPlan/optimization/strategies/scalarization_strategies/_weighted_sum.py @@ -0,0 +1 @@ +from ._base_scalarization_strategies import ScalarizationStrategyBase diff --git a/pyRadPlan/optimization/strategies/tradeoff_strategies/_base_tradeoff_strategies.py b/pyRadPlan/optimization/strategies/tradeoff_strategies/_base_tradeoff_strategies.py new file mode 100644 index 0000000..d856b4f --- /dev/null +++ b/pyRadPlan/optimization/strategies/tradeoff_strategies/_base_tradeoff_strategies.py @@ -0,0 +1,25 @@ +from abc import ABC +from typing import ClassVar +import numpy as np + +class TradeoffStrategyBase(ABC): + """ + To be written later + Abstract class for tradeoff exploration methods + + Parameters + ----------- + + Attributes + ---------- + """ + + short_name: ClassVar[str] + name: ClassVar[str] + ScalarizationMethod: tbd + + def __init__(self, params: dict): + pass + + def solve(x: np.ndarray[float]) -> list[np.ndarray[float]]: + pass \ No newline at end of file diff --git a/pyRadPlan/optimization/strategies/tradeoff_strategies/_factory.py b/pyRadPlan/optimization/strategies/tradeoff_strategies/_factory.py new file mode 100644 index 0000000..e158a42 --- /dev/null +++ b/pyRadPlan/optimization/strategies/tradeoff_strategies/_factory.py @@ -0,0 +1,71 @@ +"""Factory method to manage available tradeoff exploration method implementations. """ + +import warnings +import logging +from typing import Union, Type +from ._base_tradeoff_strategies import TradeoffStrategyBase + +TRADEOFFSTRATEGIES = {} + +logger = logging.getLogger(__name__) + + +def register_tradeoff_strategy(tradeoff_cls: Type[TradeoffStrategyBase]) -> None: + """ + Register a new tradeoff strategy. + + Parameters + ---------- + tradeoff_cls : type + A Tradeoff Strategy class. + """ + if not issubclass(tradeoff_cls, TradeoffStrategyBase): + raise ValueError("Tradeoff strategy must be a subclass of TradeoffStrategyBase.") + + if tradeoff_cls.short_name is None: + raise ValueError("Tradeoff strategy must have a 'short_name' attribute.") + + if tradeoff_cls.name is None: + raise ValueError("Tradeoff strategy must have a 'name' attribute.") + + tradeoff_name = tradeoff_cls.short_name + if tradeoff_name in TRADEOFFSTRATEGIES: + warnings.warn(f"Tradeoff strategy '{tradeoff_name}' is already registered.") + else: + TRADEOFFSTRATEGIES[tradeoff_name] = tradeoff_cls + + +def get_available_tradeoff_strategies() -> dict[str, Type[TradeoffStrategyBase]]: + """ + Get a list of available tradeoff strategies based on the plan. + + Returns + ------- + list + A list of available tradeoff strategies. + """ + return TRADEOFFSTRATEGIES + + +def get_tradeoff_strategy(tradeoff_desc: Union[str, dict]): + """ + Returns a tradeoff strategy based on a descriptive parameter. + + Parameters + ---------- + tradeoff_desc : Union[str, dict] + A string with the strategy name, or a dictionary with the strategy configuration + + Returns + ------- + TradeoffStrategyBase + A solver instance + """ + if isinstance(tradeoff_desc, str): + tradeoff_strategy = TRADEOFFSTRATEGIES[tradeoff_desc]() + elif isinstance(tradeoff_desc, dict): + raise NotImplementedError("Tradeoff strategy configuration from dictionary not implemented yet.") + else: + raise ValueError(f"Invalid tradeoff strategy description: {tradeoff_desc}") + + return tradeoff_strategy diff --git a/pyRadPlan/optimization/strategies/tradeoff_strategies/_single_plan.py b/pyRadPlan/optimization/strategies/tradeoff_strategies/_single_plan.py new file mode 100644 index 0000000..e69de29 From f453ad64599609553a197acf96e555a9ecc6a682 Mon Sep 17 00:00:00 2001 From: Tobias Becher Date: Thu, 20 Feb 2025 15:29:50 +0100 Subject: [PATCH 03/10] Added is_convex and is_linear attribute to objectives --- pyRadPlan/optimization/objectives/_max_dvh.py | 2 +- pyRadPlan/optimization/objectives/_min_dvh.py | 1 + pyRadPlan/optimization/objectives/_objective.py | 8 ++++++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/pyRadPlan/optimization/objectives/_max_dvh.py b/pyRadPlan/optimization/objectives/_max_dvh.py index 580fcfe..f574046 100644 --- a/pyRadPlan/optimization/objectives/_max_dvh.py +++ b/pyRadPlan/optimization/objectives/_max_dvh.py @@ -22,7 +22,7 @@ class MaxDVH(Objective): """ name = "Max DVH" - + is_convex = True d: Annotated[float, Field(default=30.0, ge=0.0), ParameterMetadata(kind="reference")] v_max: Annotated[ float, Field(default=50.0, ge=0.0, le=100.0), ParameterMetadata(kind="relative_volume") diff --git a/pyRadPlan/optimization/objectives/_min_dvh.py b/pyRadPlan/optimization/objectives/_min_dvh.py index f1ab0f1..c81babc 100644 --- a/pyRadPlan/optimization/objectives/_min_dvh.py +++ b/pyRadPlan/optimization/objectives/_min_dvh.py @@ -22,6 +22,7 @@ class MinDVH(Objective): """ name = "Min DVH" + is_convex = True d: Annotated[float, Field(default=30.0, ge=0.0), ParameterMetadata(kind="reference")] v_min: Annotated[ diff --git a/pyRadPlan/optimization/objectives/_objective.py b/pyRadPlan/optimization/objectives/_objective.py index d1f8ab5..7221e4c 100644 --- a/pyRadPlan/optimization/objectives/_objective.py +++ b/pyRadPlan/optimization/objectives/_objective.py @@ -72,10 +72,18 @@ class Objective(PyRadPlanBaseModel): Weight/Priority assigned to the objective function. quantity : str The quantity this objective is connected to (e.g. 'physical_dose', 'RBExDose'). + is_linear : bool + bool to indicate if the objective is linear. + is_convex : bool + bool to indicate if the objective is convex. """ name: ClassVar[str] has_hessian: ClassVar[bool] = False + + is_linear: bool = Field(default=False) + is_convex: bool = Field(default=True) + priority: float = Field(default=1.0, ge=0.0, alias="penalty") quantity: str = Field(default="physical_dose") From 089ca93e6546cbe509120db39ea5e6ee0f573ef7 Mon Sep 17 00:00:00 2001 From: Bjoern Zobrist Date: Thu, 20 Feb 2025 16:11:24 +0100 Subject: [PATCH 04/10] added init for tradeoff strategies --- .../strategies/tradeoff_strategies/__init__.py | 16 ++++++++++++++++ .../tradeoff_strategies/_single_plan.py | 4 ++++ 2 files changed, 20 insertions(+) create mode 100644 pyRadPlan/optimization/strategies/tradeoff_strategies/__init__.py diff --git a/pyRadPlan/optimization/strategies/tradeoff_strategies/__init__.py b/pyRadPlan/optimization/strategies/tradeoff_strategies/__init__.py new file mode 100644 index 0000000..4007673 --- /dev/null +++ b/pyRadPlan/optimization/strategies/tradeoff_strategies/__init__.py @@ -0,0 +1,16 @@ +"""Tradeoff strategy module providing different tradeoff strategies for pyRadPlan.""" + +from ._factory import register_tradeoff_strategy, get_available_tradeoff_strategies, get_tradeoff_strategy +from ._base_tradeoff_strategies import TradeoffStrategyBase +from ._single_plan import SinglePlan + +register_tradeoff_strategy(SinglePlan) + + +__all__ = [ + "TradeoffStrategyBase", + "SinglePlan", + "register_tradeoff_strategy", + "get_available_tradeoff_strategies", + "get_tradeoff_strategy", +] diff --git a/pyRadPlan/optimization/strategies/tradeoff_strategies/_single_plan.py b/pyRadPlan/optimization/strategies/tradeoff_strategies/_single_plan.py index e69de29..a84b6d4 100644 --- a/pyRadPlan/optimization/strategies/tradeoff_strategies/_single_plan.py +++ b/pyRadPlan/optimization/strategies/tradeoff_strategies/_single_plan.py @@ -0,0 +1,4 @@ +from ._base_tradeoff_strategies import TradeoffStrategyBase + +class SinglePlan(TradeoffStrategyBase): + pass \ No newline at end of file From 1ead00c619894e2d915502e3218649f75999ea95 Mon Sep 17 00:00:00 2001 From: Bjoern Zobrist Date: Thu, 20 Feb 2025 16:11:50 +0100 Subject: [PATCH 05/10] is linear/convex changed to class variable --- pyRadPlan/optimization/objectives/_max_dvh.py | 3 ++- pyRadPlan/optimization/objectives/_min_dvh.py | 4 ++-- pyRadPlan/optimization/objectives/_objective.py | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/pyRadPlan/optimization/objectives/_max_dvh.py b/pyRadPlan/optimization/objectives/_max_dvh.py index f574046..efe858b 100644 --- a/pyRadPlan/optimization/objectives/_max_dvh.py +++ b/pyRadPlan/optimization/objectives/_max_dvh.py @@ -22,7 +22,8 @@ class MaxDVH(Objective): """ name = "Max DVH" - is_convex = True + is_convex = False + is_linear = True d: Annotated[float, Field(default=30.0, ge=0.0), ParameterMetadata(kind="reference")] v_max: Annotated[ float, Field(default=50.0, ge=0.0, le=100.0), ParameterMetadata(kind="relative_volume") diff --git a/pyRadPlan/optimization/objectives/_min_dvh.py b/pyRadPlan/optimization/objectives/_min_dvh.py index c81babc..aab72b1 100644 --- a/pyRadPlan/optimization/objectives/_min_dvh.py +++ b/pyRadPlan/optimization/objectives/_min_dvh.py @@ -22,8 +22,8 @@ class MinDVH(Objective): """ name = "Min DVH" - is_convex = True - + is_convex = False + is_linear = True d: Annotated[float, Field(default=30.0, ge=0.0), ParameterMetadata(kind="reference")] v_min: Annotated[ float, Field(default=95.0, ge=0.0, le=100.0), ParameterMetadata(kind="relative_volume") diff --git a/pyRadPlan/optimization/objectives/_objective.py b/pyRadPlan/optimization/objectives/_objective.py index 7221e4c..b6cbdae 100644 --- a/pyRadPlan/optimization/objectives/_objective.py +++ b/pyRadPlan/optimization/objectives/_objective.py @@ -81,8 +81,8 @@ class Objective(PyRadPlanBaseModel): name: ClassVar[str] has_hessian: ClassVar[bool] = False - is_linear: bool = Field(default=False) - is_convex: bool = Field(default=True) + is_linear: ClassVar[bool] = False + is_convex: ClassVar[bool] = True priority: float = Field(default=1.0, ge=0.0, alias="penalty") quantity: str = Field(default="physical_dose") From 9c2043d69a62fc6db86a234ab0ba487bc5241ec5 Mon Sep 17 00:00:00 2001 From: Tobias Becher Date: Thu, 20 Feb 2025 17:13:59 +0100 Subject: [PATCH 06/10] Bugfixes --- .../optimization/problems/_nonlin_fluence.py | 18 +++++------ pyRadPlan/optimization/problems/_optiprob.py | 30 +++++++++-------- .../problems/_simple_least_squares.py | 18 +++++------ pyRadPlan/optimization/strategies/__init__.py | 0 .../scalarization_strategies/__init__.py | 19 +++++++++++ .../_base_scalarization_strategies.py | 8 ++--- .../scalarization_strategies/_factory.py | 11 ++++++- .../scalarization_strategies/_weighted_sum.py | 32 +++++++++++++++++++ .../_base_tradeoff_strategies.py | 4 +-- .../tradeoff_strategies/_single_plan.py | 4 ++- 10 files changed, 104 insertions(+), 40 deletions(-) create mode 100644 pyRadPlan/optimization/strategies/__init__.py create mode 100644 pyRadPlan/optimization/strategies/scalarization_strategies/__init__.py diff --git a/pyRadPlan/optimization/problems/_nonlin_fluence.py b/pyRadPlan/optimization/problems/_nonlin_fluence.py index 36e44ff..5bbf9f0 100644 --- a/pyRadPlan/optimization/problems/_nonlin_fluence.py +++ b/pyRadPlan/optimization/problems/_nonlin_fluence.py @@ -64,7 +64,7 @@ def _initialize(self): self.solver.bounds = (0.0, np.inf) self.solver.max_iter = 500 - def _objective_functions(self, x: NDArray) -> NDArray: + def _evaluate_objective_functions(self, x: NDArray) -> NDArray: """Define the objective functions.""" q_vectors = {} @@ -97,11 +97,11 @@ def _objective_functions(self, x: NDArray) -> NDArray: def _objective_function(self, x: NDArray) -> np.float64: t = time.time() - f = np.sum(self._objective_functions(x)) + f = np.sum(self._evaluate_objective_functions(x)) self._obj_times.append(time.time() - t) return f - def _objective_jacobian(self, x: NDArray) -> NDArray: + def _evaluate_objective_jacobian(self, x: NDArray) -> NDArray: """Define the objective jacobian.""" q_vectors = {} @@ -177,27 +177,27 @@ def _objective_jacobian(self, x: NDArray) -> NDArray: def _objective_gradient(self, x: NDArray) -> NDArray: t = time.time() - jac = np.sum(self._objective_jacobian(x), axis=0) + jac = np.sum(self._evaluate_objective_jacobian(x), axis=0) self._deriv_times.append(time.time() - t) return jac - def _objective_hessian(self, x: NDArray) -> NDArray: + def _evaluate_objective_hessian(self, x: NDArray) -> NDArray: """Define the objective hessian.""" return {} - def _constraint_functions(self, x: NDArray) -> NDArray: + def _evaluate_constraint_functions(self, x: NDArray) -> NDArray: """Define the constraint functions.""" return None - def _constraint_jacobian(self, x: NDArray) -> NDArray: + def _evaluate_constraint_jacobian(self, x: NDArray) -> NDArray: """Define the constraint jacobian.""" return None - def _constraint_jacobian_structure(self) -> NDArray: + def _evaluate_constraint_jacobian_structure(self) -> NDArray: """Define the constraint jacobian structure.""" return None - def _variable_bounds(self, x: NDArray) -> NDArray: + def _get_variable_bounds(self, x: NDArray) -> NDArray: """Define the variable bounds.""" return {} diff --git a/pyRadPlan/optimization/problems/_optiprob.py b/pyRadPlan/optimization/problems/_optiprob.py index a3d3354..50891ec 100644 --- a/pyRadPlan/optimization/problems/_optiprob.py +++ b/pyRadPlan/optimization/problems/_optiprob.py @@ -72,7 +72,7 @@ class PlanningProblem(ABC): def __init__(self, pln: Union[Plan, dict] = None): self._scenario_model = None self.scalarization_strategy = 'weighted_sum' #TODO: Name to change - self.tradeoff_strategy = 'single_plan' #TODO: Name to change + self.tradeoff_strategy = 'single' #TODO: Name to change self.solver = "ipopt" self.apply_overlap = True @@ -97,23 +97,25 @@ def __init__(self, pln: Union[Plan, dict] = None): #check tradeoff strategy tradeoff_strategies = get_available_tradeoff_strategies() if self.tradeoff_strategy not in tradeoff_strategies: + tradeoff_names = list(tradeoff_strategies.keys()) warnings.warn( f"Tradeoff strategy {self.tradeoff_strategy} not available. Choose from {tradeoff_strategies}" ", and we will choose the first available one for you!" ) - self.tradeoff_strategy = tradeoff_strategies[0] + self.tradeoff_strategy = tradeoff_names[0] #check scalarization strategy scalarization_strategies = get_available_scalarization_strategies() - if self.scalarization_strategy not in scalarization_strategies: + if self.scalarization_strategy not in scalarization_strategies: + scalarization_names = list(scalarization_strategies.keys()) warnings.warn( f"Scalarization strategy {self.scalarization_strategy} not available. Choose from {scalarization_strategies}" ", and we will choose the first available one for you!" ) - self.scalarization_strategy = scalarization_strategies[0] + self.scalarization_strategy = scalarization_names[0] def assign_properties_from_pln(self, pln: Plan, warn_when_property_changed: bool = False): @@ -300,22 +302,22 @@ class LinearPlanningProblem(PlanningProblem): """Abstract Class for all Treatment Planning Problems.""" @abstractmethod - def _evalulate_objective_functions(self, x: np.ndarray) -> np.ndarray: + def _evaluate_objective_functions(self, x: np.ndarray) -> np.ndarray: """Define the objective functions.""" @abstractmethod - def _evalulate_objective_jacobian(self, x: np.ndarray) -> np.ndarray: + def _evaluate_objective_jacobian(self, x: np.ndarray) -> np.ndarray: """Define the objective jacobian.""" - def _evalulate_objective_hessian(self, x: np.ndarray) -> np.ndarray: + def _evaluate_objective_hessian(self, x: np.ndarray) -> np.ndarray: """Define the objective hessian.""" return {} - def _evalulate_constraint_functions(self, x: np.ndarray) -> np.ndarray: + def _evaluate_constraint_functions(self, x: np.ndarray) -> np.ndarray: """Define the constraint functions.""" return None - def _evalulate_constraint_jacobian(self, x: np.ndarray) -> np.ndarray: + def _evaluate_constraint_jacobian(self, x: np.ndarray) -> np.ndarray: """Define the constraint jacobian.""" return None @@ -331,22 +333,22 @@ class NonLinearPlanningProblem(PlanningProblem): """Abstract Class for all Treatment Planning Problems.""" @abstractmethod - def _evalulate_objective_functions(self, x: np.ndarray) -> np.ndarray: + def _evaluate_objective_functions(self, x: np.ndarray) -> np.ndarray: """Define the objective functions.""" @abstractmethod - def _evalulate_objective_jacobian(self, x: np.ndarray) -> np.ndarray: + def _evaluate_objective_jacobian(self, x: np.ndarray) -> np.ndarray: """Define the objective jacobian.""" - def _evalulate_objective_hessian(self, x: np.ndarray) -> np.ndarray: + def _evaluate_objective_hessian(self, x: np.ndarray) -> np.ndarray: """Define the objective hessian.""" return {} - def _evalulate_constraint_functions(self, x: np.ndarray) -> np.ndarray: + def _evaluate_constraint_functions(self, x: np.ndarray) -> np.ndarray: """Define the constraint functions.""" return None - def _evalulate_constraint_jacobian(self, x: np.ndarray) -> np.ndarray: + def _evaluate_constraint_jacobian(self, x: np.ndarray) -> np.ndarray: """Define the constraint jacobian.""" return None diff --git a/pyRadPlan/optimization/problems/_simple_least_squares.py b/pyRadPlan/optimization/problems/_simple_least_squares.py index 2a9e51f..ed23815 100644 --- a/pyRadPlan/optimization/problems/_simple_least_squares.py +++ b/pyRadPlan/optimization/problems/_simple_least_squares.py @@ -45,7 +45,7 @@ def _initialize(self): "ftol": 1e-4, } - def _objective_functions(self, x: np.ndarray) -> np.ndarray: + def _evaluate_objective_functions(self, x: np.ndarray) -> np.ndarray: """Define the objective functions.""" if not np.array_equal(x, self._w_cache): @@ -61,9 +61,9 @@ def _objective_functions(self, x: np.ndarray) -> np.ndarray: return np.array([target_fun, patient_fun]) def _objective_function(self, x: np.ndarray) -> np.float64: - return np.dot(np.asarray(self.penalties), self._objective_functions(x)) + return np.dot(np.asarray(self.penalties), self._evaluate_objective_functions(x)) - def _objective_jacobian(self, x: np.ndarray) -> np.ndarray: + def _evaluate_objective_jacobian(self, x: np.ndarray) -> np.ndarray: """Define the objective jacobian.""" if not np.array_equal(x, self._w_cache): @@ -89,25 +89,25 @@ def _objective_jacobian(self, x: np.ndarray) -> np.ndarray: return self._grad_cache def _objective_gradient(self, x: np.ndarray) -> np.ndarray: - return np.sum(self._objective_jacobian(x) * self.penalties, axis=1) + return np.sum(self._evaluate_objective_jacobian(x) * self.penalties, axis=1) - def _objective_hessian(self, x: np.ndarray) -> np.ndarray: + def _evaluate_objective_hessian(self, x: np.ndarray) -> np.ndarray: """Define the objective hessian.""" return {} - def _constraint_functions(self, x: np.ndarray) -> np.ndarray: + def _evaluate_constraint_functions(self, x: np.ndarray) -> np.ndarray: """Define the constraint functions.""" return None - def _constraint_jacobian(self, x: np.ndarray) -> np.ndarray: + def _evaluate_constraint_jacobian(self, x: np.ndarray) -> np.ndarray: """Define the constraint jacobian.""" return None - def _constraint_jacobian_structure(self) -> np.ndarray: + def _evaluate_constraint_jacobian_structure(self) -> np.ndarray: """Define the constraint jacobian structure.""" return None - def _variable_bounds(self, x: np.ndarray) -> np.ndarray: + def _get_variable_bounds(self, x: np.ndarray) -> np.ndarray: """Define the variable bounds.""" return {} diff --git a/pyRadPlan/optimization/strategies/__init__.py b/pyRadPlan/optimization/strategies/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyRadPlan/optimization/strategies/scalarization_strategies/__init__.py b/pyRadPlan/optimization/strategies/scalarization_strategies/__init__.py new file mode 100644 index 0000000..e78712b --- /dev/null +++ b/pyRadPlan/optimization/strategies/scalarization_strategies/__init__.py @@ -0,0 +1,19 @@ +"""Scalarization strategy module providing different scalarization strategies for pyRadPlan.""" + +from ._factory import register_scalarization_strategy, get_available_scalarization_strategies, get_scalarization_strategy + + + +from ._base_scalarization_strategies import ScalarizationStrategyBase +from ._weighted_sum import WeightedSum + +register_scalarization_strategy(WeightedSum) + + +__all__ = [ + "ScalarizationStrategyBase", + "WeightedSum", + "register_scalarization_strategy", + "get_available_scalarization_strategies", + "get_scalarization_strategy", +] diff --git a/pyRadPlan/optimization/strategies/scalarization_strategies/_base_scalarization_strategies.py b/pyRadPlan/optimization/strategies/scalarization_strategies/_base_scalarization_strategies.py index 0c84e94..5fc3314 100644 --- a/pyRadPlan/optimization/strategies/scalarization_strategies/_base_scalarization_strategies.py +++ b/pyRadPlan/optimization/strategies/scalarization_strategies/_base_scalarization_strategies.py @@ -23,8 +23,8 @@ class ScalarizationStrategyBase(ABC): # TODO def __init__(self, - evaluate_objectives: callable[np.ndarray[float],list[np.ndarray[float]]], - evaluate_constraints: callable[np.ndarray[float],list[np.ndarray[float]]], + evaluate_objectives: Callable[np.ndarray[float],list[np.ndarray[float]]], + evaluate_constraints: Callable[np.ndarray[float],list[np.ndarray[float]]], evaluate_x_gradients, evaluate__jacobian, etc, @@ -47,11 +47,11 @@ def variable_lower_bounds() -> np.ndarray[float]: pass @abstractmethod - def get_linear_constraints(self) -> dict[Index, LinearConstraint]: + def get_linear_constraints(self):# -> dict[Index, LinearConstraint]: pass @abstractmethod - def get_nonlinear_constraints(self) -> dict[Index, NonlinearConstraint]: + def get_nonlinear_constraints(self):# -> dict[Index, NonlinearConstraint]: pass @abstractmethod diff --git a/pyRadPlan/optimization/strategies/scalarization_strategies/_factory.py b/pyRadPlan/optimization/strategies/scalarization_strategies/_factory.py index 8684650..8c723e2 100644 --- a/pyRadPlan/optimization/strategies/scalarization_strategies/_factory.py +++ b/pyRadPlan/optimization/strategies/scalarization_strategies/_factory.py @@ -35,6 +35,7 @@ def register_scalarization_strategy(scalarization_cls: Type[ScalarizationStrateg SCALARIZATIONSTRATEGIES[scalarization_name] = scalarization_cls + def get_available_scalarization_strategies() -> dict[str]: """ Get a list of available scalarization strategies based on the plan. @@ -62,7 +63,15 @@ def get_scalarization_strategy(scalarization_desc: Union[str, dict]): A strategy instance """ if isinstance(scalarization_desc, str): - strategy = SCALARIZATIONSTRATEGIES[scalarization_desc]() + strategy = SCALARIZATIONSTRATEGIES[scalarization_desc]( + evaluate_objectives=None, + evaluate_constraints=None, + evaluate_x_gradients=None, + evaluate_jacobian=None, + etc=None, + parameters=None + ) + elif isinstance(scalarization_desc, dict): raise NotImplementedError("Scalarization strategy configuration from dictionary not implemented yet.") else: diff --git a/pyRadPlan/optimization/strategies/scalarization_strategies/_weighted_sum.py b/pyRadPlan/optimization/strategies/scalarization_strategies/_weighted_sum.py index 1a75b47..16eb8ab 100644 --- a/pyRadPlan/optimization/strategies/scalarization_strategies/_weighted_sum.py +++ b/pyRadPlan/optimization/strategies/scalarization_strategies/_weighted_sum.py @@ -1 +1,33 @@ from ._base_scalarization_strategies import ScalarizationStrategyBase + +class WeightedSum(ScalarizationStrategyBase): + name = "Weighted sum scalarization strategy" + short_name = "weighted_sum" + + def __init__(self, + evaluate_objectives, + evaluate_constraints, + evaluate_x_gradients, + evaluate_jacobian, + etc, + parameters): + + pass + + def variable_lower_bounds(): + pass + def variable_upper_bounds(): + pass + + def get_linear_constraints(self): + pass + + def get_nonlinear_constraints(self): + pass + + def evaluate_objective(x): + pass + + def evaluate_constraints(x): + pass + diff --git a/pyRadPlan/optimization/strategies/tradeoff_strategies/_base_tradeoff_strategies.py b/pyRadPlan/optimization/strategies/tradeoff_strategies/_base_tradeoff_strategies.py index d856b4f..3751d20 100644 --- a/pyRadPlan/optimization/strategies/tradeoff_strategies/_base_tradeoff_strategies.py +++ b/pyRadPlan/optimization/strategies/tradeoff_strategies/_base_tradeoff_strategies.py @@ -16,9 +16,9 @@ class TradeoffStrategyBase(ABC): short_name: ClassVar[str] name: ClassVar[str] - ScalarizationMethod: tbd + ScalarizationStrategy: str - def __init__(self, params: dict): + def __init__(self): pass def solve(x: np.ndarray[float]) -> list[np.ndarray[float]]: diff --git a/pyRadPlan/optimization/strategies/tradeoff_strategies/_single_plan.py b/pyRadPlan/optimization/strategies/tradeoff_strategies/_single_plan.py index a84b6d4..a9607d1 100644 --- a/pyRadPlan/optimization/strategies/tradeoff_strategies/_single_plan.py +++ b/pyRadPlan/optimization/strategies/tradeoff_strategies/_single_plan.py @@ -1,4 +1,6 @@ from ._base_tradeoff_strategies import TradeoffStrategyBase class SinglePlan(TradeoffStrategyBase): - pass \ No newline at end of file + name = "Single Plan Tradeoff Strategy" + short_name = "single" + ScalarizationStrategy = "WeightedSum" From 1f872a8e1d71237f4e782f94eda59bf8f481c26c Mon Sep 17 00:00:00 2001 From: Tobias Becher Date: Fri, 21 Feb 2025 11:07:40 +0100 Subject: [PATCH 07/10] Added callbacks to strategies - InversePlanningProblem added - Added callbacks for function evaluations to scalarization and tradeoff problem - Moved scalarization strategy instance creation to tradeoff strategy constructor --- pyRadPlan/optimization/problems/_optiprob.py | 102 ++++++++---------- .../_base_scalarization_strategies.py | 13 ++- .../scalarization_strategies/_factory.py | 9 +- .../scalarization_strategies/_weighted_sum.py | 33 +++--- .../_base_tradeoff_strategies.py | 8 +- .../tradeoff_strategies/_factory.py | 11 +- 6 files changed, 84 insertions(+), 92 deletions(-) diff --git a/pyRadPlan/optimization/problems/_optiprob.py b/pyRadPlan/optimization/problems/_optiprob.py index 50891ec..1e77fa3 100644 --- a/pyRadPlan/optimization/problems/_optiprob.py +++ b/pyRadPlan/optimization/problems/_optiprob.py @@ -94,29 +94,6 @@ def __init__(self, pln: Union[Plan, dict] = None): self.solver = solver_names[0] - #check tradeoff strategy - tradeoff_strategies = get_available_tradeoff_strategies() - if self.tradeoff_strategy not in tradeoff_strategies: - tradeoff_names = list(tradeoff_strategies.keys()) - warnings.warn( - f"Tradeoff strategy {self.tradeoff_strategy} not available. Choose from {tradeoff_strategies}" - ", and we will choose the first available one for you!" - ) - - self.tradeoff_strategy = tradeoff_names[0] - - - #check scalarization strategy - scalarization_strategies = get_available_scalarization_strategies() - if self.scalarization_strategy not in scalarization_strategies: - scalarization_names = list(scalarization_strategies.keys()) - warnings.warn( - f"Scalarization strategy {self.scalarization_strategy} not available. Choose from {scalarization_strategies}" - ", and we will choose the first available one for you!" - ) - - self.scalarization_strategy = scalarization_names[0] - def assign_properties_from_pln(self, pln: Plan, warn_when_property_changed: bool = False): """ @@ -252,13 +229,6 @@ def _initialize(self): # set solver options self.solver = get_solver(self.solver) - #set tradeoff strategy (options?) - self.tradeoff_strategy = get_tradeoff_strategy(self.tradeoff_strategy) - - #set scalarization method (options?) - self.scalarization_strategy = get_scalarization_strategy(self.scalarization_strategy) - - # initial point def solve( self, @@ -296,41 +266,57 @@ def solve( self._initialize() return self._solve() + +class InversePlanningProblem(PlanningProblem): + def __init__(self, pln: Union[Plan, dict] = None): + super().__init__(pln) + #TODO: set up the scalarization strategy and tradeoff strategy + #check tradeoff strategy + tradeoff_strategies = get_available_tradeoff_strategies() + if self.tradeoff_strategy not in tradeoff_strategies: + tradeoff_names = list(tradeoff_strategies.keys()) + warnings.warn( + f"Tradeoff strategy {self.tradeoff_strategy} not available. Choose from {tradeoff_strategies}" + ", and we will choose the first available one for you!" + ) -class LinearPlanningProblem(PlanningProblem): - """Abstract Class for all Treatment Planning Problems.""" + self.tradeoff_strategy = tradeoff_names[0] - @abstractmethod - def _evaluate_objective_functions(self, x: np.ndarray) -> np.ndarray: - """Define the objective functions.""" - @abstractmethod - def _evaluate_objective_jacobian(self, x: np.ndarray) -> np.ndarray: - """Define the objective jacobian.""" + #check scalarization strategy + scalarization_strategies = get_available_scalarization_strategies() + if self.scalarization_strategy not in scalarization_strategies: + scalarization_names = list(scalarization_strategies.keys()) + warnings.warn( + f"Scalarization strategy {self.scalarization_strategy} not available. Choose from {scalarization_strategies}" + ", and we will choose the first available one for you!" + ) - def _evaluate_objective_hessian(self, x: np.ndarray) -> np.ndarray: - """Define the objective hessian.""" - return {} + self.scalarization_strategy = scalarization_names[0] - def _evaluate_constraint_functions(self, x: np.ndarray) -> np.ndarray: - """Define the constraint functions.""" - return None + #generate a dictionary with callbacks that can be passed to strategies + self.callbacks = { + "evaluate_objective_functions": self._evaluate_objective_functions, + "evaluate_objective_jacobian": self._evaluate_objective_jacobian, + "evaluate_objective_hessian": self._evaluate_objective_hessian, + "evaluate_constraint_functions": self._evaluate_constraint_functions, + "evaluate_constraint_jacobian": self._evaluate_constraint_jacobian, + "get_constraint_jacobian_structure": self._get_constraint_jacobian_structure, + "get_variable_bounds": self._get_variable_bounds + } + + def _initialize(self): + super()._initialize() - def _evaluate_constraint_jacobian(self, x: np.ndarray) -> np.ndarray: - """Define the constraint jacobian.""" - return None + #set scalarization method (options?) + scalarization_strategy = get_scalarization_strategy(self.scalarization_strategy,self.callbacks)#TODO: Pass options + + #set tradeoff strategy (options?) + self.tradeoff_strategy = get_tradeoff_strategy(self.tradeoff_strategy,self.callbacks,scalarization_strategy) - def _get_constraint_jacobian_structure(self) -> np.ndarray: - """Define the constraint jacobian structure.""" - return None - def _get_variable_bounds(self, x: np.ndarray) -> np.ndarray: - """Define the variable bounds.""" - return np.array([0.0, np.inf], dtype=np.float64) -class NonLinearPlanningProblem(PlanningProblem): - """Abstract Class for all Treatment Planning Problems.""" @abstractmethod def _evaluate_objective_functions(self, x: np.ndarray) -> np.ndarray: @@ -359,3 +345,9 @@ def _get_constraint_jacobian_structure(self) -> np.ndarray: def _get_variable_bounds(self, x: np.ndarray) -> np.ndarray: """Define the variable bounds.""" return np.array([0.0, np.inf], dtype=np.float64) + + + + +class NonLinearPlanningProblem(InversePlanningProblem): + """Abstract Class for all Treatment Planning Problems.""" diff --git a/pyRadPlan/optimization/strategies/scalarization_strategies/_base_scalarization_strategies.py b/pyRadPlan/optimization/strategies/scalarization_strategies/_base_scalarization_strategies.py index 5fc3314..eb534df 100644 --- a/pyRadPlan/optimization/strategies/scalarization_strategies/_base_scalarization_strategies.py +++ b/pyRadPlan/optimization/strategies/scalarization_strategies/_base_scalarization_strategies.py @@ -8,6 +8,10 @@ class ScalarizationStrategyBase(ABC): """ Abstract Base Class for Scalarization Strategy Implementations / Interfaces. + Parameters + ---------- + callbacks : dict[str, Callable] + Functions in the planning problem that are required for the actual optimization Attributes ---------- name : ClassVar[str] @@ -23,15 +27,10 @@ class ScalarizationStrategyBase(ABC): # TODO def __init__(self, - evaluate_objectives: Callable[np.ndarray[float],list[np.ndarray[float]]], - evaluate_constraints: Callable[np.ndarray[float],list[np.ndarray[float]]], - evaluate_x_gradients, - evaluate__jacobian, - etc, - parameters: dict + callbacks: dict[str, Callable], ): # Implementations of class should also manage the concatenating the additional variables and separating them - pass + self.callbacks = callbacks def __repr__(self) -> str: return f"Scalarization Strategy {self.name} ({self.short_name})" diff --git a/pyRadPlan/optimization/strategies/scalarization_strategies/_factory.py b/pyRadPlan/optimization/strategies/scalarization_strategies/_factory.py index 8c723e2..042e5b5 100644 --- a/pyRadPlan/optimization/strategies/scalarization_strategies/_factory.py +++ b/pyRadPlan/optimization/strategies/scalarization_strategies/_factory.py @@ -48,7 +48,7 @@ def get_available_scalarization_strategies() -> dict[str]: return SCALARIZATIONSTRATEGIES -def get_scalarization_strategy(scalarization_desc: Union[str, dict]): +def get_scalarization_strategy(scalarization_desc: Union[str, dict],eval_callbacks) -> ScalarizationStrategyBase: """ Returns a scalarization strategy instance based on a descriptive parameter. @@ -64,12 +64,7 @@ def get_scalarization_strategy(scalarization_desc: Union[str, dict]): """ if isinstance(scalarization_desc, str): strategy = SCALARIZATIONSTRATEGIES[scalarization_desc]( - evaluate_objectives=None, - evaluate_constraints=None, - evaluate_x_gradients=None, - evaluate_jacobian=None, - etc=None, - parameters=None + eval_callbacks ) elif isinstance(scalarization_desc, dict): diff --git a/pyRadPlan/optimization/strategies/scalarization_strategies/_weighted_sum.py b/pyRadPlan/optimization/strategies/scalarization_strategies/_weighted_sum.py index 16eb8ab..d20e0a6 100644 --- a/pyRadPlan/optimization/strategies/scalarization_strategies/_weighted_sum.py +++ b/pyRadPlan/optimization/strategies/scalarization_strategies/_weighted_sum.py @@ -1,23 +1,21 @@ +from typing import Union +import numpy as np from ._base_scalarization_strategies import ScalarizationStrategyBase - class WeightedSum(ScalarizationStrategyBase): name = "Weighted sum scalarization strategy" short_name = "weighted_sum" def __init__(self, - evaluate_objectives, - evaluate_constraints, - evaluate_x_gradients, - evaluate_jacobian, - etc, - parameters): - - pass + callbacks, + weights: Union[np.ndarray[float],list[float],None] = None): + #functions in the planning problem that are required for the actual optimization + super().__init__(callbacks) + self.weights = np.asarray(weights) - def variable_lower_bounds(): - pass - def variable_upper_bounds(): - pass + def variable_lower_bounds(self): + return self.callbacks['get_variable_bounds']()[0] + def variable_upper_bounds(self): + return self.callbacks['get_variable_bounds']()[1] def get_linear_constraints(self): pass @@ -25,9 +23,8 @@ def get_linear_constraints(self): def get_nonlinear_constraints(self): pass - def evaluate_objective(x): - pass - - def evaluate_constraints(x): - pass + def evaluate_objective(self,x): + return self.weights @ self.callbacks['evaluate_objectives'](x) + def evaluate_constraints(self,x): + return self.callbacks['evaluate_constraints'](x) diff --git a/pyRadPlan/optimization/strategies/tradeoff_strategies/_base_tradeoff_strategies.py b/pyRadPlan/optimization/strategies/tradeoff_strategies/_base_tradeoff_strategies.py index 3751d20..4940bc6 100644 --- a/pyRadPlan/optimization/strategies/tradeoff_strategies/_base_tradeoff_strategies.py +++ b/pyRadPlan/optimization/strategies/tradeoff_strategies/_base_tradeoff_strategies.py @@ -1,6 +1,7 @@ from abc import ABC from typing import ClassVar import numpy as np +from ..scalarization_strategies._base_scalarization_strategies import ScalarizationStrategyBase class TradeoffStrategyBase(ABC): """ @@ -16,10 +17,11 @@ class TradeoffStrategyBase(ABC): short_name: ClassVar[str] name: ClassVar[str] - ScalarizationStrategy: str + scalarization_strategy: str - def __init__(self): - pass + def __init__(self,callbacks: dict[str, callable],scalarization_strategy: ScalarizationStrategyBase): + self.callbacks = callbacks + self.scalarization_strategy = scalarization_strategy def solve(x: np.ndarray[float]) -> list[np.ndarray[float]]: pass \ No newline at end of file diff --git a/pyRadPlan/optimization/strategies/tradeoff_strategies/_factory.py b/pyRadPlan/optimization/strategies/tradeoff_strategies/_factory.py index e158a42..20841f6 100644 --- a/pyRadPlan/optimization/strategies/tradeoff_strategies/_factory.py +++ b/pyRadPlan/optimization/strategies/tradeoff_strategies/_factory.py @@ -4,6 +4,7 @@ import logging from typing import Union, Type from ._base_tradeoff_strategies import TradeoffStrategyBase +from ..scalarization_strategies._base_scalarization_strategies import ScalarizationStrategyBase TRADEOFFSTRATEGIES = {} @@ -47,7 +48,9 @@ def get_available_tradeoff_strategies() -> dict[str, Type[TradeoffStrategyBase]] return TRADEOFFSTRATEGIES -def get_tradeoff_strategy(tradeoff_desc: Union[str, dict]): +def get_tradeoff_strategy(tradeoff_desc: Union[str, dict], + callbacks: dict[str, callable], + scalarization_strategy: ScalarizationStrategyBase) -> TradeoffStrategyBase: """ Returns a tradeoff strategy based on a descriptive parameter. @@ -55,6 +58,10 @@ def get_tradeoff_strategy(tradeoff_desc: Union[str, dict]): ---------- tradeoff_desc : Union[str, dict] A string with the strategy name, or a dictionary with the strategy configuration + callbacks : dict[str, callable] + A dictionary with the functions in the planning problem that are required for the actual optimization + scalarization_strategy : ScalarizationStrategyBase + A scalarization strategy instance Returns ------- @@ -62,7 +69,7 @@ def get_tradeoff_strategy(tradeoff_desc: Union[str, dict]): A solver instance """ if isinstance(tradeoff_desc, str): - tradeoff_strategy = TRADEOFFSTRATEGIES[tradeoff_desc]() + tradeoff_strategy = TRADEOFFSTRATEGIES[tradeoff_desc](callbacks,scalarization_strategy) elif isinstance(tradeoff_desc, dict): raise NotImplementedError("Tradeoff strategy configuration from dictionary not implemented yet.") else: From 7efbdd81a7b5e198e3a821fe1f558bd12b1edfcb Mon Sep 17 00:00:00 2001 From: Tobias Becher Date: Fri, 21 Feb 2025 13:07:49 +0100 Subject: [PATCH 08/10] Added solve method to strategies --- .../optimization/problems/_nonlin_fluence.py | 3 ++- pyRadPlan/optimization/problems/_optiprob.py | 5 ++-- .../_base_scalarization_strategies.py | 7 ++++++ .../scalarization_strategies/_weighted_sum.py | 4 ++++ .../_base_tradeoff_strategies.py | 23 ++++++++++++------- .../tradeoff_strategies/_factory.py | 5 ++-- .../tradeoff_strategies/_single_plan.py | 6 ++++- 7 files changed, 38 insertions(+), 15 deletions(-) diff --git a/pyRadPlan/optimization/problems/_nonlin_fluence.py b/pyRadPlan/optimization/problems/_nonlin_fluence.py index 5bbf9f0..fa159b6 100644 --- a/pyRadPlan/optimization/problems/_nonlin_fluence.py +++ b/pyRadPlan/optimization/problems/_nonlin_fluence.py @@ -203,11 +203,12 @@ def _get_variable_bounds(self, x: NDArray) -> NDArray: def _solve(self) -> tuple[NDArray, dict]: """Solve the problem.""" - self._deriv_times = [] self._obj_times = [] x0 = np.zeros((self._dij.total_num_of_bixels,), dtype=np.float64) + self.tradeoff_strategy.solve(x0) + t = time.time() result = self.solver.solve(x0) self._solve_time = time.time() - t diff --git a/pyRadPlan/optimization/problems/_optiprob.py b/pyRadPlan/optimization/problems/_optiprob.py index 1e77fa3..fe7ef4e 100644 --- a/pyRadPlan/optimization/problems/_optiprob.py +++ b/pyRadPlan/optimization/problems/_optiprob.py @@ -310,10 +310,11 @@ def _initialize(self): super()._initialize() #set scalarization method (options?) - scalarization_strategy = get_scalarization_strategy(self.scalarization_strategy,self.callbacks)#TODO: Pass options + #scalarization_strategy = get_scalarization_strategy(self.scalarization_strategy,self.callbacks)#TODO: Pass options #set tradeoff strategy (options?) - self.tradeoff_strategy = get_tradeoff_strategy(self.tradeoff_strategy,self.callbacks,scalarization_strategy) + self.tradeoff_strategy = get_tradeoff_strategy(self.tradeoff_strategy,self.callbacks,self.scalarization_strategy) + self.tradeoff_strategy._initialize() diff --git a/pyRadPlan/optimization/strategies/scalarization_strategies/_base_scalarization_strategies.py b/pyRadPlan/optimization/strategies/scalarization_strategies/_base_scalarization_strategies.py index eb534df..429dd53 100644 --- a/pyRadPlan/optimization/strategies/scalarization_strategies/_base_scalarization_strategies.py +++ b/pyRadPlan/optimization/strategies/scalarization_strategies/_base_scalarization_strategies.py @@ -68,5 +68,12 @@ def solve(self, x: np.ndarray[float]) -> np.ndarray[float]: def is_objective_convex() -> bool: pass + def solve(self,x: np.ndarray[float]) -> np.ndarray[float]: + return self._solve(x) + + @abstractmethod + def _solve(self,x: np.ndarray[float]) -> np.ndarray[float]: + pass + def _call_solver_interface(solver: str, params: dict) -> np.ndarray[float]: pass \ No newline at end of file diff --git a/pyRadPlan/optimization/strategies/scalarization_strategies/_weighted_sum.py b/pyRadPlan/optimization/strategies/scalarization_strategies/_weighted_sum.py index d20e0a6..52edb5f 100644 --- a/pyRadPlan/optimization/strategies/scalarization_strategies/_weighted_sum.py +++ b/pyRadPlan/optimization/strategies/scalarization_strategies/_weighted_sum.py @@ -28,3 +28,7 @@ def evaluate_objective(self,x): def evaluate_constraints(self,x): return self.callbacks['evaluate_constraints'](x) + + def _solve(self,x: np.ndarray[float]) -> list[np.ndarray[float]]: + print('Hello World!') + return x diff --git a/pyRadPlan/optimization/strategies/tradeoff_strategies/_base_tradeoff_strategies.py b/pyRadPlan/optimization/strategies/tradeoff_strategies/_base_tradeoff_strategies.py index 4940bc6..9732efa 100644 --- a/pyRadPlan/optimization/strategies/tradeoff_strategies/_base_tradeoff_strategies.py +++ b/pyRadPlan/optimization/strategies/tradeoff_strategies/_base_tradeoff_strategies.py @@ -1,8 +1,7 @@ -from abc import ABC -from typing import ClassVar +from abc import ABC,abstractmethod +from typing import ClassVar, Union import numpy as np -from ..scalarization_strategies._base_scalarization_strategies import ScalarizationStrategyBase - +from ..scalarization_strategies import ScalarizationStrategyBase, get_scalarization_strategy class TradeoffStrategyBase(ABC): """ To be written later @@ -17,11 +16,19 @@ class TradeoffStrategyBase(ABC): short_name: ClassVar[str] name: ClassVar[str] - scalarization_strategy: str + scalarization_strategy: ScalarizationStrategyBase - def __init__(self,callbacks: dict[str, callable],scalarization_strategy: ScalarizationStrategyBase): + def __init__(self,callbacks: dict[str, callable],scalarization_desc: Union[str,dict]): self.callbacks = callbacks - self.scalarization_strategy = scalarization_strategy + self.scalarization_strategy = scalarization_desc + + def solve(self,x: np.ndarray[float]) -> list[np.ndarray[float]]: + return self._solve(x) + + def _initialize(self): + self.scalarization_strategy = get_scalarization_strategy(self.scalarization_strategy,self.callbacks)#TODO: Pass options - def solve(x: np.ndarray[float]) -> list[np.ndarray[float]]: + + @abstractmethod + def _solve(self,x: np.ndarray[float]) -> list[np.ndarray[float]]: pass \ No newline at end of file diff --git a/pyRadPlan/optimization/strategies/tradeoff_strategies/_factory.py b/pyRadPlan/optimization/strategies/tradeoff_strategies/_factory.py index 20841f6..59f86ac 100644 --- a/pyRadPlan/optimization/strategies/tradeoff_strategies/_factory.py +++ b/pyRadPlan/optimization/strategies/tradeoff_strategies/_factory.py @@ -4,7 +4,6 @@ import logging from typing import Union, Type from ._base_tradeoff_strategies import TradeoffStrategyBase -from ..scalarization_strategies._base_scalarization_strategies import ScalarizationStrategyBase TRADEOFFSTRATEGIES = {} @@ -50,7 +49,7 @@ def get_available_tradeoff_strategies() -> dict[str, Type[TradeoffStrategyBase]] def get_tradeoff_strategy(tradeoff_desc: Union[str, dict], callbacks: dict[str, callable], - scalarization_strategy: ScalarizationStrategyBase) -> TradeoffStrategyBase: + scalarization_desc: Union[str,dict]) -> TradeoffStrategyBase: """ Returns a tradeoff strategy based on a descriptive parameter. @@ -69,7 +68,7 @@ def get_tradeoff_strategy(tradeoff_desc: Union[str, dict], A solver instance """ if isinstance(tradeoff_desc, str): - tradeoff_strategy = TRADEOFFSTRATEGIES[tradeoff_desc](callbacks,scalarization_strategy) + tradeoff_strategy = TRADEOFFSTRATEGIES[tradeoff_desc](callbacks,scalarization_desc) elif isinstance(tradeoff_desc, dict): raise NotImplementedError("Tradeoff strategy configuration from dictionary not implemented yet.") else: diff --git a/pyRadPlan/optimization/strategies/tradeoff_strategies/_single_plan.py b/pyRadPlan/optimization/strategies/tradeoff_strategies/_single_plan.py index a9607d1..4f29d0b 100644 --- a/pyRadPlan/optimization/strategies/tradeoff_strategies/_single_plan.py +++ b/pyRadPlan/optimization/strategies/tradeoff_strategies/_single_plan.py @@ -1,6 +1,10 @@ +import numpy as np from ._base_tradeoff_strategies import TradeoffStrategyBase class SinglePlan(TradeoffStrategyBase): name = "Single Plan Tradeoff Strategy" short_name = "single" - ScalarizationStrategy = "WeightedSum" + scalarization_strategy = "WeightedSum" + + def _solve(self,x: np.ndarray[float]) -> list[np.ndarray[float]]: + return self.scalarization_strategy.solve(x) From 9c22612f42aa0db29040ef4118373e840848cf86 Mon Sep 17 00:00:00 2001 From: Tobias Becher Date: Fri, 28 Feb 2025 17:39:26 +0100 Subject: [PATCH 09/10] Finished first prototype for weighted sum --- .../optimization/problems/_nonlin_fluence.py | 61 +++++----------- pyRadPlan/optimization/problems/_optiprob.py | 22 +++--- .../_base_scalarization_strategies.py | 38 +++++++--- .../scalarization_strategies/_factory.py | 10 ++- .../scalarization_strategies/_weighted_sum.py | 70 ++++++++++++++++--- .../_base_tradeoff_strategies.py | 21 ++++-- .../tradeoff_strategies/_factory.py | 10 ++- 7 files changed, 152 insertions(+), 80 deletions(-) diff --git a/pyRadPlan/optimization/problems/_nonlin_fluence.py b/pyRadPlan/optimization/problems/_nonlin_fluence.py index fa159b6..baa7be9 100644 --- a/pyRadPlan/optimization/problems/_nonlin_fluence.py +++ b/pyRadPlan/optimization/problems/_nonlin_fluence.py @@ -37,15 +37,13 @@ class NonLinearFluencePlanningProblem(NonLinearPlanningProblem): bypass_objective_jacobian: bool def __init__(self, pln: Union[Plan, dict] = None): - self.bypass_objective_jacobian = True + self.bypass_objective_jacobian = False self._target_voxels = None self._patient_voxels = None self._grad_cache_intermediate = None self._grad_cache = None - self._obj_times = [] - self._deriv_times = [] self._solve_time = None super().__init__(pln) @@ -54,15 +52,15 @@ def _initialize(self): """Initialize this problem.""" super()._initialize() - # Check if the solver is adequate to solve this problem - # TODO: check that it can do constraints - if not isinstance(self.solver, NonLinearOptimizer): - raise ValueError("Solver must be an instance of SolverBase") - - self.solver.objective = self._objective_function - self.solver.gradient = self._objective_gradient - self.solver.bounds = (0.0, np.inf) - self.solver.max_iter = 500 + # # Check if the solver is adequate to solve this problem + # # TODO: check that it can do constraints + # # if not isinstance(self.solver, NonLinearOptimizer): + # # raise ValueError("Solver must be an instance of SolverBase") + # self.solver = get_solver(self.solver) + # self.solver.objective = self._objective_function + # self.solver.gradient = self._objective_gradient + # self.solver.bounds = (0.0, np.inf) + # self.solver.max_iter = 500 def _evaluate_objective_functions(self, x: NDArray) -> NDArray: """Define the objective functions.""" @@ -85,8 +83,7 @@ def _evaluate_objective_functions(self, x: NDArray) -> NDArray: f_vals.append( sum( [ - obj.priority - * obj.compute_objective(q_vectors[obj.quantity].flat[scen_ix][ix]) + obj.compute_objective(q_vectors[obj.quantity].flat[scen_ix][ix]) for scen_ix in q_scenarios[obj.quantity] ] ) @@ -95,11 +92,9 @@ def _evaluate_objective_functions(self, x: NDArray) -> NDArray: # return as numpy array return np.asarray(f_vals, dtype=np.float64) - def _objective_function(self, x: NDArray) -> np.float64: - t = time.time() - f = np.sum(self._evaluate_objective_functions(x)) - self._obj_times.append(time.time() - t) - return f + # def _objective_function(self, x: NDArray) -> np.float64: + # f = np.sum(self._evaluate_objective_functions(x)) + # return f def _evaluate_objective_jacobian(self, x: NDArray) -> NDArray: """Define the objective jacobian.""" @@ -142,8 +137,7 @@ def _evaluate_objective_jacobian(self, x: NDArray) -> NDArray: q_cache_index = self._q_cache_index[cnt] for scen_ix in q_scenarios[obj.quantity]: self._grad_cache_intermediate[obj.quantity][q_cache_index, ix] += ( - obj.priority - * obj.compute_gradient(q_vectors[obj.quantity].flat[scen_ix][ix]) + obj.compute_gradient(q_vectors[obj.quantity].flat[scen_ix][ix]) ) cnt += 1 @@ -175,12 +169,6 @@ def _evaluate_objective_jacobian(self, x: NDArray) -> NDArray: return self._grad_cache - def _objective_gradient(self, x: NDArray) -> NDArray: - t = time.time() - jac = np.sum(self._evaluate_objective_jacobian(x), axis=0) - self._deriv_times.append(time.time() - t) - return jac - def _evaluate_objective_hessian(self, x: NDArray) -> NDArray: """Define the objective hessian.""" return {} @@ -203,28 +191,13 @@ def _get_variable_bounds(self, x: NDArray) -> NDArray: def _solve(self) -> tuple[NDArray, dict]: """Solve the problem.""" - self._deriv_times = [] - self._obj_times = [] x0 = np.zeros((self._dij.total_num_of_bixels,), dtype=np.float64) - self.tradeoff_strategy.solve(x0) - t = time.time() - result = self.solver.solve(x0) + result = self.tradeoff_strategy.solve(x0) + #result = self.solver.solve(x0) self._solve_time = time.time() - t - logger.info( - "%d Objective function evaluations, avg. time: %g +/- %g s", - len(self._obj_times), - np.mean(self._obj_times), - np.std(self._obj_times), - ) - logger.info( - "%d Derivative evaluations, avg. time: %g +/- %g s", - len(self._deriv_times), - np.mean(self._deriv_times), - np.std(self._deriv_times), - ) logger.info("Solver time: %g s", self._solve_time) return result diff --git a/pyRadPlan/optimization/problems/_optiprob.py b/pyRadPlan/optimization/problems/_optiprob.py index fe7ef4e..d6ede1c 100644 --- a/pyRadPlan/optimization/problems/_optiprob.py +++ b/pyRadPlan/optimization/problems/_optiprob.py @@ -1,5 +1,6 @@ from abc import ABC, abstractmethod -from typing import Union, ClassVar +from typing import Union, ClassVar, Union, cast + import warnings import logging @@ -16,9 +17,10 @@ from pyRadPlan.quantities import FluenceDependentQuantity, get_quantity from ..objectives import get_objective -from ..solvers import get_available_solvers, get_solver, SolverBase +from ..solvers import get_available_solvers, SolverBase from ..strategies.scalarization_strategies import get_available_scalarization_strategies, get_scalarization_strategy, ScalarizationStrategyBase #TODO: Name to change from ..strategies.tradeoff_strategies import get_available_tradeoff_strategies, get_tradeoff_strategy, TradeoffStrategyBase #TODO: Name to change +from ..objectives import Objective logger = logging.getLogger(__name__) @@ -51,9 +53,9 @@ class PlanningProblem(ABC): possible_radiation_modes: list[str] = ["photons", "protons", "helium", "carbon", "oxygen"] apply_overlap: bool - solver: Union[str, dict, SolverBase] - tradeoff_strategy: TradeoffStrategyBase #TODO: Name to change - scalarization_strategy: ScalarizationStrategyBase # TODO: Name to change + solver: Union[str, dict] + tradeoff_strategy: Union[str,dict,TradeoffStrategyBase] #TODO: Name to change + scalarization_strategy: Union[str,dict] # TODO: Name to change # Private properties _ct: CT @@ -173,17 +175,19 @@ def _initialize(self): # sanitize objectives and constraints and manage required quantities objectives = [] quantity_ids = [] + priorities = [] for voi in self._cst.vois: if len(voi.objectives) > 0: # get the index list cube_ix = voi.indices_numpy objs = [get_objective(obj) for obj in voi.objectives] - objectives.append((cube_ix, objs)) quantity_ids.extend([obj.quantity for obj in objs]) + priorities.extend([obj.priority for obj in objs]) self._objective_list = objectives + self._priorities = np.array(priorities) # unique quantities quantity_ids = list(set(quantity_ids)) @@ -227,7 +231,7 @@ def _initialize(self): # self._quantities = quantity_obj_info # set solver options - self.solver = get_solver(self.solver) + #self.solver = get_solver(self.solver) def solve( @@ -313,9 +317,7 @@ def _initialize(self): #scalarization_strategy = get_scalarization_strategy(self.scalarization_strategy,self.callbacks)#TODO: Pass options #set tradeoff strategy (options?) - self.tradeoff_strategy = get_tradeoff_strategy(self.tradeoff_strategy,self.callbacks,self.scalarization_strategy) - self.tradeoff_strategy._initialize() - + self.tradeoff_strategy = get_tradeoff_strategy(self.tradeoff_strategy,self.callbacks,self.scalarization_strategy,self._priorities,self.solver) diff --git a/pyRadPlan/optimization/strategies/scalarization_strategies/_base_scalarization_strategies.py b/pyRadPlan/optimization/strategies/scalarization_strategies/_base_scalarization_strategies.py index 429dd53..924cc01 100644 --- a/pyRadPlan/optimization/strategies/scalarization_strategies/_base_scalarization_strategies.py +++ b/pyRadPlan/optimization/strategies/scalarization_strategies/_base_scalarization_strategies.py @@ -1,8 +1,10 @@ + """Scalarization Strategy Base Classes for Planning Problems.""" -from typing import ClassVar, Callable +from typing import ClassVar, Callable, Union from abc import ABC, abstractmethod import numpy as np +from ...solvers import get_solver, SolverBase class ScalarizationStrategyBase(ABC): """ @@ -18,24 +20,40 @@ class ScalarizationStrategyBase(ABC): Full name of the scalarization strategy short_name : ClassVar[str] Short name of the scalarization strategy + callbacks : dict[str, Callable] + Functions in the planning problem that are required for the actual optimization + solver : Union[str, dict, SolverBase] + Solver to be used for optimization """ - name = ClassVar[str] - short_name = ClassVar[str] + name: ClassVar[str] + short_name: ClassVar[str] + # scalarization_params: dict + callbacks: dict[str, Callable] + solver: Union[str,dict,SolverBase] # properties # TODO - def __init__(self, + def __init__(self, + scalarization_model_params, #TODO: Define type callbacks: dict[str, Callable], + solver: Union[str,dict] ): # Implementations of class should also manage the concatenating the additional variables and separating them + self.scalarization_model_params = scalarization_model_params self.callbacks = callbacks + self.solver = solver + self._obj_times = [] + self._deriv_times = [] + + def __repr__(self) -> str: return f"Scalarization Strategy {self.name} ({self.short_name})" - + def _initialize(self): + self.solver = get_solver(self.solver) @abstractmethod def variable_upper_bounds() -> np.ndarray[float]: @@ -61,14 +79,18 @@ def evaluate_objective(x: np.ndarray) -> float: def evaluate_constraints(x: np.ndarray) -> np.ndarray: print("Most of the time this will be the objective constraints and the constraints from the scalarization method") - def solve(self, x: np.ndarray[float]) -> np.ndarray[float]: - print("Do something") - self._call_solver_interface(self.solver, self.solver_params) + # def solve(self, x: np.ndarray[float]) -> np.ndarray[float]: + # print("Do something") + # self._call_solver_interface(self.solver, self.solver_params) def is_objective_convex() -> bool: pass def solve(self,x: np.ndarray[float]) -> np.ndarray[float]: + self._initialize() + self._obj_times = [] + self._deriv_times = [] + return self._solve(x) @abstractmethod diff --git a/pyRadPlan/optimization/strategies/scalarization_strategies/_factory.py b/pyRadPlan/optimization/strategies/scalarization_strategies/_factory.py index 042e5b5..8e90489 100644 --- a/pyRadPlan/optimization/strategies/scalarization_strategies/_factory.py +++ b/pyRadPlan/optimization/strategies/scalarization_strategies/_factory.py @@ -48,7 +48,11 @@ def get_available_scalarization_strategies() -> dict[str]: return SCALARIZATIONSTRATEGIES -def get_scalarization_strategy(scalarization_desc: Union[str, dict],eval_callbacks) -> ScalarizationStrategyBase: +def get_scalarization_strategy( + scalarization_desc: Union[str, dict], + scalarization_model_params, #TODO: Define type, + eval_callbacks:dict, + solver_desc: Union[str,dict]) -> ScalarizationStrategyBase: """ Returns a scalarization strategy instance based on a descriptive parameter. @@ -64,7 +68,9 @@ def get_scalarization_strategy(scalarization_desc: Union[str, dict],eval_callbac """ if isinstance(scalarization_desc, str): strategy = SCALARIZATIONSTRATEGIES[scalarization_desc]( - eval_callbacks + scalarization_model_params, + eval_callbacks, + solver_desc ) elif isinstance(scalarization_desc, dict): diff --git a/pyRadPlan/optimization/strategies/scalarization_strategies/_weighted_sum.py b/pyRadPlan/optimization/strategies/scalarization_strategies/_weighted_sum.py index 52edb5f..6b98b74 100644 --- a/pyRadPlan/optimization/strategies/scalarization_strategies/_weighted_sum.py +++ b/pyRadPlan/optimization/strategies/scalarization_strategies/_weighted_sum.py @@ -1,16 +1,27 @@ from typing import Union import numpy as np +import time +import logging + + from ._base_scalarization_strategies import ScalarizationStrategyBase + +logger = logging.getLogger(__name__) + class WeightedSum(ScalarizationStrategyBase): name = "Weighted sum scalarization strategy" short_name = "weighted_sum" + weights: Union[np.ndarray[float],list[float],None] def __init__(self, - callbacks, - weights: Union[np.ndarray[float],list[float],None] = None): + scalarization_model_params, + callbacks:dict[str,callable], + solver: Union[str,dict]): +# weights: Union[np.ndarray[float],list[float]] = None): #functions in the planning problem that are required for the actual optimization - super().__init__(callbacks) - self.weights = np.asarray(weights) + super().__init__(scalarization_model_params,callbacks,solver) + #self.weights = np.asarray(weights) + def variable_lower_bounds(self): return self.callbacks['get_variable_bounds']()[0] @@ -23,12 +34,55 @@ def get_linear_constraints(self): def get_nonlinear_constraints(self): pass - def evaluate_objective(self,x): - return self.weights @ self.callbacks['evaluate_objectives'](x) + def evaluate_objective(x): + pass + + def evaluate_objective_jacobian(x): + pass + + + def _evaluate_objective_function(self, x: np.ndarray) -> np.float64: + t = time.time() + f = self.scalarization_model_params@self.callbacks["evaluate_objective_functions"](x) + self._obj_times.append(time.time() - t) + + #self.weights@self.callbacks["evaluate_objective_functions"](x) + return f + + def _evaluate_objective_gradient(self, x: np.ndarray) -> np.ndarray: + t = time.time() + + jac = np.sum(self.callbacks["evaluate_objective_jacobian"](x)*self.scalarization_model_params[:,None],axis=0) + self._deriv_times.append(time.time() - t) + return jac + + def evaluate_constraints(self,x): return self.callbacks['evaluate_constraints'](x) + def _solve(self,x: np.ndarray[float]) -> list[np.ndarray[float]]: - print('Hello World!') - return x + self.solver.objective = self._evaluate_objective_function + self.solver.gradient = self._evaluate_objective_gradient + self.solver.bounds = (0.0, np.inf) + self.solver.max_iter = 500 + + result = self.solver.solve(x) + + #needs to be moved + logger.info( + "%d Objective function evaluations, avg. time: %g +/- %g s", + len(self._obj_times), + np.mean(self._obj_times), + np.std(self._obj_times), + ) + logger.info( + "%d Derivative evaluations, avg. time: %g +/- %g s", + len(self._deriv_times), + np.mean(self._deriv_times), + np.std(self._deriv_times), + ) + + return result + diff --git a/pyRadPlan/optimization/strategies/tradeoff_strategies/_base_tradeoff_strategies.py b/pyRadPlan/optimization/strategies/tradeoff_strategies/_base_tradeoff_strategies.py index 9732efa..7e60d8c 100644 --- a/pyRadPlan/optimization/strategies/tradeoff_strategies/_base_tradeoff_strategies.py +++ b/pyRadPlan/optimization/strategies/tradeoff_strategies/_base_tradeoff_strategies.py @@ -2,6 +2,7 @@ from typing import ClassVar, Union import numpy as np from ..scalarization_strategies import ScalarizationStrategyBase, get_scalarization_strategy + class TradeoffStrategyBase(ABC): """ To be written later @@ -16,19 +17,29 @@ class TradeoffStrategyBase(ABC): short_name: ClassVar[str] name: ClassVar[str] - scalarization_strategy: ScalarizationStrategyBase + scalarization_strategy: Union[str,dict,ScalarizationStrategyBase] + #scalarization_model_params, #TODO: Define type + callbacks: dict[str, callable] + solver: Union[str,dict] - def __init__(self,callbacks: dict[str, callable],scalarization_desc: Union[str,dict]): + def __init__(self, + callbacks: dict[str, callable], + scalarization_desc: Union[str,dict], + scalarization_model_params, #TODO: Define type, + solver_desc: Union[str,dict] + ): self.callbacks = callbacks self.scalarization_strategy = scalarization_desc + self.scalarization_model_params = scalarization_model_params + self.solver = solver_desc def solve(self,x: np.ndarray[float]) -> list[np.ndarray[float]]: + self._initialize() return self._solve(x) def _initialize(self): - self.scalarization_strategy = get_scalarization_strategy(self.scalarization_strategy,self.callbacks)#TODO: Pass options - - + self.scalarization_strategy = get_scalarization_strategy(self.scalarization_strategy,self.scalarization_model_params,self.callbacks,self.solver)#TODO: Pass options + @abstractmethod def _solve(self,x: np.ndarray[float]) -> list[np.ndarray[float]]: pass \ No newline at end of file diff --git a/pyRadPlan/optimization/strategies/tradeoff_strategies/_factory.py b/pyRadPlan/optimization/strategies/tradeoff_strategies/_factory.py index 59f86ac..21a45bc 100644 --- a/pyRadPlan/optimization/strategies/tradeoff_strategies/_factory.py +++ b/pyRadPlan/optimization/strategies/tradeoff_strategies/_factory.py @@ -49,7 +49,9 @@ def get_available_tradeoff_strategies() -> dict[str, Type[TradeoffStrategyBase]] def get_tradeoff_strategy(tradeoff_desc: Union[str, dict], callbacks: dict[str, callable], - scalarization_desc: Union[str,dict]) -> TradeoffStrategyBase: + scalarization_desc: Union[str,dict], + scalarization_model_params, #TODO: Define type, + solver_desc: Union[str,dict]) -> TradeoffStrategyBase: """ Returns a tradeoff strategy based on a descriptive parameter. @@ -59,8 +61,10 @@ def get_tradeoff_strategy(tradeoff_desc: Union[str, dict], A string with the strategy name, or a dictionary with the strategy configuration callbacks : dict[str, callable] A dictionary with the functions in the planning problem that are required for the actual optimization - scalarization_strategy : ScalarizationStrategyBase + scalarization_desc : Union[str, dict] A scalarization strategy instance + solver_desc : Union[str, dict] + A string with the solver name, or a dictionary with the solver configuration Returns ------- @@ -68,7 +72,7 @@ def get_tradeoff_strategy(tradeoff_desc: Union[str, dict], A solver instance """ if isinstance(tradeoff_desc, str): - tradeoff_strategy = TRADEOFFSTRATEGIES[tradeoff_desc](callbacks,scalarization_desc) + tradeoff_strategy = TRADEOFFSTRATEGIES[tradeoff_desc](callbacks,scalarization_desc, scalarization_model_params,solver_desc) elif isinstance(tradeoff_desc, dict): raise NotImplementedError("Tradeoff strategy configuration from dictionary not implemented yet.") else: From 2ce0b15eb7476ffecc20115c24fb520f25115d4b Mon Sep 17 00:00:00 2001 From: Tobias Becher Date: Fri, 28 Feb 2025 18:27:07 +0100 Subject: [PATCH 10/10] ruff formatter changes --- .../optimization/new_templates/objectives.py | 2 +- .../new_templates/planning_problem_sketch.py | 27 ++++---- .../new_templates/scalarization_method.py | 23 ++++--- .../tradeoff_exploration_method.py | 3 +- .../optimization/problems/_nonlin_fluence.py | 3 +- pyRadPlan/optimization/problems/_optiprob.py | 66 ++++++++++--------- .../scalarization_strategies/__init__.py | 7 +- .../_base_scalarization_strategies.py | 38 ++++++----- .../scalarization_strategies/_factory.py | 20 +++--- .../scalarization_strategies/_weighted_sum.py | 53 ++++++++------- .../tradeoff_strategies/__init__.py | 6 +- .../_base_tradeoff_strategies.py | 43 +++++++----- .../tradeoff_strategies/_factory.py | 22 ++++--- .../tradeoff_strategies/_single_plan.py | 3 +- 14 files changed, 174 insertions(+), 142 deletions(-) diff --git a/pyRadPlan/optimization/new_templates/objectives.py b/pyRadPlan/optimization/new_templates/objectives.py index 09b3579..e438b58 100644 --- a/pyRadPlan/optimization/new_templates/objectives.py +++ b/pyRadPlan/optimization/new_templates/objectives.py @@ -1,7 +1,7 @@ import numpy as np -class abstract_objective(): +class abstract_objective: def __init__(self): pass diff --git a/pyRadPlan/optimization/new_templates/planning_problem_sketch.py b/pyRadPlan/optimization/new_templates/planning_problem_sketch.py index 3996e99..71796c3 100644 --- a/pyRadPlan/optimization/new_templates/planning_problem_sketch.py +++ b/pyRadPlan/optimization/new_templates/planning_problem_sketch.py @@ -1,11 +1,14 @@ import numpy as np from enum import Enum -class PlanningProblem(): - def __init__(self, - scalarization_method: str | Enum, - tradeoff_exploration_method: str | Enum | None, - method_parameters: dict): + +class PlanningProblem: + def __init__( + self, + scalarization_method: str | Enum, + tradeoff_exploration_method: str | Enum | None, + method_parameters: dict, + ): pass def evaluate_objectives(x: np.ndarray[float]) -> list[np.ndarray[float]]: @@ -40,11 +43,11 @@ def solve(y: np.ndarray[float]) -> list[np.ndarray[float]]: pass def make_tradeoff_exploration_method_instance( - evaluate_objectives: callable[np.ndarray[float],list[np.ndarray[float]]], - evaluate_constraints: callable[np.ndarray[float],list[np.ndarray[float]]], - evaluate_x_gradients, - etc, - scalarization_method: str, - method_parameters: dict, - ) -> TradeoffExplorationMethod: + evaluate_objectives: callable[np.ndarray[float], list[np.ndarray[float]]], + evaluate_constraints: callable[np.ndarray[float], list[np.ndarray[float]]], + evaluate_x_gradients, + etc, + scalarization_method: str, + method_parameters: dict, + ) -> TradeoffExplorationMethod: pass diff --git a/pyRadPlan/optimization/new_templates/scalarization_method.py b/pyRadPlan/optimization/new_templates/scalarization_method.py index ed7ecdc..f5a8774 100644 --- a/pyRadPlan/optimization/new_templates/scalarization_method.py +++ b/pyRadPlan/optimization/new_templates/scalarization_method.py @@ -1,15 +1,16 @@ import numpy as np -class ScalarizationMethod(): - def __init__(self, - evaluate_objectives: callable[np.ndarray[float],list[np.ndarray[float]]], - evaluate_constraints: callable[np.ndarray[float],list[np.ndarray[float]]], - evaluate_x_gradients, - etc, - parameters: dict - ): - # Implementations of class should also manage the concatenating the additional variables and separating them +class ScalarizationMethod: + def __init__( + self, + evaluate_objectives: callable[np.ndarray[float], list[np.ndarray[float]]], + evaluate_constraints: callable[np.ndarray[float], list[np.ndarray[float]]], + evaluate_x_gradients, + etc, + parameters: dict, + ): + # Implementations of class should also manage the concatenating the additional variables and separating them pass def variable_upper_bounds() -> np.ndarray[float]: @@ -28,7 +29,9 @@ def evaluate_objective(x: np.ndarray) -> float: print("This is not the same as evaluating objectives. E.g. make weighted sum") def evaluate_constraints(x: np.ndarray) -> np.ndarray: - print("Most of the time this will be the objective constraints and the constraints from the scalarization method") + print( + "Most of the time this will be the objective constraints and the constraints from the scalarization method" + ) def solve(self, x: np.ndarray[float]) -> np.ndarray[float]: print("Do something") diff --git a/pyRadPlan/optimization/new_templates/tradeoff_exploration_method.py b/pyRadPlan/optimization/new_templates/tradeoff_exploration_method.py index 5b70364..e943b02 100644 --- a/pyRadPlan/optimization/new_templates/tradeoff_exploration_method.py +++ b/pyRadPlan/optimization/new_templates/tradeoff_exploration_method.py @@ -1,6 +1,7 @@ import numpy as np -class TradeoffExplorationMethod(): + +class TradeoffExplorationMethod: def __init__(self, params: dict): pass diff --git a/pyRadPlan/optimization/problems/_nonlin_fluence.py b/pyRadPlan/optimization/problems/_nonlin_fluence.py index baa7be9..247c974 100644 --- a/pyRadPlan/optimization/problems/_nonlin_fluence.py +++ b/pyRadPlan/optimization/problems/_nonlin_fluence.py @@ -8,7 +8,6 @@ from ...plan import Plan from ._optiprob import NonLinearPlanningProblem -from ..solvers import NonLinearOptimizer from ..objectives import Objective logger = logging.getLogger(__name__) @@ -195,7 +194,7 @@ def _solve(self) -> tuple[NDArray, dict]: x0 = np.zeros((self._dij.total_num_of_bixels,), dtype=np.float64) t = time.time() result = self.tradeoff_strategy.solve(x0) - #result = self.solver.solve(x0) + # result = self.solver.solve(x0) self._solve_time = time.time() - t logger.info("Solver time: %g s", self._solve_time) diff --git a/pyRadPlan/optimization/problems/_optiprob.py b/pyRadPlan/optimization/problems/_optiprob.py index d6ede1c..bcbb942 100644 --- a/pyRadPlan/optimization/problems/_optiprob.py +++ b/pyRadPlan/optimization/problems/_optiprob.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from typing import Union, ClassVar, Union, cast +from typing import ClassVar, Union import warnings import logging @@ -17,10 +17,15 @@ from pyRadPlan.quantities import FluenceDependentQuantity, get_quantity from ..objectives import get_objective -from ..solvers import get_available_solvers, SolverBase -from ..strategies.scalarization_strategies import get_available_scalarization_strategies, get_scalarization_strategy, ScalarizationStrategyBase #TODO: Name to change -from ..strategies.tradeoff_strategies import get_available_tradeoff_strategies, get_tradeoff_strategy, TradeoffStrategyBase #TODO: Name to change -from ..objectives import Objective +from ..solvers import get_available_solvers +from ..strategies.scalarization_strategies import ( + get_available_scalarization_strategies, +) # TODO: Name to change +from ..strategies.tradeoff_strategies import ( + get_available_tradeoff_strategies, + get_tradeoff_strategy, + TradeoffStrategyBase, +) # TODO: Name to change logger = logging.getLogger(__name__) @@ -54,8 +59,8 @@ class PlanningProblem(ABC): apply_overlap: bool solver: Union[str, dict] - tradeoff_strategy: Union[str,dict,TradeoffStrategyBase] #TODO: Name to change - scalarization_strategy: Union[str,dict] # TODO: Name to change + tradeoff_strategy: Union[str, dict, TradeoffStrategyBase] # TODO: Name to change + scalarization_strategy: Union[str, dict] # TODO: Name to change # Private properties _ct: CT @@ -73,8 +78,8 @@ class PlanningProblem(ABC): def __init__(self, pln: Union[Plan, dict] = None): self._scenario_model = None - self.scalarization_strategy = 'weighted_sum' #TODO: Name to change - self.tradeoff_strategy = 'single' #TODO: Name to change + self.scalarization_strategy = "weighted_sum" # TODO: Name to change + self.tradeoff_strategy = "single" # TODO: Name to change self.solver = "ipopt" self.apply_overlap = True @@ -96,7 +101,6 @@ def __init__(self, pln: Union[Plan, dict] = None): self.solver = solver_names[0] - def assign_properties_from_pln(self, pln: Plan, warn_when_property_changed: bool = False): """ Assign properties from a Plan object to the Planning Problem. @@ -231,8 +235,7 @@ def _initialize(self): # self._quantities = quantity_obj_info # set solver options - #self.solver = get_solver(self.solver) - + # self.solver = get_solver(self.solver) def solve( self, @@ -270,13 +273,13 @@ def solve( self._initialize() return self._solve() - -class InversePlanningProblem(PlanningProblem): + +class InversePlanningProblem(PlanningProblem): def __init__(self, pln: Union[Plan, dict] = None): super().__init__(pln) - #TODO: set up the scalarization strategy and tradeoff strategy - #check tradeoff strategy + # TODO: set up the scalarization strategy and tradeoff strategy + # check tradeoff strategy tradeoff_strategies = get_available_tradeoff_strategies() if self.tradeoff_strategy not in tradeoff_strategies: tradeoff_names = list(tradeoff_strategies.keys()) @@ -287,11 +290,10 @@ def __init__(self, pln: Union[Plan, dict] = None): self.tradeoff_strategy = tradeoff_names[0] - - #check scalarization strategy + # check scalarization strategy scalarization_strategies = get_available_scalarization_strategies() - if self.scalarization_strategy not in scalarization_strategies: - scalarization_names = list(scalarization_strategies.keys()) + if self.scalarization_strategy not in scalarization_strategies: + scalarization_names = list(scalarization_strategies.keys()) warnings.warn( f"Scalarization strategy {self.scalarization_strategy} not available. Choose from {scalarization_strategies}" ", and we will choose the first available one for you!" @@ -299,7 +301,7 @@ def __init__(self, pln: Union[Plan, dict] = None): self.scalarization_strategy = scalarization_names[0] - #generate a dictionary with callbacks that can be passed to strategies + # generate a dictionary with callbacks that can be passed to strategies self.callbacks = { "evaluate_objective_functions": self._evaluate_objective_functions, "evaluate_objective_jacobian": self._evaluate_objective_jacobian, @@ -307,19 +309,23 @@ def __init__(self, pln: Union[Plan, dict] = None): "evaluate_constraint_functions": self._evaluate_constraint_functions, "evaluate_constraint_jacobian": self._evaluate_constraint_jacobian, "get_constraint_jacobian_structure": self._get_constraint_jacobian_structure, - "get_variable_bounds": self._get_variable_bounds + "get_variable_bounds": self._get_variable_bounds, } - + def _initialize(self): super()._initialize() - #set scalarization method (options?) - #scalarization_strategy = get_scalarization_strategy(self.scalarization_strategy,self.callbacks)#TODO: Pass options - - #set tradeoff strategy (options?) - self.tradeoff_strategy = get_tradeoff_strategy(self.tradeoff_strategy,self.callbacks,self.scalarization_strategy,self._priorities,self.solver) - + # set scalarization method (options?) + # scalarization_strategy = get_scalarization_strategy(self.scalarization_strategy,self.callbacks)#TODO: Pass options + # set tradeoff strategy (options?) + self.tradeoff_strategy = get_tradeoff_strategy( + self.tradeoff_strategy, + self.callbacks, + self.scalarization_strategy, + self._priorities, + self.solver, + ) @abstractmethod def _evaluate_objective_functions(self, x: np.ndarray) -> np.ndarray: @@ -350,7 +356,5 @@ def _get_variable_bounds(self, x: np.ndarray) -> np.ndarray: return np.array([0.0, np.inf], dtype=np.float64) - - class NonLinearPlanningProblem(InversePlanningProblem): """Abstract Class for all Treatment Planning Problems.""" diff --git a/pyRadPlan/optimization/strategies/scalarization_strategies/__init__.py b/pyRadPlan/optimization/strategies/scalarization_strategies/__init__.py index e78712b..87329f5 100644 --- a/pyRadPlan/optimization/strategies/scalarization_strategies/__init__.py +++ b/pyRadPlan/optimization/strategies/scalarization_strategies/__init__.py @@ -1,7 +1,10 @@ """Scalarization strategy module providing different scalarization strategies for pyRadPlan.""" -from ._factory import register_scalarization_strategy, get_available_scalarization_strategies, get_scalarization_strategy - +from ._factory import ( + register_scalarization_strategy, + get_available_scalarization_strategies, + get_scalarization_strategy, +) from ._base_scalarization_strategies import ScalarizationStrategyBase diff --git a/pyRadPlan/optimization/strategies/scalarization_strategies/_base_scalarization_strategies.py b/pyRadPlan/optimization/strategies/scalarization_strategies/_base_scalarization_strategies.py index 924cc01..558167c 100644 --- a/pyRadPlan/optimization/strategies/scalarization_strategies/_base_scalarization_strategies.py +++ b/pyRadPlan/optimization/strategies/scalarization_strategies/_base_scalarization_strategies.py @@ -1,4 +1,3 @@ - """Scalarization Strategy Base Classes for Planning Problems.""" from typing import ClassVar, Callable, Union @@ -6,6 +5,7 @@ import numpy as np from ...solvers import get_solver, SolverBase + class ScalarizationStrategyBase(ABC): """ Abstract Base Class for Scalarization Strategy Implementations / Interfaces. @@ -14,6 +14,7 @@ class ScalarizationStrategyBase(ABC): ---------- callbacks : dict[str, Callable] Functions in the planning problem that are required for the actual optimization + Attributes ---------- name : ClassVar[str] @@ -30,28 +31,27 @@ class ScalarizationStrategyBase(ABC): short_name: ClassVar[str] # scalarization_params: dict callbacks: dict[str, Callable] - solver: Union[str,dict,SolverBase] + solver: Union[str, dict, SolverBase] # properties # TODO - def __init__(self, - scalarization_model_params, #TODO: Define type - callbacks: dict[str, Callable], - solver: Union[str,dict] - ): - # Implementations of class should also manage the concatenating the additional variables and separating them + def __init__( + self, + scalarization_model_params, # TODO: Define type + callbacks: dict[str, Callable], + solver: Union[str, dict], + ): + # Implementations of class should also manage the concatenating the additional variables and separating them self.scalarization_model_params = scalarization_model_params self.callbacks = callbacks self.solver = solver self._obj_times = [] self._deriv_times = [] - - def __repr__(self) -> str: return f"Scalarization Strategy {self.name} ({self.short_name})" - + def _initialize(self): self.solver = get_solver(self.solver) @@ -64,11 +64,11 @@ def variable_lower_bounds() -> np.ndarray[float]: pass @abstractmethod - def get_linear_constraints(self):# -> dict[Index, LinearConstraint]: + def get_linear_constraints(self): # -> dict[Index, LinearConstraint]: pass @abstractmethod - def get_nonlinear_constraints(self):# -> dict[Index, NonlinearConstraint]: + def get_nonlinear_constraints(self): # -> dict[Index, NonlinearConstraint]: pass @abstractmethod @@ -77,7 +77,9 @@ def evaluate_objective(x: np.ndarray) -> float: @abstractmethod def evaluate_constraints(x: np.ndarray) -> np.ndarray: - print("Most of the time this will be the objective constraints and the constraints from the scalarization method") + print( + "Most of the time this will be the objective constraints and the constraints from the scalarization method" + ) # def solve(self, x: np.ndarray[float]) -> np.ndarray[float]: # print("Do something") @@ -86,16 +88,16 @@ def evaluate_constraints(x: np.ndarray) -> np.ndarray: def is_objective_convex() -> bool: pass - def solve(self,x: np.ndarray[float]) -> np.ndarray[float]: + def solve(self, x: np.ndarray[float]) -> np.ndarray[float]: self._initialize() self._obj_times = [] self._deriv_times = [] return self._solve(x) - + @abstractmethod - def _solve(self,x: np.ndarray[float]) -> np.ndarray[float]: + def _solve(self, x: np.ndarray[float]) -> np.ndarray[float]: pass def _call_solver_interface(solver: str, params: dict) -> np.ndarray[float]: - pass \ No newline at end of file + pass diff --git a/pyRadPlan/optimization/strategies/scalarization_strategies/_factory.py b/pyRadPlan/optimization/strategies/scalarization_strategies/_factory.py index 8e90489..a7bc2e8 100644 --- a/pyRadPlan/optimization/strategies/scalarization_strategies/_factory.py +++ b/pyRadPlan/optimization/strategies/scalarization_strategies/_factory.py @@ -35,7 +35,6 @@ def register_scalarization_strategy(scalarization_cls: Type[ScalarizationStrateg SCALARIZATIONSTRATEGIES[scalarization_name] = scalarization_cls - def get_available_scalarization_strategies() -> dict[str]: """ Get a list of available scalarization strategies based on the plan. @@ -49,10 +48,11 @@ def get_available_scalarization_strategies() -> dict[str]: def get_scalarization_strategy( - scalarization_desc: Union[str, dict], - scalarization_model_params, #TODO: Define type, - eval_callbacks:dict, - solver_desc: Union[str,dict]) -> ScalarizationStrategyBase: + scalarization_desc: Union[str, dict], + scalarization_model_params, # TODO: Define type, + eval_callbacks: dict, + solver_desc: Union[str, dict], +) -> ScalarizationStrategyBase: """ Returns a scalarization strategy instance based on a descriptive parameter. @@ -68,13 +68,13 @@ def get_scalarization_strategy( """ if isinstance(scalarization_desc, str): strategy = SCALARIZATIONSTRATEGIES[scalarization_desc]( - scalarization_model_params, - eval_callbacks, - solver_desc + scalarization_model_params, eval_callbacks, solver_desc ) - + elif isinstance(scalarization_desc, dict): - raise NotImplementedError("Scalarization strategy configuration from dictionary not implemented yet.") + raise NotImplementedError( + "Scalarization strategy configuration from dictionary not implemented yet." + ) else: raise ValueError(f"Invalid scalarization strategy description: {scalarization_desc}") diff --git a/pyRadPlan/optimization/strategies/scalarization_strategies/_weighted_sum.py b/pyRadPlan/optimization/strategies/scalarization_strategies/_weighted_sum.py index 6b98b74..2468edc 100644 --- a/pyRadPlan/optimization/strategies/scalarization_strategies/_weighted_sum.py +++ b/pyRadPlan/optimization/strategies/scalarization_strategies/_weighted_sum.py @@ -8,25 +8,25 @@ logger = logging.getLogger(__name__) + class WeightedSum(ScalarizationStrategyBase): name = "Weighted sum scalarization strategy" short_name = "weighted_sum" - weights: Union[np.ndarray[float],list[float],None] - - def __init__(self, - scalarization_model_params, - callbacks:dict[str,callable], - solver: Union[str,dict]): -# weights: Union[np.ndarray[float],list[float]] = None): - #functions in the planning problem that are required for the actual optimization - super().__init__(scalarization_model_params,callbacks,solver) - #self.weights = np.asarray(weights) - + weights: Union[np.ndarray[float], list[float], None] + + def __init__( + self, scalarization_model_params, callbacks: dict[str, callable], solver: Union[str, dict] + ): + # weights: Union[np.ndarray[float],list[float]] = None): + # functions in the planning problem that are required for the actual optimization + super().__init__(scalarization_model_params, callbacks, solver) + # self.weights = np.asarray(weights) def variable_lower_bounds(self): - return self.callbacks['get_variable_bounds']()[0] + return self.callbacks["get_variable_bounds"]()[0] + def variable_upper_bounds(self): - return self.callbacks['get_variable_bounds']()[1] + return self.callbacks["get_variable_bounds"]()[1] def get_linear_constraints(self): pass @@ -36,41 +36,41 @@ def get_nonlinear_constraints(self): def evaluate_objective(x): pass - + def evaluate_objective_jacobian(x): pass - def _evaluate_objective_function(self, x: np.ndarray) -> np.float64: t = time.time() - f = self.scalarization_model_params@self.callbacks["evaluate_objective_functions"](x) + f = self.scalarization_model_params @ self.callbacks["evaluate_objective_functions"](x) self._obj_times.append(time.time() - t) - #self.weights@self.callbacks["evaluate_objective_functions"](x) + # self.weights@self.callbacks["evaluate_objective_functions"](x) return f def _evaluate_objective_gradient(self, x: np.ndarray) -> np.ndarray: t = time.time() - jac = np.sum(self.callbacks["evaluate_objective_jacobian"](x)*self.scalarization_model_params[:,None],axis=0) + jac = np.sum( + self.callbacks["evaluate_objective_jacobian"](x) + * self.scalarization_model_params[:, None], + axis=0, + ) self._deriv_times.append(time.time() - t) return jac + def evaluate_constraints(self, x): + return self.callbacks["evaluate_constraints"](x) - - def evaluate_constraints(self,x): - return self.callbacks['evaluate_constraints'](x) - - - def _solve(self,x: np.ndarray[float]) -> list[np.ndarray[float]]: + def _solve(self, x: np.ndarray[float]) -> list[np.ndarray[float]]: self.solver.objective = self._evaluate_objective_function self.solver.gradient = self._evaluate_objective_gradient self.solver.bounds = (0.0, np.inf) self.solver.max_iter = 500 result = self.solver.solve(x) - - #needs to be moved + + # needs to be moved logger.info( "%d Objective function evaluations, avg. time: %g +/- %g s", len(self._obj_times), @@ -85,4 +85,3 @@ def _solve(self,x: np.ndarray[float]) -> list[np.ndarray[float]]: ) return result - diff --git a/pyRadPlan/optimization/strategies/tradeoff_strategies/__init__.py b/pyRadPlan/optimization/strategies/tradeoff_strategies/__init__.py index 4007673..0042df8 100644 --- a/pyRadPlan/optimization/strategies/tradeoff_strategies/__init__.py +++ b/pyRadPlan/optimization/strategies/tradeoff_strategies/__init__.py @@ -1,6 +1,10 @@ """Tradeoff strategy module providing different tradeoff strategies for pyRadPlan.""" -from ._factory import register_tradeoff_strategy, get_available_tradeoff_strategies, get_tradeoff_strategy +from ._factory import ( + register_tradeoff_strategy, + get_available_tradeoff_strategies, + get_tradeoff_strategy, +) from ._base_tradeoff_strategies import TradeoffStrategyBase from ._single_plan import SinglePlan diff --git a/pyRadPlan/optimization/strategies/tradeoff_strategies/_base_tradeoff_strategies.py b/pyRadPlan/optimization/strategies/tradeoff_strategies/_base_tradeoff_strategies.py index 7e60d8c..d46ff0b 100644 --- a/pyRadPlan/optimization/strategies/tradeoff_strategies/_base_tradeoff_strategies.py +++ b/pyRadPlan/optimization/strategies/tradeoff_strategies/_base_tradeoff_strategies.py @@ -1,15 +1,16 @@ -from abc import ABC,abstractmethod +from abc import ABC, abstractmethod from typing import ClassVar, Union import numpy as np from ..scalarization_strategies import ScalarizationStrategyBase, get_scalarization_strategy + class TradeoffStrategyBase(ABC): """ To be written later Abstract class for tradeoff exploration methods Parameters - ----------- + ---------- Attributes ---------- @@ -17,29 +18,35 @@ class TradeoffStrategyBase(ABC): short_name: ClassVar[str] name: ClassVar[str] - scalarization_strategy: Union[str,dict,ScalarizationStrategyBase] - #scalarization_model_params, #TODO: Define type + scalarization_strategy: Union[str, dict, ScalarizationStrategyBase] + # scalarization_model_params, #TODO: Define type callbacks: dict[str, callable] - solver: Union[str,dict] - - def __init__(self, - callbacks: dict[str, callable], - scalarization_desc: Union[str,dict], - scalarization_model_params, #TODO: Define type, - solver_desc: Union[str,dict] - ): + solver: Union[str, dict] + + def __init__( + self, + callbacks: dict[str, callable], + scalarization_desc: Union[str, dict], + scalarization_model_params, # TODO: Define type, + solver_desc: Union[str, dict], + ): self.callbacks = callbacks self.scalarization_strategy = scalarization_desc self.scalarization_model_params = scalarization_model_params self.solver = solver_desc - def solve(self,x: np.ndarray[float]) -> list[np.ndarray[float]]: + def solve(self, x: np.ndarray[float]) -> list[np.ndarray[float]]: self._initialize() return self._solve(x) - + def _initialize(self): - self.scalarization_strategy = get_scalarization_strategy(self.scalarization_strategy,self.scalarization_model_params,self.callbacks,self.solver)#TODO: Pass options - + self.scalarization_strategy = get_scalarization_strategy( + self.scalarization_strategy, + self.scalarization_model_params, + self.callbacks, + self.solver, + ) # TODO: Pass options + @abstractmethod - def _solve(self,x: np.ndarray[float]) -> list[np.ndarray[float]]: - pass \ No newline at end of file + def _solve(self, x: np.ndarray[float]) -> list[np.ndarray[float]]: + pass diff --git a/pyRadPlan/optimization/strategies/tradeoff_strategies/_factory.py b/pyRadPlan/optimization/strategies/tradeoff_strategies/_factory.py index 21a45bc..0b48969 100644 --- a/pyRadPlan/optimization/strategies/tradeoff_strategies/_factory.py +++ b/pyRadPlan/optimization/strategies/tradeoff_strategies/_factory.py @@ -1,4 +1,4 @@ -"""Factory method to manage available tradeoff exploration method implementations. """ +"""Factory method to manage available tradeoff exploration method implementations.""" import warnings import logging @@ -47,11 +47,13 @@ def get_available_tradeoff_strategies() -> dict[str, Type[TradeoffStrategyBase]] return TRADEOFFSTRATEGIES -def get_tradeoff_strategy(tradeoff_desc: Union[str, dict], - callbacks: dict[str, callable], - scalarization_desc: Union[str,dict], - scalarization_model_params, #TODO: Define type, - solver_desc: Union[str,dict]) -> TradeoffStrategyBase: +def get_tradeoff_strategy( + tradeoff_desc: Union[str, dict], + callbacks: dict[str, callable], + scalarization_desc: Union[str, dict], + scalarization_model_params, # TODO: Define type, + solver_desc: Union[str, dict], +) -> TradeoffStrategyBase: """ Returns a tradeoff strategy based on a descriptive parameter. @@ -72,9 +74,13 @@ def get_tradeoff_strategy(tradeoff_desc: Union[str, dict], A solver instance """ if isinstance(tradeoff_desc, str): - tradeoff_strategy = TRADEOFFSTRATEGIES[tradeoff_desc](callbacks,scalarization_desc, scalarization_model_params,solver_desc) + tradeoff_strategy = TRADEOFFSTRATEGIES[tradeoff_desc]( + callbacks, scalarization_desc, scalarization_model_params, solver_desc + ) elif isinstance(tradeoff_desc, dict): - raise NotImplementedError("Tradeoff strategy configuration from dictionary not implemented yet.") + raise NotImplementedError( + "Tradeoff strategy configuration from dictionary not implemented yet." + ) else: raise ValueError(f"Invalid tradeoff strategy description: {tradeoff_desc}") diff --git a/pyRadPlan/optimization/strategies/tradeoff_strategies/_single_plan.py b/pyRadPlan/optimization/strategies/tradeoff_strategies/_single_plan.py index 4f29d0b..89b7f88 100644 --- a/pyRadPlan/optimization/strategies/tradeoff_strategies/_single_plan.py +++ b/pyRadPlan/optimization/strategies/tradeoff_strategies/_single_plan.py @@ -1,10 +1,11 @@ import numpy as np from ._base_tradeoff_strategies import TradeoffStrategyBase + class SinglePlan(TradeoffStrategyBase): name = "Single Plan Tradeoff Strategy" short_name = "single" scalarization_strategy = "WeightedSum" - def _solve(self,x: np.ndarray[float]) -> list[np.ndarray[float]]: + def _solve(self, x: np.ndarray[float]) -> list[np.ndarray[float]]: return self.scalarization_strategy.solve(x)