Optimization#

pyRadPlan separates the optimization workflow into three orthogonal concepts:

  • Problems — define what is being optimized (variables, constraints, structure of the objective function).

  • Solvers — define how the mathematical program is solved (algorithm, convergence criteria).

  • Objectives — define clinical goals attached to individual structures (DVH goals, dose targets, dose limits).

This separation allows objectives to be reused across different problem formulations and solvers to be swapped without touching clinical goal definitions.

Running optimization#

The high-level entry point is fluence_optimization():

from pyRadPlan import fluence_optimization

fluence = fluence_optimization(ct, cst, stf, dij, pln)

Under the hood, this function:

  1. Instantiates the planning problem configured in pln.prop_opt["problem"].

  2. Reads objectives from each VOI in cst.

  3. Resolves required quantities from dij.

  4. Calls the solver and returns the optimal fluence vector.

Planning problems#

A planning problem defines the optimization variable and the structure of the objective function. The problem class is selected via pln.prop_opt["problem"]:

Key

Description

"nonlin_fluence"

Nonlinear beamlet fluence optimization. Variables are non-negative beamlet weights; the objective is the weighted sum of clinical-goal penalty functions.

pln.prop_opt = {"problem": "nonlin_fluence"}

Problems are registered at import time and can be extended by registering additional PlanningProblem subclasses with register_problem().

Solvers#

A solver implements the mathematical optimization algorithm. The solver is selected via the "solver" key inside pln.prop_opt or directly on the problem object:

Key

Description

"ipopt"

(default) IPOPT interior-point optimizer. Handles large-scale nonlinear programs efficiently. Suitable for the full fluence optimization problem.

"scipy"

SciPy minimization (scipy.optimize.minimize). Multiple sub-methods available (L-BFGS-B, SLSQP, etc.). Lighter dependency, good for smaller problems.

pln.prop_opt = {
    "problem": "nonlin_fluence",
    "solver": "scipy",
}

The currently registered fluence problem constrains fluence weights to be non-negative.

Objectives#

Objectives are penalty or constraint functions that express clinical goals. They are attached directly to VOI objects inside the structure set and are collected automatically during optimization.

Each objective targets a quantity (e.g. "physical_dose", "rbe_x_dose"), has a priority (weight in the combined objective), and may reference a dose level or DVH parameter.

Available objectives#

Class

Description

SquaredDeviation

Penalizes squared deviations from a reference dose. Good all-rounder for PTV coverage.

SquaredUnderdosing

Penalizes only dose below the reference (one-sided). Use for target coverage without over-irradiation penalty.

SquaredOverdosing

Penalizes only dose above the reference. Use for OAR sparing without coverage trade-off.

MeanDose

Penalizes the mean dose in the structure.

MaxDVH

Penalizes violation of a maximum DVH constraint (Dx < limit).

MinDVH

Penalizes violation of a minimum DVH constraint (Dx > limit).

EUD

Equivalent Uniform Dose penalty (generalized EUD formulation).

SquaredMimicking

Penalizes deviation from a reference dose distribution (plan mimicking).

Attaching objectives to structures#

from pyRadPlan.optimization.objectives import SquaredDeviation, SquaredOverdosing

ptv = next(v for v in cst.vois if v.voi_type == "TARGET")
ptv.objectives = [
    SquaredDeviation(priority=1000, d_ref=60.0, quantity="physical_dose"),
]

oar = next(v for v in cst.vois if v.name == "Spinal_Cord")
oar.objectives = [
    SquaredOverdosing(priority=500, d_max=45.0, quantity="physical_dose"),
]

Objectives are pydantic models, so they can be serialized and shared as JSON.

Compute backend#

The optimization problem runs internally against the Python Array API standard, so the compute backend can be switched without modifying any algorithm code:

from pyRadPlan import xp_utils

xp_utils.PREFER_GPU = False
xp_utils.PREFERRED_CPU_ARRAY_BACKEND = "numpy"
xp_utils.PREFER_GPU = True
xp_utils.PREFERRED_GPU_ARRAY_BACKEND = "cupy"

The quantity resolver chooses the current preferred namespace through pyRadPlan.core.xp_utils.choose_array_api_namespace() and converts Dij matrices into that namespace when quantities are resolved.