Skip to content

problem

Test problems

desdeo.problem.testproblems

Pre-defined multiobjective optimization problems.

Pre-defined problems for, e.g., testing and illustration purposed are defined here.

best_cake_problem

best_cake_problem() -> Problem

Defines the best cake problem.

Source code in desdeo/problem/testproblems/cake_problem.py
def best_cake_problem() -> Problem:
    """Defines the best cake problem."""
    variable_inits = [
        ("Flour", 0.70),
        ("Sugar", 0.10),
        ("Butter", 0.40),
        ("Eggs", 0.50),
        ("Milk", 0.20),
        ("Baking powder", 0.80),
    ]
    variables = [
        Variable(
            name=var[0],
            symbol=f"x{i + 1}",
            variable_type=VariableTypeEnum.real,
            lowerbound=0.0,
            upperbound=1.0,
            initial_value=var[1],
        )
        for i, var in enumerate(variable_inits)
    ]

    constants_init = [
        ("T1", 0.60),
        ("T2", 0.35),
        ("T3", 0.25),
        ("T4", 0.30),
        ("T5", 0.35),
        ("T6", 0.40),
        ("INV_D1", 1.0 / 0.60),
        ("INV_D2", 1.0 / 0.65),
        ("INV_D3", 1.0 / 0.75),
        ("INV_D4", 1.0 / 0.70),
        ("INV_D5", 1.0 / 0.65),
        ("INV_D6", 1.0 / 0.60),
        ("Y_LIQ_STAR", 0.5 * 0.35 + 0.3 * 0.30 + 0.2 * 0.25),
        ("INV_D_YLIQ", 1.0 / 0.685),
        ("SBAR_STAR", (0.35 + 0.5 * 0.25) / 1.5),
        ("W25_STAR", U(0.35) * U(0.35)),
        ("INV_DW25", 1.0 / 0.8281),
        ("W35_STAR", U(0.25) * U(0.35)),
        ("INV_DW35", 1.0 / 0.6825),
    ]

    constants = [Constant(name=const[0], symbol=const[0], value=const[1]) for const in constants_init]

    objectives = [
        Objective(
            name="Dry/crumb error",
            symbol="dry_crumb",
            func=f0_str(),
            ideal=0.0,
            nadir=14.0,
            objective_type=ObjectiveTypeEnum.analytical,
            is_twice_differentiable=True,  # right?
        ),
        Objective(
            name="Sweetness/texture off-target",
            symbol="sweet_texture",
            func=f1_str(),
            ideal=0.0,
            nadir=14.0,
            objective_type=ObjectiveTypeEnum.analytical,
            is_twice_differentiable=True,
        ),
        Objective(
            name="Rise/collapse risk",
            symbol="rise_collapse",
            func=f2_str(),
            ideal=0.0,
            nadir=14.0,
            objective_type=ObjectiveTypeEnum.analytical,
            is_twice_differentiable=True,
        ),
        Objective(
            name="Moistness/grease imbalance",
            symbol="moistness_grease",
            func=f3_str(),
            ideal=0.0,
            nadir=14.0,
            objective_type=ObjectiveTypeEnum.analytical,
            is_twice_differentiable=True,
        ),
        Objective(
            name="Browning/burn risk",
            symbol="browning_burn",
            func=f4_str(),
            ideal=0.0,
            nadir=14.0,
            objective_type=ObjectiveTypeEnum.analytical,
            is_twice_differentiable=True,
        ),
    ]

    return Problem(
        name="Cake problem",
        description="Try to find the most delicious cake!",
        constants=constants,
        variables=variables,
        objectives=objectives,
    )

binh_and_korn

binh_and_korn(
    maximize: tuple[bool] = (False, False),
) -> Problem

Create a pydantic dataclass representation of the Binh and Korn problem.

The function has two objective functions, two variables, and two constraint functions. For testing purposes, it can be chosen whether the firs and second objective should be maximized instead.

Parameters:

Name Type Description Default
maximize tuple[bool]

whether the first or second objective should be maximized or not. Defaults to (False, False).

(False, False)
References

Binh T. and Korn U. (1997) MOBES: A Multiobjective Evolution Strategy for Constrained Optimization Problems. In: Proceedings of the Third International Conference on Genetic Algorithms. Czech Republic. pp. 176-182.

Source code in desdeo/problem/testproblems/binh_and_korn_problem.py
def binh_and_korn(maximize: tuple[bool] = (False, False)) -> Problem:
    """Create a pydantic dataclass representation of the Binh and Korn problem.

    The function has two objective functions, two variables, and two constraint functions.
    For testing purposes, it can be chosen whether the firs and second objective should
    be maximized instead.

    Arguments:
        maximize (tuple[bool]): whether the first or second objective should be
            maximized or not. Defaults to (False, False).

    References:
        Binh T. and Korn U. (1997) MOBES: A Multiobjective Evolution Strategy for Constrained Optimization Problems.
            In: Proceedings of the Third International Conference on Genetic Algorithms. Czech Republic. pp. 176-182.
    """
    # These constants are for demonstrative purposes.
    constant_1 = Constant(name="Four", symbol="c_1", value=4)
    constant_2 = Constant(name="Five", symbol="c_2", value=5)

    variable_1 = Variable(
        name="The first variable", symbol="x_1", variable_type="real", lowerbound=0, upperbound=5, initial_value=2.5
    )
    variable_2 = Variable(
        name="The second variable", symbol="x_2", variable_type="real", lowerbound=0, upperbound=3, initial_value=1.5
    )

    objective_1 = Objective(
        name="Objective 1",
        symbol="f_1",
        func=f"{'-' if maximize[0] else ''}(c_1 * x_1**2 + c_1*x_2**2)",
        # func=["Add", ["Multiply", "c_1", ["Square", "x_1"]], ["Multiply", "c_1", ["Square", "x_2"]]],
        maximize=maximize[0],
        ideal=0,
        nadir=140 if not maximize[0] else -140,
        is_linear=False,
        is_convex=True,
        is_twice_differentiable=True,
    )
    objective_2 = Objective(
        name="Objective 2",
        symbol="f_2",
        # func=["Add", ["Square", ["Subtract", "x_1", "c_2"]], ["Square", ["Subtract", "x_2", "c_2"]]],
        func=f"{'-' if maximize[1] else ''}((x_1 - c_2)**2 + (x_2 - c_2)**2)",
        maximize=maximize[1],
        ideal=0,
        nadir=50 if not maximize[0] else -50,
        is_linear=False,
        is_convex=True,
        is_twice_differentiable=True,
    )

    constraint_1 = Constraint(
        name="Constraint 1",
        symbol="g_1",
        cons_type="<=",
        func=["Add", ["Square", ["Subtract", "x_1", "c_2"]], ["Square", "x_2"], -25],
        is_linear=False,
        is_convex=True,
        is_twice_differentiable=True,
    )

    constraint_2 = Constraint(
        name="Constraint 2",
        symbol="g_2",
        cons_type="<=",
        func=["Add", ["Negate", ["Square", ["Subtract", "x_1", 8]]], ["Negate", ["Square", ["Add", "x_2", 3]]], 7.7],
        is_linear=False,
        is_convex=True,
        is_twice_differentiable=True,
    )

    return Problem(
        name="The Binh and Korn function",
        description="The two-objective problem used in the paper by Binh and Korn.",
        constants=[constant_1, constant_2],
        variables=[variable_1, variable_2],
        objectives=[objective_1, objective_2],
        constraints=[constraint_1, constraint_2],
        is_twice_differentiable=True,
    )

dmitry_forest_problem_disc

dmitry_forest_problem_disc() -> Problem

Implements the dmitry forest problem using Pareto front representation.

Returns:

Name Type Description
Problem Problem

A problem instance representing the forest problem.

Source code in desdeo/problem/testproblems/dmitry_forest_problem_discrete.py
def dmitry_forest_problem_disc() -> Problem:
    """Implements the dmitry forest problem using Pareto front representation.

    Returns:
        Problem: A problem instance representing the forest problem.
    """
    # __file__ is desdeo/problem/testproblems/dmitry_forest_problem_discrete.py
    # CSV is at <repo_root>/tests/data/dmitry_discrete_repr/...
    path = (
        Path(__file__).resolve().parent.parent.parent.parent
        / "tests/data/dmitry_discrete_repr/dmitry_forest_problem_non_dom_solns.csv"
    )

    obj_names = ["Rev", "HA", "Carb", "DW"]

    var_name = "index"

    data = pl.read_csv(
        path,
        has_header=True,
        columns=["Rev", "HA", "Carb", "DW"],
        separator=",",  # decimal_comma=True
    )

    variables = [
        Variable(
            name=var_name,
            symbol=var_name,
            variable_type=VariableTypeEnum.integer,
            lowerbound=0,
            upperbound=len(data) - 1,
            initial_value=0,
        )
    ]

    objectives = [
        Objective(
            name=obj_name,
            symbol=obj_name,
            objective_type=ObjectiveTypeEnum.data_based,
            ideal=data[obj_name].max(),
            nadir=data[obj_name].min(),
            maximize=True,
        )
        for obj_name in obj_names
    ]

    discrete_def = DiscreteRepresentation(
        variable_values={"index": list(range(len(data)))},
        objective_values=data[[obj.symbol for obj in objectives]].to_dict(),
    )

    return Problem(
        name="Dmitry Forest Problem (Discrete)",
        description="Defines a forest problem with four objectives: revenue, habitat availability, carbon storage, and deadwood.",
        variables=variables,
        objectives=objectives,
        discrete_representation=discrete_def,
        is_twice_differentiable=False,
    )

dtlz2

dtlz2(n_variables: int, n_objectives: int) -> Problem

Defines the DTLZ2 test problem.

The objective functions for DTLZ2 are defined as follows, for \(i = 1\) to \(M\):

\[\begin{equation} \underset{\mathbf{x}}{\operatorname{min}} f_i(\mathbf{x}) = (1+g(\mathbf{x}_M)) \prod_{j=1}^{M-i} \cos\left(x_j \frac{\pi}{2}\right) \times \begin{cases} 1 & \text{if } i=1 \\ \sin\left(x_{(M-i+1)}\frac{\pi}{2}\right) & \text{otherwise}, \end{cases} \end{equation}\]

where

\[\begin{equation} g(\mathbf{x}_M) = \sum_{x_i \in \mathbf{x}_M} \left( x_i - 0.5 \right)^2, \end{equation}\]

and \(\mathbf{x}_M\) represents the last \(n-k\) dimensions of the decision vector. Pareto optimal solutions to the DTLZ2 problem consist of \(x_i = 0.5\) for all \(x_i \in\mathbf{x}_{M}\), and \(\sum{i=1}^{M} f_i^2 = 1\).

Parameters:

Name Type Description Default
n_variables int

number of variables.

required
n_objectives int

number of objective functions.

required

Returns:

Name Type Description
Problem Problem

an instance of the DTLZ2 problem with n_variables variables and n_objectives objective functions.

References

Deb, K., Thiele, L., Laumanns, M., Zitzler, E. (2005). Scalable Test Problems for Evolutionary Multiobjective Optimization. In: Abraham, A., Jain, L., Goldberg, R. (eds) Evolutionary Multiobjective Optimization. Advanced Information and Knowledge Processing. Springer.

Source code in desdeo/problem/testproblems/dtlz2_problem.py
def dtlz2(n_variables: int, n_objectives: int) -> Problem:
    r"""Defines the DTLZ2 test problem.

    The objective functions for DTLZ2 are defined as follows, for $i = 1$ to $M$:

    \begin{equation}
        \underset{\mathbf{x}}{\operatorname{min}}
        f_i(\mathbf{x}) = (1+g(\mathbf{x}_M)) \prod_{j=1}^{M-i} \cos\left(x_j \frac{\pi}{2}\right) \times
        \begin{cases}
        1 & \text{if } i=1 \\
        \sin\left(x_{(M-i+1)}\frac{\pi}{2}\right) & \text{otherwise},
        \end{cases}
    \end{equation}

    where

    \begin{equation}
    g(\mathbf{x}_M) = \sum_{x_i \in \mathbf{x}_M} \left( x_i - 0.5 \right)^2,
    \end{equation}

    and $\mathbf{x}_M$ represents the last $n-k$ dimensions of the decision vector.
    Pareto optimal solutions to the DTLZ2 problem consist of $x_i = 0.5$ for
    all $x_i \in\mathbf{x}_{M}$, and $\sum{i=1}^{M} f_i^2 = 1$.

    Args:
        n_variables (int): number of variables.
        n_objectives (int): number of objective functions.

    Returns:
        Problem: an instance of the DTLZ2 problem with `n_variables` variables and `n_objectives` objective
            functions.

    References:
        Deb, K., Thiele, L., Laumanns, M., Zitzler, E. (2005). Scalable Test
            Problems for Evolutionary Multiobjective Optimization. In: Abraham, A.,
            Jain, L., Goldberg, R. (eds) Evolutionary Multiobjective Optimization.
            Advanced Information and Knowledge Processing. Springer.
    """
    # function g
    g_symbol = "g"
    g_expr = " + ".join([f"(x_{i} - 0.5)**2" for i in range(n_objectives, n_variables + 1)])
    g_expr = "1 + " + g_expr

    objectives = []
    for m in range(1, n_objectives + 1):
        # function f_m
        prod_expr = " * ".join([f"Cos(0.5 * {np.pi} * x_{i})" for i in range(1, n_objectives - m + 1)])
        if m > 1:
            prod_expr += f"{' * ' if prod_expr != "" else ""}Sin(0.5 * {np.pi} * x_{n_objectives - m + 1})"
        if prod_expr == "":
            prod_expr = "1"  # When m == n_objectives, the product is empty, implying f_M = g.
        f_m_expr = f"({g_symbol}) * ({prod_expr})"

        objectives.append(
            Objective(
                name=f"f_{m}",
                symbol=f"f_{m}",
                func=f_m_expr,
                maximize=False,
                ideal=0,
                nadir=1,  # Assuming the range of g and the trigonometric functions
                is_convex=False,
                is_linear=False,
                is_twice_differentiable=True,
            )
        )

    variables = [
        Variable(
            name=f"x_{i}",
            symbol=f"x_{i}",
            variable_type=VariableTypeEnum.real,
            lowerbound=0,
            upperbound=1,
            initial_value=1.0,
        )
        for i in range(1, n_variables + 1)
    ]

    extras = [
        ExtraFunction(
            name="g", symbol=g_symbol, func=g_expr, is_convex=False, is_linear=False, is_twice_differentiable=True
        ),
    ]

    return Problem(
        name="dtlz2",
        description="The DTLZ2 test problem.",
        variables=variables,
        objectives=objectives,
        extra_funcs=extras,
    )

forest_problem_discrete

forest_problem_discrete() -> Problem

Implements the forest problem using Pareto front representation.

Returns:

Name Type Description
Problem Problem

A problem instance representing the forest problem.

Source code in desdeo/problem/testproblems/forest_problem.py
def forest_problem_discrete() -> Problem:
    """Implements the forest problem using Pareto front representation.

    Returns:
        Problem: A problem instance representing the forest problem.
    """
    filename = "datasets/forest_holding_4.csv"

    path = Path(__file__).parent.parent.parent.parent / filename

    obj_names = ["stock", "harvest_value", "npv"]

    var_name = "index"

    data = pl.read_csv(
        path, has_header=True, columns=["stock", "harvest_value", "npv"], separator=";", decimal_comma=True
    )

    variables = [
        Variable(
            name=var_name,
            symbol=var_name,
            variable_type=VariableTypeEnum.integer,
            lowerbound=0,
            upperbound=len(data) - 1,
            initial_value=0,
        )
    ]

    objectives = [
        Objective(
            name=obj_name,
            symbol=obj_name,
            objective_type=ObjectiveTypeEnum.data_based,
            ideal=data[obj_name].max(),
            nadir=data[obj_name].min(),
            maximize=True,
        )
        for obj_name in obj_names
    ]

    discrete_def = DiscreteRepresentation(
        variable_values={"index": list(range(len(data)))},
        objective_values=data[[obj.symbol for obj in objectives]].to_dict(),
    )

    return Problem(
        name="Finnish Forest Problem (Discrete)",
        description="Defines a forest problem with three objectives: stock, harvest value, and net present value.",
        variables=variables,
        objectives=objectives,
        discrete_representation=discrete_def,
    )

mcwb_equilateral_tbeam_problem

mcwb_equilateral_tbeam_problem() -> Problem

Defines the multiobjective cantilever welded beam (MCWB) optimization problem...

Defines the multiobjective cantilever welded beam (MCWB) optimization problem using an equilateral T-beam cross-section.

The objective functions and constraints for the MCWB design problem are defined as follows:

Objectives: 1. Minimize the total cost, \( f_1 = W_c + B_c \), where \( W_c \) is the weld cost and \( B_c \) is the beam cost. 2. Minimize the deflection of the beam, \( f_2 = \frac{P L^3}{3 E I_x} \), where \( P \) is the applied load, \( L \) is the beam length, \( E \) is the Young's modulus, and \( I_x \) is the moment of inertia.

Constraints: 1. Shear stress constraint: \( \tau \leq \tau_{max} \), where \( \tau \) is the combined shear stress and \( \tau_{max} \) is the maximum shear stress. 2. Normal stress constraint: \( \sigma_x \leq \sigma_{max} \), where \( \sigma_x \) is the bending stress and \( \sigma_{max} \) is the maximum allowable normal stress. 3. Buckling constraint: \( P \leq P_c \), where \( P_c \) is the critical buckling load. 4. Weld height constraint: \( x_1 \leq x_4 \), ensuring that the weld height \( x_1 \) is less than or equal to the flange/web thickness \( x_4 \). 5. Flange thickness constraint: \( x_4 \geq x_3 \), ensuring that the flange thickness \( x_4 \) is greater than or equal to the beam height \( x_3 \).

Where: - \( x_1 \) is the weld height. - \( x_2 \) is the weld length. - \( x_3 \) is the beam height. - \( x_4 \) is the beam thickness (flange/web thickness).

The parameters are defined as: - \( P = 30000 \, \text{N} \) (load), - \( L = 0.5 \, \text{m} \) (beam length), - \( E = 200 \times 10^9 \, \text{Pa} \) (Young's modulus), - \( \tau_{max} = 95 \times 10^6 \, \text{Pa} \) (max shear stress), - \( \sigma_{max} = 200 \times 10^6 \, \text{Pa} \) (max normal stress), - \( C_w = 209600 \, \text{\$/m}^3 \) (welding cost factor), - \( C_s = 0.7 \, \text{\$/kg} \) (price of HRC steel), - \( \rho_s = 7850 \, \text{kg/m}^3 \) (steel density), - \( C_b = 0.7 \times 7850 \, \text{\$/m}^3 \) (beam material cost factor), - \( K = 2 \) (cantilever beam coefficient), - \( \pi = 3.141592653589793 \) (constant for calculations), - \( \delta_t = 0.045 \) (tolerance factor for calculations).

Returns:

Name Type Description
Problem Problem

An instance of the multiobjective cantilever welded beam optimization problem using an equilateral T-beam cross-section.

Source code in desdeo/problem/testproblems/mcwb_problem.py
def mcwb_equilateral_tbeam_problem() -> Problem:
    r"""Defines the multiobjective cantilever welded beam (MCWB) optimization problem...

    Defines the multiobjective cantilever welded beam (MCWB) optimization problem using an equilateral
    T-beam cross-section.

    The objective functions and constraints for the MCWB design problem are defined as follows:

    Objectives:
    1. Minimize the total cost, \( f_1 = W_c + B_c \), where \( W_c \) is the weld cost and \( B_c \) is the beam cost.
    2. Minimize the deflection of the beam, \( f_2 = \frac{P L^3}{3 E I_x} \), where \( P \) is the applied load,
        \( L \) is the beam length, \( E \) is the Young's modulus, and \( I_x \) is the moment of inertia.

    Constraints:
    1. Shear stress constraint: \( \tau \leq \tau_{max} \), where \( \tau \) is the combined shear stress and
        \( \tau_{max} \) is the maximum shear stress.
    2. Normal stress constraint: \( \sigma_x \leq \sigma_{max} \), where \( \sigma_x \) is the bending stress and
        \( \sigma_{max} \) is the maximum allowable normal stress.
    3. Buckling constraint: \( P \leq P_c \), where \( P_c \) is the critical buckling load.
    4. Weld height constraint: \( x_1 \leq x_4 \), ensuring that the weld height \( x_1 \) is less than or equal
        to the flange/web thickness \( x_4 \).
    5. Flange thickness constraint: \( x_4 \geq x_3 \), ensuring that the flange thickness \( x_4 \) is greater
        than or equal to the beam height \( x_3 \).

    Where:
    - \( x_1 \) is the weld height.
    - \( x_2 \) is the weld length.
    - \( x_3 \) is the beam height.
    - \( x_4 \) is the beam thickness (flange/web thickness).

    The parameters are defined as:
    - \( P = 30000 \, \text{N} \) (load),
    - \( L = 0.5 \, \text{m} \) (beam length),
    - \( E = 200 \times 10^9 \, \text{Pa} \) (Young's modulus),
    - \( \tau_{max} = 95 \times 10^6 \, \text{Pa} \) (max shear stress),
    - \( \sigma_{max} = 200 \times 10^6 \, \text{Pa} \) (max normal stress),
    - \( C_w = 209600 \, \text{\$/m}^3 \) (welding cost factor),
    - \( C_s = 0.7 \, \text{\$/kg} \) (price of HRC steel),
    - \( \rho_s = 7850 \, \text{kg/m}^3 \) (steel density),
    - \( C_b = 0.7 \times 7850 \, \text{\$/m}^3 \) (beam material cost factor),
    - \( K = 2 \) (cantilever beam coefficient),
    - \( \pi = 3.141592653589793 \) (constant for calculations),
    - \( \delta_t = 0.045 \) (tolerance factor for calculations).

    Returns:
        Problem: An instance of the multiobjective cantilever welded beam optimization problem
            using an equilateral T-beam cross-section.
    """
    # Variables
    variables = [
        Variable(
            name="x_1", symbol="x_1", variable_type=VariableTypeEnum.real, lowerbound=0.005, upperbound=0.25
        ),  # weld height
        Variable(
            name="x_2", symbol="x_2", variable_type=VariableTypeEnum.real, lowerbound=0.01, upperbound=0.3
        ),  # weld length
        Variable(
            name="x_3", symbol="x_3", variable_type=VariableTypeEnum.real, lowerbound=0.01, upperbound=0.25
        ),  # beam height
        Variable(
            name="x_4", symbol="x_4", variable_type=VariableTypeEnum.real, lowerbound=0.005, upperbound=0.25
        ),  # beam thickness (flange/web thickness)
    ]

    # Constants
    constants = CONSTANTS

    # Extra Functions (Intermediate Calculations)
    extra_functions = [
        ExtraFunction(
            name="cross_section_area",
            symbol="A",
            func="x_4 * x_3 + (x_3 - x_4) * x_4",  # t * h + (h - t) * t
        ),
        ExtraFunction(
            name="moment_of_inertia", symbol="I_x", func="(x_4 * x_3 ** 3) / 12 + ((x_3 - x_4) * x_4 ** 3) / 12"
        ),
        *EXTRAFUNCTION,
    ]

    # Objectives (minimize cost, minimize deflection)
    objectives = OBJECTIVES

    # Constraints
    constraints = [
        *CONSTRAINTS,
        Constraint(
            name="g_4",
            symbol="g_4",
            cons_type=ConstraintTypeEnum.LTE,
            func="(x_1 - x_4) / (0.25 - 0.005)",  # weld height <= flange thickness
        ),
        Constraint(
            name="g_5",
            symbol="g_5",
            cons_type=ConstraintTypeEnum.LTE,
            func="(x_4 - x_3) / (0.25 - 0.005)",  # flange thickness >= beam height
        ),
    ]

    return Problem(
        name="MCWB Equilateral T-Beam",
        description="Multiobjective optimization of a welded T-beam with an equilateral cross-section.",
        constants=constants,
        variables=variables,
        extra_funcs=extra_functions,
        objectives=objectives,
        constraints=constraints,
    )

mcwb_hollow_rectangular_problem

mcwb_hollow_rectangular_problem() -> Problem

Defines the multiobjective cantilever welded beam (MCWB) optimization problem...

Defines the multiobjective cantilever welded beam (MCWB) optimization problem using a hollow rectangular cross-section.

The objective functions and constraints for the MCWB design problem are defined as follows:

Objectives: 1. Minimize the total cost, \( f_1 = W_c + B_c \), where \( W_c \) is the weld cost and \( B_c \) is the beam cost. 2. Minimize the deflection of the beam, \( f_2 = \frac{P L^3}{3 E I_x} \), where \( P \) is the applied load, \( L \) is the beam length, \( E \) is the Young's modulus, and \( I_x \) is the moment of inertia.

Constraints: 1. Shear stress constraint: \( \tau \leq \tau_{max} \), where \( \tau \) is the combined shear stress and \( \tau_{max} \) is the maximum shear stress. 2. Normal stress constraint: \( \sigma_x \leq \sigma_{max} \), where \( \sigma_x \) is the bending stress and \( \sigma_{max} \) is the maximum allowable normal stress. 3. Buckling constraint: \( P \leq P_c \), where \( P_c \) is the critical buckling load. 4. Weld height constraint: \( x_1 \leq x_4 \), ensuring that the weld height \( x_1 \) is less than or equal to the flange thickness \( x_4 \). 5. Wall thickness constraint: \( t \geq h \), where \( t \) is the wall thickness and \( h \) is the outer height.

Where: - \( x_1 \) is the weld height. - \( x_2 \) is the weld length. - \( x_3 \) is the outer height of the beam. - \( x_4 \) is the outer width of the beam. - \( x_5 \) is the wall thickness of the hollow beam.

The parameters are defined as: - \( P = 30000 \, \text{N} \) (load), - \( L = 0.5 \, \text{m} \) (beam length), - \( E = 200 \times 10^9 \, \text{Pa} \) (Young's modulus), - \( \tau_{max} = 95 \times 10^6 \, \text{Pa} \) (max shear stress), - \( \sigma_{max} = 200 \times 10^6 \, \text{Pa} \) (max normal stress), - \( C_w = 209600 \, \text{\$/m}^3 \) (welding cost factor), - \( C_s = 0.7 \, \text{\$/kg} \) (price of HRC steel), - \( \rho_s = 7850 \, \text{kg/m}^3 \) (steel density), - \( C_b = 0.7 \times 7850 \, \text{\$/m}^3 \) (beam material cost factor), - \( K = 2 \) (cantilever beam coefficient), - \( \pi = 3.141592653589793 \) (constant for calculations), - \( \delta_t = 0.045 \) (tolerance factor for calculations).

Returns:

Name Type Description
Problem Problem

An instance of the multiobjective cantilever welded beam optimization problem using a hollow rectangular cross-section.

Source code in desdeo/problem/testproblems/mcwb_problem.py
def mcwb_hollow_rectangular_problem() -> Problem:
    r"""Defines the multiobjective cantilever welded beam (MCWB) optimization problem...

    Defines the multiobjective cantilever welded beam (MCWB) optimization problem using a
    hollow rectangular cross-section.

    The objective functions and constraints for the MCWB design problem are defined as follows:

    Objectives:
    1. Minimize the total cost, \( f_1 = W_c + B_c \), where \( W_c \) is the weld cost and \( B_c \) is the beam cost.
    2. Minimize the deflection of the beam, \( f_2 = \frac{P L^3}{3 E I_x} \), where \( P \) is the applied load,
        \( L \) is the beam length, \( E \) is the Young's modulus, and \( I_x \) is the moment of inertia.

    Constraints:
    1. Shear stress constraint: \( \tau \leq \tau_{max} \), where \( \tau \) is the combined shear stress and
        \( \tau_{max} \) is the maximum shear stress.
    2. Normal stress constraint: \( \sigma_x \leq \sigma_{max} \), where \( \sigma_x \) is the bending stress and
        \( \sigma_{max} \) is the maximum allowable normal stress.
    3. Buckling constraint: \( P \leq P_c \), where \( P_c \) is the critical buckling load.
    4. Weld height constraint: \( x_1 \leq x_4 \), ensuring that the weld height \( x_1 \) is less than or equal to
        the flange thickness \( x_4 \).
    5. Wall thickness constraint: \( t \geq h \), where \( t \) is the wall thickness and \( h \) is the outer height.

    Where:
    - \( x_1 \) is the weld height.
    - \( x_2 \) is the weld length.
    - \( x_3 \) is the outer height of the beam.
    - \( x_4 \) is the outer width of the beam.
    - \( x_5 \) is the wall thickness of the hollow beam.

    The parameters are defined as:
    - \( P = 30000 \, \text{N} \) (load),
    - \( L = 0.5 \, \text{m} \) (beam length),
    - \( E = 200 \times 10^9 \, \text{Pa} \) (Young's modulus),
    - \( \tau_{max} = 95 \times 10^6 \, \text{Pa} \) (max shear stress),
    - \( \sigma_{max} = 200 \times 10^6 \, \text{Pa} \) (max normal stress),
    - \( C_w = 209600 \, \text{\$/m}^3 \) (welding cost factor),
    - \( C_s = 0.7 \, \text{\$/kg} \) (price of HRC steel),
    - \( \rho_s = 7850 \, \text{kg/m}^3 \) (steel density),
    - \( C_b = 0.7 \times 7850 \, \text{\$/m}^3 \) (beam material cost factor),
    - \( K = 2 \) (cantilever beam coefficient),
    - \( \pi = 3.141592653589793 \) (constant for calculations),
    - \( \delta_t = 0.045 \) (tolerance factor for calculations).

    Returns:
        Problem: An instance of the multiobjective cantilever welded beam optimization problem using
            a hollow rectangular cross-section.
    """
    # Constants
    constants = CONSTANTS

    # Variables
    variables = [
        Variable(
            name="x_1", symbol="x_1", variable_type=VariableTypeEnum.real, lowerbound=0.005, upperbound=0.15
        ),  # weld height
        Variable(
            name="x_2", symbol="x_2", variable_type=VariableTypeEnum.real, lowerbound=0.01, upperbound=0.3
        ),  # weld length
        Variable(
            name="x_3", symbol="x_3", variable_type=VariableTypeEnum.real, lowerbound=0.01, upperbound=0.3
        ),  # outer height
        Variable(
            name="x_4", symbol="x_4", variable_type=VariableTypeEnum.real, lowerbound=0.01, upperbound=0.15
        ),  # outer width
        Variable(
            name="x_5", symbol="x_5", variable_type=VariableTypeEnum.real, lowerbound=0.01, upperbound=0.03
        ),  # wall thickness
    ]

    # Extra Functions
    extra_functions = [
        ExtraFunction(name="cross_section_area", symbol="A", func="(x_4 * x_3) - ((x_4 - 2*x_5) * (x_3 - 2*x_5))"),
        ExtraFunction(
            name="moment_of_inertia", symbol="I_x", func="((x_4 * x_3**3)/12) - (((x_4 - 2*x_5) * (x_3 - 2*x_5)**3)/12)"
        ),
        *EXTRAFUNCTION,
    ]

    # Objectives
    objectives = OBJECTIVES

    # Constraints
    constraints = [
        *CONSTRAINTS,
        Constraint(name="g_4", symbol="g_4", cons_type=ConstraintTypeEnum.LTE, func="(x_1 - x_4) / (0.25 - 0.005)"),
        Constraint(
            name="g_5",
            symbol="g_5",
            cons_type=ConstraintTypeEnum.LTE,
            func="(x_4 - x_3) / (0.25 - 0.005)",  # Ensures t >= h
        ),
    ]

    return Problem(
        name="MCWB Hollow Rectangular",
        description="Multiobjective optimization of a welded beam with hollow rectangular cross-section.",
        constants=constants,
        variables=variables,
        extra_funcs=extra_functions,
        objectives=objectives,
        constraints=constraints,
    )

mcwb_ragsdell1976_problem

mcwb_ragsdell1976_problem() -> Problem

Defines the multiobjective cantilever welded beam (MCWB) optimization problem...

Defines the multiobjective cantilever welded beam (MCWB) optimization problem based on the framework proposed by Ragsdell (1976).

This problem involves optimizing the design of a welded cantilever beam considering welding costs, material costs, and stress constraints. The goal is to minimize both the total cost and the deflection of the beam, while adhering to the given constraints on material properties, geometry, and loading conditions.

Objectives: 1. Minimize the total cost, \( f_1 = W_c + B_c \), where: - \( W_c \) is the weld cost, calculated as the sum of welding labor cost and material cost. - \( B_c \) is the beam material cost, based on the beam's dimensions and the cost of the steel used. 2. Minimize the deflection of the beam, \( f_2 = \frac{P L^3}{3 E I_x} \), where: - \( P \) is the applied load. - \( L \) is the beam length. - \( E \) is the Young's modulus. - \( I_x \) is the moment of inertia of the beam's cross-section.

Constraints: 1. Weld height constraint: \( x_1 \leq x_4 \), ensuring that the weld height \( x_1 \) is less than or equal to the beam width \( x_4 \) (flange thickness). 2. The problem also considers the material constraints on maximum shear stress and normal stress, but these are not explicitly listed as constraints in this setup.

Where: - \( x_1 \) is the height of the weld. - \( x_2 \) is the length of the weld. - \( x_3 \) is the height of the beam. - \( x_4 \) is the width of the beam.

Constants: - \( P = 30000 \, \text{N} \) (load), - \( L = 0.5 \, \text{m} \) (beam length), - \( E = 200 \times 10^9 \, \text{Pa} \) (Young's modulus), - \( \tau_{\text{max}} = 95 \times 10^6 \, \text{Pa} \) (maximum shear stress), - \( \sigma_{\text{max}} = 200 \times 10^6 \, \text{Pa} \) (maximum normal stress), - \( C_{\text{wl}} = 1 \, \text{\$/in} \) (welding labor cost), - \( C_{\text{wm}} = 0.10471 \, \text{\$/in} \) (welding material cost), - \( C_{\text{w}} = 1 \times 0.10471 \, \text{\$/in} \) (total welding cost), - \( C_s = 0.7 \, \text{\$/kg} \) (steel price), - \( \rho_s = 7850 \, \text{kg/m}^3 \) (steel density), - \( C_b = 0.04811 \, \text{\$/in} \) (beam material cost), - \( K = 2 \) (cantilever beam coefficient), - \( \pi = 3.141592653589793 \) (constant), - \( \delta_t = 0.05 - 0.005 \) (tolerance factor).

Intermediate Functions: 1. Cross-sectional area: \( A = x_3 \times x_4 \). 2. Moment of inertia: \( I_x = \frac{x_4 \times x_3^3}{12} \), representing the beam's resistance to bending.

Returns:

Name Type Description
Problem Problem

An instance of the multiobjective cantilever welded beam optimization problem based on Ragsdell's method (1976).

Source code in desdeo/problem/testproblems/mcwb_problem.py
def mcwb_ragsdell1976_problem() -> Problem:
    r"""Defines the multiobjective cantilever welded beam (MCWB) optimization problem...

    Defines the multiobjective cantilever welded beam (MCWB) optimization problem
    based on the framework proposed by Ragsdell (1976).

    This problem involves optimizing the design of a welded cantilever beam
    considering welding costs, material costs, and stress constraints. The goal
    is to minimize both the total cost and the deflection of the beam, while
    adhering to the given constraints on material properties, geometry, and
    loading conditions.

    Objectives:
    1. Minimize the total cost, \( f_1 = W_c + B_c \), where:
        - \( W_c \) is the weld cost, calculated as the sum of welding labor cost and material cost.
        - \( B_c \) is the beam material cost, based on the beam's dimensions and the cost of the steel used.
    2. Minimize the deflection of the beam, \( f_2 = \frac{P L^3}{3 E I_x} \), where:
        - \( P \) is the applied load.
        - \( L \) is the beam length.
        - \( E \) is the Young's modulus.
        - \( I_x \) is the moment of inertia of the beam's cross-section.

    Constraints:
    1. **Weld height constraint**: \( x_1 \leq x_4 \), ensuring that the weld height \( x_1 \) is less than or equal
        to the beam width \( x_4 \) (flange thickness).
    2. The problem also considers the material constraints on maximum shear stress and normal stress, but these
        are not explicitly listed as constraints in this setup.

    Where:
    - \( x_1 \) is the height of the weld.
    - \( x_2 \) is the length of the weld.
    - \( x_3 \) is the height of the beam.
    - \( x_4 \) is the width of the beam.

    Constants:
    - \( P = 30000 \, \text{N} \) (load),
    - \( L = 0.5 \, \text{m} \) (beam length),
    - \( E = 200 \times 10^9 \, \text{Pa} \) (Young's modulus),
    - \( \tau_{\text{max}} = 95 \times 10^6 \, \text{Pa} \) (maximum shear stress),
    - \( \sigma_{\text{max}} = 200 \times 10^6 \, \text{Pa} \) (maximum normal stress),
    - \( C_{\text{wl}} = 1 \, \text{\$/in} \) (welding labor cost),
    - \( C_{\text{wm}} = 0.10471 \, \text{\$/in} \) (welding material cost),
    - \( C_{\text{w}} = 1 \times 0.10471 \, \text{\$/in} \) (total welding cost),
    - \( C_s = 0.7 \, \text{\$/kg} \) (steel price),
    - \( \rho_s = 7850 \, \text{kg/m}^3 \) (steel density),
    - \( C_b = 0.04811 \, \text{\$/in} \) (beam material cost),
    - \( K = 2 \) (cantilever beam coefficient),
    - \( \pi = 3.141592653589793 \) (constant),
    - \( \delta_t = 0.05 - 0.005 \) (tolerance factor).

    Intermediate Functions:
    1. **Cross-sectional area**: \( A = x_3 \times x_4 \).
    2. **Moment of inertia**: \( I_x = \frac{x_4 \times x_3^3}{12} \), representing the beam's resistance to bending.

    Returns:
        Problem: An instance of the multiobjective cantilever welded beam optimization problem
            based on Ragsdell's method (1976).
    """
    # Variables (decision variables: weld height, weld length, beam height, beam width)
    variables = [
        Variable(
            name="x_1", symbol="x_1", variable_type=VariableTypeEnum.real, lowerbound=0.125, upperbound=5
        ),  # height of weld [in]
        Variable(
            name="x_2", symbol="x_2", variable_type=VariableTypeEnum.real, lowerbound=0.1, upperbound=10
        ),  # length of weld [in]
        Variable(
            name="x_3", symbol="x_3", variable_type=VariableTypeEnum.real, lowerbound=0.1, upperbound=10
        ),  # height of beam [in]
        Variable(
            name="x_4", symbol="x_4", variable_type=VariableTypeEnum.real, lowerbound=0.1, upperbound=5
        ),  # width of beam [in]
    ]

    constants = [
        Constant(name="P", symbol="P", value=30000),  # Load [N]
        Constant(name="L", symbol="L", value=0.5),  # Beam length [m]
        Constant(name="E", symbol="E", value=200e9),  # Young's modulus [Pa]
        Constant(name="tau_max", symbol="tau_max", value=95e6),  # Max shear stress [Pa]
        Constant(name="sigma_max", symbol="sigma_max", value=200e6),  # Max normal stress [Pa]
        Constant(name="C_wl", symbol="C_wl", value=1),  # Welding labor cost [$/in]
        Constant(name="C_wm", symbol="C_wm", value=0.10471),  # Welding material cost [$/in]
        Constant(name="C_w", symbol="C_w", value=1 * 0.10471),  # Total welding cost [$/in]
        Constant(name="steel_cost", symbol="C_s", value=0.7),  # Price of HRC steel [$/kg]
        Constant(name="steel_density", symbol="rho_s", value=7850),  # Steel density [kg/m^3]
        Constant(name="C_b", symbol="C_b", value=0.04811),  # Beam material cost [$/in]
        Constant(name="K", symbol="K", value=2),  # Cantilever beam coefficient
        Constant(name="pi", symbol="pi", value=3.141592653589793),
        Constant(name="delta_t", symbol="delta_t", value=0.05 - 0.005),
    ]

    # Extra Functions (Intermediate Calculations)
    extra_functions = [
        ExtraFunction(name="cross_section_area", symbol="A", func="x_3 * x_4"),  # A = h * b
        ExtraFunction(name="moment_of_inertia", symbol="I_x", func="(x_4 * x_3**3) / 12"),
        # I_x = (b * h³) / 12
        *EXTRAFUNCTION,
    ]

    # Objectives (minimize cost, minimize deflection)
    objectives = OBJECTIVES

    # NO DUMMY CONSTRAINTS
    # Constraints
    extra_functions = [
        ExtraFunction(name="cross_section_area", symbol="A", func="x_3 * x_4"),  # A = h * b
        ExtraFunction(name="moment_of_inertia", symbol="I_x", func="(x_4 * x_3**3) / 12"),
        # I_x = (b * h³) / 12
        *EXTRAFUNCTION,
    ]

    # Constraints
    constraints = [
        *CONSTRAINTS,
        Constraint(
            name="g_4",
            symbol="g_4",
            cons_type=ConstraintTypeEnum.LTE,
            func="(x_1 - x_4) / (0.25 - 0.005)",  # Ensures x_1 <= x_4 (weld height <= flange thickness)
        ),
    ]

    return Problem(
        name="MCWB Ragsdell1976",
        description=(
            "Optimization of a welded beam based on Ragsdell's method (1976), "
            "considering welding and material costs and stress constraints."
        ),
        constants=constants,
        variables=variables,
        extra_funcs=extra_functions,
        objectives=objectives,
        constraints=constraints,
    )

mcwb_solid_rectangular_problem

mcwb_solid_rectangular_problem() -> Problem

Defines the multiobjective cantilever welded beam (MCWB) optimization problem.

The objective functions and constraints for the MCWB design problem are defined as follows:

Objectives: 1. Minimize the total cost, \( f_1 = W_c + B_c \), where \( W_c \) is the weld cost and \( B_c \) is the beam cost. 2. Minimize the deflection of the beam, \( f_2 = \frac{P L^3}{3 E I_x} \), where \( P \) is the applied load, \( L \) is the beam length, \( E \) is the Young's modulus, and \( I_x \) is the moment of inertia.

Constraints: 1. Shear stress constraint: \( \tau \leq \tau_{max} \), where \( \tau \) is the combined shear stress and \( \tau_{max} \) is the maximum shear stress. 2. Normal stress constraint: \( \sigma_x \leq \sigma_{max} \), where \( \sigma_x \) is the bending stress and \( \sigma_{max} \) is the maximum allowable normal stress. 3. Buckling constraint: \( P \leq P_c \), where \( P_c \) is the critical buckling load. 4. Weld height constraint: \( x_1 \leq x_4 \), ensuring that the weld height \( x_1 \) is less than or equal to the flange thickness \( x_4 \).

Where: - \( x_1 \) is the weld height. - \( x_2 \) is the weld length. - \( x_3 \) is the beam height. - \( x_4 \) is the beam width.

The parameters are defined as: - \( P = 30000 \, \text{N} \) (load), - \( L = 0.5 \, \text{m} \) (beam length), - \( E = 200 \times 10^9 \, \text{Pa} \) (Young's modulus), - \( \tau_{max} = 95 \times 10^6 \, \text{Pa} \) (max shear stress), - \( \sigma_{max} = 200 \times 10^6 \, \text{Pa} \) (max normal stress), - \( C_w = 209600 \, \text{\$/m}^3 \) (welding cost factor), - \( C_s = 0.7 \, \text{\$/kg} \) (price of HRC steel), - \( \rho_s = 7850 \, \text{kg/m}^3 \) (steel density), - \( C_b = 0.7 \times 7850 \, \text{\$/m}^3 \) (beam material cost factor), - \( K = 2 \) (cantilever beam coefficient), - \( \pi = 3.141592653589793 \) (constant for calculations), - \( \delta_t = 0.045 \) (tolerance factor for calculations).

Returns:

Name Type Description
Problem Problem

An instance of the multiobjective cantilever welded beam optimization problem.

Source code in desdeo/problem/testproblems/mcwb_problem.py
def mcwb_solid_rectangular_problem() -> Problem:
    r"""Defines the multiobjective cantilever welded beam (MCWB) optimization problem.

    The objective functions and constraints for the MCWB design problem are defined as follows:

    Objectives:
    1. Minimize the total cost, \( f_1 = W_c + B_c \), where \( W_c \) is the weld cost and \( B_c \) is the beam cost.
    2. Minimize the deflection of the beam, \( f_2 = \frac{P L^3}{3 E I_x} \), where \( P \) is the applied load,
        \( L \) is the beam length, \( E \) is the Young's modulus, and \( I_x \) is the moment of inertia.

    Constraints:
    1. Shear stress constraint: \( \tau \leq \tau_{max} \), where \( \tau \) is the combined shear stress
        and \( \tau_{max} \) is the maximum shear stress.
    2. Normal stress constraint: \( \sigma_x \leq \sigma_{max} \), where \( \sigma_x \) is the bending stress and
        \( \sigma_{max} \) is the maximum allowable normal stress.
    3. Buckling constraint: \( P \leq P_c \), where \( P_c \) is the critical buckling load.
    4. Weld height constraint: \( x_1 \leq x_4 \), ensuring that the weld height \( x_1 \) is less than or equal to
        the flange thickness \( x_4 \).

    Where:
    - \( x_1 \) is the weld height.
    - \( x_2 \) is the weld length.
    - \( x_3 \) is the beam height.
    - \( x_4 \) is the beam width.

    The parameters are defined as:
    - \( P = 30000 \, \text{N} \) (load),
    - \( L = 0.5 \, \text{m} \) (beam length),
    - \( E = 200 \times 10^9 \, \text{Pa} \) (Young's modulus),
    - \( \tau_{max} = 95 \times 10^6 \, \text{Pa} \) (max shear stress),
    - \( \sigma_{max} = 200 \times 10^6 \, \text{Pa} \) (max normal stress),
    - \( C_w = 209600 \, \text{\$/m}^3 \) (welding cost factor),
    - \( C_s = 0.7 \, \text{\$/kg} \) (price of HRC steel),
    - \( \rho_s = 7850 \, \text{kg/m}^3 \) (steel density),
    - \( C_b = 0.7 \times 7850 \, \text{\$/m}^3 \) (beam material cost factor),
    - \( K = 2 \) (cantilever beam coefficient),
    - \( \pi = 3.141592653589793 \) (constant for calculations),
    - \( \delta_t = 0.045 \) (tolerance factor for calculations).

    Returns:
        Problem: An instance of the multiobjective cantilever welded beam optimization problem.
    """
    # Variables
    variables = [
        Variable(
            name="x_1", symbol="x_1", variable_type=VariableTypeEnum.real, lowerbound=0.005, upperbound=0.15
        ),  # height of weld
        Variable(
            name="x_2", symbol="x_2", variable_type=VariableTypeEnum.real, lowerbound=0.01, upperbound=0.3
        ),  # length of weld
        Variable(
            name="x_3", symbol="x_3", variable_type=VariableTypeEnum.real, lowerbound=0.01, upperbound=0.3
        ),  # height of beam
        Variable(
            name="x_4", symbol="x_4", variable_type=VariableTypeEnum.real, lowerbound=0.01, upperbound=0.15
        ),  # width of beam
    ]

    # Constants
    constants = CONSTANTS

    # Extra Functions (Intermediate Calculations)
    extra_functions = [
        ExtraFunction(name="cross_section_area", symbol="A", func="x_3 * x_4"),  # A = h * b
        ExtraFunction(name="moment_of_inertia", symbol="I_x", func="(x_4 * x_3**3) / 12"),  # I_x = (b * h³) / 12
        *EXTRAFUNCTION,
    ]

    # Objectives (minimize cost, minimize deflection)
    objectives = OBJECTIVES

    # NO DUMMY CONSTRAINTS
    # Constraints
    constraints = [
        *CONSTRAINTS,
        Constraint(
            name="g_4",
            symbol="g_4",
            cons_type=ConstraintTypeEnum.LTE,
            func="(x_1 - x_4) / (0.25 - 0.005)",  # Ensures x_1 <= x_4 (weld height <= flange thickness)
        ),
    ]

    return Problem(
        name="MCWB Solid Rectangular",
        description="Multiobjective optimization of a welded beam using a solid rectangular cross-section.",
        constants=constants,
        variables=variables,
        extra_funcs=extra_functions,
        objectives=objectives,
        constraints=constraints,
    )

mcwb_square_channel_problem

mcwb_square_channel_problem() -> Problem

Defines the multiobjective cantilever welded beam (MCWB) optimization problem...

Defines the multiobjective cantilever welded beam (MCWB) optimization problem using a square channel cross-section.

The objective functions and constraints for the MCWB design problem are defined as follows:

Objectives: 1. Minimize the total cost, \( f_1 = W_c + B_c \), where \( W_c \) is the weld cost and \( B_c \) is the beam cost. 2. Minimize the deflection of the beam, \( f_2 = \frac{P L^3}{3 E I_x} \), where \( P \) is the applied load, \( L \) is the beam length, \( E \) is the Young's modulus, and \( I_x \) is the moment of inertia.

Constraints: 1. Weld height constraint: \( x_1 \leq x_4 \), ensuring that the weld height \( x_1 \) is less than or equal to the flange thickness \( x_4 \). 2. Beam width constraint: \( x_4 \geq x_3 \), ensuring that the beam width \( x_4 \) is greater than or equal to the beam height \( x_3 \). 3. Web thickness constraint: \( x_6 \geq \frac{x_3}{2} \), ensuring that the web thickness \( x_6 \) is greater than or equal to half the beam height \( x_3 \). 4. Flange thickness constraint: \( x_5 \geq x_4 \), ensuring that the flange thickness \( x_5 \) is greater than or equal to the beam width \( x_4 \).

Where: - \( x_1 \) is the weld height. - \( x_2 \) is the weld length. - \( x_3 \) is the beam height. - \( x_4 \) is the beam width. - \( x_5 \) is the flange thickness. - \( x_6 \) is the web thickness.

The parameters are defined as: - \( P = 30000 \, \text{N} \) (load), - \( L = 0.5 \, \text{m} \) (beam length), - \( E = 200 \times 10^9 \, \text{Pa} \) (Young's modulus), - \( \tau_{max} = 95 \times 10^6 \, \text{Pa} \) (max shear stress), - \( \sigma_{max} = 200 \times 10^6 \, \text{Pa} \) (max normal stress), - \( C_w = 209600 \, \text{\$/m}^3 \) (welding cost factor), - \( C_s = 0.7 \, \text{\$/kg} \) (price of HRC steel), - \( \rho_s = 7850 \, \text{kg/m}^3 \) (steel density), - \( C_b = 0.7 \times 7850 \, \text{\$/m}^3 \) (beam material cost factor), - \( K = 2 \) (cantilever beam coefficient), - \( \pi = 3.141592653589793 \) (constant for calculations), - \( \delta_t = 0.045 \) (tolerance factor for calculations).

Returns:

Name Type Description
Problem Problem

An instance of the multiobjective cantilever welded beam optimization problem using a square channel cross-section.

Source code in desdeo/problem/testproblems/mcwb_problem.py
def mcwb_square_channel_problem() -> Problem:
    r"""Defines the multiobjective cantilever welded beam (MCWB) optimization problem...

    Defines the multiobjective cantilever welded beam (MCWB) optimization problem using a square channel cross-section.

    The objective functions and constraints for the MCWB design problem are defined as follows:

    Objectives:
    1. Minimize the total cost, \( f_1 = W_c + B_c \), where \( W_c \) is the weld cost and \( B_c \) is the beam cost.
    2. Minimize the deflection of the beam, \( f_2 = \frac{P L^3}{3 E I_x} \), where \( P \) is the applied load,
        \( L \) is the beam length, \( E \) is the Young's modulus, and \( I_x \) is the moment of inertia.

    Constraints:
    1. Weld height constraint: \( x_1 \leq x_4 \), ensuring that the weld height \( x_1 \) is less than or equal to
        the flange thickness \( x_4 \).
    2. Beam width constraint: \( x_4 \geq x_3 \), ensuring that the beam width \( x_4 \) is greater than or equal to the
        beam height \( x_3 \).
    3. Web thickness constraint: \( x_6 \geq \frac{x_3}{2} \), ensuring that the web thickness \( x_6 \) is greater
        than or equal to half the beam height \( x_3 \).
    4. Flange thickness constraint: \( x_5 \geq x_4 \), ensuring that the flange thickness \( x_5 \) is greater than
        or equal to the beam width \( x_4 \).

    Where:
    - \( x_1 \) is the weld height.
    - \( x_2 \) is the weld length.
    - \( x_3 \) is the beam height.
    - \( x_4 \) is the beam width.
    - \( x_5 \) is the flange thickness.
    - \( x_6 \) is the web thickness.

    The parameters are defined as:
    - \( P = 30000 \, \text{N} \) (load),
    - \( L = 0.5 \, \text{m} \) (beam length),
    - \( E = 200 \times 10^9 \, \text{Pa} \) (Young's modulus),
    - \( \tau_{max} = 95 \times 10^6 \, \text{Pa} \) (max shear stress),
    - \( \sigma_{max} = 200 \times 10^6 \, \text{Pa} \) (max normal stress),
    - \( C_w = 209600 \, \text{\$/m}^3 \) (welding cost factor),
    - \( C_s = 0.7 \, \text{\$/kg} \) (price of HRC steel),
    - \( \rho_s = 7850 \, \text{kg/m}^3 \) (steel density),
    - \( C_b = 0.7 \times 7850 \, \text{\$/m}^3 \) (beam material cost factor),
    - \( K = 2 \) (cantilever beam coefficient),
    - \( \pi = 3.141592653589793 \) (constant for calculations),
    - \( \delta_t = 0.045 \) (tolerance factor for calculations).

    Returns:
        Problem: An instance of the multiobjective cantilever welded beam optimization problem
            using a square channel cross-section.
    """
    # Variables
    variables = [
        Variable(
            name="x_1", symbol="x_1", variable_type=VariableTypeEnum.real, lowerbound=0.0005, upperbound=0.15
        ),  # weld height (a)
        Variable(
            name="x_2", symbol="x_2", variable_type=VariableTypeEnum.real, lowerbound=0.01, upperbound=0.3
        ),  # weld length (l)
        Variable(
            name="x_3", symbol="x_3", variable_type=VariableTypeEnum.real, lowerbound=0.01, upperbound=0.25
        ),  # beam height (h)
        Variable(
            name="x_4", symbol="x_4", variable_type=VariableTypeEnum.real, lowerbound=0.01, upperbound=0.15
        ),  # beam width (b)
        Variable(
            name="x_5", symbol="x_5", variable_type=VariableTypeEnum.real, lowerbound=0.0075, upperbound=0.03
        ),  # flange thickness (t)
        Variable(
            name="x_6", symbol="x_6", variable_type=VariableTypeEnum.real, lowerbound=0.0075, upperbound=0.03
        ),  # web thickness (u)
    ]

    # Constants
    constants = CONSTANTS

    # Extra Functions (Intermediate Calculations)
    extra_functions = [
        ExtraFunction(name="cross_section_area", symbol="A", func="(x_3 * x_4) - ((x_4 - x_5) * (x_3 - 2 * x_6))"),
        ExtraFunction(
            name="moment_of_inertia",
            symbol="I_x",
            func="(x_4 * x_3 ** 3) / 12 - ((x_4 - x_5) * (x_3 - 2 * x_6) ** 3) / 12",
        ),
        *EXTRAFUNCTION,
    ]

    # Objectives (minimize cost, minimize deflection, etc.)
    objectives = OBJECTIVES

    # Constraints
    constraints = [
        *CONSTRAINTS,
        # Weld height constraint (g_4)
        Constraint(
            name="g_4",
            symbol="g_4",
            cons_type=ConstraintTypeEnum.LTE,
            func="(x_1 - x_4) / (0.15 - 0.0075)",  # weld height <= flange thickness
        ),
        # Beam width >= weld height (g_5)
        Constraint(
            name="g_5",
            symbol="g_5",
            cons_type=ConstraintTypeEnum.LTE,
            func="(x_4 - x_3) / (0.25 - 0.0075)",  # beam width >= beam height
        ),
        # Cross-section geometric constraints (g_6 and g_7)
        Constraint(
            name="g_6",
            symbol="g_6",
            cons_type=ConstraintTypeEnum.LTE,
            func="(x_6 - x_3 / 2) / (0.03 - 0.0075)",
            # web thickness must be greater than half the beam height (normalized)
        ),
        Constraint(
            name="g_7",
            symbol="g_7",
            cons_type=ConstraintTypeEnum.LTE,
            func="(x_5 - x_4) / (0.03 - 0.0075)",  # flange thickness >= beam width
        ),
    ]

    return Problem(
        name="MCWB Square Channel",
        description=(
            "Multiobjective optimization of a welded square channel beam with constraints on geometry "
            "and load-bearing capacity."
        ),
        constants=constants,
        variables=variables,
        extra_funcs=extra_functions,
        objectives=objectives,
        constraints=constraints,
    )

mcwb_tapered_channel_problem

mcwb_tapered_channel_problem() -> Problem

Defines the multiobjective cantilever welded beam (MCWB) optimization problem...

Defines the multiobjective cantilever welded beam (MCWB) optimization problem using a tapered channel cross-section.

The objective functions and constraints for the MCWB design problem are defined as follows:

Objectives: 1. Minimize the total cost, \( f_1 = W_c + B_c \), where \( W_c \) is the weld cost and \( B_c \) is the beam cost. 2. Minimize the deflection of the beam, \( f_2 = \frac{P L^3}{3 E I_x} \), where \( P \) is the applied load, \( L \) is the beam length, \( E \) is the Young's modulus, and \( I_x \) is the moment of inertia.

Constraints: 1. Weld height constraint: \( x_1 \leq x_5 \), ensuring that the weld height \( x_1 \) is less than or equal to the outer flange thickness \( x_5 \). 2. Beam width constraint: \( x_4 \geq x_3 \), ensuring that the beam width \( x_4 \) is greater than or equal to the beam height \( x_3 \). 3. Inner flange height constraint: \( x_6 \geq \frac{x_3}{2} \), ensuring that the inner flange height \( x_6 \) is greater than or equal to half the beam height \( x_3 \). 4. Web thickness constraint: \( x_7 \leq x_4 \), ensuring that the web thickness \( x_7 \) is less than or equal to the beam width \( x_4 \). 5. Outer flange thickness constraint: \( x_5 \leq x_6 \), ensuring that the outer flange thickness \( x_5 \) is less than or equal to the inner flange thickness \( x_6 \).

Where: - \( x_1 \) is the weld height. - \( x_2 \) is the weld length. - \( x_3 \) is the beam height. - \( x_4 \) is the beam width. - \( x_5 \) is the outer flange thickness. - \( x_6 \) is the inner flange thickness. - \( x_7 \) is the web thickness.

The parameters are defined as: - \( P = 30000 \, \text{N} \) (load), - \( L = 0.5 \, \text{m} \) (beam length), - \( E = 200 \times 10^9 \, \text{Pa} \) (Young's modulus), - \( \tau_{max} = 95 \times 10^6 \, \text{Pa} \) (max shear stress), - \( \sigma_{max} = 200 \times 10^6 \, \text{Pa} \) (max normal stress), - \( C_w = 209600 \, \text{\$/m}^3 \) (welding cost factor), - \( C_s = 0.7 \, \text{\$/kg} \) (price of HRC steel), - \( \rho_s = 7850 \, \text{kg/m}^3 \) (steel density), - \( C_b = 0.7 \times 7850 \, \text{\$/m}^3 \) (beam material cost factor), - \( K = 2 \) (cantilever beam coefficient), - \( \pi = 3.141592653589793 \) (constant for calculations), - \( \delta_t = 0.045 \) (tolerance factor for calculations).

Returns:

Name Type Description
Problem Problem

An instance of the multiobjective cantilever welded beam optimization problem using a tapered channel cross-section.

Source code in desdeo/problem/testproblems/mcwb_problem.py
def mcwb_tapered_channel_problem() -> Problem:
    r"""Defines the multiobjective cantilever welded beam (MCWB) optimization problem...

    Defines the multiobjective cantilever welded beam (MCWB) optimization problem
    using a tapered channel cross-section.

    The objective functions and constraints for the MCWB design problem are defined as follows:

    Objectives:
    1. Minimize the total cost, \( f_1 = W_c + B_c \), where \( W_c \) is the weld cost and \( B_c \) is the beam cost.
    2. Minimize the deflection of the beam, \( f_2 = \frac{P L^3}{3 E I_x} \), where \( P \) is the applied load,
        \( L \) is the beam length, \( E \) is the Young's modulus, and \( I_x \) is the moment of inertia.

    Constraints:
    1. Weld height constraint: \( x_1 \leq x_5 \), ensuring that the weld height \( x_1 \) is less than or equal to the
        outer flange thickness \( x_5 \).
    2. Beam width constraint: \( x_4 \geq x_3 \), ensuring that the beam width \( x_4 \) is greater than or equal to the
        beam height \( x_3 \).
    3. Inner flange height constraint: \( x_6 \geq \frac{x_3}{2} \), ensuring that the inner flange height \( x_6 \) is
        greater than or equal to half the beam height \( x_3 \).
    4. Web thickness constraint: \( x_7 \leq x_4 \), ensuring that the web thickness \( x_7 \) is less than or equal to
        the beam width \( x_4 \).
    5. Outer flange thickness constraint: \( x_5 \leq x_6 \), ensuring that the outer flange thickness \( x_5 \) is less
        than or equal to the inner flange thickness \( x_6 \).

    Where:
    - \( x_1 \) is the weld height.
    - \( x_2 \) is the weld length.
    - \( x_3 \) is the beam height.
    - \( x_4 \) is the beam width.
    - \( x_5 \) is the outer flange thickness.
    - \( x_6 \) is the inner flange thickness.
    - \( x_7 \) is the web thickness.

    The parameters are defined as:
    - \( P = 30000 \, \text{N} \) (load),
    - \( L = 0.5 \, \text{m} \) (beam length),
    - \( E = 200 \times 10^9 \, \text{Pa} \) (Young's modulus),
    - \( \tau_{max} = 95 \times 10^6 \, \text{Pa} \) (max shear stress),
    - \( \sigma_{max} = 200 \times 10^6 \, \text{Pa} \) (max normal stress),
    - \( C_w = 209600 \, \text{\$/m}^3 \) (welding cost factor),
    - \( C_s = 0.7 \, \text{\$/kg} \) (price of HRC steel),
    - \( \rho_s = 7850 \, \text{kg/m}^3 \) (steel density),
    - \( C_b = 0.7 \times 7850 \, \text{\$/m}^3 \) (beam material cost factor),
    - \( K = 2 \) (cantilever beam coefficient),
    - \( \pi = 3.141592653589793 \) (constant for calculations),
    - \( \delta_t = 0.045 \) (tolerance factor for calculations).

    Returns:
        Problem: An instance of the multiobjective cantilever welded beam optimization problem
            using a tapered channel cross-section.
    """
    # Variables
    variables = [
        Variable(
            name="x_1", symbol="x_1", variable_type=VariableTypeEnum.real, lowerbound=0.005, upperbound=0.15
        ),  # weld height (a)
        Variable(
            name="x_2", symbol="x_2", variable_type=VariableTypeEnum.real, lowerbound=0.01, upperbound=0.3
        ),  # weld length (l)
        Variable(
            name="x_3", symbol="x_3", variable_type=VariableTypeEnum.real, lowerbound=0.01, upperbound=0.2
        ),  # beam height (h)
        Variable(
            name="x_4", symbol="x_4", variable_type=VariableTypeEnum.real, lowerbound=0.01, upperbound=0.15
        ),  # beam width (b)
        Variable(
            name="x_5", symbol="x_5", variable_type=VariableTypeEnum.real, lowerbound=0.01, upperbound=0.03
        ),  # flange thickness outer (u)
        Variable(
            name="x_6", symbol="x_6", variable_type=VariableTypeEnum.real, lowerbound=0.01, upperbound=2 * 0.03
        ),  # flange thickness inner (v)
        Variable(
            name="x_7", symbol="x_7", variable_type=VariableTypeEnum.real, lowerbound=0.01, upperbound=0.03
        ),  # web thickness (t)
    ]

    # Constants
    constants = CONSTANTS

    # Extra Functions (Intermediate Calculations)
    extra_functions = [
        # b_inner: the inner width of the flange (b - t)
        ExtraFunction(name="b_inner", symbol="b_inner", func="x_4 - x_5"),
        # A_web: area of the web (h * t)
        ExtraFunction(name="A_web", symbol="A_web", func="x_3 * x_5"),
        # A_flange: area of the flange (b_inner * (u + v))
        ExtraFunction(name="A_flange", symbol="A_flange", func="b_inner * (x_6 + x_7)"),
        # Total cross-sectional area (A = A_web + A_flange)
        ExtraFunction(name="cross_section_area", symbol="A", func="A_web + A_flange"),
        # Moment of inertia for the outer rectangle (Ix_rectangle)
        ExtraFunction(name="Ix_rectangle", symbol="Ix_rectangle", func="(x_4 * x_3 ** 3) / 12"),
        ExtraFunction(name="flange_to_flange_outer", symbol="flange_to_flange_outer", func="x_3 - 2 * x_6"),
        ExtraFunction(name="flange_to_flange_inner", symbol="flange_to_flange_inner", func="x_3 - 2 * x_7"),
        ExtraFunction(
            name="slope_flange",
            symbol="slope_flange",
            func="(flange_to_flange_outer - flange_to_flange_inner) / 2 * (x_4 - x_5)",
        ),
        # Moment of inertia for the outer flange (Ix_flange_outer)
        ExtraFunction(
            name="Ix_flange_outer", symbol="Ix_flange_outer", func="(flange_to_flange_outer ** 4 / 8 * slope_flange)"
        ),
        # Moment of inertia for the inner flange (Ix_flange_inner)
        ExtraFunction(
            name="Ix_flange_inner",
            symbol="Ix_flange_inner",
            func="(flange_to_flange_inner) ** 4 / 8 * slope_flange / 12",
        ),
        ExtraFunction(name="Ix_flange", symbol="Ix_flange", func="Ix_flange_outer - Ix_flange_inner"),
        # Total moment of inertia: Ix = Ix_rectangle + Ix_flange_outer - Ix_flange_inner
        ExtraFunction(name="moment_of_inertia", symbol="I_x", func="Ix_rectangle + Ix_flange"),
        *EXTRAFUNCTION,
    ]

    # Objectives (minimize cost, minimize deflection, etc.)
    objectives = OBJECTIVES

    # Constraints
    constraints = [
        *CONSTRAINTS,
        # Weld height constraint (g_4) - weld height should be less than or equal to flange thickness
        Constraint(
            name="g_4",
            symbol="g_4",
            cons_type=ConstraintTypeEnum.LTE,
            func="(x_1 - x_5) / (0.03 - 0.01)",  # Weld height should be <= outer flange thickness
        ),
        # Beam width constraint (g_5) - beam width should be greater than or equal to beam height
        Constraint(
            name="g_5",
            symbol="g_5",
            cons_type=ConstraintTypeEnum.LTE,
            func="(x_4 - x_3) / (0.2 - 0.01)",  # Beam width should be >= beam height
        ),
        # Cross-section geometric constraints (g_6 and g_7)
        # Inner flange height must be greater than or equal to half the beam height
        Constraint(
            name="g_6",
            symbol="g_6",
            cons_type=ConstraintTypeEnum.LTE,
            func="(x_6 - x_3 / 2) / (0.03 - 0.01)",  # Inner flange thickness must be greater than half the beam height
        ),
        # Web thickness constraint: web thickness should be less than the beam width
        Constraint(
            name="g_7",
            symbol="g_7",
            cons_type=ConstraintTypeEnum.LTE,
            func="(x_7 - x_4) / (0.03 - 0.01)",  # Web thickness should be <= beam width
        ),
        # Outer flange thickness constraint: outer flange thickness must be less than or equal to inner flange thickness
        Constraint(
            name="g_8",
            symbol="g_8",
            cons_type=ConstraintTypeEnum.LTE,
            func="(x_5 - x_6) / (0.03 - 0.01)",  # Outer flange thickness <= inner flange thickness
        ),
    ]

    return Problem(
        name="MCWB Tapered Channel",
        description=(
            "Multiobjective optimization of a welded tapered channel beam with constraints on geometry "
            "and load-bearing capacity."
        ),
        constants=constants,
        variables=variables,
        extra_funcs=extra_functions,
        objectives=objectives,
        constraints=constraints,
    )

mixed_variable_dimensions_problem

mixed_variable_dimensions_problem()

Defines a problem with variables with mixed dimensions.

Has both Variable and TensorVariable types of variables, where the TensorVariables have varying number of dimensions. Mostly for testing purposes.

Source code in desdeo/problem/testproblems/mixed_variable_dimenrions_problem.py
def mixed_variable_dimensions_problem():
    """Defines a problem with variables with mixed dimensions.

    Has both Variable and TensorVariable types of variables, where the TensorVariables have
    varying number of dimensions. Mostly for testing purposes.
    """
    x = Variable(
        name="Regular variable",
        symbol="x",
        variable_type=VariableTypeEnum.real,
        lowerbound=-1.0,
        upperbound=1.0,
        initial_value=0.5,
    )

    y = TensorVariable(
        name="1D vector",
        symbol="Y",
        shape=[5],
        variable_type=VariableTypeEnum.real,
        lowerbounds=5,
        upperbounds=5,
        initial_values=5,
    )

    z = TensorVariable(
        name="2D vector",
        symbol="Z",
        shape=[5, 2],
        variable_type=VariableTypeEnum.real,
        lowerbounds=-100,
        upperbounds=100,
        initial_values=1,
    )

    a = TensorVariable(
        name="2D vector",
        symbol="A",
        shape=[2, 3, 2],
        variable_type=VariableTypeEnum.real,
        lowerbounds=-100,
        upperbounds=100,
        initial_values=1,
    )

    dummy = Objective(
        name="dummy objective, not relevant",
        symbol="f_1",
        func="x - Y[1]",
        maximize=False,
        ideal=-1000,
        nadir=1000,
        is_linear=True,
        is_convex=True,
        is_twice_differentiable=True,
    )

    dummy2 = Objective(
        name="dummy objective, not relevant",
        symbol="f_2",
        func="-x + Y[1]",
        maximize=False,
        ideal=-1000,
        nadir=1000,
        is_linear=True,
        is_convex=True,
        is_twice_differentiable=True,
    )

    return Problem(
        name="Mixed variable dimensions problem",
        description="A problem with variables with mixed dimensions. For testing.",
        variables=[x, y, z, a],
        objectives=[dummy, dummy2],
    )

momip_ti2

momip_ti2() -> Problem

Defines the mixed-integer multiobjective optimization problem test instance 2 (TI2).

The problem has four variables, two continuous and two integer. The Pareto optimal solutions hold for solutions with x_1^2 + x_2^2 = 0.25 and (x_3, x_4) = {(0, -1), (-1, 0)}.

References

Eichfelder, G., Gerlach, T., & Warnow, L. (n.d.). Test Instances for Multiobjective Mixed-Integer Nonlinear Optimization.

Source code in desdeo/problem/testproblems/momip_problem.py
def momip_ti2() -> Problem:
    """Defines the mixed-integer multiobjective optimization problem test instance 2 (TI2).

    The problem has four variables, two continuous and two integer. The Pareto optimal solutions
    hold for solutions with x_1^2 + x_2^2 = 0.25 and (x_3, x_4) = {(0, -1), (-1, 0)}.

    References:
        Eichfelder, G., Gerlach, T., & Warnow, L. (n.d.). Test Instances for
            Multiobjective Mixed-Integer Nonlinear Optimization.
    """
    x_1 = Variable(name="x_1", symbol="x_1", variable_type=VariableTypeEnum.real, lowerbound=-1.0, upperbound=1.0)
    x_2 = Variable(name="x_2", symbol="x_2", variable_type=VariableTypeEnum.real, lowerbound=-1.0, upperbound=1.0)
    x_3 = Variable(name="x_3", symbol="x_3", variable_type=VariableTypeEnum.integer, lowerbound=-1, upperbound=1)
    x_4 = Variable(name="x_4", symbol="x_4", variable_type=VariableTypeEnum.integer, lowerbound=-1, upperbound=1)

    f_1 = Objective(
        name="f_1",
        symbol="f_1",
        func="x_1 + x_3",
        objective_type=ObjectiveTypeEnum.analytical,
        maximize=False,
        is_linear=True,
        is_convex=True,
        is_twice_differentiable=True,
    )
    f_2 = Objective(
        name="f_2",
        symbol="f_2",
        func="x_2 + x_4",
        objective_type=ObjectiveTypeEnum.analytical,
        maximize=False,
        is_linear=True,
        is_convex=True,
        is_twice_differentiable=True,
    )

    con_1 = Constraint(
        name="g_1",
        symbol="g_1",
        cons_type=ConstraintTypeEnum.LTE,
        func="x_1**2 + x_2**2 - 0.25",
        is_linear=False,
        is_convex=False,
        is_twice_differentiable=True,
    )
    con_2 = Constraint(
        name="g_2",
        symbol="g_2",
        cons_type=ConstraintTypeEnum.LTE,
        func="x_3**2 + x_4**2 - 1",
        is_linear=False,
        is_convex=False,
        is_twice_differentiable=True,
    )

    return Problem(
        name="MOMIP Test Instance 2",
        description="Test instance 2",
        variables=[x_1, x_2, x_3, x_4],
        constraints=[con_1, con_2],
        objectives=[f_1, f_2],
    )

momip_ti7

momip_ti7() -> Problem

Defines the mixed-integer multiobjective optimization problem test instance 7 (T7).

The problem is defined as follows:

\[\begin{align} &\min_{\mathbf{x}} & f_1(\mathbf{x}) & = x_1 + x_4 \\ &\max_{\mathbf{x}} & f_2(\mathbf{x}) & = -(x_2 + x_5) \\ &\min_{\mathbf{x}} & f_3(\mathbf{x}) & = x_3 + x_6 \\ &\text{s.t.,} & x_1^2 +x_2^2 + x_3^2 & \leq 1,\\ & & x_4^2 + x_5^2 + x_6^2 & \leq 1,\\ & & -1 \leq x_i \leq 1&\;\text{for}\;i=\{1,2,3\},\\ & & x_i \in \{-1, 0, 1\}&\;\text{for}\;i=\{4, 5, 6\}. \end{align}\]

In the problem, \(x_1, x_2, x_3\) are real-valued and \(x_4, x_5, x_6\) are integer-valued. The problem is convex and differentiable.

The Pareto optimal integer assignments are \((x_4, x_5, x_6) \in {(0,0,-1), (0, -1, 0), (-1,0,0)}\), and the real-valued assignments are \(\{x_1, x_2, x_3 \in \mathbb{R}^3 | x_1^2 + x_2^2 + x_3^2 = 1, x_1 \leq 0, x_2 \leq 0, x_3 \leq 0\}\). Unlike in the original definition, \(f_2\) is formulated to be maximized instead of minimized.

References

Eichfelder, G., Gerlach, T., & Warnow, L. (n.d.). Test Instances for Multiobjective Mixed-Integer Nonlinear Optimization.

Source code in desdeo/problem/testproblems/momip_problem.py
def momip_ti7() -> Problem:
    r"""Defines the mixed-integer multiobjective optimization problem test instance 7 (T7).

    The problem is defined as follows:

    \begin{align}
        &\min_{\mathbf{x}} & f_1(\mathbf{x}) & = x_1 + x_4 \\
        &\max_{\mathbf{x}} & f_2(\mathbf{x}) & = -(x_2 + x_5) \\
        &\min_{\mathbf{x}} & f_3(\mathbf{x}) & = x_3 + x_6 \\
        &\text{s.t.,}   & x_1^2 +x_2^2 + x_3^2 & \leq 1,\\
        &               & x_4^2 + x_5^2 + x_6^2 & \leq 1,\\
        &               & -1 \leq x_i \leq 1&\;\text{for}\;i=\{1,2,3\},\\
        &               & x_i \in \{-1, 0, 1\}&\;\text{for}\;i=\{4, 5, 6\}.
    \end{align}

    In the problem, $x_1, x_2, x_3$ are real-valued and $x_4, x_5, x_6$ are integer-valued. The problem
    is convex and differentiable.

    The Pareto optimal integer assignments are $(x_4, x_5, x_6) \in {(0,0,-1), (0, -1, 0), (-1,0,0)}$,
    and the real-valued assignments are $\{x_1, x_2, x_3 \in \mathbb{R}^3 |
    x_1^2 + x_2^2 + x_3^2 = 1, x_1 \leq 0, x_2 \leq 0, x_3 \leq 0\}$. Unlike in the original definition,
    $f_2$ is formulated to be maximized instead of minimized.

    References:
        Eichfelder, G., Gerlach, T., & Warnow, L. (n.d.). Test Instances for
            Multiobjective Mixed-Integer Nonlinear Optimization.
    """
    x_1 = Variable(name="x_1", symbol="x_1", variable_type=VariableTypeEnum.real)
    x_2 = Variable(name="x_2", symbol="x_2", variable_type=VariableTypeEnum.real)
    x_3 = Variable(name="x_3", symbol="x_3", variable_type=VariableTypeEnum.real)
    x_4 = Variable(name="x_4", symbol="x_4", variable_type=VariableTypeEnum.integer, lowerbound=-1, upperbound=1)
    x_5 = Variable(name="x_5", symbol="x_5", variable_type=VariableTypeEnum.integer, lowerbound=-1, upperbound=1)
    x_6 = Variable(name="x_6", symbol="x_6", variable_type=VariableTypeEnum.integer, lowerbound=-1, upperbound=1)

    f_1 = Objective(
        name="f_1",
        symbol="f_1",
        func="x_1 + x_4",
        objective_type=ObjectiveTypeEnum.analytical,
        ideal=-3,
        nadir=3,
        maximize=False,
        is_linear=True,
        is_convex=True,
        is_twice_differentiable=True,
    )
    f_2 = Objective(
        name="f_2",
        symbol="f_2",
        func="-(x_2 + x_5)",
        objective_type=ObjectiveTypeEnum.analytical,
        ideal=3,
        nadir=-3,
        maximize=True,
        is_linear=True,
        is_convex=True,
        is_twice_differentiable=True,
    )
    f_3 = Objective(
        name="f_3",
        symbol="f_3",
        func="x_3 + x_6",
        objective_type=ObjectiveTypeEnum.analytical,
        ideal=-3,
        nadir=3,
        maximize=False,
        is_linear=True,
        is_convex=True,
        is_twice_differentiable=True,
    )

    con_1 = Constraint(
        name="g_1",
        symbol="g_1",
        cons_type=ConstraintTypeEnum.LTE,
        func="x_1**2 + x_2**2 + x_3**2 - 1",
        is_linear=False,
        is_convex=False,
        is_twice_differentiable=True,
    )
    con_2 = Constraint(
        name="g_2",
        symbol="g_2",
        cons_type=ConstraintTypeEnum.LTE,
        func="x_4**2 + x_5**2 + x_6**2 - 1",
        is_linear=False,
        is_convex=False,
        is_twice_differentiable=True,
    )

    return Problem(
        name="MOMIP Test Instance 7",
        description="Test instance 17",
        variables=[x_1, x_2, x_3, x_4, x_5, x_6],
        constraints=[con_1, con_2],
        objectives=[f_1, f_2, f_3],
    )

multi_valued_constraint_problem

multi_valued_constraint_problem() -> Problem

Defines a test problem with a multi-valued constraint.

The problem has two objectives, two variables, and two constraints, the other of which, is multi-valued. The problem is defined as follows:

Returns:

Name Type Description
Problem Problem

the problem model.

Source code in desdeo/problem/testproblems/multi_valued_constraints.py
def multi_valued_constraint_problem() -> Problem:
    r"""Defines a test problem with a multi-valued constraint.

     The problem has two objectives, two variables, and two constraints, the other of which, is multi-valued.
     The problem is defined as follows:
    \[
         \begin{aligned}
         \text{Min} \quad
         & f_1(x_1, x_2, y) = x_1^2 + x_2^2 + y^2, \\[4pt]
         \text{Min} \quad
         & f_2(x_1, x_2, y) = (x_1 - 2)^2 + (x_2 - 1)^2 + (y - 1)^2, \\[6pt]
         \text{subject to} \quad
         & g(x_1, x_2, y) = x_1^2 + x_2 + y - 2 \le 0, \\[4pt]
         & G(x_1, x_2) = A
         \begin{bmatrix}
         x_1 \\[2pt]
         x_2
         \end{bmatrix}
         \le 0,
         \quad
         A =
         \begin{bmatrix}
         1 & -1 \\[2pt]
         -1 & -2
         \end{bmatrix}.
         \end{aligned}
    \]


    Returns:
         Problem: the problem model.
    """
    xs = TensorVariable(
        name="x",
        symbol="X",
        variable_type=VariableTypeEnum.real,
        shape=[2, 1],
        lowerbounds=-5.0,
        upperbounds=5.0,
        initial_values=0.1,
    )

    y = Variable(
        name="y",
        symbol="y",
        variable_type=VariableTypeEnum.real,
        lowerbound=-10.0,
        upperbound=10.0,
        initial_value=0.1,
    )

    a = TensorConstant(name="A", symbol="A", shape=[2, 2], values=[[1.0, -1.0], [-1.0, -2.0]])

    one = Constant(name="one", symbol="one", value=1.0)

    f_1_expr = "X[1, 1]**2 + X[2, 1]**2 + y**2"
    f_2_expr = "(X[1, 1] - 2)**2 + (X[2, 1] - one)**2 + (y - one)**2"

    g_1_expr = "X[1, 1]**2 + X[2, 1] + y - 2"
    big_g_expr = "A @ X"

    f_1 = Objective(
        name="f1",
        symbol="f_1",
        func=f_1_expr,
        objective_type=ObjectiveTypeEnum.analytical,
        ideal=0.0,
        nadir=150.0,
        is_twice_differentiable=True,
    )

    f_2 = Objective(
        name="f2",
        symbol="f_2",
        func=f_2_expr,
        ideal=0.0,
        nadir=206.0,
        objective_type=ObjectiveTypeEnum.analytical,
        is_twice_differentiable=True,
    )

    g_1 = Constraint(
        name="g1", symbol="g_1", cons_type=ConstraintTypeEnum.LTE, func=g_1_expr, is_twice_differentiable=True
    )

    big_g = Constraint(
        name="big_g",
        symbol="G",
        cons_type=ConstraintTypeEnum.LTE,
        func=big_g_expr,
        is_twice_differentiable=True,
        is_linear=True,
        is_convex=True,
    )

    return Problem(
        name="Multi-valued-constraint problem",
        description="Problem for testing problems with multi-valued constraints.",
        constants=[a, one],
        variables=[xs, y],
        constraints=[g_1, big_g],
        objectives=[f_1, f_2],
    )

nimbus_test_problem

nimbus_test_problem() -> Problem

Defines the test problem utilized in the article describing Synchronous NIMBUS.

Defines the following multiobjective optimization problem:

\[\begin{align} &\max_{\mathbf{x}} & f_1(\mathbf{x}) &= x_1 x_2\\ &\min_{\mathbf{x}} & f_2(\mathbf{x}) &= (x_1 - 4)^2 + x_2^2\\ &\min_{\mathbf{x}} & f_3(\mathbf{x}) &= -x_1 - x_2\\ &\min_{\mathbf{x}} & f_4(\mathbf{x}) &= x_1 - x_2\\ &\min_{\mathbf{x}} & f_5(\mathbf{x}) &= 50 x_1^4 + 10 x_2^4 \\ &\min_{\mathbf{x}} & f_6(\mathbf{x}) &= 30 (x_1 - 5)^4 + 100 (x_2 - 3)^4\\ &\text{s.t.,} && 1 \leq x_i \leq 3\quad i=\{1,2\}, \end{align}\]

with the following ideal point \(\mathbf{z}^\star = \left[9.0, 2.0, -6.0, -2.0, 60.0, 480.0 \right]\) and nadir point \(\mathbf{z}^\text{nad} = \left[ 1.0, 18.0, -2.0, 2.0, 4860.0, 9280.0 \right]\).

References

Miettinen, K., & Mäkelä, M. M. (2006). Synchronous approach in interactive multiobjective optimization. European Journal of Operational Research, 170(3), 909–922. https://doi.org/10.1016/j.ejor.2004.07.052

Returns:

Name Type Description
Problem Problem

the NIMBUS test problem.

Source code in desdeo/problem/testproblems/nimbus_problem.py
def nimbus_test_problem() -> Problem:
    r"""Defines the test problem utilized in the article describing Synchronous NIMBUS.

    Defines the following multiobjective optimization problem:

    \begin{align}
        &\max_{\mathbf{x}} & f_1(\mathbf{x}) &= x_1 x_2\\
        &\min_{\mathbf{x}} & f_2(\mathbf{x}) &= (x_1 - 4)^2 + x_2^2\\
        &\min_{\mathbf{x}} & f_3(\mathbf{x}) &= -x_1 - x_2\\
        &\min_{\mathbf{x}} & f_4(\mathbf{x}) &= x_1 - x_2\\
        &\min_{\mathbf{x}} & f_5(\mathbf{x}) &= 50 x_1^4 + 10 x_2^4 \\
        &\min_{\mathbf{x}} & f_6(\mathbf{x}) &= 30 (x_1 - 5)^4 + 100 (x_2 - 3)^4\\
        &\text{s.t.,}     && 1 \leq x_i \leq 3\quad i=\{1,2\},
    \end{align}

    with the following ideal point
    $\mathbf{z}^\star = \left[9.0, 2.0, -6.0, -2.0, 60.0, 480.0 \right]$ and nadir point
    $\mathbf{z}^\text{nad} = \left[ 1.0, 18.0, -2.0, 2.0, 4860.0, 9280.0 \right]$.

    References:
        Miettinen, K., & Mäkelä, M. M. (2006). Synchronous approach in
            interactive multiobjective optimization. European Journal of Operational
            Research, 170(3), 909–922. https://doi.org/10.1016/j.ejor.2004.07.052


    Returns:
        Problem: the NIMBUS test problem.
    """
    variables = [
        Variable(
            name="x_1",
            symbol="x_1",
            variable_type=VariableTypeEnum.real,
            initial_value=1.0,
            lowerbound=1.0,
            upperbound=3.0,
        ),
        Variable(
            name="x_2",
            symbol="x_2",
            variable_type=VariableTypeEnum.real,
            initial_value=1.0,
            lowerbound=1.0,
            upperbound=3.0,
        ),
    ]

    f_1_expr = "x_1 * x_2"
    f_2_expr = "(x_1 - 4)**2 + x_2**2"
    f_3_expr = "-x_1 - x_2"
    f_4_expr = "x_1 - x_2"
    f_5_expr = "50 * x_1**4 + 10 * x_2**4"
    f_6_expr = "30 * (x_1 - 5)**4 + 100*(x_2 - 3)**4"

    objectives = [
        Objective(
            name="Objective 1",
            symbol="f_1",
            func=f_1_expr,
            maximize=True,
            objective_type=ObjectiveTypeEnum.analytical,
            ideal=9.0,
            nadir=1.0,
            is_convex=False,
            is_linear=False,
            is_twice_differentiable=True,
        ),
        Objective(
            name="Objective 2",
            symbol="f_2",
            func=f_2_expr,
            maximize=False,
            objective_type=ObjectiveTypeEnum.analytical,
            ideal=2.0,
            nadir=18.0,
            is_convex=False,
            is_linear=False,
            is_twice_differentiable=True,
        ),
        Objective(
            name="Objective 3",
            symbol="f_3",
            func=f_3_expr,
            maximize=False,
            objective_type=ObjectiveTypeEnum.analytical,
            ideal=-6.0,
            nadir=-2.0,
            is_convex=True,
            is_linear=True,
            is_twice_differentiable=True,
        ),
        Objective(
            name="Objective 4",
            symbol="f_4",
            func=f_4_expr,
            maximize=False,
            objective_type=ObjectiveTypeEnum.analytical,
            ideal=-2.0,
            nadir=2.0,
            is_convex=True,
            is_linear=True,
            is_twice_differentiable=True,
        ),
        Objective(
            name="Objective 5",
            symbol="f_5",
            func=f_5_expr,
            maximize=False,
            objective_type=ObjectiveTypeEnum.analytical,
            ideal=60.0,
            nadir=4860.0,
            is_convex=False,
            is_linear=False,
            is_twice_differentiable=True,
        ),
        Objective(
            name="Objective 6",
            symbol="f_6",
            func=f_6_expr,
            maximize=False,
            objective_type=ObjectiveTypeEnum.analytical,
            ideal=480.0,
            nadir=9280.0,
            is_convex=False,
            is_linear=False,
            is_twice_differentiable=True,
        ),
    ]

    return Problem(
        name="NIMBUS test problem",
        description="The test problem used in the Synchronous NIMBUS article",
        variables=variables,
        objectives=objectives,
    )

pareto_navigator_test_problem

pareto_navigator_test_problem() -> Problem

Defines the problem utilized in the (convex) Pareto navigator article.

The problem is defined as follows:

\[\begin{align} &\min_{\mathbf{x}} & f_1(\mathbf{x}) & = -x_1 - x_2 + 5 \\ &\min_{\mathbf{x}} & f_2(\mathbf{x}) & = 0.2(x_1^2 -10x_1 + x_2^2 - 4x_2 + 11) \\ &\min_{\mathbf{x}} & f_3(\mathbf{x}) & = (5 - x_1)(x_2 - 11)\\ &\text{s.t.,} & 3x_1 + x_2 - 12 & \leq 0,\\ & & 2x_1 + x_2 - 9 & \leq 0,\\ & & x_1 + 2x_2 - 12 & \leq 0,\\ & & 0 \leq x_1 & \leq 4,\\ & & 0 \leq x_2 & \leq 6. \end{align}\]

The problem comes with seven pre-defined Pareto optimal solutions that were utilized in the original article as well. From these, the ideal and nadir points of the problem are approximated also.

References

Eskelinen, P., Miettinen, K., Klamroth, K., & Hakanen, J. (2010). Pareto navigator for interactive nonlinear multiobjective optimization. OR Spectrum, 32(1), 211-227.

Source code in desdeo/problem/testproblems/pareto_navigator_problem.py
def pareto_navigator_test_problem() -> Problem:
    r"""Defines the problem utilized in the (convex) Pareto navigator article.

    The problem is defined as follows:

    \begin{align}
        &\min_{\mathbf{x}} & f_1(\mathbf{x}) & = -x_1 - x_2 + 5 \\
        &\min_{\mathbf{x}} & f_2(\mathbf{x}) & = 0.2(x_1^2 -10x_1 + x_2^2 - 4x_2 + 11) \\
        &\min_{\mathbf{x}} & f_3(\mathbf{x}) & = (5 - x_1)(x_2 - 11)\\
        &\text{s.t.,}   & 3x_1 + x_2 - 12 & \leq 0,\\
        &               & 2x_1 + x_2 - 9 & \leq 0,\\
        &               & x_1 + 2x_2 - 12 & \leq 0,\\
        &               & 0 \leq x_1 & \leq 4,\\
        &               & 0 \leq x_2 & \leq 6.
    \end{align}

    The problem comes with seven pre-defined Pareto optimal solutions that were
    utilized in the original article as well. From these, the ideal and nadir
    points of the problem are approximated also.

    References:
        Eskelinen, P., Miettinen, K., Klamroth, K., & Hakanen, J. (2010). Pareto
            navigator for interactive nonlinear multiobjective optimization. OR
            Spectrum, 32(1), 211-227.
    """
    x_1 = Variable(name="x_1", symbol="x_1", variable_type=VariableTypeEnum.real, lowerbound=0, upperbound=4)
    x_2 = Variable(name="x_2", symbol="x_2", variable_type=VariableTypeEnum.real, lowerbound=0, upperbound=6)

    f_1 = Objective(
        name="f_1",
        symbol="f_1",
        func="-x_1 - x_2 + 5",
        objective_type=ObjectiveTypeEnum.analytical,
        ideal=-2.0,
        nadir=5.0,
        maximize=False,
    )
    f_2 = Objective(
        name="f_2",
        symbol="f_2",
        func="0.2*(x_1**2 - 10*x_1 + x_2**2 - 4*x_2 + 11)",
        objective_type=ObjectiveTypeEnum.analytical,
        ideal=-3.1,
        nadir=4.60,
        maximize=False,
    )
    f_3 = Objective(
        name="f_3",
        symbol="f_3",
        func="(5 - x_1) * (x_2 - 11)",
        objective_type=ObjectiveTypeEnum.analytical,
        ideal=-55.0,
        nadir=-14.25,
        maximize=False,
    )

    con_1 = Constraint(name="g_1", symbol="g_1", func="3*x_1 + x_2 - 12", cons_type=ConstraintTypeEnum.LTE)
    con_2 = Constraint(name="g_2", symbol="g_2", func="2*x_1 + x_2 - 9", cons_type=ConstraintTypeEnum.LTE)
    con_3 = Constraint(name="g_3", symbol="g_3", func="x_1 + 2*x_2 - 12", cons_type=ConstraintTypeEnum.LTE)

    representation = DiscreteRepresentation(
        variable_values={"x_1": [0, 0, 0, 0, 0, 0, 0], "x_2": [0, 0, 0, 0, 0, 0, 0]},
        objective_values={
            "f_1": [-2.0, -1.0, 0.0, 1.38, 1.73, 2.48, 5.0],
            "f_2": [0.0, 4.6, -3.1, 0.62, 1.72, 1.45, 2.2],
            "f_3": [-18.0, -25.0, -14.25, -35.33, -38.64, -42.41, -55.0],
        },
        non_dominated=True,
    )

    return Problem(
        name="The (convex) Pareto navigator problem.",
        description="The test problem used in the (convex) Pareto navigator paper.",
        variables=[x_1, x_2],
        objectives=[f_1, f_2, f_3],
        constraints=[con_1, con_2, con_3],
        discrete_representation=representation,
    )

re21

re21(
    f: float = 10.0,
    sigma: float = 10.0,
    e: float = 2.0 * 100000.0,
    l: float = 200.0,
) -> Problem

Defines the four bar truss design problem.

The objective functions and constraints for the four bar truss design problem are defined as follows:

\[\begin{align} &\min_{\mathbf{x}} & f_1(\mathbf{x}) & = L(2x_1 + \sqrt{2}x_2 + \sqrt{x_3} + x_4) \\ &\min_{\mathbf{x}} & f_2(\mathbf{x}) & = \frac{FL}{E}\left(\frac{2}{x_1} + \frac{2\sqrt{2}}{x_2} - \frac{2\sqrt{2}}{x_3} + \frac{2}{x_4}\right) \\ \end{align}\]

where \(x_1, x_4 \in [a, 3a]\), \(x_2, x_3 \in [\sqrt{2}a, 3a]\), and \(a = F/\sigma\). The parameters are defined as \(F = 10\) \(kN\), \(E = 2e^5\) \(kN/cm^2\), \(L = 200\) \(cm\), and \(\sigma = 10\) \(kN/cm^2\).

References

Cheng, F. Y., & Li, X. S. (1999). Generalized center method for multiobjective engineering optimization. Engineering Optimization, 31(5), 641-661.

Tanabe, R. & Ishibuchi, H. (2020). An easy-to-use real-world multi-objective optimization problem suite. Applied soft computing, 89, 106078. https://doi.org/10.1016/j.asoc.2020.106078.

https://github.com/ryojitanabe/reproblems/blob/master/reproblem_python_ver/reproblem.py

Parameters:

Name Type Description Default
f float

Force (kN). Defaults to 10.0.

10.0
sigma optional

Stress (kN/cm^2). Defaults to 10.0.

10.0
e float

Young modulus? (kN/cm^2). Defaults to 2.0 * 1e5.

2.0 * 100000.0
l float

Length (cm). Defaults to 200.0.

200.0

Returns:

Name Type Description
Problem Problem

an instance of the four bar truss design problem.

Source code in desdeo/problem/testproblems/re_problem.py
def re21(f: float = 10.0, sigma: float = 10.0, e: float = 2.0 * 1e5, l: float = 200.0) -> Problem:
    r"""Defines the four bar truss design problem.

    The objective functions and constraints for the four bar truss design problem are defined as follows:

    \begin{align}
        &\min_{\mathbf{x}} & f_1(\mathbf{x}) & = L(2x_1 + \sqrt{2}x_2 + \sqrt{x_3} + x_4) \\
        &\min_{\mathbf{x}} & f_2(\mathbf{x}) & = \frac{FL}{E}\left(\frac{2}{x_1} + \frac{2\sqrt{2}}{x_2}
        - \frac{2\sqrt{2}}{x_3} + \frac{2}{x_4}\right) \\
    \end{align}

    where $x_1, x_4 \in [a, 3a]$, $x_2, x_3 \in [\sqrt{2}a, 3a]$, and $a = F/\sigma$.
    The parameters are defined as $F = 10$ $kN$, $E = 2e^5$ $kN/cm^2$, $L = 200$ $cm$, and $\sigma = 10$ $kN/cm^2$.

    References:
        Cheng, F. Y., & Li, X. S. (1999). Generalized center method for multiobjective engineering optimization.
            Engineering Optimization, 31(5), 641-661.

        Tanabe, R. & Ishibuchi, H. (2020). An easy-to-use real-world multi-objective
            optimization problem suite. Applied soft computing, 89, 106078.
            https://doi.org/10.1016/j.asoc.2020.106078.

        https://github.com/ryojitanabe/reproblems/blob/master/reproblem_python_ver/reproblem.py

    Args:
        f (float, optional): Force (kN). Defaults to 10.0.
        sigma (float. optional): Stress (kN/cm^2). Defaults to 10.0.
        e (float, optional): Young modulus? (kN/cm^2). Defaults to 2.0 * 1e5.
        l (float, optional): Length (cm). Defaults to 200.0.

    Returns:
        Problem: an instance of the four bar truss design problem.
    """
    a = f / sigma

    x_1 = Variable(
        name="x_1",
        symbol="x_1",
        variable_type=VariableTypeEnum.real,
        lowerbound=a,
        upperbound=3 * a,
        initial_value=2 * a,
    )
    x_2 = Variable(
        name="x_2",
        symbol="x_2",
        variable_type=VariableTypeEnum.real,
        lowerbound=np.sqrt(2.0) * a,
        upperbound=3 * a,
        initial_value=2 * a,
    )
    x_3 = Variable(
        name="x_3",
        symbol="x_3",
        variable_type=VariableTypeEnum.real,
        lowerbound=np.sqrt(2.0) * a,
        upperbound=3 * a,
        initial_value=2 * a,
    )
    x_4 = Variable(
        name="x_4",
        symbol="x_4",
        variable_type=VariableTypeEnum.real,
        lowerbound=a,
        upperbound=3 * a,
        initial_value=2 * a,
    )

    f_1 = Objective(
        name="f_1",
        symbol="f_1",
        func=f"{l} * ((2 * x_1) + {np.sqrt(2.0)} * x_2 + Sqrt(x_3) + x_4)",
        objective_type=ObjectiveTypeEnum.analytical,
        is_linear=False,
        is_convex=False,  # Not checked
        is_twice_differentiable=True,
    )
    f_2 = Objective(
        name="f_2",
        symbol="f_2",
        func=f"({(f * l) / e} * ((2.0 / x_1) + (2.0 * {np.sqrt(2.0)} / x_2) - "
             f"(2.0 * {np.sqrt(2.0)} / x_3) + (2.0 / x_4)))",
        objective_type=ObjectiveTypeEnum.analytical,
        is_linear=False,
        is_convex=False,  # Not checked
        is_twice_differentiable=True,
    )

    return Problem(
        name="RE21",
        description="the four bar truss design problem",
        variables=[x_1, x_2, x_3, x_4],
        objectives=[f_1, f_2],
    )

re22

re22() -> Problem

The reinforced concrete beam design problem.

The objective functions and constraints for the reinforced concrete beam design problem are defined as follows:

\[\begin{align} &\min_{\mathbf{x}} & f_1(\mathbf{x}) & = 29.4x_1 + 0.6x_2x_3 \\ &\min_{\mathbf{x}} & f_2(\mathbf{x}) & = \sum_{i=1}^2 \max\{g_i(\mathbf{x}), 0\} \\ &\text{s.t.,} & g_1(\mathbf{x}) & = x_1x_3 - 7.735\frac{x_1^2}{x_2} - 180 \geq 0,\\ & & g_2(\mathbf{x}) & = 4 - \frac{x_3}{x_2} \geq 0. \end{align}\]

where \(x_2 \in [0, 20]\) and \(x_3 \in [0, 40]\).

References

Amir, H. M., & Hasegawa, T. (1989). Nonlinear mixed-discrete structural optimization. Journal of Structural Engineering, 115(3), 626-646.

Tanabe, R. & Ishibuchi, H. (2020). An easy-to-use real-world multi-objective optimization problem suite. Applied soft computing, 89, 106078. https://doi.org/10.1016/j.asoc.2020.106078.

https://github.com/ryojitanabe/reproblems/blob/master/reproblem_python_ver/reproblem.py

Returns:

Name Type Description
Problem Problem

an instance of the reinforced concrete beam design problem.

Source code in desdeo/problem/testproblems/re_problem.py
def re22() -> Problem:
    r"""The reinforced concrete beam design problem.

    The objective functions and constraints for the reinforced concrete beam design problem are defined as follows:

    \begin{align}
        &\min_{\mathbf{x}} & f_1(\mathbf{x}) & = 29.4x_1 + 0.6x_2x_3 \\
        &\min_{\mathbf{x}} & f_2(\mathbf{x}) & = \sum_{i=1}^2 \max\{g_i(\mathbf{x}), 0\} \\
        &\text{s.t.,}   & g_1(\mathbf{x}) & = x_1x_3 - 7.735\frac{x_1^2}{x_2} - 180 \geq 0,\\
        & & g_2(\mathbf{x}) & = 4 - \frac{x_3}{x_2} \geq 0.
    \end{align}

    where $x_2 \in [0, 20]$ and $x_3 \in [0, 40]$.

    References:
        Amir, H. M., & Hasegawa, T. (1989). Nonlinear mixed-discrete structural optimization.
            Journal of Structural Engineering, 115(3), 626-646.

        Tanabe, R. & Ishibuchi, H. (2020). An easy-to-use real-world multi-objective
            optimization problem suite. Applied soft computing, 89, 106078.
            https://doi.org/10.1016/j.asoc.2020.106078.

        https://github.com/ryojitanabe/reproblems/blob/master/reproblem_python_ver/reproblem.py

    Returns:
        Problem: an instance of the reinforced concrete beam design problem.
    """
    x_2 = Variable(
        name="x_2", symbol="x_2", variable_type=VariableTypeEnum.real, lowerbound=0, upperbound=20, initial_value=10
    )
    x_3 = Variable(
        name="x_3", symbol="x_3", variable_type=VariableTypeEnum.real, lowerbound=0, upperbound=40, initial_value=20
    )

    # x_1 pre-defined discrete values
    feasible_values = np.array(
        [
            0.20,
            0.31,
            0.40,
            0.44,
            0.60,
            0.62,
            0.79,
            0.80,
            0.88,
            0.93,
            1.0,
            1.20,
            1.24,
            1.32,
            1.40,
            1.55,
            1.58,
            1.60,
            1.76,
            1.80,
            1.86,
            2.0,
            2.17,
            2.20,
            2.37,
            2.40,
            2.48,
            2.60,
            2.64,
            2.79,
            2.80,
            3.0,
            3.08,
            3.10,
            3.16,
            3.41,
            3.52,
            3.60,
            3.72,
            3.95,
            3.96,
            4.0,
            4.03,
            4.20,
            4.34,
            4.40,
            4.65,
            4.74,
            4.80,
            4.84,
            5.0,
            5.28,
            5.40,
            5.53,
            5.72,
            6.0,
            6.16,
            6.32,
            6.60,
            7.11,
            7.20,
            7.80,
            7.90,
            8.0,
            8.40,
            8.69,
            9.0,
            9.48,
            10.27,
            11.0,
            11.06,
            11.85,
            12.0,
            13.0,
            14.0,
            15.0,
        ]
    )

    variables = [x_2, x_3]

    # forming a set of variables and a constraint to make sure x_1 is from the set of feasible values
    x_1_eprs = []
    for i in range(len(feasible_values)):
        x = Variable(
            name=f"x_1_{i}", symbol=f"x_1_{i}", variable_type=VariableTypeEnum.binary, lowerbound=0, upperbound=1
        )
        variables.append(x)
        expr = f"x_1_{i} * {feasible_values[i]}"
        x_1_eprs.append(expr)
    x_1_eprs = " + ".join(x_1_eprs)

    sum_expr = [f"x_1_{i}" for i in range(len(feasible_values))]
    sum_expr = " + ".join(sum_expr) + " - 1"

    x_1_con = Constraint(
        name="x_1_con", symbol="x_1_con", cons_type=ConstraintTypeEnum.EQ, func=sum_expr, is_linear=True
    )

    g_1 = Constraint(
        name="g_1",
        symbol="g_1",
        cons_type=ConstraintTypeEnum.LTE,
        func=f"- (({x_1_eprs}) * x_3 - 7.735 * (({x_1_eprs})**2 / x_2) - 180)",
        is_linear=False,
        is_convex=False,  # Not checked
        is_twice_differentiable=True,
    )
    g_2 = Constraint(
        name="g_2",
        symbol="g_2",
        cons_type=ConstraintTypeEnum.LTE,
        func="-(4 - x_3 / x_2)",
        is_linear=False,
        is_convex=False,  # Not checked
        is_twice_differentiable=True,
    )

    f_1 = Objective(
        name="f_1",
        symbol="f_1",
        func=f"29.4 * ({x_1_eprs}) + 0.6 * x_2 * x_3",
        objective_type=ObjectiveTypeEnum.analytical,
        is_linear=False,
        is_convex=False,  # Not checked
        is_twice_differentiable=True,
    )
    f_2 = Objective(
        name="f_2",
        symbol="f_2",
        func=f"Max(({x_1_eprs}) * x_3 - 7.735 * (({x_1_eprs})**2 / x_2) - 180, 0) + Max(4 - x_3 / x_2, 0)",
        objective_type=ObjectiveTypeEnum.analytical,
        is_linear=False,
        is_convex=False,  # Not checked
        is_twice_differentiable=False,
    )
    return Problem(
        name="re22",
        description="The reinforced concrete beam design problem",
        variables=variables,
        objectives=[f_1, f_2],
        constraints=[g_1, g_2, x_1_con],
    )

re23

re23() -> Problem

The pressure vessel design problem.

The objective functions and constraints for the pressure vessel design problem are defined as follows:

\[\begin{align} &\min_{\mathbf{x}} & f_1(\mathbf{x}) & = 0.6224x_1x_3x_4 + 1.7781x_2x_3^2 + 3.1661x_1^2x_4 + 19.84x_1^2x_3 \\ &\min_{\mathbf{x}} & f_2(\mathbf{x}) & = \sum_{i=1}^3 \max\{g_i(\mathbf{x}), 0\} \\ &\text{s.t.,} & g_1(\mathbf{x}) & = -x_1 + 0.0193x_3 \leq 0,\\ & & g_2(\mathbf{x}) & = -x_2 + 0.00954x_3 \leq 0, \\ & & g_3(\mathbf{x}) & = -\pi x_3^2x_4 - \frac{4}{3}\pi x_3^3 + 1\,296\,000 \leq 0. \end{align}\]
where $x_1, x_2 \in \{1,\dots,100\}$, $x_3 \in [10, 200]$, and $x_4 \in [10, 240]$. $x_1$ and $x_2$ are
integer multiples of 0.0625. $x_1$, $x_2$, $x_3$, and $x_4$ represent the thicknesses of
the shell, the head of a pressure vessel, the inner radius, and the length of
the cylindrical section, respectively. We determined the ranges of $x_2$ and $x_3$
according to [S.3].
References

Kannan, B. K., & Kramer, S. N. (1994). An augmented Lagrange multiplier based method for mixed integer discrete continuous optimization and its applications to mechanical design.

Tanabe, R. & Ishibuchi, H. (2020). An easy-to-use real-world multi-objective optimization problem suite. Applied soft computing, 89, 106078. https://doi.org/10.1016/j.asoc.2020.106078.

https://github.com/ryojitanabe/reproblems/blob/master/reproblem_python_ver/reproblem.py

Returns:

Name Type Description
Problem Problem

an instance of the pressure vessel design problem.

Source code in desdeo/problem/testproblems/re_problem.py
def re23() -> Problem:
    r"""The pressure vessel design problem.

    The objective functions and constraints for the pressure vessel design problem are defined as follows:

    \begin{align}
        &\min_{\mathbf{x}} & f_1(\mathbf{x}) & = 0.6224x_1x_3x_4 + 1.7781x_2x_3^2 + 3.1661x_1^2x_4 + 19.84x_1^2x_3 \\
        &\min_{\mathbf{x}} & f_2(\mathbf{x}) & = \sum_{i=1}^3 \max\{g_i(\mathbf{x}), 0\} \\
        &\text{s.t.,}   & g_1(\mathbf{x}) & = -x_1 + 0.0193x_3 \leq 0,\\
        & & g_2(\mathbf{x}) & = -x_2 + 0.00954x_3 \leq 0, \\
        & & g_3(\mathbf{x}) & = -\pi x_3^2x_4 - \frac{4}{3}\pi x_3^3 + 1\,296\,000 \leq 0.
    \end{align}

        where $x_1, x_2 \in \{1,\dots,100\}$, $x_3 \in [10, 200]$, and $x_4 \in [10, 240]$. $x_1$ and $x_2$ are
        integer multiples of 0.0625. $x_1$, $x_2$, $x_3$, and $x_4$ represent the thicknesses of
        the shell, the head of a pressure vessel, the inner radius, and the length of
        the cylindrical section, respectively. We determined the ranges of $x_2$ and $x_3$
        according to [S.3].

    References:
        Kannan, B. K., & Kramer, S. N. (1994). An augmented Lagrange multiplier based method
            for mixed integer discrete continuous optimization and its applications to mechanical design.

        Tanabe, R. & Ishibuchi, H. (2020). An easy-to-use real-world multi-objective
            optimization problem suite. Applied soft computing, 89, 106078.
            https://doi.org/10.1016/j.asoc.2020.106078.

        https://github.com/ryojitanabe/reproblems/blob/master/reproblem_python_ver/reproblem.py

    Returns:
        Problem: an instance of the pressure vessel design problem.
    """
    x_1 = Variable(name="x_1", symbol="x_1", variable_type=VariableTypeEnum.integer, lowerbound=1, upperbound=100)
    x_2 = Variable(name="x_2", symbol="x_2", variable_type=VariableTypeEnum.integer, lowerbound=1, upperbound=100)
    x_3 = Variable(name="x_3", symbol="x_3", variable_type=VariableTypeEnum.real, lowerbound=10, upperbound=200)
    x_4 = Variable(name="x_4", symbol="x_4", variable_type=VariableTypeEnum.real, lowerbound=10, upperbound=240)

    # variables x_1 and x_2 are integer multiples of 0.0625
    x_1_exprs = "(0.0625 * x_1)"
    x_2_exprs = "(0.0625 * x_2)"

    g_1 = Constraint(
        name="g_1",
        symbol="g_1",
        cons_type=ConstraintTypeEnum.LTE,
        func=f"-({x_1_exprs} - 0.0193 * x_3)",
        is_linear=True,
        is_convex=False,  # Not checked
        is_twice_differentiable=True,
    )
    g_2 = Constraint(
        name="g_2",
        symbol="g_2",
        cons_type=ConstraintTypeEnum.LTE,
        func=f"-({x_2_exprs} - 0.00954 * x_3)",
        is_linear=True,
        is_convex=False,  # Not checked
        is_twice_differentiable=True,
    )
    g_3 = Constraint(
        name="g_3",
        symbol="g_3",
        cons_type=ConstraintTypeEnum.LTE,
        func=f"-({np.pi} * x_3**2 * x_4 + (4/3) * {np.pi} * x_3**3 - 1296000)",
        is_linear=False,
        is_convex=False,  # Not checked
        is_twice_differentiable=True,
    )

    f_1 = Objective(
        name="f_1",
        symbol="f_1",
        func=f"0.6224 * {x_1_exprs} * x_3 * x_4 + (1.7781 * {x_2_exprs} * x_3**2) + "
             f"(3.1661 * {x_1_exprs}**2 * x_4) + (19.84 * {x_1_exprs}**2 * x_3)",
        objective_type=ObjectiveTypeEnum.analytical,
        is_linear=False,
        is_convex=False,  # Not checked
        is_twice_differentiable=True,
    )
    f_2 = Objective(
        name="f_2",
        symbol="f_2",
        func=f"Max({x_1_exprs} - 0.0193 * x_3, 0) + Max({x_2_exprs} - 0.00954 * x_3, 0) + "
             f"Max({np.pi} * x_3**2 * x_4 + (4/3) * {np.pi} * x_3**3 - 1296000, 0)",
        objective_type=ObjectiveTypeEnum.analytical,
        is_linear=False,
        is_convex=False,  # Not checked
        is_twice_differentiable=False,
    )
    return Problem(
        name="re23",
        description="The pressure vessel design problem",
        variables=[x_1, x_2, x_3, x_4],
        objectives=[f_1, f_2],
        constraints=[g_1, g_2, g_3],
    )

re24

re24() -> Problem

The hatch cover design problem.

The objective functions and constraints for the hatch cover design problem are defined as follows:

\[\begin{align} &\min_{\mathbf{x}} & f_1(\mathbf{x}) & = x_1 + 120x_2 \\ &\min_{\mathbf{x}} & f_2(\mathbf{x}) & = \sum_{i=1}^4 \max\{g_i(\mathbf{x}), 0\} \\ &\text{s.t.,} & g_1(\mathbf{x}) & = 1.0 - \frac{\sigma_b}{\sigma_{b,max}} \geq 0,\\ & & g_2(\mathbf{x}) & = 1.0 - \frac{\tau}{\tau_{max}} \geq 0, \\ & & g_3(\mathbf{x}) & = 1.0 - \frac{\delta}{\delta_{max}} \geq 0, \\ & & g_4(\mathbf{x}) & = 1.0 - \frac{\sigma_b}{\sigma_{k}} \geq 0, \end{align}\]

where \(x_1 \in [0.5, 4]\) and \(x_2 \in [4, 50]\). The parameters are defined as \(\sigma_{b,max} = 700 kg/cm^2\), \(\tau_{max} = 450 kg/cm\), \(\delta_{max} = 1.5 cm\), \(\sigma_k = Ex_1^2/100 kg/cm^2\), \(\sigma_b = 4500/(x_1x_2) kg/cm^2\), \(\tau = 1800/x_2 kg/cm^2\), \(\delta = 56.2 \times 10^4/(Ex_1x_2^2)\), and \(E = 700\,000 kg/cm^2\).

References

Amir, H. M., & Hasegawa, T. (1989). Nonlinear mixed-discrete structural optimization. Journal of Structural Engineering, 115(3), 626-646.

Tanabe, R. & Ishibuchi, H. (2020). An easy-to-use real-world multi-objective optimization problem suite. Applied soft computing, 89, 106078. https://doi.org/10.1016/j.asoc.2020.106078.

https://github.com/ryojitanabe/reproblems/blob/master/reproblem_python_ver/reproblem.py

Returns:

Name Type Description
Problem Problem

an instance of the hatch cover design problem.

Source code in desdeo/problem/testproblems/re_problem.py
def re24() -> Problem:
    r"""The hatch cover design problem.

    The objective functions and constraints for the hatch cover design problem are defined as follows:

    \begin{align}
        &\min_{\mathbf{x}} & f_1(\mathbf{x}) & = x_1 + 120x_2 \\
        &\min_{\mathbf{x}} & f_2(\mathbf{x}) & = \sum_{i=1}^4 \max\{g_i(\mathbf{x}), 0\} \\
        &\text{s.t.,}   & g_1(\mathbf{x}) & = 1.0 - \frac{\sigma_b}{\sigma_{b,max}} \geq 0,\\
        & & g_2(\mathbf{x}) & = 1.0 - \frac{\tau}{\tau_{max}} \geq 0, \\
        & & g_3(\mathbf{x}) & = 1.0 - \frac{\delta}{\delta_{max}} \geq 0, \\
        & & g_4(\mathbf{x}) & = 1.0 - \frac{\sigma_b}{\sigma_{k}} \geq 0,
    \end{align}

    where $x_1 \in [0.5, 4]$ and $x_2 \in [4, 50]$. The parameters are defined as $\sigma_{b,max} = 700 kg/cm^2$,
    $\tau_{max} = 450 kg/cm$, $\delta_{max} = 1.5 cm$, $\sigma_k = Ex_1^2/100 kg/cm^2$,
    $\sigma_b = 4500/(x_1x_2) kg/cm^2$, $\tau = 1800/x_2 kg/cm^2$, $\delta = 56.2 \times 10^4/(Ex_1x_2^2)$,
    and $E = 700\,000 kg/cm^2$.

    References:
        Amir, H. M., & Hasegawa, T. (1989). Nonlinear mixed-discrete structural optimization.
            Journal of Structural Engineering, 115(3), 626-646.

        Tanabe, R. & Ishibuchi, H. (2020). An easy-to-use real-world multi-objective
            optimization problem suite. Applied soft computing, 89, 106078.
            https://doi.org/10.1016/j.asoc.2020.106078.

        https://github.com/ryojitanabe/reproblems/blob/master/reproblem_python_ver/reproblem.py

    Returns:
        Problem: an instance of the hatch cover design problem.
    """
    x_1 = Variable(name="x_1", symbol="x_1", variable_type=VariableTypeEnum.real, lowerbound=0.5, upperbound=4)
    x_2 = Variable(name="x_2", symbol="x_2", variable_type=VariableTypeEnum.real, lowerbound=4, upperbound=50)

    sigma_b = "(4500 / (x_1 * x_2))"
    sigma_k = "((700000 * x_1**2) / 100)"
    tau = "(1800 / x_2)"
    delta = "(56.2 * 10**4 / (700000 * x_1 * x_2**2))"

    g_1 = Constraint(
        name="g_1",
        symbol="g_1",
        cons_type=ConstraintTypeEnum.LTE,
        func=f"-(1 - {sigma_b} / 700)",
        is_linear=False,
        is_convex=False,  # Not checked
        is_twice_differentiable=True,
    )
    g_2 = Constraint(
        name="g_2",
        symbol="g_2",
        cons_type=ConstraintTypeEnum.LTE,
        func=f"-(1 - {tau} / 450)",
        is_linear=False,
        is_convex=False,  # Not checked
        is_twice_differentiable=True,
    )
    g_3 = Constraint(
        name="g_3",
        symbol="g_3",
        cons_type=ConstraintTypeEnum.LTE,
        func=f"-(1 - {delta} / 1.5)",
        is_linear=False,
        is_convex=False,  # Not checked
        is_twice_differentiable=True,
    )
    g_4 = Constraint(
        name="g_4",
        symbol="g_4",
        cons_type=ConstraintTypeEnum.LTE,
        func=f"-(1 - {sigma_b} / {sigma_k})",
        is_linear=False,
        is_convex=False,  # Not checked
        is_twice_differentiable=True,
    )

    f_1 = Objective(
        name="f_1",
        symbol="f_1",
        func="x_1 + 120 * x_2",
        objective_type=ObjectiveTypeEnum.analytical,
        is_linear=True,
        is_convex=False,  # Not checked
        is_twice_differentiable=True,
    )
    f_2 = Objective(
        name="f_2",
        symbol="f_2",
        func=f"Max(-(1 - {sigma_b} / 700), 0) + Max(-(1 - {tau} / 450), 0) + "
             f"Max(-(1 - {delta} / 1.5), 0) + Max(-(1 - {sigma_b} / {sigma_k}), 0)",
        objective_type=ObjectiveTypeEnum.analytical,
        is_linear=False,
        is_convex=False,  # Not checked
        is_twice_differentiable=False,
    )
    return Problem(
        name="re24",
        description="The hatch cover design problem",
        variables=[x_1, x_2],
        objectives=[f_1, f_2],
        constraints=[g_1, g_2, g_3, g_4],
    )

river_pollution_problem

river_pollution_problem(
    *, five_objective_variant: bool = True
) -> Problem

Create a pydantic dataclass representation of the river pollution problem with either five or four variables.

The objective functions "DO city" (\(f_1\)), "DO municipality" (\(f_2), and "ROI fishery" (\)f_3\() and "ROI city" (\)f_4\() are to be maximized. If the four variant problem is used, the the "BOD deviation" objective function (\)f_5$) is not present, but if it is, it is to be minimized. The problem is defined as follows:

\[\begin{align*} \max f_1(x) &= 4.07 + 2.27 x_1 \\ \max f_2(x) &= 2.60 + 0.03 x_1 + 0.02 x_2 + \frac{0.01}{1.39 - x_1^2} + \frac{0.30}{1.39 - x_2^2} \\ \max f_3(x) &= 8.21 - \frac{0.71}{1.09 - x_1^2} \\ \max f_4(x) &= 0.96 - \frac{0.96}{1.09 - x_2^2} \\ \min f_5(x) &= \max(|x_1 - 0.65|, |x_2 - 0.65|) \\ \text{s.t.}\quad & 0.3 \leq x_1 \leq 1.0,\\ & 0.3 \leq x_2 \leq 1.0,\\ \end{align*}\]

where the fifth objective is part of the problem definition only if five_objective_variant = True.

Parameters:

Name Type Description Default
five_objective_variant bool

Whether to use to five objective function variant of the problem or not. Defaults to True.

True

Returns:

Name Type Description
Problem Problem

the river pollution problem.

References

Narula, Subhash C., and HRoland Weistroffer. "A flexible method for nonlinear multicriteria decision-making problems." IEEE Transactions on Systems, Man, and Cybernetics 19.4 (1989): 883-887.

Miettinen, Kaisa, and Marko M. Mäkelä. "Interactive method NIMBUS for nondifferentiable multiobjective optimization problems." Multicriteria Analysis: Proceedings of the XIth International Conference on MCDM, 1-6 August 1994, Coimbra, Portugal. Berlin, Heidelberg: Springer Berlin Heidelberg, 1997.

Source code in desdeo/problem/testproblems/river_pollution_problems.py
def river_pollution_problem(*, five_objective_variant: bool = True) -> Problem:
    r"""Create a pydantic dataclass representation of the river pollution problem with either five or four variables.

    The objective functions "DO city" ($f_1$), "DO municipality" ($f_2), and
    "ROI fishery" ($f_3$) and "ROI city" ($f_4$) are to be
    maximized. If the four variant problem is used, the the "BOD deviation" objective
    function ($f_5$) is not present, but if it is, it is to be minimized.
    The problem is defined as follows:

    \begin{align*}
    \max f_1(x) &= 4.07 + 2.27 x_1 \\
    \max f_2(x) &= 2.60 + 0.03 x_1 + 0.02 x_2 + \frac{0.01}{1.39 - x_1^2} + \frac{0.30}{1.39 - x_2^2} \\
    \max f_3(x) &= 8.21 - \frac{0.71}{1.09 - x_1^2} \\
    \max f_4(x) &= 0.96 - \frac{0.96}{1.09 - x_2^2} \\
    \min f_5(x) &= \max(|x_1 - 0.65|, |x_2 - 0.65|) \\
    \text{s.t.}\quad    & 0.3 \leq x_1 \leq 1.0,\\
                        & 0.3 \leq x_2 \leq 1.0,\\
    \end{align*}

    where the fifth objective is part of the problem definition only if
    `five_objective_variant = True`.

    Args:
        five_objective_variant (bool, optional): Whether to use to five
            objective function variant of the problem or not. Defaults to True.

    Returns:
        Problem: the river pollution problem.

    References:
        Narula, Subhash C., and HRoland Weistroffer. "A flexible method for
            nonlinear multicriteria decision-making problems." IEEE Transactions on
            Systems, Man, and Cybernetics 19.4 (1989): 883-887.

        Miettinen, Kaisa, and Marko M. Mäkelä. "Interactive method NIMBUS for
            nondifferentiable multiobjective optimization problems." Multicriteria
            Analysis: Proceedings of the XIth International Conference on MCDM, 1-6
            August 1994, Coimbra, Portugal. Berlin, Heidelberg: Springer Berlin
            Heidelberg, 1997.
    """
    variable_1 = Variable(
        name="BOD", symbol="x_1", variable_type="real", lowerbound=0.3, upperbound=1.0, initial_value=0.65
    )
    variable_2 = Variable(
        name="DO", symbol="x_2", variable_type="real", lowerbound=0.3, upperbound=1.0, initial_value=0.65
    )

    f_1 = "4.07 + 2.27 * x_1"
    f_2 = "2.60 + 0.03 * x_1 + 0.02 * x_2 + 0.01 / (1.39 - x_1**2) + 0.30 / (1.39 - x_2**2)"
    f_3 = "8.21 - 0.71 / (1.09 - x_1**2)"
    f_4 = "0.96 - 0.96 / (1.09 - x_2**2)"
    f_5 = "Max(Abs(x_1 - 0.65), Abs(x_2 - 0.65))"

    objective_1 = Objective(
        name="DO city",
        symbol="f_1",
        func=f_1,
        maximize=True,
        ideal=6.34,
        nadir=4.75,
        is_convex=True,
        is_linear=True,
        is_twice_differentiable=True,
    )
    objective_2 = Objective(
        name="DO municipality",
        symbol="f_2",
        func=f_2,
        maximize=True,
        ideal=3.44,
        nadir=2.85,
        is_convex=False,
        is_linear=False,
        is_twice_differentiable=True,
    )
    objective_3 = Objective(
        name="ROI fishery",
        symbol="f_3",
        func=f_3,
        maximize=True,
        ideal=7.5,
        nadir=0.32,
        is_convex=True,
        is_linear=False,
        is_twice_differentiable=True,
    )
    objective_4 = Objective(
        name="ROI city",
        symbol="f_4",
        func=f_4,
        maximize=True,
        ideal=0,
        nadir=-9.70,
        is_convex=True,
        is_linear=False,
        is_twice_differentiable=True,
    )
    objective_5 = Objective(
        name="BOD deviation",
        symbol="f_5",
        func=f_5,
        maximize=False,
        ideal=0,
        nadir=0.35,
        is_convex=False,
        is_linear=False,
        is_twice_differentiable=False,
    )

    objectives = (
        [objective_1, objective_2, objective_3, objective_4, objective_5]
        if five_objective_variant
        else [objective_1, objective_2, objective_3, objective_4]
    )

    return Problem(
        name="The river pollution problem",
        description="The river pollution problem to maximize return of investments (ROI) and dissolved oxygen (DO).",
        variables=[variable_1, variable_2],
        objectives=objectives,
    )

river_pollution_problem_discrete

river_pollution_problem_discrete(
    *, five_objective_variant: bool = True
) -> Problem

Create a pydantic dataclass representation of the river pollution problem with either five or four variables.

The objective functions "DO city" (\(f_1\)), "DO municipality" (\(f_2), and "ROI fishery" (\)f_3\() and "ROI city" (\)f_4\() are to be maximized. If the four variant problem is used, the the "BOD deviation" objective function (\)f_5$) is not present, but if it is, it is to be minimized. This version of the problem uses discrete representation of the variables and objectives and does not provide the analytical functions for the objectives.

Parameters:

Name Type Description Default
five_objective_variant bool

Whether to use to five objective function variant of the problem or not. Defaults to True.

True

Returns:

Name Type Description
Problem Problem

the river pollution problem.

References

Narula, Subhash C., and HRoland Weistroffer. "A flexible method for nonlinear multicriteria decision-making problems." IEEE Transactions on Systems, Man, and Cybernetics 19.4 (1989): 883-887.

Miettinen, Kaisa, and Marko M. Mäkelä. "Interactive method NIMBUS for nondifferentiable multiobjective optimization problems." Multicriteria Analysis: Proceedings of the XIth International Conference on MCDM, 1-6 August 1994, Coimbra, Portugal. Berlin, Heidelberg: Springer Berlin Heidelberg, 1997.

Source code in desdeo/problem/testproblems/river_pollution_problems.py
def river_pollution_problem_discrete(*, five_objective_variant: bool = True) -> Problem:
    """Create a pydantic dataclass representation of the river pollution problem with either five or four variables.

    The objective functions "DO city" ($f_1$), "DO municipality" ($f_2), and
    "ROI fishery" ($f_3$) and "ROI city" ($f_4$) are to be
    maximized. If the four variant problem is used, the the "BOD deviation" objective
    function ($f_5$) is not present, but if it is, it is to be minimized.
    This version of the problem uses discrete representation of the variables and objectives and does not provide
    the analytical functions for the objectives.

    Args:
        five_objective_variant (bool, optional): Whether to use to five
            objective function variant of the problem or not. Defaults to True.

    Returns:
        Problem: the river pollution problem.

    References:
        Narula, Subhash C., and HRoland Weistroffer. "A flexible method for
            nonlinear multicriteria decision-making problems." IEEE Transactions on
            Systems, Man, and Cybernetics 19.4 (1989): 883-887.

        Miettinen, Kaisa, and Marko M. Mäkelä. "Interactive method NIMBUS for
            nondifferentiable multiobjective optimization problems." Multicriteria
            Analysis: Proceedings of the XIth International Conference on MCDM, 1-6
            August 1994, Coimbra, Portugal. Berlin, Heidelberg: Springer Berlin
            Heidelberg, 1997.
    """
    filename = "datasets/river_poll_4_objs.csv"
    true_var_names = {"x_1": "BOD", "x_2": "DO"}
    true_obj_names = {"f1": "DO city", "f2": "DO municipality", "f3": "ROI fishery", "f4": "ROI city"}
    if five_objective_variant:
        filename = "datasets/river_poll_5_objs.csv"
        true_obj_names["f5"] = "BOD deviation"

    path = Path(__file__).parent.parent.parent.parent / filename
    data = pl.read_csv(path, has_header=True)

    variables = [
        Variable(
            name=true_var_names[varName],
            symbol=varName,
            variable_type=VariableTypeEnum.real,
            lowerbound=0.3,
            upperbound=1.0,
            initial_value=0.65,
        )
        for varName in true_var_names
    ]
    maximize = {"f1": True, "f2": True, "f3": True, "f4": True, "f5": False}
    ideal = {objName: (data[objName].max() if maximize[objName] else data[objName].min()) for objName in true_obj_names}
    nadir = {objName: (data[objName].min() if maximize[objName] else data[objName].max()) for objName in true_obj_names}
    units = {"f1": "mg/L", "f2": "mg/L", "f3": "%", "f4": "%", "f5": "mg/L"}

    objectives = [
        Objective(
            name=true_obj_names[objName],
            symbol=objName,
            func=None,
            unit=units[objName],
            objective_type=ObjectiveTypeEnum.data_based,
            maximize=maximize[objName],
            ideal=ideal[objName],
            nadir=nadir[objName],
        )
        for objName in true_obj_names
    ]

    discrete_def = DiscreteRepresentation(
        variable_values=data[list(true_var_names.keys())].to_dict(),
        objective_values=data[list(true_obj_names.keys())].to_dict(),
    )

    return Problem(
        name="The river pollution problem (Discrete)",
        description="The river pollution problem to maximize return of investments (ROI) and dissolved oxygen (DO).",
        variables=variables,
        objectives=objectives,
        discrete_representation=discrete_def,
    )

river_pollution_scenario

river_pollution_scenario() -> Problem

Defines the scenario-based uncertain variant of the river pollution problem.

The river pollution problem considers a river close to a city. There are two sources of pollution: industrial pollution from a fishery and municipal waste from the city. Two treatment plants (in the fishery and the city) are responsible for managing the pollution. Pollution is reported in pounds of biochemical oxygen demanding material (BOD), and water quality is measured in dissolved oxygen concentration (DO).

Cleaning water in the city increases the tax rate, and cleaning in the fishery reduces the return on investment. The problem is to improve the DO level in the city and at the municipality border (f1 and f2, respectively), while, at the same time, maximizing the percent return on investment at the fishery (f3) and minimizing additions to the city tax (f4).

Decision variables are:

  • x1: The proportional amount of BOD removed from water after the fishery (treatment plant 1).
  • x2: The proportional amount of BOD removed from water after the city (treatment plant 2).

The original problem considered specific values for all parameters. However, in this formulation, some parameters are deeply uncertain, and only a range of plausible values is known for each. These deeply uncertain parameters are as follows:

  • α ∈ [3, 4.24]: Water quality index after the fishery.
  • β ∈ [2.25, 2.4]: BOD reduction rate at treatment plant 1 (after the fishery).
  • δ ∈ [0.075, 0.092]: BOD reduction rate at treatment plant 2 (after the city).
  • ξ ∈ [0.067, 0.083]: Effective rate of BOD reduction at treatment plant 1 after the city.
  • η ∈ [1.2, 1.50]: Parameter used to calculate the effective BOD reduction rate at the second treatment plant.
  • r ∈ [5.1, 12.5]: Investment return rate.

The uncertain version of the river problem is formulated as follows:

\[ \\begin{equation} \\begin{array}{rll} \\text{maximize} & f_1(\\mathbf{x}) = & \\alpha + \\left(\\log\\left(\\left(\\frac{\\beta}{2} - 1.14\\right)^2\\right) + \\beta^3\\right) x_1 \\\\ \\text{maximize} & f_2(\\mathbf{x}) = & \\gamma + \\delta x_1 + \\xi x_2 + \\frac{0.01}{\\eta - x_1^2} + \\frac{0.30}{\\eta - x_2^2} \\\\ \\text{maximize} & f_3(\\mathbf{x}) = & r - \\frac{0.71}{1.09 - x_1^2} \\\\ \\text{minimize} & f_4(\\mathbf{x}) = & -0.96 + \\frac{0.96}{1.09 - x_2^2} \\\\ \\text{subject to} & & 0.3 \\leq x_1, x_2 \\leq 1.0. \\end{array} \\end{equation} \]

where \(\\gamma = \\log\\left(\\frac{\\alpha}{2} - 1\\right) + \\frac{\\alpha}{2} + 1.5\).

Returns:

Name Type Description
Problem Problem

the scenario-based river pollution problem.

References

Narula, Subhash C., and HRoland Weistroffer. "A flexible method for nonlinear multicriteria decision-making problems." IEEE Transactions on Systems, Man, and Cybernetics 19.4 (1989): 883-887.

Miettinen, Kaisa, and Marko M. Mäkelä. "Interactive method NIMBUS for nondifferentiable multiobjective optimization problems." Multicriteria Analysis: Proceedings of the XIth International Conference on MCDM, 1-6 August 1994, Coimbra, Portugal. Berlin, Heidelberg: Springer Berlin Heidelberg, 1997.

Source code in desdeo/problem/testproblems/river_pollution_problems.py
def river_pollution_scenario() -> Problem:
    r"""Defines the scenario-based uncertain variant of the river pollution problem.

    The river pollution problem considers a river close to a city.
    There are two sources of pollution: industrial pollution from a
    fishery and municipal waste from the city. Two treatment plants
    (in the fishery and the city) are responsible for managing the pollution.
    Pollution is reported in pounds of biochemical oxygen demanding material (BOD),
    and water quality is measured in dissolved oxygen concentration (DO).

    Cleaning water in the city increases the tax rate, and cleaning in the
    fishery reduces the return on investment. The problem is to improve
    the DO level in the city and at the municipality border (`f1` and `f2`, respectively),
    while, at the same time, maximizing the percent return on investment at the fishery (`f3`)
    and minimizing additions to the city tax (`f4`).

    Decision variables are:

    * `x1`: The proportional amount of BOD removed from water after the fishery (treatment plant 1).
    * `x2`: The proportional amount of BOD removed from water after the city (treatment plant 2).

    The original problem considered specific values for all parameters. However, in this formulation,
    some parameters are deeply uncertain, and only a range of plausible values is known for each.
    These deeply uncertain parameters are as follows:

    * `α ∈ [3, 4.24]`: Water quality index after the fishery.
    * `β ∈ [2.25, 2.4]`: BOD reduction rate at treatment plant 1 (after the fishery).
    * `δ ∈ [0.075, 0.092]`: BOD reduction rate at treatment plant 2 (after the city).
    * `ξ ∈ [0.067, 0.083]`: Effective rate of BOD reduction at treatment plant 1 after the city.
    * `η ∈ [1.2, 1.50]`: Parameter used to calculate the effective BOD reduction rate at the second treatment plant.
    * `r ∈ [5.1, 12.5]`: Investment return rate.

    The uncertain version of the river problem is formulated as follows:

    $$
    \\begin{equation}
    \\begin{array}{rll}
    \\text{maximize}   & f_1(\\mathbf{x}) = & \\alpha + \\left(\\log\\left(\\left(\\frac{\\beta}{2}
        - 1.14\\right)^2\\right) + \\beta^3\\right) x_1 \\\\
    \\text{maximize}   & f_2(\\mathbf{x}) = & \\gamma + \\delta x_1 + \\xi x_2 + \\frac{0.01}{\\eta - x_1^2}
        + \\frac{0.30}{\\eta - x_2^2} \\\\
    \\text{maximize}   & f_3(\\mathbf{x}) = & r - \\frac{0.71}{1.09 - x_1^2} \\\\
    \\text{minimize}   & f_4(\\mathbf{x}) = & -0.96 + \\frac{0.96}{1.09 - x_2^2} \\\\
    \\text{subject to} & & 0.3 \\leq x_1, x_2 \\leq 1.0.
    \\end{array}
    \\end{equation}
    $$

    where $\\gamma = \\log\\left(\\frac{\\alpha}{2} - 1\\right) + \\frac{\\alpha}{2} + 1.5$.

    Returns:
        Problem: the scenario-based river pollution problem.

    References:
        Narula, Subhash C., and HRoland Weistroffer. "A flexible method for
            nonlinear multicriteria decision-making problems." IEEE Transactions on
            Systems, Man, and Cybernetics 19.4 (1989): 883-887.

        Miettinen, Kaisa, and Marko M. Mäkelä. "Interactive method NIMBUS for
            nondifferentiable multiobjective optimization problems." Multicriteria
            Analysis: Proceedings of the XIth International Conference on MCDM, 1-6
            August 1994, Coimbra, Portugal. Berlin, Heidelberg: Springer Berlin
            Heidelberg, 1997.
    """  # noqa: RUF002
    num_scenarios = 6
    scenario_key_stub = "scenario"

    # defining scenario parameters
    alpha_values = [4.070, 3.868, 3.620, 3.372, 3.124, 4.116]
    beta_values = [2.270, 2.262, 2.278, 2.254, 2.270, 2.286]
    delta_values = [0.0800, 0.0869, 0.0835, 0.0903, 0.0801, 0.0767]
    xi_values = [0.0750, 0.0782, 0.0750, 0.0814, 0.0686, 0.0718]
    eta_values = [1.39, 1.47, 1.23, 1.35, 1.29, 1.41]
    r_values = [8.21, 10.28, 5.84, 11.76, 7.32, 8.80]

    # each scenario parameter is defined as its own tensor constant
    alpha_constant = TensorConstant(
        name="Water quality index after fishery", symbol="alpha", shape=[num_scenarios], values=alpha_values
    )
    beta_constant = TensorConstant(
        name="BOD reduction rate at treatment plant 1 (after the fishery)",
        symbol="beta",
        shape=[num_scenarios],
        values=beta_values,
    )
    delta_constant = TensorConstant(
        name="BOD reduction rate at treatment plant 2 (after the city)",
        symbol="delta",
        shape=[num_scenarios],
        values=delta_values,
    )
    xi_constant = TensorConstant(
        name="The effective rate of BOD reduction at treatment plant 1 (after the city)",
        symbol="xi",
        shape=[num_scenarios],
        values=xi_values,
    )
    eta_constant = TensorConstant(
        name="The effective rate of BOD reduction rate at plant 2 (after the fishery)",
        symbol="eta",
        shape=[num_scenarios],
        values=eta_values,
    )
    r_constant = TensorConstant(
        name="Investment return rate",
        symbol="r",
        shape=[num_scenarios],
        values=r_values,
    )

    constants = [alpha_constant, beta_constant, delta_constant, xi_constant, eta_constant, r_constant]

    # define variables
    x1 = Variable(
        name="BOD removed after fishery",
        symbol="x_1",
        variable_type=VariableTypeEnum.real,
        lowerbound=0.3,
        upperbound=1.0,
    )

    x2 = Variable(
        name="BOD removed after city",
        symbol="x_2",
        variable_type=VariableTypeEnum.real,
        lowerbound=0.3,
        upperbound=1.0,
    )

    variables = [x1, x2]

    # define objectives for each scenario
    objectives = []
    scenario_keys = []

    for i in range(num_scenarios):
        scenario_key = f"{scenario_key_stub}_{i + 1}"
        scenario_keys.append(scenario_key)

        gamma_expr = f"Ln(alpha[{i + 1}]/2 - 1) + alpha[{i + 1}]/2 + 1.5"

        f1_expr = f"alpha[{i + 1}] + (Ln((beta[{i + 1}]/2 - 1.14)**2) + beta[{i + 1}]**3)*x_1"
        f2_expr = (
            f"{gamma_expr} + delta[{i + 1}]*x_1 + xi[{i + 1}]*x_2 + 0.01/(eta[{i + 1}] - x_1**2) "
            f"+ 0.3/(eta[{i + 1}] - x_2**2)"
        )
        f3_expr = f"r[{i + 1}]  - 0.71/(1.09 - x_1**2)"

        # f1
        objectives.append(
            Objective(
                name="DO level city",
                symbol=f"f1_{i + 1}",
                scenario_keys=[scenario_key],
                func=f1_expr,
                objective_type=ObjectiveTypeEnum.analytical,
                maximize=True,
                is_linear=False,
                is_convex=False,
                is_twice_differentiable=True,
            )
        )

        # f2
        objectives.append(
            Objective(
                name="DO level fishery",
                symbol=f"f2_{i + 1}",
                scenario_keys=[scenario_key],
                func=f2_expr,
                objective_type=ObjectiveTypeEnum.analytical,
                maximize=True,
                is_linear=False,
                is_convex=False,
                is_twice_differentiable=True,
            )
        )

        # f3
        objectives.append(
            Objective(
                name="Return of investment",
                symbol=f"f3_{i + 1}",
                scenario_keys=[scenario_key],
                func=f3_expr,
                objective_type=ObjectiveTypeEnum.analytical,
                maximize=True,
                is_linear=False,
                is_convex=False,
                is_twice_differentiable=True,
            )
        )

    f4_expr = "-0.96 + 0.96/(1.09 - x_2**2)"

    # f4, by setting the scenario_key to None, the objective function is assumed to be part of all the scenarios.
    objectives.append(
        Objective(
            name="Addition to city tax",
            symbol="f4",
            scenario_keys=None,
            func=f4_expr,
            objective_type=ObjectiveTypeEnum.analytical,
            maximize=False,
            is_linear=False,
            is_convex=False,
            is_twice_differentiable=True,
        )
    )

    return Problem(
        name="Scenario-based river pollution problem",
        description="The scenario-based river pollution problem",
        constants=constants,
        variables=variables,
        objectives=objectives,
        scenario_keys=scenario_keys,
    )

rocket_injector_design

rocket_injector_design(original_version=False) -> Problem

The rocekt injector design problem as published in Vaidyanathan, et al. (2003).

The original version of the problem has 4 objectives. In Goel et al. (2007), the TW4 objective is dropped due to high correlation with one of the other objectives. Hence, the default version of the problem is the modified version with 3 objectives.

Parameters:

Name Type Description Default
original_version bool

If True, the original version of the problem with 4 objectives is returned.

False
References

R. Vaidyanathan, K. Tucker, N. Papila, W. Shyy, CFD-Based Design Optimization For Single Element Rocket Injector, in: AIAA Aerospace Sciences Meeting, 2003, pp. 1-21.

Goel, T., Vaidyanathan, R., Haftka, R. T., Shyy, W., Queipo, N. V., & Tucker, K. (2007). Response surface approximation of Pareto optimal front in multi-objective optimization. Computer methods in applied mechanics and engineering, 196(4-6), 879-893.

Returns:

Name Type Description
Problem Problem

The rocket injector design problem.

Source code in desdeo/problem/testproblems/rocket_injector_design_problem.py
def rocket_injector_design(original_version=False) -> Problem:
    """The rocekt injector design problem as published in Vaidyanathan, et al. (2003).

    The original version of the problem has 4 objectives. In Goel et al. (2007), the TW4 objective is dropped
    due to high correlation with one of the other objectives. Hence, the default version of the problem is the
    modified version with 3 objectives.

    Args:
        original_version (bool): If True, the original version of the problem with 4 objectives is returned.

    References:
        R. Vaidyanathan, K. Tucker, N. Papila, W. Shyy, CFD-Based Design Optimization For Single Element Rocket
        Injector, in: AIAA Aerospace Sciences Meeting, 2003, pp. 1-21.

        Goel, T., Vaidyanathan, R., Haftka, R. T., Shyy, W., Queipo, N. V., & Tucker, K. (2007).
        Response surface approximation of Pareto optimal front in multi-objective optimization.
        Computer methods in applied mechanics and engineering, 196(4-6), 879-893.

    Returns:
        Problem: The rocket injector design problem.
    """
    # Variables
    alpha = Variable(name="alpha", symbol="a", variable_type=VariableTypeEnum.real, lowerbound=0.0, upperbound=1.0)

    deltaHA = Variable(
        name="deltaHA", symbol="DHA", variable_type=VariableTypeEnum.real, lowerbound=0.0, upperbound=1.0
    )

    deltaOA = Variable(
        name="deltaOA", symbol="DOA", variable_type=VariableTypeEnum.real, lowerbound=0.0, upperbound=1.0
    )

    OPTT = Variable(name="OPTT", symbol="OPTT", variable_type=VariableTypeEnum.real, lowerbound=0.0, upperbound=1.0)

    # Objectives

    tfmax_eqn = """
                0.692+ 0.477 * a- 0.687 * DHA- 0.080 * DOA- 0.0650 * OPTT- 0.167 * a * a- 0.0129 * DHA * a
                + 0.0796 * DHA * DHA- 0.0634 * DOA * a- 0.0257 * DOA * DHA+ 0.0877 * DOA * DOA- 0.0521 * OPTT * a
                + 0.00156 * OPTT * DHA+ 0.00198 * OPTT * DOA+ 0.0184 * OPTT * OPTT
                """

    xccmax_eqn = """
                0.153- 0.322 * a+ 0.396 * DHA+ 0.424 * DOA+ 0.0226 * OPTT+ 0.175 * a * a
                + 0.0185 * DHA * a- 0.0701 * DHA * DHA- 0.251 * DOA * a+ 0.179 * DOA * DHA+ 0.0150 * DOA * DOA
                + 0.0134 * OPTT * a+ 0.0296 * OPTT * DHA+ 0.0752 * OPTT * DOA+ 0.0192 * OPTT * OPTT
                """

    ttmax_eqn = """
                0.370- 0.205 * a+ 0.0307 * DHA+ 0.108 * DOA+ 1.019 * OPTT- 0.135 * a * a+ 0.0141 * DHA * a
                + 0.0998 * DHA * DHA+ 0.208 * DOA * a- 0.0301 * DOA * DHA- 0.226 * DOA * DOA+ 0.353 * OPTT * a
                - 0.0497 * OPTT * DOA- 0.423 * OPTT * OPTT+ 0.202 * DHA * a * a- 0.281 * DOA * a * a
                - 0.342 * DHA * DHA * a- 0.245 * DHA * DHA * DOA+ 0.281 * DOA * DOA * DHA- 0.184 * OPTT * OPTT * a
                - 0.281 * DHA * a * DOA
                """

    tw4_eqn = """
                0.758 + 0.358 * a - 0.807 * DHA + 0.0925 * DOA - 0.0468 * OPTT
                - 0.172 * a * a + 0.0106 * DHA * a + 0.0697 * DHA * DHA
                - 0.146 * DOA * a - 0.0416 * DOA * DHA + 0.102 * DOA * DOA
                - 0.0694 * OPTT * a - 0.00503 * OPTT * DHA + 0.0151 * OPTT * DOA
                + 0.0173 * OPTT * OPTT
                """

    # The ideal and nadir values are estimates. If you get a better estimate, feel free to update them.
    tf_max = Objective(
        name="TF_max",
        symbol="TF_max",
        func=tfmax_eqn,
        maximize=False,
        is_linear=False,
        is_convex=False,
        is_twice_differentiable=True,
        ideal=-0.008907,
        nadir=1.002000,
    )

    xcc_max = Objective(
        name="Xcc_max",
        symbol="Xcc_max",
        func=xccmax_eqn,
        maximize=False,
        is_linear=False,
        is_convex=False,
        is_twice_differentiable=True,
        ideal=0.004883,
        nadir=1.075655,
    )

    tt_max = Objective(
        name="TT_max",
        symbol="TT_max",
        func=ttmax_eqn,
        maximize=False,
        is_linear=False,
        is_convex=False,
        is_twice_differentiable=True,
        ideal=-0.419077,
        nadir=1.092842,
    )

    tw4 = Objective(
        name="TW4",
        symbol="TW4",
        func=tw4_eqn,
        maximize=False,
        is_linear=False,
        is_convex=False,
        is_twice_differentiable=True,
        ideal=-0.013602,
        nadir=0.244688,
    )

    if original_version:  # NOQA:SIM108
        objectives = [tf_max, tw4, tt_max, xcc_max]
    else:
        objectives = [tf_max, tt_max, xcc_max]

    return Problem(
        name="Rocket Injector Design Problem",
        description=(
            "R. Vaidyanathan, K. Tucker, N. Papila, W. Shyy, CFD-Based Design Optimization For Single Element"
            " Rocket Injector, in: AIAA Aerospace Sciences Meeting, 2003, pp. 1-21."
        ),
        variables=[alpha, deltaHA, deltaOA, OPTT],
        objectives=objectives,
    )

simple_constrained_quadratic_tensor_test_problem

simple_constrained_quadratic_tensor_test_problem(
    dqp=False,
) -> Problem

Defines a simple constrained quadratic problem with tensor variables, suitable for testing purposes.

Source code in desdeo/problem/testproblems/simple_problem.py
def simple_constrained_quadratic_tensor_test_problem(dqp=False) -> Problem:
    """Defines a simple constrained quadratic problem with tensor variables, suitable for testing purposes."""
    xvar = TensorVariable(
        name="X",
        symbol="X",
        variable_type=VariableTypeEnum.real,
        shape=[
            2,
        ],
        initial_values=[1, 1],
        lowerbounds=[-10, -10],
        upperbounds=[10, 10],
    )

    mmult = TensorConstant(
        name="Mmult",
        symbol="A",
        shape=[2, 2],
        values=[[1, 0.5], [0.5, 1]],
    )

    bvector = TensorConstant(
        name="mcon",
        symbol="b",
        shape=[
            2,
        ],
        values=[1, 1],
    )

    cons = Constraint(
        name="cons",
        symbol="cons",
        cons_type=ConstraintTypeEnum.LTE,
        func="b-A@X",
        is_linear=True,
        is_convex=True,
        is_twice_differentiable=True,
    )

    obj = Objective(
        name="f_1",
        symbol="f_1",
        func="Sum(-0.5*(X**2))" if dqp else "-0.5*X@X",
        maximize=True,
        is_linear=False,
        is_convex=True,
        is_twice_differentiable=True,
    )

    return Problem(
        name="Simple constrained quadratic tensor test problem.",
        description="A simple problem for testing purposes.",
        variables=[xvar],
        constants=[mmult, bvector],
        constraints=[cons],
        objectives=[obj],
        is_twice_differentiable=True,
        is_convex=True,
    )

simple_data_problem

simple_data_problem() -> Problem

Defines a simple problem with only data-based objective functions.

Source code in desdeo/problem/testproblems/simple_problem.py
def simple_data_problem() -> Problem:
    """Defines a simple problem with only data-based objective functions."""
    constants = [Constant(name="c", symbol="c", value=1000)]

    n_var = 5
    variables = [
        Variable(
            name=f"y_{i}",
            symbol=f"y_{i}",
            variable_type=VariableTypeEnum.real,
            lowerbound=-50.0,
            upperbound=50.0,
            initial_value=0.1,
        )
        for i in range(1, n_var + 1)
    ]

    n_objectives = 3
    # only the first objective is to be maximized, the rest are to be minimized
    objectives = [
        Objective(
            name=f"g_{i}",
            symbol=f"g_{i}",
            func=None,
            objective_type=ObjectiveTypeEnum.data_based,
            maximize=i == 1,
            ideal=3000 if i == 1 else -60.0 if i == 3 else 0,  # noqa: PLR2004
            nadir=0 if i == 1 else 15 - 2.0 if i == 3 else 15,  # noqa: PLR2004
        )
        for i in range(1, n_objectives + 1)
    ]

    constraints = [Constraint(name="cons 1", symbol="c_1", cons_type=ConstraintTypeEnum.EQ, func="y_1 + y_2 - c")]

    data_len = 10
    var_data = {f"y_{i}": [i * 0.5 + j for j in range(data_len)] for i in range(1, n_var + 1)}
    obj_data = {
        "g_1": [sum(var_data[f"y_{j}"][i] for j in range(1, n_var + 1)) ** 2 for i in range(data_len)],
        "g_2": [max(var_data[f"y_{j}"][i] for j in range(1, n_var + 1)) for i in range(data_len)],
        "g_3": [-sum(var_data[f"y_{j}"][i] for j in range(1, n_var + 1)) for i in range(data_len)],
    }

    discrete_def = DiscreteRepresentation(variable_values=var_data, objective_values=obj_data)

    return Problem(
        name="Simple data problem",
        description="Simple problem with all objectives being data-based. Has constraints and a constant also.",
        constants=constants,
        variables=variables,
        objectives=objectives,
        constraints=constraints,
        discrete_representation=discrete_def,
    )

simple_integer_test_problem

simple_integer_test_problem() -> Problem

Defines a simple integer problem suitable for testing purposes.

Source code in desdeo/problem/testproblems/simple_problem.py
def simple_integer_test_problem() -> Problem:
    """Defines a simple integer problem suitable for testing purposes."""
    variables = [
        Variable(
            name="x_1",
            symbol="x_1",
            variable_type=VariableTypeEnum.integer,
            lowerbound=0,
            upperbound=10,
            initial_value=5,
        ),
        Variable(
            name="x_2",
            symbol="x_2",
            variable_type=VariableTypeEnum.integer,
            lowerbound=0,
            upperbound=10,
            initial_value=5,
        ),
        Variable(
            name="x_3",
            symbol="x_3",
            variable_type=VariableTypeEnum.integer,
            lowerbound=0,
            upperbound=10,
            initial_value=5,
        ),
        Variable(
            name="x_4",
            symbol="x_4",
            variable_type=VariableTypeEnum.integer,
            lowerbound=0,
            upperbound=10,
            initial_value=5,
        ),
    ]

    constants = [Constant(name="c", symbol="c", value=4.2)]

    f_1 = "x_1 + x_2 + x_3"
    f_2 = "x_2**x_4 - x_3**x_1"
    f_3 = "x_1 - x_2 + x_3*x_4"
    f_4 = "Max(Abs(x_1 - x_2), c) + Max(x_3, x_4)"  # c = 4.2
    f_5 = "(-x_1) * (-x_2)"

    objectives = [
        Objective(name="f_1", symbol="f_1", func=f_1, maximize=False),  # min!
        Objective(name="f_2", symbol="f_2", func=f_2, maximize=True),  # max!
        Objective(name="f_3", symbol="f_3", func=f_3, maximize=True),  # max!
        Objective(name="f_4", symbol="f_4", func=f_4, maximize=False),  # min!
        Objective(name="f_5", symbol="f_5", func=f_5, maximize=True),  # max!
    ]

    return Problem(
        name="Simple integer test problem.",
        description="A simple problem for testing purposes.",
        constants=constants,
        variables=variables,
        objectives=objectives,
    )

simple_knapsack

simple_knapsack() -> Problem

Defines a simple multiobjective knapsack problem.

Given a set of 4 items, each with a weight and three values corresponding to different objectives, the problem is defined as follows:

  • Item 1: weight = 2, values = (5, 10, 15)
  • Item 2: weight = 3, values = (4, 7, 9)
  • Item 3: weight = 1, values = (3, 5, 8)
  • Item 4: weight = 4, values = (2, 3, 5)

The problem is then to maximize the following functions:

\[\begin{align*} f_1(x) &= 5x_1 + 4x_2 + 3x_3 + 2x_4 \\ f_2(x) &= 10x_1 + 7x_2 + 5x_3 + 3x_4 \\ f_3(x) &= 15x_1 + 9x_2 + 8x_3 + 5x_4 \\ \text{s.t.}\quad & 2x_1 + 3x_2 + 1x_3 + 4x_4 \leq 7 \\ & x_i \in \{0,1\} \quad \text{for} \quad i = 1, 2, 3, 4, \end{align*}\]

where the inequality constraint is a weight constraint. The problem is a binary variable problem.

Returns:

Name Type Description
Problem Problem

the simple knapsack problem.

Source code in desdeo/problem/testproblems/knapsack_problem.py
def simple_knapsack() -> Problem:
    r"""Defines a simple multiobjective knapsack problem.

    Given a set of 4 items, each with a weight and three values corresponding to
    different objectives, the problem is defined as follows:

    -   Item 1: weight = 2, values = (5, 10, 15)
    -   Item 2: weight = 3, values = (4, 7, 9)
    -   Item 3: weight = 1, values = (3, 5, 8)
    -   Item 4: weight = 4, values = (2, 3, 5)

    The problem is then to maximize the following functions:

    \begin{align*}
    f_1(x) &= 5x_1 + 4x_2 + 3x_3 + 2x_4 \\
    f_2(x) &= 10x_1 + 7x_2 + 5x_3 + 3x_4 \\
    f_3(x) &= 15x_1 + 9x_2 + 8x_3 + 5x_4 \\
    \text{s.t.}\quad & 2x_1 + 3x_2 + 1x_3 + 4x_4 \leq 7 \\
    & x_i \in \{0,1\} \quad \text{for} \quad i = 1, 2, 3, 4,
    \end{align*}

    where the inequality constraint is a weight constraint. The problem is a binary variable problem.

    Returns:
        Problem: the simple knapsack problem.
    """
    variables = [
        Variable(
            name=f"x_{i}",
            symbol=f"x_{i}",
            variable_type=VariableTypeEnum.binary,
            lowerbound=0,
            upperbound=1,
            initial_value=0,
        )
        for i in range(1, 5)
    ]

    exprs = {
        "f1": "5*x_1 + 4*x_2 + 3*x_3 + 2*x_4",
        "f2": "10*x_1 + 7*x_2 + 5*x_3 + 3*x_4",
        "f3": "15*x_1 + 9*x_2 + 8*x_3 + 5*x_4",
    }

    ideals = {"f1": 15, "f2": 25, "f3": 37}

    objectives = [
        Objective(
            name=f"f_{i}",
            symbol=f"f_{i}",
            func=exprs[f"f{i}"],
            maximize=True,
            ideal=ideals[f"f{i}"],
            nadir=0,
            objective_type=ObjectiveTypeEnum.analytical,
            is_linear=True,
            is_convex=True,
            is_twice_differentiable=True,
        )
        for i in range(1, 4)
    ]

    constraints = [
        Constraint(
            name="Weight constraint",
            symbol="g_w",
            cons_type=ConstraintTypeEnum.LTE,
            func="2*x_1 + 3*x_2 + 1*x_3 + 4*x_4 - 7",
            is_linear=True,
            is_convex=True,
            is_twice_differentiable=True,
        )
    ]

    return Problem(
        name="Simple knapsack",
        description="A simple knapsack problem with three objectives to be maximized.",
        variables=variables,
        objectives=objectives,
        constraints=constraints,
    )

simple_knapsack_vectors

simple_knapsack_vectors()

Define a simple variant of the knapsack problem that utilizes vectors (TensorVariable and TensorConstant).

Source code in desdeo/problem/testproblems/knapsack_problem.py
def simple_knapsack_vectors():
    """Define a simple variant of the knapsack problem that utilizes vectors (TensorVariable and TensorConstant)."""
    n_items = 4
    weight_values = [2, 3, 4, 5]
    profit_values = [3, 5, 6, 8]
    efficiency_values = [4, 2, 7, 3]

    max_weight = Constant(name="Maximum weights", symbol="w_max", value=5)

    weights = TensorConstant(name="Weights of the items", symbol="W", shape=[len(weight_values)], values=weight_values)
    profits = TensorConstant(name="Profits", symbol="P", shape=[len(profit_values)], values=profit_values)
    efficiencies = TensorConstant(
        name="Efficiencies", symbol="E", shape=[len(efficiency_values)], values=efficiency_values
    )

    choices = TensorVariable(
        name="Chosen items",
        symbol="X",
        shape=[n_items],
        variable_type="binary",
        lowerbounds=n_items * [0],
        upperbounds=n_items * [1],
        initial_values=n_items * [1],
    )

    profit_objective = Objective(
        name="max profit",
        symbol="f_1",
        func="P@X",
        maximize=True,
        ideal=8,
        nadir=0,
        is_linear=True,
        is_convex=False,
        is_twice_differentiable=False,
    )

    efficiency_objective = Objective(
        name="max efficiency",
        symbol="f_2",
        func="E@X",
        maximize=True,
        ideal=7,
        nadir=0,
        is_linear=True,
        is_convex=False,
        is_twice_differentiable=False,
    )

    weight_constraint = Constraint(
        name="Weight constraint",
        symbol="g_1",
        cons_type="<=",
        func="W@X - w_max",
        is_linear=True,
        is_convex=False,
        is_twice_differentiable=False,
    )

    return Problem(
        name="Simple two-objective Knapsack problem",
        description="A simple variant of the classic combinatorial problem.",
        constants=[max_weight, weights, profits, efficiencies],
        variables=[choices],
        objectives=[profit_objective, efficiency_objective],
        constraints=[weight_constraint],
    )

simple_linear_test_problem

simple_linear_test_problem() -> Problem

Defines a simple single objective linear problem suitable for testing purposes.

Source code in desdeo/problem/testproblems/simple_problem.py
def simple_linear_test_problem() -> Problem:
    """Defines a simple single objective linear problem suitable for testing purposes."""
    variables = [
        Variable(name="x_1", symbol="x_1", variable_type="real", lowerbound=-10, upperbound=10, initial_value=5),
        Variable(name="x_2", symbol="x_2", variable_type="real", lowerbound=-10, upperbound=10, initial_value=5),
    ]

    constants = [Constant(name="c", symbol="c", value=4.2)]

    f_1 = "x_1 + x_2"

    objectives = [
        Objective(name="f_1", symbol="f_1", func=f_1, maximize=False),  # min!
    ]

    con_1 = Constraint(name="g_1", symbol="g_1", cons_type=ConstraintTypeEnum.LTE, func="c - x_1")
    con_2 = Constraint(name="g_2", symbol="g_2", cons_type=ConstraintTypeEnum.LTE, func="0.5*x_1 - x_2")

    return Problem(
        name="Simple linear test problem.",
        description="A simple problem for testing purposes.",
        constants=constants,
        variables=variables,
        constraints=[con_1, con_2],
        objectives=objectives,
    )

simple_scenario_test_problem

simple_scenario_test_problem()

Returns a simple, scenario-based multiobjective optimization test problem.

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

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

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

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

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

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

simple_test_problem

simple_test_problem() -> Problem

Defines a simple problem suitable for testing purposes.

Source code in desdeo/problem/testproblems/simple_problem.py
def simple_test_problem() -> Problem:
    """Defines a simple problem suitable for testing purposes."""
    variables = [
        Variable(name="x_1", symbol="x_1", variable_type="real", lowerbound=0, upperbound=10, initial_value=5),
        Variable(name="x_2", symbol="x_2", variable_type="real", lowerbound=0, upperbound=10, initial_value=5),
    ]

    constants = [Constant(name="c", symbol="c", value=4.2)]

    f_1 = "x_1 + x_2"
    f_2 = "x_2**3"
    f_3 = "x_1 + x_2"
    f_4 = "Max(Abs(x_1 - x_2), c)"  # c = 4.2
    f_5 = "(-x_1) * (-x_2)"

    objectives = [
        Objective(name="f_1", symbol="f_1", func=f_1, maximize=False),  # min!
        Objective(name="f_2", symbol="f_2", func=f_2, maximize=True),  # max!
        Objective(name="f_3", symbol="f_3", func=f_3, maximize=True),  # max!
        Objective(name="f_4", symbol="f_4", func=f_4, maximize=False),  # min!
        Objective(name="f_5", symbol="f_5", func=f_5, maximize=True),  # max!
    ]

    return Problem(
        name="Simple test problem.",
        description="A simple problem for testing purposes.",
        constants=constants,
        variables=variables,
        objectives=objectives,
    )

spanish_sustainability_problem_discrete

spanish_sustainability_problem_discrete()

Implements the Spanish sustainability problem using Pareto front representation.

Source code in desdeo/problem/testproblems/spanish_sustainability_problem.py
def spanish_sustainability_problem_discrete():
    """Implements the Spanish sustainability problem using Pareto front representation."""
    filename = "datasets/sustainability_spanish.csv"
    varnames = [f"x{i}" for i in range(1, 12)]
    objNames = {"f1": "social", "f2": "economic", "f3": "environmental"}

    path = Path(__file__).parent.parent.parent.parent / filename
    data = pl.read_csv(path, has_header=True)

    data = data.rename({"social": "f1", "economic": "f2", "environmental": "f3"})

    variables = [
        Variable(
            name=varname,
            symbol=varname,
            variable_type=VariableTypeEnum.real,
            lowerbound=data[varname].min(),
            upperbound=data[varname].max(),
            initial_value=data[varname].mean(),
        )
        for varname in varnames
    ]

    objectives = [
        Objective(
            name=objNames[objname],
            symbol=objname,
            objective_type=ObjectiveTypeEnum.data_based,
            ideal=data[objname].max(),
            nadir=data[objname].min(),
            maximize=True,
        )
        for objname in objNames
    ]

    discrete_def = DiscreteRepresentation(
        variable_values=data[varnames].to_dict(),
        objective_values=data[[obj.symbol for obj in objectives]].to_dict(),
    )

    return Problem(
        name="Spanish sustainability problem (Discrete)",
        description="Defines a sustainability problem with three indicators: social, ecological, and environmental.",
        variables=variables,
        objectives=objectives,
        discrete_representation=discrete_def,
    )

zdt1

zdt1(number_of_variables: int) -> Problem

Defines the ZDT1 test problem.

The problem has a variable number of decision variables and two objective functions to be minimized as follows:

\[\begin{align*} \min\quad f_1(\textbf{x}) &= x_1 \\ \min\quad f_2(\textbf{x}) &= g(\textbf{x}) \cdot h(f_1(\textbf{x}), g(\textbf{x}))\\ g(\textbf{x}) &= 1 + \frac{9}{n-1} \sum_{i=2}^{n} x_i \\ h(f_1, g) &= 1 - \sqrt{\frac{f_1}{g}}, \\ \end{align*}\]

where \(f_1\) and \(f_2\) are objective functions, \(x_1,\dots,x_n\) are decision variable, \(n\) is the number of decision variables, and \(g\) and \(h\) are auxiliary functions.

Source code in desdeo/problem/testproblems/zdt_problem.py
def zdt1(number_of_variables: int) -> Problem:
    r"""Defines the ZDT1 test problem.

    The problem has a variable number of decision variables and two objective functions to be minimized as
    follows:

    \begin{align*}
        \min\quad f_1(\textbf{x}) &= x_1 \\
        \min\quad f_2(\textbf{x}) &= g(\textbf{x}) \cdot h(f_1(\textbf{x}), g(\textbf{x}))\\
        g(\textbf{x}) &= 1 + \frac{9}{n-1} \sum_{i=2}^{n} x_i \\
        h(f_1, g) &= 1 - \sqrt{\frac{f_1}{g}}, \\
    \end{align*}

    where $f_1$ and $f_2$ are objective functions, $x_1,\dots,x_n$ are decision variable, $n$
    is the number of decision variables,
    and $g$ and $h$ are auxiliary functions.
    """
    n = number_of_variables

    # function f_1
    f1_symbol = "f_1"
    f1_expr = "x_1"

    # function g
    g_symbol = "g"
    g_expr_1 = f"1 + (9 / ({n} - 1))"
    g_expr_2 = "(" + " + ".join([f"x_{i}" for i in range(2, n + 1)]) + ")"
    g_expr = g_expr_1 + " * " + g_expr_2

    # function h(f, g)
    h_symbol = "h"
    h_expr = f"1 - Sqrt(({f1_expr}) / ({g_expr}))"

    # function f_2
    f2_symbol = "f_2"
    f2_expr = f"{g_symbol} * {h_symbol}"

    variables = [
        Variable(name=f"x_{i}", symbol=f"x_{i}", variable_type="real", lowerbound=0, upperbound=1, initial_value=0.5)
        for i in range(1, n + 1)
    ]

    objectives = [
        Objective(
            name="f_1",
            symbol=f1_symbol,
            func=f1_expr,
            maximize=False,
            ideal=0,
            nadir=1,
            is_convex=True,
            is_linear=True,
            is_twice_differentiable=True,
        ),
        Objective(
            name="f_2",
            symbol=f2_symbol,
            func=f2_expr,
            maximize=False,
            ideal=0,
            nadir=1,
            is_convex=True,
            is_linear=False,
            is_twice_differentiable=True,
        ),
    ]

    extras = [
        ExtraFunction(
            name="g", symbol=g_symbol, func=g_expr, is_convex=True, is_linear=True, is_twice_differentiable=True
        ),
        ExtraFunction(
            name="h", symbol=h_symbol, func=h_expr, is_convex=True, is_linear=False, is_twice_differentiable=True
        ),
    ]

    return Problem(
        name="zdt1",
        description="The ZDT1 test problem.",
        variables=variables,
        objectives=objectives,
        extra_funcs=extras,
        is_convex=True,
        is_linear=False,
        is_twice_differentiable=True,
    )

zdt2

zdt2(n_variables: int) -> Problem

Defines the ZDT2 test problem.

The problem has a variable number of decision variables and two objective functions to be minimized as follows:

\[\begin{align*} \min\quad f_1(\textbf{x}) &= x_1 \\ \min\quad f_2(\textbf{x}) &= g(\textbf{x}) \cdot h(f_1(\textbf{x}), g(\textbf{x}))\\ g(\textbf{x}) &= 1 + \frac{9}{n-1} \sum_{i=2}^{n} x_i \\ h(f_1, g) &= 1 - \left({\frac{f_1}{g}}\right)^2, \\ \end{align*}\]

where \(f_1\) and \(f_2\) are objective functions, \(x_1,\dots,x_n\) are decision variable, \(n\) is the number of decision variables, and \(g\) and \(h\) are auxiliary functions.

Source code in desdeo/problem/testproblems/zdt_problem.py
def zdt2(n_variables: int) -> Problem:
    r"""Defines the ZDT2 test problem.

    The problem has a variable number of decision variables and two objective functions to be minimized as
    follows:

    \begin{align*}
        \min\quad f_1(\textbf{x}) &= x_1 \\
        \min\quad f_2(\textbf{x}) &= g(\textbf{x}) \cdot h(f_1(\textbf{x}), g(\textbf{x}))\\
        g(\textbf{x}) &= 1 + \frac{9}{n-1} \sum_{i=2}^{n} x_i \\
        h(f_1, g) &= 1 - \left({\frac{f_1}{g}}\right)^2, \\
    \end{align*}

    where $f_1$ and $f_2$ are objective functions, $x_1,\dots,x_n$ are decision variable, $n$
    is the number of decision variables,
    and $g$ and $h$ are auxiliary functions.
    """
    n = n_variables

    # function f_1
    f1_symbol = "f_1"
    f1_expr = "x_1"

    # function g
    g_symbol = "g"
    g_expr_1 = f"1 + (9 / ({n} - 1))"
    g_expr_2 = "(" + " + ".join([f"x_{i}" for i in range(2, n + 1)]) + ")"
    g_expr = g_expr_1 + " * " + g_expr_2

    # function h(f, g)
    h_symbol = "h"
    h_expr = f"1 - (({f1_expr}) / ({g_expr})) ** 2"

    # function f_2
    f2_symbol = "f_2"
    f2_expr = f"{g_symbol} * {h_symbol}"

    variables = [
        Variable(name=f"x_{i}", symbol=f"x_{i}", variable_type="real", lowerbound=0, upperbound=1, initial_value=0.5)
        for i in range(1, n + 1)
    ]

    objectives = [
        Objective(
            name="f_1",
            symbol=f1_symbol,
            func=f1_expr,
            maximize=False,
            ideal=0,
            nadir=1,
            is_convex=True,
            is_linear=True,
            is_twice_differentiable=True,
        ),
        Objective(
            name="f_2",
            symbol=f2_symbol,
            func=f2_expr,
            maximize=False,
            ideal=0,
            nadir=1,
            is_convex=False,
            is_linear=False,
            is_twice_differentiable=True,
        ),
    ]

    extras = [
        ExtraFunction(
            name="g", symbol=g_symbol, func=g_expr, is_convex=True, is_linear=True, is_twice_differentiable=True
        ),
        ExtraFunction(
            name="h", symbol=h_symbol, func=h_expr, is_convex=False, is_linear=False, is_twice_differentiable=True
        ),
    ]

    return Problem(
        name="zdt2",
        description="The ZDT2 test problem.",
        variables=variables,
        objectives=objectives,
        extra_funcs=extras,
        is_convex=False,
        is_linear=False,
        is_twice_differentiable=True,
    )

zdt3

zdt3(n_variables: int) -> Problem

Defines the ZDT3 test problem.

The problem has a variable number of decision variables and two objective functions to be minimized as follows:

\[\begin{align*} \min\quad f_1(x) &= x_1 \\ \min\quad f_2(x) &= g(\textbf{x}) \cdot h(f_1(\textbf{x}), g(\textbf{x}))\\ g(\textbf{x}) &= 1 + \frac{9}{n-1} \sum_{i=2}^{n} x_i \\ h(f_1, g) &= 1 - \sqrt{\frac{f_1}{g}} - \frac{f_1}{g} \sin(10\pi f_1)), \\ \end{align*}\]

where \(f_2\) and \(f_2\) are objective functions, \(x_1,\dots,x_n\) are decision variable, \(n\) is the number of decision variables, and \(g\) and \(h\) are auxiliary functions.

Source code in desdeo/problem/testproblems/zdt_problem.py
def zdt3(
    n_variables: int,
) -> Problem:
    r"""Defines the ZDT3 test problem.

    The problem has a variable number of decision variables and two objective functions to be minimized as
    follows:

    \begin{align*}
        \min\quad f_1(x) &= x_1 \\
        \min\quad f_2(x) &= g(\textbf{x}) \cdot h(f_1(\textbf{x}), g(\textbf{x}))\\
        g(\textbf{x}) &= 1 + \frac{9}{n-1} \sum_{i=2}^{n} x_i \\
         h(f_1, g) &= 1 - \sqrt{\frac{f_1}{g}} - \frac{f_1}{g} \sin(10\pi f_1)), \\
    \end{align*}

    where $f_2$ and $f_2$ are objective functions, $x_1,\dots,x_n$ are decision variable, $n$
    is the number of decision variables,
    and $g$ and $h$ are auxiliary functions.
    """
    n = n_variables

    # function f_1
    f1_symbol = "f_1"
    f1_expr = "x_1"

    # function g
    g_symbol = "g"
    g_expr_1 = f"1 + (9 / ({n} - 1))"
    g_expr_2 = "(" + " + ".join([f"x_{i}" for i in range(2, n + 1)]) + ")"
    g_expr = g_expr_1 + " * " + g_expr_2

    # function h(f, g)
    h_symbol = "h"
    h_expr = f"1 - Sqrt(({f1_expr}) / ({g_expr})) - (({f1_expr}) / ({g_expr})) * Sin (10 * {pi} * {f1_expr}) "

    # function f_2
    f2_symbol = "f_2"
    f2_expr = f"{g_symbol} * {h_symbol}"

    variables = [
        Variable(name=f"x_{i}", symbol=f"x_{i}", variable_type="real", lowerbound=0, upperbound=1, initial_value=0.5)
        for i in range(1, n + 1)
    ]

    objectives = [
        Objective(
            name="f_1",
            symbol=f1_symbol,
            func=f1_expr,
            maximize=False,
            ideal=0,
            nadir=1,
            is_convex=True,
            is_linear=True,
            is_twice_differentiable=True,
        ),
        Objective(
            name="f_2",
            symbol=f2_symbol,
            func=f2_expr,
            maximize=False,
            ideal=-1,
            nadir=1,
            is_convex=False,
            is_linear=False,
            is_twice_differentiable=True,
        ),
    ]

    extras = [
        ExtraFunction(
            name="g", symbol=g_symbol, func=g_expr, is_convex=True, is_linear=True, is_twice_differentiable=True
        ),
        ExtraFunction(
            name="h", symbol=h_symbol, func=h_expr, is_convex=False, is_linear=False, is_twice_differentiable=True
        ),
    ]

    return Problem(
        name="zdt3",
        description="The ZDT3 test problem.",
        variables=variables,
        objectives=objectives,
        extra_funcs=extras,
        is_convex=False,
        is_linear=False,
        is_twice_differentiable=True,
    )

Problem schema

desdeo.problem.schema

Schema for the problem definition.

The problem definition is a JSON file that contains the following information:

  • Constants
  • Variables
  • Objectives
  • Extra functions
  • Scalarization functions
  • Evaluated solutions and their info

Constant

Bases: BaseModel

Model for a constant.

Source code in desdeo/problem/schema.py
class Constant(BaseModel):
    """Model for a constant."""

    model_config = ConfigDict(frozen=True, from_attributes=True, extra="forbid")

    name: str = Field(
        description=(
            "Descriptive name of the constant. This can be used in UI and visualizations. Example: 'maximum cost'."
        ),
    )
    """Descriptive name of the constant. This can be used in UI and visualizations." " Example: 'maximum cost'."""
    symbol: str = Field(
        description=(
            "Symbol to represent the constant. This will be used in the rest of the problem definition."
            " It may also be used in UIs and visualizations. Example: 'c_1'."
        ),
    )
    """ Symbol to represent the constant. This will be used in the rest of the
    problem definition.  It may also be used in UIs and visualizations. Example:
    'c_1'."""
    value: VariableType = Field(description="The value of the constant.")
    """The value of the constant."""
name class-attribute instance-attribute
name: str = Field(
    description="Descriptive name of the constant. This can be used in UI and visualizations. Example: 'maximum cost'."
)

Descriptive name of the constant. This can be used in UI and visualizations." " Example: 'maximum cost'.

symbol class-attribute instance-attribute
symbol: str = Field(
    description="Symbol to represent the constant. This will be used in the rest of the problem definition. It may also be used in UIs and visualizations. Example: 'c_1'."
)

Symbol to represent the constant. This will be used in the rest of the problem definition. It may also be used in UIs and visualizations. Example: 'c_1'.

value class-attribute instance-attribute
value: VariableType = Field(
    description="The value of the constant."
)

The value of the constant.

Constraint

Bases: BaseModel

Model for a constraint function.

Source code in desdeo/problem/schema.py
class Constraint(BaseModel):
    """Model for a constraint function."""

    model_config = ConfigDict(frozen=True, from_attributes=True, extra="forbid")

    name: str = Field(
        description=(
            "Descriptive name of the constraint. This can be used in UI and visualizations. Example: 'maximum length'."
        ),
    )
    """ Descriptive name of the constraint. This can be used in UI and
    visualizations.  Example: 'maximum length'"""
    symbol: str = Field(
        description=(
            "Symbol to represent the constraint. This will be used in the rest of the problem definition."
            " It may also be used in UIs and visualizations. Example: 'g_1'."
        ),
    )
    """ Symbol to represent the constraint. This will be used in the rest of the
    problem definition.  It may also be used in UIs and visualizations. Example:
    'g_1'.  """
    cons_type: ConstraintTypeEnum = Field(
        description=(
            "The type of the constraint. Constraints are assumed to be in a standard form where the supplied 'func'"
            " expression is on the left hand side of the constraint's expression, and on the right hand side a zero"
            " value is assume. The comparison between the left hand side and right hand side is either and quality"
            " comparison ('=') or lesser than equal comparison ('<=')."
        )
    )
    """ The type of the constraint. Constraints are assumed to be in a standard
    form where the supplied 'func' expression is on the left hand side of the
    constraint's expression, and on the right hand side a zero value is assume.
    The comparison between the left hand side and right hand side is either and
    quality comparison ('=') or lesser than equal comparison ('<=')."""
    func: list | None = Field(
        description=(
            "Function of the constraint. This is a JSON object that can be parsed into a function."
            "Must be a valid MathJSON object."
            " The symbols in the function must match objective/variable/constant symbols."
            "Can be 'None' if either 'simulator_path' or 'surrogates' is not 'None'. "
            "If 'None', either 'simulator_path' or 'surrogates' must not be 'None'."
        ),
        default=None,
    )
    """ Function of the constraint. This is a JSON object that can be parsed
    into a function.  Must be a valid MathJSON object.  The symbols in the
    function must match objective/variable/constant symbols.
    Can be 'None' if either 'simulator_path' or 'surrogates' is not 'None'.
    If 'None', either 'simulator_path' or 'surrogates' must not be 'None'."""
    simulator_path: Path | Url | None = Field(
        description=(
            "Path to a python file with the connection to simulators. Must be a valid Path."
            "Can be 'None' for if either 'func' or 'surrogates' is not 'None'."
            "If 'None', either 'func' or 'surrogates' must not be 'None'."
        ),
        default=None,
    )
    """Path to a python file with the connection to simulators. Must be a valid Path.
    Can be 'None' for if either 'func' or 'surrogates' is not 'None'.
    If 'None', either 'func' or 'surrogates' must not be 'None'."""
    surrogates: list[Path] | None = Field(
        description=(
            "A list of paths to models saved on disk. Can be 'None' for if either 'func' or 'simulator_path' "
            "is not 'None'. If 'None', either 'func' or 'simulator_path' must not be 'None'."
        ),
        default=None,
    )
    """A list of paths to models saved on disk. Can be 'None' for if either 'func' or 'simulator_path'
    is not 'None'. If 'None', either 'func' or 'simulator_path' must not be 'None'."""
    is_linear: bool = Field(
        description="Whether the constraint is linear or not. Defaults to True, e.g., a linear constraint is assumed.",
        default=True,
    )
    """Whether the constraint is linear or not. Defaults to True, e.g., a linear
    constraint is assumed. Defaults to `True`."""
    is_convex: bool = Field(
        description="Whether the function expression is convex or not (non-convex). Defaults to `False`.", default=False
    )
    """Whether the function expression is convex or not (non-convex). Defaults to `False`."""
    is_twice_differentiable: bool = Field(
        description="Whether the function expression is twice differentiable or not. Defaults to `False`", default=False
    )
    """Whether the function expression is twice differentiable or not. Defaults to `False`"""
    scenario_keys: list[str] | None = Field(
        description="Optional. The keys of the scenarios the constraint belongs to.", default=None
    )
    """Optional. The keys of the scenarios the constraint belongs to."""

    _parse_infix_to_func = field_validator("func", mode="before")(parse_infix_to_func)
    _parse_scenario_key_singleton_to_list = field_validator("scenario_keys", mode="before")(
        parse_scenario_key_singleton_to_list
    )
cons_type class-attribute instance-attribute
cons_type: ConstraintTypeEnum = Field(
    description="The type of the constraint. Constraints are assumed to be in a standard form where the supplied 'func' expression is on the left hand side of the constraint's expression, and on the right hand side a zero value is assume. The comparison between the left hand side and right hand side is either and quality comparison ('=') or lesser than equal comparison ('<=')."
)

The type of the constraint. Constraints are assumed to be in a standard form where the supplied 'func' expression is on the left hand side of the constraint's expression, and on the right hand side a zero value is assume. The comparison between the left hand side and right hand side is either and quality comparison ('=') or lesser than equal comparison ('<=').

func class-attribute instance-attribute
func: list | None = Field(
    description="Function of the constraint. This is a JSON object that can be parsed into a function.Must be a valid MathJSON object. The symbols in the function must match objective/variable/constant symbols.Can be 'None' if either 'simulator_path' or 'surrogates' is not 'None'. If 'None', either 'simulator_path' or 'surrogates' must not be 'None'.",
    default=None,
)

Function of the constraint. This is a JSON object that can be parsed into a function. Must be a valid MathJSON object. The symbols in the function must match objective/variable/constant symbols. Can be 'None' if either 'simulator_path' or 'surrogates' is not 'None'. If 'None', either 'simulator_path' or 'surrogates' must not be 'None'.

is_convex class-attribute instance-attribute
is_convex: bool = Field(
    description="Whether the function expression is convex or not (non-convex). Defaults to `False`.",
    default=False,
)

Whether the function expression is convex or not (non-convex). Defaults to False.

is_linear class-attribute instance-attribute
is_linear: bool = Field(
    description="Whether the constraint is linear or not. Defaults to True, e.g., a linear constraint is assumed.",
    default=True,
)

Whether the constraint is linear or not. Defaults to True, e.g., a linear constraint is assumed. Defaults to True.

is_twice_differentiable class-attribute instance-attribute
is_twice_differentiable: bool = Field(
    description="Whether the function expression is twice differentiable or not. Defaults to `False`",
    default=False,
)

Whether the function expression is twice differentiable or not. Defaults to False

name class-attribute instance-attribute
name: str = Field(
    description="Descriptive name of the constraint. This can be used in UI and visualizations. Example: 'maximum length'."
)

Descriptive name of the constraint. This can be used in UI and visualizations. Example: 'maximum length'

scenario_keys class-attribute instance-attribute
scenario_keys: list[str] | None = Field(
    description="Optional. The keys of the scenarios the constraint belongs to.",
    default=None,
)

Optional. The keys of the scenarios the constraint belongs to.

simulator_path class-attribute instance-attribute
simulator_path: Path | Url | None = Field(
    description="Path to a python file with the connection to simulators. Must be a valid Path.Can be 'None' for if either 'func' or 'surrogates' is not 'None'.If 'None', either 'func' or 'surrogates' must not be 'None'.",
    default=None,
)

Path to a python file with the connection to simulators. Must be a valid Path. Can be 'None' for if either 'func' or 'surrogates' is not 'None'. If 'None', either 'func' or 'surrogates' must not be 'None'.

surrogates class-attribute instance-attribute
surrogates: list[Path] | None = Field(
    description="A list of paths to models saved on disk. Can be 'None' for if either 'func' or 'simulator_path' is not 'None'. If 'None', either 'func' or 'simulator_path' must not be 'None'.",
    default=None,
)

A list of paths to models saved on disk. Can be 'None' for if either 'func' or 'simulator_path' is not 'None'. If 'None', either 'func' or 'simulator_path' must not be 'None'.

symbol class-attribute instance-attribute
symbol: str = Field(
    description="Symbol to represent the constraint. This will be used in the rest of the problem definition. It may also be used in UIs and visualizations. Example: 'g_1'."
)

Symbol to represent the constraint. This will be used in the rest of the problem definition. It may also be used in UIs and visualizations. Example: 'g_1'.

ConstraintTypeEnum

Bases: str, Enum

An enumerator for supported constraint expression types.

Source code in desdeo/problem/schema.py
class ConstraintTypeEnum(str, Enum):
    """An enumerator for supported constraint expression types."""

    EQ = "="
    """An equality constraint."""
    LTE = "<="  # less than or equal
    """An inequality constraint of type 'less than or equal'."""
EQ class-attribute instance-attribute
EQ = '='

An equality constraint.

LTE class-attribute instance-attribute
LTE = '<='

An inequality constraint of type 'less than or equal'.

DiscreteRepresentation

Bases: BaseModel

Model to represent discrete objective function and decision variable pairs.

Can be used alongside an analytical representation as well.

Used with Objectives of type 'data_based' by default. Each of the decision variable values and objective functions values are ordered in their respective dict entries. This means that the decision variable values found at variable_values['x_i'][j] correspond to the objective function values found at objective_values['f_i'][j] for all i and some j.

Source code in desdeo/problem/schema.py
class DiscreteRepresentation(BaseModel):
    """Model to represent discrete objective function and decision variable pairs.

    Can be used alongside an analytical representation as well.

    Used with Objectives of type 'data_based' by default. Each of the decision
    variable values and objective functions values are ordered in their
    respective dict entries. This means that the decision variable values found
    at `variable_values['x_i'][j]` correspond to the objective function values
    found at `objective_values['f_i'][j]` for all `i` and some `j`.
    """

    model_config = ConfigDict(frozen=True, from_attributes=True, extra="forbid")

    variable_values: dict[str, list[VariableType]] = Field(
        description=(
            "A dictionary with decision variable values. Each dict key points to a list of all the decision "
            "variable values available for the decision variable given in the key. "
            "The keys must match the 'symbols' defined for the decision variables."
        )
    )
    """ A dictionary with decision variable values. Each dict key points to a
    list of all the decision variable values available for the decision variable
    given in the key.  The keys must match the 'symbols' defined for the
    decision variables."""
    objective_values: dict[str, list[float]] = Field(
        description=(
            "A dictionary with objective function values. Each dict key points to a list of all the objective "
            "function values available for the objective function given in the key. The keys must match the 'symbols' "
            "defined for the objective functions."
        )
    )
    """ A dictionary with objective function values. Each dict key points to a
    list of all the objective function values available for the objective
    function given in the key. The keys must match the 'symbols' defined for the
    objective functions."""
    non_dominated: bool = Field(
        description=(
            "Indicates whether the representation consists of non-dominated points or not."
            "If False, some method can employ non-dominated sorting, which might slow an interactive method down."
        ),
        default=False,
    )
    """ Indicates whether the representation consists of non-dominated points or
    not.  If False, some method can employ non-dominated sorting, which might
    slow an interactive method down. Defaults to `False`."""
non_dominated class-attribute instance-attribute
non_dominated: bool = Field(
    description="Indicates whether the representation consists of non-dominated points or not.If False, some method can employ non-dominated sorting, which might slow an interactive method down.",
    default=False,
)

Indicates whether the representation consists of non-dominated points or not. If False, some method can employ non-dominated sorting, which might slow an interactive method down. Defaults to False.

objective_values class-attribute instance-attribute
objective_values: dict[str, list[float]] = Field(
    description="A dictionary with objective function values. Each dict key points to a list of all the objective function values available for the objective function given in the key. The keys must match the 'symbols' defined for the objective functions."
)

A dictionary with objective function values. Each dict key points to a list of all the objective function values available for the objective function given in the key. The keys must match the 'symbols' defined for the objective functions.

variable_values class-attribute instance-attribute
variable_values: dict[str, list[VariableType]] = Field(
    description="A dictionary with decision variable values. Each dict key points to a list of all the decision variable values available for the decision variable given in the key. The keys must match the 'symbols' defined for the decision variables."
)

A dictionary with decision variable values. Each dict key points to a list of all the decision variable values available for the decision variable given in the key. The keys must match the 'symbols' defined for the decision variables.

ExtraFunction

Bases: BaseModel

Model for extra functions.

These functions can, e.g., be functions that are re-used in the problem formulation, or they are needed for other computations related to the problem.

Source code in desdeo/problem/schema.py
class ExtraFunction(BaseModel):
    """Model for extra functions.

    These functions can, e.g., be functions that are re-used in the problem formulation, or
    they are needed for other computations related to the problem.
    """

    model_config = ConfigDict(frozen=True, from_attributes=True, extra="forbid")

    name: str = Field(
        description=("Descriptive name of the function. Example: 'normalization'."),
    )
    """Descriptive name of the function. Example: 'normalization'."""
    symbol: str = Field(
        description=(
            "Symbol to represent the function. This will be used in the rest of the problem definition."
            " It may also be used in UIs and visualizations. Example: 'avg'."
        ),
    )
    """ Symbol to represent the function. This will be used in the rest of the
    problem definition.  It may also be used in UIs and visualizations. Example:
    'avg'."""
    func: list | None = Field(
        description=(
            "The string representing the function. This is a JSON object that can be parsed into a function."
            "Must be a valid MathJSON object."
            " The symbols in the function must match symbols defined for objective/variable/constant."
            "Can be 'None' if either 'simulator_path' or 'surrogates' is not 'None'. "
            "If 'None', either 'simulator_path' or 'surrogates' must not be 'None'."
        ),
        default=None,
    )
    """ The string representing the function. This is a JSON object that can be
    parsed into a function.  Must be a valid MathJSON object.  The symbols in
    the function must match symbols defined for objective/variable/constant.
    Can be 'None' if either 'simulator_path' or 'surrogates' is not 'None'.
    If 'None', either 'simulator_path' or 'surrogates' must not be 'None'."""
    simulator_path: Path | None = Field(
        description=(
            "Path to a python file with the connection to simulators. Must be a valid Path."
            "Can be 'None' for 'analytical', 'data_based' or 'surrogate' functions."
            "If 'None', either 'func' or 'surrogates' must not be 'None'."
        ),
        default=None,
    )
    """Path to a python file with the connection to simulators. Must be a valid Path.
    Can be 'None' for 'analytical', 'data_based' or 'surrogate' functions.
    If 'None', either 'func' or 'surrogates' must not be 'None'."""
    surrogates: list[Path] | None = Field(
        description=(
            "A list of paths to models saved on disk. Can be 'None' for 'analytical', 'data_based "
            "or 'simulator' functions. If 'None', either 'func' or 'simulator_path' must "
            "not be 'None'."
        ),
        default=None,
    )
    """A list of paths to models saved on disk. Can be 'None' for 'analytical', 'data_based
    or 'simulator' functions. If 'None', either 'func' or 'simulator_path' must
    not be 'None'."""
    is_linear: bool = Field(
        description="Whether the function expression is linear or not. Defaults to `False`.", default=False
    )
    """Whether the function expression is linear or not. Defaults to `False`."""
    is_convex: bool = Field(
        description="Whether the function expression is convex or not (non-convex). Defaults to `False`.", default=False
    )
    """Whether the function expression is convex or not (non-convex). Defaults to `False`."""
    is_twice_differentiable: bool = Field(
        description="Whether the function expression is twice differentiable or not. Defaults to `False`", default=False
    )
    """Whether the function expression is twice differentiable or not. Defaults to `False`"""
    scenario_keys: list[str] | None = Field(
        description="Optional. The keys of the scenario the extra functions belongs to.", default=None
    )
    """Optional. The keys of the scenarios the extra functions belongs to."""

    _parse_infix_to_func = field_validator("func", mode="before")(parse_infix_to_func)
    _parse_scenario_key_singleton_to_list = field_validator("scenario_keys", mode="before")(
        parse_scenario_key_singleton_to_list
    )
func class-attribute instance-attribute
func: list | None = Field(
    description="The string representing the function. This is a JSON object that can be parsed into a function.Must be a valid MathJSON object. The symbols in the function must match symbols defined for objective/variable/constant.Can be 'None' if either 'simulator_path' or 'surrogates' is not 'None'. If 'None', either 'simulator_path' or 'surrogates' must not be 'None'.",
    default=None,
)

The string representing the function. This is a JSON object that can be parsed into a function. Must be a valid MathJSON object. The symbols in the function must match symbols defined for objective/variable/constant. Can be 'None' if either 'simulator_path' or 'surrogates' is not 'None'. If 'None', either 'simulator_path' or 'surrogates' must not be 'None'.

is_convex class-attribute instance-attribute
is_convex: bool = Field(
    description="Whether the function expression is convex or not (non-convex). Defaults to `False`.",
    default=False,
)

Whether the function expression is convex or not (non-convex). Defaults to False.

is_linear class-attribute instance-attribute
is_linear: bool = Field(
    description="Whether the function expression is linear or not. Defaults to `False`.",
    default=False,
)

Whether the function expression is linear or not. Defaults to False.

is_twice_differentiable class-attribute instance-attribute
is_twice_differentiable: bool = Field(
    description="Whether the function expression is twice differentiable or not. Defaults to `False`",
    default=False,
)

Whether the function expression is twice differentiable or not. Defaults to False

name class-attribute instance-attribute
name: str = Field(
    description="Descriptive name of the function. Example: 'normalization'."
)

Descriptive name of the function. Example: 'normalization'.

scenario_keys class-attribute instance-attribute
scenario_keys: list[str] | None = Field(
    description="Optional. The keys of the scenario the extra functions belongs to.",
    default=None,
)

Optional. The keys of the scenarios the extra functions belongs to.

simulator_path class-attribute instance-attribute
simulator_path: Path | None = Field(
    description="Path to a python file with the connection to simulators. Must be a valid Path.Can be 'None' for 'analytical', 'data_based' or 'surrogate' functions.If 'None', either 'func' or 'surrogates' must not be 'None'.",
    default=None,
)

Path to a python file with the connection to simulators. Must be a valid Path. Can be 'None' for 'analytical', 'data_based' or 'surrogate' functions. If 'None', either 'func' or 'surrogates' must not be 'None'.

surrogates class-attribute instance-attribute
surrogates: list[Path] | None = Field(
    description="A list of paths to models saved on disk. Can be 'None' for 'analytical', 'data_based or 'simulator' functions. If 'None', either 'func' or 'simulator_path' must not be 'None'.",
    default=None,
)

A list of paths to models saved on disk. Can be 'None' for 'analytical', 'data_based or 'simulator' functions. If 'None', either 'func' or 'simulator_path' must not be 'None'.

symbol class-attribute instance-attribute
symbol: str = Field(
    description="Symbol to represent the function. This will be used in the rest of the problem definition. It may also be used in UIs and visualizations. Example: 'avg'."
)

Symbol to represent the function. This will be used in the rest of the problem definition. It may also be used in UIs and visualizations. Example: 'avg'.

Objective

Bases: BaseModel

Model for an objective function.

Source code in desdeo/problem/schema.py
class Objective(BaseModel):
    """Model for an objective function."""

    model_config = ConfigDict(frozen=True, from_attributes=True, extra="forbid")

    """A longer description for the objective."""
    description: str | None = Field(
        description=(
            "A longer description of the objective function. This can be used in UI and visualizations. \
            Meant to have longer text than what name should have."
        ),
        default=None,
    )
    name: str = Field(
        description=(
            "Descriptive name of the objective function. This can be used in UI and visualizations. Example: 'time'."
        ),
    )
    """Descriptive name of the objective function. This can be used in UI and visualizations."""
    symbol: str = Field(
        description=(
            "Symbol to represent the objective function. This will be used in the rest of the problem definition."
            " It may also be used in UIs and visualizations. Example: 'f_1'."
        ),
    )
    """ Symbol to represent the objective function. This will be used in the
    rest of the problem definition.  It may also be used in UIs and
    visualizations. Example: 'f_1'."""
    unit: str | None = Field(
        description=(
            "The unit of the objective function. This is optional. Used in UIs and visualizations. Example: 'seconds'"
            " or 'millions of hectares'."
        ),
        default=None,
    )
    """The unit of the objective function. This is optional. Used in UIs and visualizations. Example: 'seconds' or
    'millions of hectares'. Defaults to `None`."""
    func: list | None = Field(
        description=(
            "The objective function. This is a JSON object that can be parsed into a function."
            "Must be a valid MathJSON object. The symbols in the function must match the symbols defined for "
            "variable/constant/extra function. Can be 'None' for 'data_based', 'simulator' or "
            "'surrogate' objective functions. If 'None', either 'simulator_path' or 'surrogates' must "
            "not be 'None'."
        ),
        default=None,
    )
    """ The objective function. This is a JSON object that can be parsed into a function.
    Must be a valid MathJSON object. The symbols in the function must match the symbols defined for
    variable/constant/extra function. Can be 'None' for 'data_based', 'simulator' or
    'surrogate' objective functions. If 'None', either 'simulator_path' or 'surrogates' must
    not be 'None'."""
    simulator_path: Path | Url | None = Field(
        description=(
            "Path to a python file or http server with the connection to simulators. Must be a valid Path or url."
            "Can be 'None' for 'analytical', 'data_based' or 'surrogate' objective functions."
            "If 'None', either 'func' or 'surrogates' must not be 'None'."
        ),
        default=None,
    )
    """Path to a python file with the connection to simulators. Must be a valid Path.
    Can be 'None' for 'analytical', 'data_based' or 'surrogate' objective functions.
    If 'None', either 'func' or 'surrogates' must not be 'None'."""
    surrogates: list[Path] | None = Field(
        description=(
            "A list of paths to models saved on disk. Can be 'None' for 'analytical', 'data_based "
            "or 'simulator' objective functions. If 'None', either 'func' or 'simulator_path' must "
            "not be 'None'."
        ),
        default=None,
    )
    """A list of paths to models saved on disk. Can be 'None' for 'analytical', 'data_based
    or 'simulator' objective functions. If 'None', either 'func' or 'simulator_path' must
    not be 'None'."""
    maximize: bool = Field(
        description="Whether the objective function is to be maximized or minimized.",
        default=False,
    )
    """Whether the objective function is to be maximized or minimized. Defaults to `False`."""
    ideal: float | None = Field(description="Ideal value of the objective. This is optional.", default=None)
    """Ideal value of the objective. This is optional. Defaults to `None`."""
    nadir: float | None = Field(description="Nadir value of the objective. This is optional.", default=None)
    """Nadir value of the objective. This is optional. Defaults to `None`."""

    objective_type: ObjectiveTypeEnum = Field(
        description=(
            "The type of objective function. 'analytical' means the objective function value is calculated "
            "based on 'func'. 'data_based' means the objective function value should be retrieved from a table. "
            "In case of 'data_based' objective function, the 'func' field is ignored. Defaults to 'analytical'."
        ),
        default=ObjectiveTypeEnum.analytical,
    )
    """ The type of objective function. 'analytical' means the objective
    function value is calculated based on 'func'. 'data_based' means the
    objective function value should be retrieved from a table.  In case of
    'data_based' objective function, the 'func' field is ignored. Defaults to
    'analytical'. Defaults to 'analytical'."""
    is_linear: bool = Field(
        description="Whether the function expression is linear or not. Defaults to `False`.", default=False
    )
    """Whether the function expression is linear or not. Defaults to `False`."""
    is_convex: bool = Field(
        description="Whether the function expression is convex or not (non-convex). Defaults to `False`.", default=False
    )
    """Whether the function expression is convex or not (non-convex). Defaults to `False`."""
    is_twice_differentiable: bool = Field(
        description="Whether the function expression is twice differentiable or not. Defaults to `False`", default=False
    )
    """Whether the function expression is twice differentiable or not. Defaults to `False`"""
    scenario_keys: list[str] | None = Field(
        description="Optional. The keys of the scenarios the objective function belongs to.", default=None
    )
    """Optional. The keys of the scenarios the objective function belongs to."""

    _parse_infix_to_func = field_validator("func", mode="before")(parse_infix_to_func)
    _parse_scenario_key_singleton_to_list = field_validator("scenario_keys", mode="before")(
        parse_scenario_key_singleton_to_list
    )
func class-attribute instance-attribute
func: list | None = Field(
    description="The objective function. This is a JSON object that can be parsed into a function.Must be a valid MathJSON object. The symbols in the function must match the symbols defined for variable/constant/extra function. Can be 'None' for 'data_based', 'simulator' or 'surrogate' objective functions. If 'None', either 'simulator_path' or 'surrogates' must not be 'None'.",
    default=None,
)

The objective function. This is a JSON object that can be parsed into a function. Must be a valid MathJSON object. The symbols in the function must match the symbols defined for variable/constant/extra function. Can be 'None' for 'data_based', 'simulator' or 'surrogate' objective functions. If 'None', either 'simulator_path' or 'surrogates' must not be 'None'.

ideal class-attribute instance-attribute
ideal: float | None = Field(
    description="Ideal value of the objective. This is optional.",
    default=None,
)

Ideal value of the objective. This is optional. Defaults to None.

is_convex class-attribute instance-attribute
is_convex: bool = Field(
    description="Whether the function expression is convex or not (non-convex). Defaults to `False`.",
    default=False,
)

Whether the function expression is convex or not (non-convex). Defaults to False.

is_linear class-attribute instance-attribute
is_linear: bool = Field(
    description="Whether the function expression is linear or not. Defaults to `False`.",
    default=False,
)

Whether the function expression is linear or not. Defaults to False.

is_twice_differentiable class-attribute instance-attribute
is_twice_differentiable: bool = Field(
    description="Whether the function expression is twice differentiable or not. Defaults to `False`",
    default=False,
)

Whether the function expression is twice differentiable or not. Defaults to False

maximize class-attribute instance-attribute
maximize: bool = Field(
    description="Whether the objective function is to be maximized or minimized.",
    default=False,
)

Whether the objective function is to be maximized or minimized. Defaults to False.

model_config class-attribute instance-attribute
model_config = ConfigDict(
    frozen=True, from_attributes=True, extra="forbid"
)

A longer description for the objective.

nadir class-attribute instance-attribute
nadir: float | None = Field(
    description="Nadir value of the objective. This is optional.",
    default=None,
)

Nadir value of the objective. This is optional. Defaults to None.

name class-attribute instance-attribute
name: str = Field(
    description="Descriptive name of the objective function. This can be used in UI and visualizations. Example: 'time'."
)

Descriptive name of the objective function. This can be used in UI and visualizations.

objective_type class-attribute instance-attribute
objective_type: ObjectiveTypeEnum = Field(
    description="The type of objective function. 'analytical' means the objective function value is calculated based on 'func'. 'data_based' means the objective function value should be retrieved from a table. In case of 'data_based' objective function, the 'func' field is ignored. Defaults to 'analytical'.",
    default=analytical,
)

The type of objective function. 'analytical' means the objective function value is calculated based on 'func'. 'data_based' means the objective function value should be retrieved from a table. In case of 'data_based' objective function, the 'func' field is ignored. Defaults to 'analytical'. Defaults to 'analytical'.

scenario_keys class-attribute instance-attribute
scenario_keys: list[str] | None = Field(
    description="Optional. The keys of the scenarios the objective function belongs to.",
    default=None,
)

Optional. The keys of the scenarios the objective function belongs to.

simulator_path class-attribute instance-attribute
simulator_path: Path | Url | None = Field(
    description="Path to a python file or http server with the connection to simulators. Must be a valid Path or url.Can be 'None' for 'analytical', 'data_based' or 'surrogate' objective functions.If 'None', either 'func' or 'surrogates' must not be 'None'.",
    default=None,
)

Path to a python file with the connection to simulators. Must be a valid Path. Can be 'None' for 'analytical', 'data_based' or 'surrogate' objective functions. If 'None', either 'func' or 'surrogates' must not be 'None'.

surrogates class-attribute instance-attribute
surrogates: list[Path] | None = Field(
    description="A list of paths to models saved on disk. Can be 'None' for 'analytical', 'data_based or 'simulator' objective functions. If 'None', either 'func' or 'simulator_path' must not be 'None'.",
    default=None,
)

A list of paths to models saved on disk. Can be 'None' for 'analytical', 'data_based or 'simulator' objective functions. If 'None', either 'func' or 'simulator_path' must not be 'None'.

symbol class-attribute instance-attribute
symbol: str = Field(
    description="Symbol to represent the objective function. This will be used in the rest of the problem definition. It may also be used in UIs and visualizations. Example: 'f_1'."
)

Symbol to represent the objective function. This will be used in the rest of the problem definition. It may also be used in UIs and visualizations. Example: 'f_1'.

unit class-attribute instance-attribute
unit: str | None = Field(
    description="The unit of the objective function. This is optional. Used in UIs and visualizations. Example: 'seconds' or 'millions of hectares'.",
    default=None,
)

The unit of the objective function. This is optional. Used in UIs and visualizations. Example: 'seconds' or 'millions of hectares'. Defaults to None.

ObjectiveTypeEnum

Bases: str, Enum

An enumerator for supported objective function types.

Source code in desdeo/problem/schema.py
class ObjectiveTypeEnum(str, Enum):
    """An enumerator for supported objective function types."""

    analytical = "analytical"
    """An objective function with an analytical formulation. E.g., it can be
    expressed with mathematical expressions, such as x_1 + x_2."""
    data_based = "data_based"
    """A data-based objective function. It is assumed that when such an
    objective is present in a `Problem`, then there is a
    `DiscreteRepresentation` available with values representing the objective
    function."""
    simulator = "simulator"
    """A simulator based objective function. It is assumed that a Path (str)
    to a simulator file that connects a simulator to DESDEO is present in
    the `Objective` and also in the list of simulators in the `Problem`."""
    surrogate = "surrogate"
    """A surrogate based objective function. It is assumed that a Path (str)
    to a surrogate saved on the disk is present in the `Objective` and also in
    the list of simulators in the `Problem`."""
analytical class-attribute instance-attribute
analytical = 'analytical'

An objective function with an analytical formulation. E.g., it can be expressed with mathematical expressions, such as x_1 + x_2.

data_based class-attribute instance-attribute
data_based = 'data_based'

A data-based objective function. It is assumed that when such an objective is present in a Problem, then there is a DiscreteRepresentation available with values representing the objective function.

simulator class-attribute instance-attribute
simulator = 'simulator'

A simulator based objective function. It is assumed that a Path (str) to a simulator file that connects a simulator to DESDEO is present in the Objective and also in the list of simulators in the Problem.

surrogate class-attribute instance-attribute
surrogate = 'surrogate'

A surrogate based objective function. It is assumed that a Path (str) to a surrogate saved on the disk is present in the Objective and also in the list of simulators in the Problem.

Problem

Bases: BaseModel

Model for a problem definition.

Source code in desdeo/problem/schema.py
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
class Problem(BaseModel):
    """Model for a problem definition."""

    model_config = ConfigDict(frozen=True, extra="forbid")

    _scalarization_index: int = PrivateAttr(default=1)
    # TODO: make init to communicate the _scalarization_index to a new model

    @classmethod
    def from_problemdb(cls, db_instance: "ProblemDB") -> "Problem":
        """."""
        constants = [Constant.model_validate(const) for const in db_instance.constants] + [
            TensorConstant.model_validate(const) for const in db_instance.tensor_constants
        ]

        return cls(
            name=db_instance.name,
            description=db_instance.description,
            is_convex=db_instance.is_convex,
            is_linear=db_instance.is_linear,
            is_twice_differentiable=db_instance.is_twice_differentiable,
            scenario_keys=db_instance.scenario_keys,
            constants=constants if constants != [] else None,
            variables=[Variable.model_validate(var) for var in db_instance.variables]
            + [TensorVariable.model_validate(var) for var in db_instance.tensor_variables],
            objectives=[Objective.model_validate(obj) for obj in db_instance.objectives],
            constraints=[Constraint.model_validate(const) for const in db_instance.constraints]
            if db_instance.constraints != []
            else None,
            scalarization_funcs=[ScalarizationFunction.model_validate(scal) for scal in db_instance.scalarization_funcs]
            if db_instance.scalarization_funcs != []
            else None,
            extra_funcs=[ExtraFunction.model_validate(extra) for extra in db_instance.extra_funcs]
            if db_instance.extra_funcs != []
            else None,
            discrete_representation=DiscreteRepresentation.model_validate(db_instance.discrete_representation)
            if db_instance.discrete_representation is not None
            else None,
            simulators=[Simulator.model_validate(sim) for sim in db_instance.simulators]
            if db_instance.simulators != []
            else None,
        )

    @model_validator(mode="after")
    def set_default_scalarization_names(self) -> "Problem":
        """Check the scalarization functions for symbols with value 'None'.

        If found, names them systematically
        'scal_i', where 'i' is a running index stored in an instance attribute.
        """
        if self.scalarization_funcs is None:
            return self

        for func in self.scalarization_funcs:
            if func.symbol is None:
                func.symbol = f"scal_{self._scalarization_index}"
                self._scalarization_index += 1

        return self

    @model_validator(mode="after")
    def check_for_non_unique_symbols(self) -> "Problem":
        """Check that all the symbols defined in the different fields are unique."""
        symbols = self.get_all_symbols()

        # symbol is always populated
        symbol_counts = Counter(symbols)

        # collect duplicates, if they exist
        duplicates = {symbol: count for symbol, count in symbol_counts.items() if count > 1}

        if duplicates:
            # if any duplicates are found, raise a value error and report the duplicate symbols.
            msg = "Non-unique symbols found in the Problem model."
            for symbol, count in duplicates.items():
                msg += f" Symbol '{symbol}' occurs {count} times."

            raise ValueError(msg)

        return self

    def get_all_symbols(self) -> list[str]:
        """Collects and returns all the symbols symbols currently defined in the model."""
        # collect all symbols
        symbols = [variable.symbol for variable in self.variables]
        symbols += [objective.symbol for objective in self.objectives]
        if self.constants is not None:
            symbols += [constant.symbol for constant in self.constants]
        if self.constraints is not None:
            symbols += [constraint.symbol for constraint in self.constraints]
        if self.extra_funcs is not None:
            symbols += [extra.symbol for extra in self.extra_funcs]
        if self.scalarization_funcs is not None:
            symbols += [scalarization.symbol for scalarization in self.scalarization_funcs]

        return symbols

    def add_scalarization(self, new_scal: ScalarizationFunction) -> "Problem":
        """Adds a new scalarization function to the model.

        If no symbol is defined, adds a name with the format 'scal_i'.

        Does not modify the original problem model, but instead returns a copy of it with the added
        scalarization function.

        Args:
            new_scal (ScalarizationFunction): Scalarization functions to be added to the model.

        Raises:
            ValueError: Raised when a ScalarizationFunction is given with a symbol that already exists in the model.

        Returns:
            Problem: a copy of the problem with the added scalarization function.
        """
        if new_scal.symbol is None:
            new_scal.symbol = f"scal_{self._scalarization_index}"
            self._scalarization_index += 1

        if self.scalarization_funcs is None:
            return self.model_copy(update={"scalarization_funcs": [new_scal]})
        symbols = self.get_all_symbols()
        symbols.append(new_scal.symbol)
        symbol_counts = Counter(symbols)
        duplicates = {symbol: count for symbol, count in symbol_counts.items() if count > 1}

        if duplicates:
            msg = "Non-unique symbols found in the Problem model."
            for symbol, count in duplicates.items():
                msg += f" Symbol '{symbol}' occurs {count} times."

            raise ValueError(msg)

        return self.model_copy(update={"scalarization_funcs": [*self.scalarization_funcs, new_scal]})

    def update_ideal_and_nadir(
        self,
        new_ideal: dict[str, VariableType | None] | None = None,
        new_nadir: dict[str, VariableType | None] | None = None,
    ) -> "Problem":
        """Update the ideal and nadir values of the problem.

        Args:
            new_ideal (dict[str, VariableType  |  None] | None): _description_
            new_nadir (dict[str, VariableType  |  None] | None): _description_
        """
        updated_objectives = []
        for objective in self.objectives:
            new_objective = objective.model_copy(
                update={
                    **(
                        {"ideal": new_ideal[objective.symbol]}
                        if new_ideal is not None and objective.symbol in new_ideal
                        else {}
                    ),
                    **(
                        {"nadir": new_nadir[objective.symbol]}
                        if new_nadir is not None and objective.symbol in new_nadir
                        else {}
                    ),
                }
            )

            updated_objectives.append(new_objective)

        return self.model_copy(update={"objectives": updated_objectives})

    def add_constraints(self, new_constraints: list[Constraint]) -> "Problem":
        """Adds new constraints to the problem model.

        Does not modify the original problem model, but instead returns a copy of it with
        the added constraints. The symbols of the new constraints to be added must be
        unique.

        Args:
            new_constraints (list[Constraint]): the new `Constraint`s to be added to the model.

        Raises:
            TypeError: when the `new_constraints` is not a list.
            ValueError: when duplicate symbols are found among the new_constraints, or
                any of the new constraints utilized an existing symbol in the problem's model.

        Returns:
            Problem: a copy of the problem with the added constraints.
        """
        if not isinstance(new_constraints, list):
            # not a list
            msg = "The argument `new_constraints` must be a list."
            raise TypeError(msg)

        all_symbols = self.get_all_symbols()
        new_symbols = [const.symbol for const in new_constraints]

        if len(new_symbols) > len(set(new_symbols)):
            # duplicate symbols in the new constraint functions
            msg = "Duplicate symbols found in the new constraint functions to be added."
            raise ValueError(msg)

        for s in new_symbols:
            if s in all_symbols:
                # symbol already exists
                msg = "A symbol was provided for a new constraint that already exists in the problem definition."
                raise ValueError(msg)

        # proceed to add the new constraints
        return self.model_copy(
            update={
                "constraints": new_constraints if self.constraints is None else [*self.constraints, *new_constraints]
            }
        )

    def add_variables(self, new_variables: list[Variable | TensorVariable]) -> "Problem":
        """Adds new variables to the problem model.

        Does not modify the original problem model, but instead returns a copy of it with
        the added variables. The symbols of the new variables to be added must be
        unique.

        Args:
            new_variables (list[Variable | TensorVariable]): the new variables to be added to the model.

        Raises:
            TypeError: when the `new_variables` is not a list.
            ValueError: when duplicate symbols are found among the new_variables, or
                any of the new variables utilized an existing symbol in the problem's model.

        Returns:
            Problem: a copy of the problem with the added variables.
        """
        if not isinstance(new_variables, list):
            # not a list
            msg = "The argument `new_variables` must be a list."
            raise TypeError(msg)

        all_symbols = self.get_all_symbols()
        new_symbols = [const.symbol for const in new_variables]

        if len(new_symbols) > len(set(new_symbols)):
            # duplicate symbols in the new variables
            msg = "Duplicate symbols found in the new variables to be added."
            raise ValueError(msg)

        for s in new_symbols:
            if s in all_symbols:
                # symbol already exists
                msg = "A symbol was provided for a new variable that already exists in the problem definition."
                raise ValueError(msg)

        # proceed to add the new variables, assumed existing variables are defined
        return self.model_copy(update={"variables": [*self.variables, *new_variables]})

    def get_flattened_variables(self) -> list[Variable]:
        """Return a list of the (flattened) variables of the problem.

        Returns a list of the variables defined for the problem so that any TensorVariables are flattened.

        Returns:
            list[Variable]: list of (flattened) variables.
        """
        return [
            item
            for var in self.variables
            for item in (var.to_variables() if isinstance(var, TensorVariable) else [var])
        ]

    def get_constraint(self, symbol: str) -> Constraint | None:
        """Return a copy of a `Constant` with the given symbol.

        Args:
            symbol (str): the symbol of the constraint.

        Returns:
            Constant | None: the copy of the constraint with the given symbol, or `None` if the constraint is not found.
                Also return `None` if no constraints have been defined for the problem.
        """
        if self.constraints is None:
            # no constraints defined
            return None
        for constraint in self.constraints:
            if constraint.symbol == symbol:
                return constraint.model_copy()

        # did not find symbol
        return None

    def get_variable(self, symbol: str) -> Variable | TensorVariable | None:
        """Return a copy of a `Variable` with the given symbol.

        Args:
            symbol (str): the symbol of the variable.

        Returns:
            Variable | TensorVariable | None: the copy of the variable with the given symbol,
                or `None` if the variable is not found.
        """
        for variable in self.variables:
            if variable.symbol == symbol:
                # variable found
                return variable.model_copy()

        # variable not found
        return None

    def get_objective(self, symbol: str, *, copy: bool = True) -> Objective | None:
        """Return a copy of an `Objective` with the given symbol.

        Args:
            symbol (str): the symbol of the objective.
            copy (bool): if True, return a copy of the objective, otherwise, return a reference. Defaults to True.

        Returns:
            Objective | None: the copy of the objective with the given symbol, or `None` if the objective is not found.
        """
        for objective in self.objectives:
            if objective.symbol == symbol:
                # objective found
                if copy:
                    # return a copy of the objective
                    return objective.model_copy()

                # return a reference instead
                return objective

        # objective not found
        return None

    def get_scalarization(self, symbol: str) -> ScalarizationFunction | None:
        """Return a copy of a `ScalarizationFunction` with the given symbol.

        Args:
            symbol (str): the symbol of the scalarization function.

        Returns:
            ScalarizationFunction | None: the copy of the scalarization function with the given symbol, or `None` if the
                scalarization function is not found. Returns `None` also when no scalarization functions have been
                defined for the problem.
        """
        if self.scalarization_funcs is None:
            # no scalarization functions defined
            return None

        for scal in self.scalarization_funcs:
            if scal.symbol == symbol:
                # scalarization function found
                return scal.model_copy()

        # scalarization function is not found
        return None

    def get_ideal_point(self) -> dict[str, float | None]:
        """Get the ideal point of the problem as an objective dict.

        Returns an objective dict containing the ideal values of the
        the problem for each objective function. These values may be `None`.

        Returns:
            dict[str, float | None] | None: an objective dict with the ideal
                point values (which may be `None`), or `None`.
        """
        return {f"{obj.symbol}": obj.ideal for obj in self.objectives}

    def get_nadir_point(self) -> dict[str, float | None]:
        """Get the nadir point of the problem as an objective dict.

        Returns an objective dict containing the nadir values of the
        the problem for each objective function. These values may be `None`.

        Returns:
            dict[str, float | None] | None: an objective dict with the nadir
                point values (which may be `None`), or `None`.
        """
        return {f"{obj.symbol}": obj.nadir for obj in self.objectives}

    @property
    def variable_domain(self) -> VariableDomainTypeEnum:
        """Check the variables defined for the problem and returns the type of their domain.

        Checks the variable types defined for the problem and tells if the
        problem is continuous, integer, binary, or mixed-integer.

        Returns:
            VariableDomainEnum: whether the problem is continuous, integer, binary, or mixed-integer.
        """
        variable_types = [var.variable_type for var in self.variables]

        if all(t == VariableTypeEnum.real for t in variable_types):
            # all variables are real valued -> continuous problem
            return VariableDomainTypeEnum.continuous

        if all(t == VariableTypeEnum.binary for t in variable_types):
            # all variables are binary valued -> binary problem
            return VariableDomainTypeEnum.binary

        if all(t in [VariableTypeEnum.integer, VariableTypeEnum.binary] for t in variable_types):
            # all variables are integer or binary -> integer problem
            return VariableDomainTypeEnum.integer

        # mixed problem
        return VariableDomainTypeEnum.mixed

    @property
    def is_convex(self) -> bool:
        """Check if all the functions expressions in the problem are convex.

        Note:
            If the field "is_convex" is explicitly set, then the provided value is returned.

            Otherwise, this method just checks all the functions expressions present in the problem
            and return true if all of them are convex. For complicated problems, this might
            result in an incorrect results. User discretion is advised.

        Returns:
            bool: whether the problem is convex or not.
        """
        if self.is_convex_ is not None:
            return self.is_convex_

        is_convex_values = (
            [obj.is_convex for obj in self.objectives]
            + ([con.is_convex for con in self.constraints] if self.constraints is not None else [])
            + ([extra.is_convex for extra in self.extra_funcs] if self.extra_funcs is not None else [])
            + ([scal.is_convex for scal in self.scalarization_funcs] if self.scalarization_funcs is not None else [])
        )

        return all(is_convex_values)

    @property
    def is_linear(self) -> bool:
        """Check if all the functions expressions in the problem are linear.

        Note:
            If the field "is_linear" is explicitly set, then the provided value is returned.

            Otherwise, this method just checks all the functions expressions present in the problem
            and return true if all of them are linear. For complicated problems, this might
            result in an incorrect results. User discretion is advised.

        Returns:
            bool: whether the problem is linear or not.
        """
        if self.is_linear_ is not None:
            return self.is_linear_

        is_linear_values = (
            [obj.is_linear for obj in self.objectives]
            + ([con.is_linear for con in self.constraints] if self.constraints is not None else [])
            + ([extra.is_linear for extra in self.extra_funcs] if self.extra_funcs is not None else [])
            + ([scal.is_linear for scal in self.scalarization_funcs] if self.scalarization_funcs is not None else [])
        )

        return all(is_linear_values)

    @property
    def is_twice_differentiable(self) -> bool:
        """Check if all the functions expressions in the problem are twice differentiable.

        Note:
            If the field "is_twice_differentiable" is explicitly set, then the provided value is returned.

            Otherwise, this method just checks all the functions expressions present in the problem
            and return true if all of them are twice differentiable. For complicated problems, this might
            result in an incorrect results. User discretion is advised.

        Returns:
            bool: whether the problem is twice differentiable or not.
        """
        if self.is_twice_differentiable_ is not None:
            return self.is_twice_differentiable_

        is_diff_values = (
            [obj.is_twice_differentiable for obj in self.objectives]
            + ([con.is_twice_differentiable for con in self.constraints] if self.constraints is not None else [])
            + ([extra.is_twice_differentiable for extra in self.extra_funcs] if self.extra_funcs is not None else [])
            + (
                [scal.is_twice_differentiable for scal in self.scalarization_funcs]
                if self.scalarization_funcs is not None
                else []
            )
        )

        return all(is_diff_values)

    def get_scenario_problem(self, target_keys: str | list[str]) -> "Problem":
        """Returns a new Problem with fields belonging to a specified scenario.

        The new problem will have the fields `objectives`, `constraints`, `extra_funcs`,
        and `scalarization_funcs` with only the entries that belong to the specified
        scenario. The other entries will remain unchanged.

        Note:
            Fields with their `scenario_key` being `None` are assumed to belong to all scenarios,
            and are thus always included in each scenario.

        Args:
            target_keys (str | list[str]): the key or keys of the scenario(s) we wish to get.

        Raises:
            ValueError: (some of) the given `target_keys` has not been defined to be a scenario
                in the problem.

        Returns:
            Problem: a new problem with only the field that belong to the specified scenario.
        """
        if isinstance(target_keys, str):
            # if just a single key is given, make a list out of it.abs
            target_keys = [target_keys]

        # the any matches any keys
        if self.scenario_keys is None or not any(element in target_keys for element in self.scenario_keys):
            # invalid scenario
            msg = (
                f"The scenario '{target_keys}' has not been defined to be a valid scenario, or the problem has no "
                "scenarios defined."
            )
            raise ValueError(msg)

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

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

    def save_to_json(self, path: Path) -> None:
        """Save the Problem model in JSON format to a file.

        Args:
            path (Path): path to the file the model should be saved to.

        """
        json_content = self.model_dump_json(indent=4)
        path.write_text(json_content, encoding="utf-8")

    @classmethod
    def load_json(cls, path: Path) -> "Problem":
        """Load a Problem model stored in a JSON file.

        Args:
            path (Path): path to file storing a Problem model in JSON format.

        Returns:
            Problem: the as defined in the data.
        """
        json_data = path.read_text()

        return cls.model_validate_json(json_data, by_name=True)

    name: str = Field(
        description="Name of the problem.",
    )
    """Name of the problem."""
    description: str = Field(description="Description of the problem.")
    """Description of the problem."""
    constants: list[Constant | TensorConstant] | None = Field(
        description="Optional list of the constants present in the problem.", default=None
    )
    """List of the constants present in the problem. Defaults to `None`."""
    variables: list[Variable | TensorVariable] = Field(description="List of variables present in the problem.")
    """List of variables present in the problem."""
    objectives: list[Objective] = Field(description="List of the objectives present in the problem.")
    """List of the objectives present in the problem."""
    constraints: list[Constraint] | None = Field(
        description="Optional list of constraints present in the problem.",
        default=None,
    )
    """Optional list of constraints present in the problem. Defaults to `None`."""
    extra_funcs: list[ExtraFunction] | None = Field(
        description="Optional list of extra functions. Use this if some function is repeated multiple times.",
        default=None,
    )
    """Optional list of extra functions. Use this if some function is repeated multiple times. Defaults to `None`."""
    scalarization_funcs: list[ScalarizationFunction] | None = Field(
        description="Optional list of scalarization functions of the problem.", default=None
    )
    """Optional list of scalarization functions of the problem. Defaults to `None`."""
    discrete_representation: DiscreteRepresentation | None = Field(
        description=(
            "Optional. Required when there are one or more 'data_based' Objectives. The corresponding values "
            "of the 'data_based' objective function will be fetched from this with the given variable values. "
            "Is also utilized for methods which require both an analytical and discrete representation of a problem."
        ),
        default=None,
    )
    """Optional. Required when there are one or more 'data_based' Objectives.
    The corresponding values of the 'data_based' objective function will be
    fetched from this with the given variable values.  Is also utilized for
    methods which require both an analytical and discrete representation of a
    problem. Defaults to `None`."""
    scenario_keys: list[str] | None = Field(
        description=(
            "Optional. The scenario keys defined for the problem. Each key will point to a subset of objectives, "
            "constraints, extra functions, and scalarization functions that have the same scenario key defined to them."
            "If None, then the problem is assumed to not contain scenarios."
        ),
        default=None,
    )
    """Optional. The scenario keys defined for the problem. Each key will point
    to a subset of objectives, " "constraints, extra functions, and
    scalarization functions that have the same scenario key defined to them."
    "If None, then the problem is assumed to not contain scenarios."""
    simulators: list[Simulator] | None = Field(
        description=(
            "Optional. The simulators used by the problem. Required when there are one or more "
            "Objectives defined by simulators. The corresponding values of the 'simulator' objective "
            "function will be fetched from these simulators with the given variable values."
        ),
        default=None,
    )
    """Optional. The simulators used by the problem. Required when there are one or more
    Objectives defined by simulators. The corresponding values of the 'simulator' objective
    function will be fetched from these simulators with the given variable values.
    Defaults to `None`."""
    is_convex_: bool | None = Field(
        description=(
            "Optional. Used to manually indicate if the problem, as a whole, can be considered to be convex. "
            "If set to `None`, this property will be automatically inferred from the "
            "respective properties of other attributes."
        ),
        default=None,
        alias="is_convex",
    )
    """Optional. Used to manually indicate if the problem, as a whole, can be considered to be convex. "
    "If set to `None`, this property will be automatically inferred from the "
    "respective properties of other attributes."""
    is_linear_: bool | None = Field(
        description=(
            "Optional. Used to manually indicate if the problem, as a whole, can be considered to be linear. "
            "If set to `None`, this property will be automatically inferred from the "
            "respective properties of other attributes."
        ),
        default=None,
        alias="is_linear",
    )
    """Optional. Used to manually indicate if the problem, as a whole, can be considered to be linear. "
    "If set to `None`, this property will be automatically inferred from the "
    "respective properties of other attributes."""
    is_twice_differentiable_: bool | None = Field(
        description=(
            "Optional. Used to manually indicate if the problem, as a whole, can be considered to be twice "
            "differentiable. If set to `None`, this property will be automatically inferred from the "
            "respective properties of other attributes."
        ),
        default=None,
        alias="is_twice_differentiable",
    )
    """Optional. Used to manually indicate if the problem, as a whole, can be considered to be twice "
    "differentiable. If set to `None`, this property will be automatically inferred from the "
    "respective properties of other attributes."""
constants class-attribute instance-attribute
constants: list[Constant | TensorConstant] | None = Field(
    description="Optional list of the constants present in the problem.",
    default=None,
)

List of the constants present in the problem. Defaults to None.

constraints class-attribute instance-attribute
constraints: list[Constraint] | None = Field(
    description="Optional list of constraints present in the problem.",
    default=None,
)

Optional list of constraints present in the problem. Defaults to None.

description class-attribute instance-attribute
description: str = Field(
    description="Description of the problem."
)

Description of the problem.

discrete_representation class-attribute instance-attribute
discrete_representation: DiscreteRepresentation | None = (
    Field(
        description="Optional. Required when there are one or more 'data_based' Objectives. The corresponding values of the 'data_based' objective function will be fetched from this with the given variable values. Is also utilized for methods which require both an analytical and discrete representation of a problem.",
        default=None,
    )
)

Optional. Required when there are one or more 'data_based' Objectives. The corresponding values of the 'data_based' objective function will be fetched from this with the given variable values. Is also utilized for methods which require both an analytical and discrete representation of a problem. Defaults to None.

extra_funcs class-attribute instance-attribute
extra_funcs: list[ExtraFunction] | None = Field(
    description="Optional list of extra functions. Use this if some function is repeated multiple times.",
    default=None,
)

Optional list of extra functions. Use this if some function is repeated multiple times. Defaults to None.

is_convex property
is_convex: bool

Check if all the functions expressions in the problem are convex.

Note

If the field "is_convex" is explicitly set, then the provided value is returned.

Otherwise, this method just checks all the functions expressions present in the problem and return true if all of them are convex. For complicated problems, this might result in an incorrect results. User discretion is advised.

Returns:

Name Type Description
bool bool

whether the problem is convex or not.

is_convex_ class-attribute instance-attribute
is_convex_: bool | None = Field(
    description="Optional. Used to manually indicate if the problem, as a whole, can be considered to be convex. If set to `None`, this property will be automatically inferred from the respective properties of other attributes.",
    default=None,
    alias="is_convex",
)

Optional. Used to manually indicate if the problem, as a whole, can be considered to be convex. " "If set to None, this property will be automatically inferred from the " "respective properties of other attributes.

is_linear property
is_linear: bool

Check if all the functions expressions in the problem are linear.

Note

If the field "is_linear" is explicitly set, then the provided value is returned.

Otherwise, this method just checks all the functions expressions present in the problem and return true if all of them are linear. For complicated problems, this might result in an incorrect results. User discretion is advised.

Returns:

Name Type Description
bool bool

whether the problem is linear or not.

is_linear_ class-attribute instance-attribute
is_linear_: bool | None = Field(
    description="Optional. Used to manually indicate if the problem, as a whole, can be considered to be linear. If set to `None`, this property will be automatically inferred from the respective properties of other attributes.",
    default=None,
    alias="is_linear",
)

Optional. Used to manually indicate if the problem, as a whole, can be considered to be linear. " "If set to None, this property will be automatically inferred from the " "respective properties of other attributes.

is_twice_differentiable property
is_twice_differentiable: bool

Check if all the functions expressions in the problem are twice differentiable.

Note

If the field "is_twice_differentiable" is explicitly set, then the provided value is returned.

Otherwise, this method just checks all the functions expressions present in the problem and return true if all of them are twice differentiable. For complicated problems, this might result in an incorrect results. User discretion is advised.

Returns:

Name Type Description
bool bool

whether the problem is twice differentiable or not.

is_twice_differentiable_ class-attribute instance-attribute
is_twice_differentiable_: bool | None = Field(
    description="Optional. Used to manually indicate if the problem, as a whole, can be considered to be twice differentiable. If set to `None`, this property will be automatically inferred from the respective properties of other attributes.",
    default=None,
    alias="is_twice_differentiable",
)

Optional. Used to manually indicate if the problem, as a whole, can be considered to be twice " "differentiable. If set to None, this property will be automatically inferred from the " "respective properties of other attributes.

name class-attribute instance-attribute
name: str = Field(description='Name of the problem.')

Name of the problem.

objectives class-attribute instance-attribute
objectives: list[Objective] = Field(
    description="List of the objectives present in the problem."
)

List of the objectives present in the problem.

scalarization_funcs class-attribute instance-attribute
scalarization_funcs: list[ScalarizationFunction] | None = (
    Field(
        description="Optional list of scalarization functions of the problem.",
        default=None,
    )
)

Optional list of scalarization functions of the problem. Defaults to None.

scenario_keys class-attribute instance-attribute
scenario_keys: list[str] | None = Field(
    description="Optional. The scenario keys defined for the problem. Each key will point to a subset of objectives, constraints, extra functions, and scalarization functions that have the same scenario key defined to them.If None, then the problem is assumed to not contain scenarios.",
    default=None,
)

Optional. The scenario keys defined for the problem. Each key will point to a subset of objectives, " "constraints, extra functions, and scalarization functions that have the same scenario key defined to them." "If None, then the problem is assumed to not contain scenarios.

simulators class-attribute instance-attribute
simulators: list[Simulator] | None = Field(
    description="Optional. The simulators used by the problem. Required when there are one or more Objectives defined by simulators. The corresponding values of the 'simulator' objective function will be fetched from these simulators with the given variable values.",
    default=None,
)

Optional. The simulators used by the problem. Required when there are one or more Objectives defined by simulators. The corresponding values of the 'simulator' objective function will be fetched from these simulators with the given variable values. Defaults to None.

variable_domain property
variable_domain: VariableDomainTypeEnum

Check the variables defined for the problem and returns the type of their domain.

Checks the variable types defined for the problem and tells if the problem is continuous, integer, binary, or mixed-integer.

Returns:

Name Type Description
VariableDomainEnum VariableDomainTypeEnum

whether the problem is continuous, integer, binary, or mixed-integer.

variables class-attribute instance-attribute
variables: list[Variable | TensorVariable] = Field(
    description="List of variables present in the problem."
)

List of variables present in the problem.

add_constraints
add_constraints(
    new_constraints: list[Constraint],
) -> Problem

Adds new constraints to the problem model.

Does not modify the original problem model, but instead returns a copy of it with the added constraints. The symbols of the new constraints to be added must be unique.

Parameters:

Name Type Description Default
new_constraints list[Constraint]

the new Constraints to be added to the model.

required

Raises:

Type Description
TypeError

when the new_constraints is not a list.

ValueError

when duplicate symbols are found among the new_constraints, or any of the new constraints utilized an existing symbol in the problem's model.

Returns:

Name Type Description
Problem Problem

a copy of the problem with the added constraints.

Source code in desdeo/problem/schema.py
def add_constraints(self, new_constraints: list[Constraint]) -> "Problem":
    """Adds new constraints to the problem model.

    Does not modify the original problem model, but instead returns a copy of it with
    the added constraints. The symbols of the new constraints to be added must be
    unique.

    Args:
        new_constraints (list[Constraint]): the new `Constraint`s to be added to the model.

    Raises:
        TypeError: when the `new_constraints` is not a list.
        ValueError: when duplicate symbols are found among the new_constraints, or
            any of the new constraints utilized an existing symbol in the problem's model.

    Returns:
        Problem: a copy of the problem with the added constraints.
    """
    if not isinstance(new_constraints, list):
        # not a list
        msg = "The argument `new_constraints` must be a list."
        raise TypeError(msg)

    all_symbols = self.get_all_symbols()
    new_symbols = [const.symbol for const in new_constraints]

    if len(new_symbols) > len(set(new_symbols)):
        # duplicate symbols in the new constraint functions
        msg = "Duplicate symbols found in the new constraint functions to be added."
        raise ValueError(msg)

    for s in new_symbols:
        if s in all_symbols:
            # symbol already exists
            msg = "A symbol was provided for a new constraint that already exists in the problem definition."
            raise ValueError(msg)

    # proceed to add the new constraints
    return self.model_copy(
        update={
            "constraints": new_constraints if self.constraints is None else [*self.constraints, *new_constraints]
        }
    )
add_scalarization
add_scalarization(
    new_scal: ScalarizationFunction,
) -> Problem

Adds a new scalarization function to the model.

If no symbol is defined, adds a name with the format 'scal_i'.

Does not modify the original problem model, but instead returns a copy of it with the added scalarization function.

Parameters:

Name Type Description Default
new_scal ScalarizationFunction

Scalarization functions to be added to the model.

required

Raises:

Type Description
ValueError

Raised when a ScalarizationFunction is given with a symbol that already exists in the model.

Returns:

Name Type Description
Problem Problem

a copy of the problem with the added scalarization function.

Source code in desdeo/problem/schema.py
def add_scalarization(self, new_scal: ScalarizationFunction) -> "Problem":
    """Adds a new scalarization function to the model.

    If no symbol is defined, adds a name with the format 'scal_i'.

    Does not modify the original problem model, but instead returns a copy of it with the added
    scalarization function.

    Args:
        new_scal (ScalarizationFunction): Scalarization functions to be added to the model.

    Raises:
        ValueError: Raised when a ScalarizationFunction is given with a symbol that already exists in the model.

    Returns:
        Problem: a copy of the problem with the added scalarization function.
    """
    if new_scal.symbol is None:
        new_scal.symbol = f"scal_{self._scalarization_index}"
        self._scalarization_index += 1

    if self.scalarization_funcs is None:
        return self.model_copy(update={"scalarization_funcs": [new_scal]})
    symbols = self.get_all_symbols()
    symbols.append(new_scal.symbol)
    symbol_counts = Counter(symbols)
    duplicates = {symbol: count for symbol, count in symbol_counts.items() if count > 1}

    if duplicates:
        msg = "Non-unique symbols found in the Problem model."
        for symbol, count in duplicates.items():
            msg += f" Symbol '{symbol}' occurs {count} times."

        raise ValueError(msg)

    return self.model_copy(update={"scalarization_funcs": [*self.scalarization_funcs, new_scal]})
add_variables
add_variables(
    new_variables: list[Variable | TensorVariable],
) -> Problem

Adds new variables to the problem model.

Does not modify the original problem model, but instead returns a copy of it with the added variables. The symbols of the new variables to be added must be unique.

Parameters:

Name Type Description Default
new_variables list[Variable | TensorVariable]

the new variables to be added to the model.

required

Raises:

Type Description
TypeError

when the new_variables is not a list.

ValueError

when duplicate symbols are found among the new_variables, or any of the new variables utilized an existing symbol in the problem's model.

Returns:

Name Type Description
Problem Problem

a copy of the problem with the added variables.

Source code in desdeo/problem/schema.py
def add_variables(self, new_variables: list[Variable | TensorVariable]) -> "Problem":
    """Adds new variables to the problem model.

    Does not modify the original problem model, but instead returns a copy of it with
    the added variables. The symbols of the new variables to be added must be
    unique.

    Args:
        new_variables (list[Variable | TensorVariable]): the new variables to be added to the model.

    Raises:
        TypeError: when the `new_variables` is not a list.
        ValueError: when duplicate symbols are found among the new_variables, or
            any of the new variables utilized an existing symbol in the problem's model.

    Returns:
        Problem: a copy of the problem with the added variables.
    """
    if not isinstance(new_variables, list):
        # not a list
        msg = "The argument `new_variables` must be a list."
        raise TypeError(msg)

    all_symbols = self.get_all_symbols()
    new_symbols = [const.symbol for const in new_variables]

    if len(new_symbols) > len(set(new_symbols)):
        # duplicate symbols in the new variables
        msg = "Duplicate symbols found in the new variables to be added."
        raise ValueError(msg)

    for s in new_symbols:
        if s in all_symbols:
            # symbol already exists
            msg = "A symbol was provided for a new variable that already exists in the problem definition."
            raise ValueError(msg)

    # proceed to add the new variables, assumed existing variables are defined
    return self.model_copy(update={"variables": [*self.variables, *new_variables]})
check_for_non_unique_symbols
check_for_non_unique_symbols() -> Problem

Check that all the symbols defined in the different fields are unique.

Source code in desdeo/problem/schema.py
@model_validator(mode="after")
def check_for_non_unique_symbols(self) -> "Problem":
    """Check that all the symbols defined in the different fields are unique."""
    symbols = self.get_all_symbols()

    # symbol is always populated
    symbol_counts = Counter(symbols)

    # collect duplicates, if they exist
    duplicates = {symbol: count for symbol, count in symbol_counts.items() if count > 1}

    if duplicates:
        # if any duplicates are found, raise a value error and report the duplicate symbols.
        msg = "Non-unique symbols found in the Problem model."
        for symbol, count in duplicates.items():
            msg += f" Symbol '{symbol}' occurs {count} times."

        raise ValueError(msg)

    return self
from_problemdb classmethod
from_problemdb(db_instance: ProblemDB) -> Problem

.

Source code in desdeo/problem/schema.py
@classmethod
def from_problemdb(cls, db_instance: "ProblemDB") -> "Problem":
    """."""
    constants = [Constant.model_validate(const) for const in db_instance.constants] + [
        TensorConstant.model_validate(const) for const in db_instance.tensor_constants
    ]

    return cls(
        name=db_instance.name,
        description=db_instance.description,
        is_convex=db_instance.is_convex,
        is_linear=db_instance.is_linear,
        is_twice_differentiable=db_instance.is_twice_differentiable,
        scenario_keys=db_instance.scenario_keys,
        constants=constants if constants != [] else None,
        variables=[Variable.model_validate(var) for var in db_instance.variables]
        + [TensorVariable.model_validate(var) for var in db_instance.tensor_variables],
        objectives=[Objective.model_validate(obj) for obj in db_instance.objectives],
        constraints=[Constraint.model_validate(const) for const in db_instance.constraints]
        if db_instance.constraints != []
        else None,
        scalarization_funcs=[ScalarizationFunction.model_validate(scal) for scal in db_instance.scalarization_funcs]
        if db_instance.scalarization_funcs != []
        else None,
        extra_funcs=[ExtraFunction.model_validate(extra) for extra in db_instance.extra_funcs]
        if db_instance.extra_funcs != []
        else None,
        discrete_representation=DiscreteRepresentation.model_validate(db_instance.discrete_representation)
        if db_instance.discrete_representation is not None
        else None,
        simulators=[Simulator.model_validate(sim) for sim in db_instance.simulators]
        if db_instance.simulators != []
        else None,
    )
get_all_symbols
get_all_symbols() -> list[str]

Collects and returns all the symbols symbols currently defined in the model.

Source code in desdeo/problem/schema.py
def get_all_symbols(self) -> list[str]:
    """Collects and returns all the symbols symbols currently defined in the model."""
    # collect all symbols
    symbols = [variable.symbol for variable in self.variables]
    symbols += [objective.symbol for objective in self.objectives]
    if self.constants is not None:
        symbols += [constant.symbol for constant in self.constants]
    if self.constraints is not None:
        symbols += [constraint.symbol for constraint in self.constraints]
    if self.extra_funcs is not None:
        symbols += [extra.symbol for extra in self.extra_funcs]
    if self.scalarization_funcs is not None:
        symbols += [scalarization.symbol for scalarization in self.scalarization_funcs]

    return symbols
get_constraint
get_constraint(symbol: str) -> Constraint | None

Return a copy of a Constant with the given symbol.

Parameters:

Name Type Description Default
symbol str

the symbol of the constraint.

required

Returns:

Type Description
Constraint | None

Constant | None: the copy of the constraint with the given symbol, or None if the constraint is not found. Also return None if no constraints have been defined for the problem.

Source code in desdeo/problem/schema.py
def get_constraint(self, symbol: str) -> Constraint | None:
    """Return a copy of a `Constant` with the given symbol.

    Args:
        symbol (str): the symbol of the constraint.

    Returns:
        Constant | None: the copy of the constraint with the given symbol, or `None` if the constraint is not found.
            Also return `None` if no constraints have been defined for the problem.
    """
    if self.constraints is None:
        # no constraints defined
        return None
    for constraint in self.constraints:
        if constraint.symbol == symbol:
            return constraint.model_copy()

    # did not find symbol
    return None
get_flattened_variables
get_flattened_variables() -> list[Variable]

Return a list of the (flattened) variables of the problem.

Returns a list of the variables defined for the problem so that any TensorVariables are flattened.

Returns:

Type Description
list[Variable]

list[Variable]: list of (flattened) variables.

Source code in desdeo/problem/schema.py
def get_flattened_variables(self) -> list[Variable]:
    """Return a list of the (flattened) variables of the problem.

    Returns a list of the variables defined for the problem so that any TensorVariables are flattened.

    Returns:
        list[Variable]: list of (flattened) variables.
    """
    return [
        item
        for var in self.variables
        for item in (var.to_variables() if isinstance(var, TensorVariable) else [var])
    ]
get_ideal_point
get_ideal_point() -> dict[str, float | None]

Get the ideal point of the problem as an objective dict.

Returns an objective dict containing the ideal values of the the problem for each objective function. These values may be None.

Returns:

Type Description
dict[str, float | None]

dict[str, float | None] | None: an objective dict with the ideal point values (which may be None), or None.

Source code in desdeo/problem/schema.py
def get_ideal_point(self) -> dict[str, float | None]:
    """Get the ideal point of the problem as an objective dict.

    Returns an objective dict containing the ideal values of the
    the problem for each objective function. These values may be `None`.

    Returns:
        dict[str, float | None] | None: an objective dict with the ideal
            point values (which may be `None`), or `None`.
    """
    return {f"{obj.symbol}": obj.ideal for obj in self.objectives}
get_nadir_point
get_nadir_point() -> dict[str, float | None]

Get the nadir point of the problem as an objective dict.

Returns an objective dict containing the nadir values of the the problem for each objective function. These values may be None.

Returns:

Type Description
dict[str, float | None]

dict[str, float | None] | None: an objective dict with the nadir point values (which may be None), or None.

Source code in desdeo/problem/schema.py
def get_nadir_point(self) -> dict[str, float | None]:
    """Get the nadir point of the problem as an objective dict.

    Returns an objective dict containing the nadir values of the
    the problem for each objective function. These values may be `None`.

    Returns:
        dict[str, float | None] | None: an objective dict with the nadir
            point values (which may be `None`), or `None`.
    """
    return {f"{obj.symbol}": obj.nadir for obj in self.objectives}
get_objective
get_objective(
    symbol: str, *, copy: bool = True
) -> Objective | None

Return a copy of an Objective with the given symbol.

Parameters:

Name Type Description Default
symbol str

the symbol of the objective.

required
copy bool

if True, return a copy of the objective, otherwise, return a reference. Defaults to True.

True

Returns:

Type Description
Objective | None

Objective | None: the copy of the objective with the given symbol, or None if the objective is not found.

Source code in desdeo/problem/schema.py
def get_objective(self, symbol: str, *, copy: bool = True) -> Objective | None:
    """Return a copy of an `Objective` with the given symbol.

    Args:
        symbol (str): the symbol of the objective.
        copy (bool): if True, return a copy of the objective, otherwise, return a reference. Defaults to True.

    Returns:
        Objective | None: the copy of the objective with the given symbol, or `None` if the objective is not found.
    """
    for objective in self.objectives:
        if objective.symbol == symbol:
            # objective found
            if copy:
                # return a copy of the objective
                return objective.model_copy()

            # return a reference instead
            return objective

    # objective not found
    return None
get_scalarization
get_scalarization(
    symbol: str,
) -> ScalarizationFunction | None

Return a copy of a ScalarizationFunction with the given symbol.

Parameters:

Name Type Description Default
symbol str

the symbol of the scalarization function.

required

Returns:

Type Description
ScalarizationFunction | None

ScalarizationFunction | None: the copy of the scalarization function with the given symbol, or None if the scalarization function is not found. Returns None also when no scalarization functions have been defined for the problem.

Source code in desdeo/problem/schema.py
def get_scalarization(self, symbol: str) -> ScalarizationFunction | None:
    """Return a copy of a `ScalarizationFunction` with the given symbol.

    Args:
        symbol (str): the symbol of the scalarization function.

    Returns:
        ScalarizationFunction | None: the copy of the scalarization function with the given symbol, or `None` if the
            scalarization function is not found. Returns `None` also when no scalarization functions have been
            defined for the problem.
    """
    if self.scalarization_funcs is None:
        # no scalarization functions defined
        return None

    for scal in self.scalarization_funcs:
        if scal.symbol == symbol:
            # scalarization function found
            return scal.model_copy()

    # scalarization function is not found
    return None
get_scenario_problem
get_scenario_problem(
    target_keys: str | list[str],
) -> Problem

Returns a new Problem with fields belonging to a specified scenario.

The new problem will have the fields objectives, constraints, extra_funcs, and scalarization_funcs with only the entries that belong to the specified scenario. The other entries will remain unchanged.

Note

Fields with their scenario_key being None are assumed to belong to all scenarios, and are thus always included in each scenario.

Parameters:

Name Type Description Default
target_keys str | list[str]

the key or keys of the scenario(s) we wish to get.

required

Raises:

Type Description
ValueError

(some of) the given target_keys has not been defined to be a scenario in the problem.

Returns:

Name Type Description
Problem Problem

a new problem with only the field that belong to the specified scenario.

Source code in desdeo/problem/schema.py
def get_scenario_problem(self, target_keys: str | list[str]) -> "Problem":
    """Returns a new Problem with fields belonging to a specified scenario.

    The new problem will have the fields `objectives`, `constraints`, `extra_funcs`,
    and `scalarization_funcs` with only the entries that belong to the specified
    scenario. The other entries will remain unchanged.

    Note:
        Fields with their `scenario_key` being `None` are assumed to belong to all scenarios,
        and are thus always included in each scenario.

    Args:
        target_keys (str | list[str]): the key or keys of the scenario(s) we wish to get.

    Raises:
        ValueError: (some of) the given `target_keys` has not been defined to be a scenario
            in the problem.

    Returns:
        Problem: a new problem with only the field that belong to the specified scenario.
    """
    if isinstance(target_keys, str):
        # if just a single key is given, make a list out of it.abs
        target_keys = [target_keys]

    # the any matches any keys
    if self.scenario_keys is None or not any(element in target_keys for element in self.scenario_keys):
        # invalid scenario
        msg = (
            f"The scenario '{target_keys}' has not been defined to be a valid scenario, or the problem has no "
            "scenarios defined."
        )
        raise ValueError(msg)

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

    return self.model_copy(
        update={
            "objectives": scenario_objectives,
            "constraints": scenario_constraints,
            "extra_funcs": scenario_extras,
            "scalarization_funcs": scenario_scals,
        }
    )
get_variable
get_variable(
    symbol: str,
) -> Variable | TensorVariable | None

Return a copy of a Variable with the given symbol.

Parameters:

Name Type Description Default
symbol str

the symbol of the variable.

required

Returns:

Type Description
Variable | TensorVariable | None

Variable | TensorVariable | None: the copy of the variable with the given symbol, or None if the variable is not found.

Source code in desdeo/problem/schema.py
def get_variable(self, symbol: str) -> Variable | TensorVariable | None:
    """Return a copy of a `Variable` with the given symbol.

    Args:
        symbol (str): the symbol of the variable.

    Returns:
        Variable | TensorVariable | None: the copy of the variable with the given symbol,
            or `None` if the variable is not found.
    """
    for variable in self.variables:
        if variable.symbol == symbol:
            # variable found
            return variable.model_copy()

    # variable not found
    return None
load_json classmethod
load_json(path: Path) -> Problem

Load a Problem model stored in a JSON file.

Parameters:

Name Type Description Default
path Path

path to file storing a Problem model in JSON format.

required

Returns:

Name Type Description
Problem Problem

the as defined in the data.

Source code in desdeo/problem/schema.py
@classmethod
def load_json(cls, path: Path) -> "Problem":
    """Load a Problem model stored in a JSON file.

    Args:
        path (Path): path to file storing a Problem model in JSON format.

    Returns:
        Problem: the as defined in the data.
    """
    json_data = path.read_text()

    return cls.model_validate_json(json_data, by_name=True)
save_to_json
save_to_json(path: Path) -> None

Save the Problem model in JSON format to a file.

Parameters:

Name Type Description Default
path Path

path to the file the model should be saved to.

required
Source code in desdeo/problem/schema.py
def save_to_json(self, path: Path) -> None:
    """Save the Problem model in JSON format to a file.

    Args:
        path (Path): path to the file the model should be saved to.

    """
    json_content = self.model_dump_json(indent=4)
    path.write_text(json_content, encoding="utf-8")
set_default_scalarization_names
set_default_scalarization_names() -> Problem

Check the scalarization functions for symbols with value 'None'.

If found, names them systematically 'scal_i', where 'i' is a running index stored in an instance attribute.

Source code in desdeo/problem/schema.py
@model_validator(mode="after")
def set_default_scalarization_names(self) -> "Problem":
    """Check the scalarization functions for symbols with value 'None'.

    If found, names them systematically
    'scal_i', where 'i' is a running index stored in an instance attribute.
    """
    if self.scalarization_funcs is None:
        return self

    for func in self.scalarization_funcs:
        if func.symbol is None:
            func.symbol = f"scal_{self._scalarization_index}"
            self._scalarization_index += 1

    return self
update_ideal_and_nadir
update_ideal_and_nadir(
    new_ideal: dict[str, VariableType | None] | None = None,
    new_nadir: dict[str, VariableType | None] | None = None,
) -> Problem

Update the ideal and nadir values of the problem.

Parameters:

Name Type Description Default
new_ideal dict[str, VariableType | None] | None

description

None
new_nadir dict[str, VariableType | None] | None

description

None
Source code in desdeo/problem/schema.py
def update_ideal_and_nadir(
    self,
    new_ideal: dict[str, VariableType | None] | None = None,
    new_nadir: dict[str, VariableType | None] | None = None,
) -> "Problem":
    """Update the ideal and nadir values of the problem.

    Args:
        new_ideal (dict[str, VariableType  |  None] | None): _description_
        new_nadir (dict[str, VariableType  |  None] | None): _description_
    """
    updated_objectives = []
    for objective in self.objectives:
        new_objective = objective.model_copy(
            update={
                **(
                    {"ideal": new_ideal[objective.symbol]}
                    if new_ideal is not None and objective.symbol in new_ideal
                    else {}
                ),
                **(
                    {"nadir": new_nadir[objective.symbol]}
                    if new_nadir is not None and objective.symbol in new_nadir
                    else {}
                ),
            }
        )

        updated_objectives.append(new_objective)

    return self.model_copy(update={"objectives": updated_objectives})

ScalarizationFunction

Bases: BaseModel

Model for scalarization of the problem.

Source code in desdeo/problem/schema.py
class ScalarizationFunction(BaseModel):
    """Model for scalarization of the problem."""

    model_config = ConfigDict(from_attributes=True, extra="forbid")

    name: str = Field(description=("Name of the scalarization function."))
    """Name of the scalarization function."""
    symbol: str | None = Field(
        description=(
            "Optional symbol to represent the scalarization function. This may be used in UIs and visualizations."
        ),
        default=None,
    )
    """Optional symbol to represent the scalarization function. This may be used
    in UIs and visualizations. Defaults to `None`."""
    func: list = Field(
        description=(
            "Function representation of the scalarization. This is a JSON object that can be parsed into a function."
            "Must be a valid MathJSON object."
            " The symbols in the function must match the symbols defined for objective/variable/constant/extra"
            " function."
        ),
    )
    """ Function representation of the scalarization. This is a JSON object that
    can be parsed into a function.  Must be a valid MathJSON object. The
    symbols in the function must match the symbols defined for
    objective/variable/constant/extra function."""
    is_linear: bool = Field(
        description="Whether the function expression is linear or not. Defaults to `False`.", default=False
    )
    """Whether the function expression is linear or not. Defaults to `False`."""
    is_convex: bool = Field(
        description="Whether the function expression is convex or not (non-convex). Defaults to `False`.",
        default=False,
    )
    """Whether the function expression is convex or not (non-convex). Defaults to `False`."""
    is_twice_differentiable: bool = Field(
        description="Whether the function expression is twice differentiable or not. Defaults to `False`",
        default=False,
    )
    """Whether the function expression is twice differentiable or not. Defaults to `False`"""
    scenario_keys: list[str] = Field(
        description="Optional. The keys of the scenarios the scalarization function belongs to.", default=None
    )
    """Optional. The keys of the scenarios the scalarization function belongs to."""

    _parse_infix_to_func = field_validator("func", mode="before")(parse_infix_to_func)
    _parse_scenario_key_singleton_to_list = field_validator("scenario_keys", mode="before")(
        parse_scenario_key_singleton_to_list
    )
func class-attribute instance-attribute
func: list = Field(
    description="Function representation of the scalarization. This is a JSON object that can be parsed into a function.Must be a valid MathJSON object. The symbols in the function must match the symbols defined for objective/variable/constant/extra function."
)

Function representation of the scalarization. This is a JSON object that can be parsed into a function. Must be a valid MathJSON object. The symbols in the function must match the symbols defined for objective/variable/constant/extra function.

is_convex class-attribute instance-attribute
is_convex: bool = Field(
    description="Whether the function expression is convex or not (non-convex). Defaults to `False`.",
    default=False,
)

Whether the function expression is convex or not (non-convex). Defaults to False.

is_linear class-attribute instance-attribute
is_linear: bool = Field(
    description="Whether the function expression is linear or not. Defaults to `False`.",
    default=False,
)

Whether the function expression is linear or not. Defaults to False.

is_twice_differentiable class-attribute instance-attribute
is_twice_differentiable: bool = Field(
    description="Whether the function expression is twice differentiable or not. Defaults to `False`",
    default=False,
)

Whether the function expression is twice differentiable or not. Defaults to False

name class-attribute instance-attribute
name: str = Field(
    description="Name of the scalarization function."
)

Name of the scalarization function.

scenario_keys class-attribute instance-attribute
scenario_keys: list[str] = Field(
    description="Optional. The keys of the scenarios the scalarization function belongs to.",
    default=None,
)

Optional. The keys of the scenarios the scalarization function belongs to.

symbol class-attribute instance-attribute
symbol: str | None = Field(
    description="Optional symbol to represent the scalarization function. This may be used in UIs and visualizations.",
    default=None,
)

Optional symbol to represent the scalarization function. This may be used in UIs and visualizations. Defaults to None.

Simulator

Bases: BaseModel

Model for simulator data.

One of file or url must be provided, but not both.

Source code in desdeo/problem/schema.py
class Simulator(BaseModel):
    """Model for simulator data.

    One of `file` or `url` must be provided, but not both.
    """

    model_config = ConfigDict(frozen=True, from_attributes=True, extra="forbid")

    name: str = Field(
        description=("Descriptive name of the simulator. This can be used in UI and visualizations."),
    )
    """Descriptive name of the simulator. This can be used in UI and visualizations."""
    symbol: str = Field(
        description=(
            "Symbol to represent the simulator. This will be used in the rest of the problem definition."
            " It may also be used in UIs and visualizations."
        ),
    )
    file: Path | None = Field(description=("Path to a python file with the connection to simulators."), default=None)
    """Path to a python file with the connection to simulators."""
    url: Url | None = Field(
        description=(
            "Optional. URL to the simulator. A GET request to this URL should be used to evaluate solutions in batches."
        ),
        default=None,
    )
    """Optional. A URL to the simulator. A GET request to this URL should be used to evaluate solutions in batches."""
    parameter_options: dict | None = Field(
        description=(
            "Parameters to the simulator that are not decision variables, but affect the results."
            "Format is similar to decision variables. Can be 'None'."
        ),
        default=None,
    )
    """Parameters to the simulator that are not decision variables, but affect the results.
    Format is similar to decision variables. Can be 'None'."""

    # Check that either file or url is provided, but not both
    @model_validator(mode="after")
    def check_file_or_url(self) -> Self:
        """Ensure that either file or url is provided, but not both."""
        if self.file is None and self.url is None:
            raise ValueError("Either 'file' or 'url' must be provided.")
        if self.file is not None and self.url is not None:
            raise ValueError("Only one of 'file' or 'url' can be provided.")
        return self
file class-attribute instance-attribute
file: Path | None = Field(
    description="Path to a python file with the connection to simulators.",
    default=None,
)

Path to a python file with the connection to simulators.

name class-attribute instance-attribute
name: str = Field(
    description="Descriptive name of the simulator. This can be used in UI and visualizations."
)

Descriptive name of the simulator. This can be used in UI and visualizations.

parameter_options class-attribute instance-attribute
parameter_options: dict | None = Field(
    description="Parameters to the simulator that are not decision variables, but affect the results.Format is similar to decision variables. Can be 'None'.",
    default=None,
)

Parameters to the simulator that are not decision variables, but affect the results. Format is similar to decision variables. Can be 'None'.

url class-attribute instance-attribute
url: Url | None = Field(
    description="Optional. URL to the simulator. A GET request to this URL should be used to evaluate solutions in batches.",
    default=None,
)

Optional. A URL to the simulator. A GET request to this URL should be used to evaluate solutions in batches.

check_file_or_url
check_file_or_url() -> Self

Ensure that either file or url is provided, but not both.

Source code in desdeo/problem/schema.py
@model_validator(mode="after")
def check_file_or_url(self) -> Self:
    """Ensure that either file or url is provided, but not both."""
    if self.file is None and self.url is None:
        raise ValueError("Either 'file' or 'url' must be provided.")
    if self.file is not None and self.url is not None:
        raise ValueError("Only one of 'file' or 'url' can be provided.")
    return self

TensorConstant

Bases: BaseModel

Model for a tensor containing constant values.

Source code in desdeo/problem/schema.py
class TensorConstant(BaseModel):
    """Model for a tensor containing constant values."""

    model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True, from_attributes=True, extra="forbid")

    name: str = Field(description="Descriptive name of the tensor representing the values. E.g., 'distances'")
    """Descriptive name of the tensor representing the values. E.g., 'distances'"""
    symbol: str = Field(
        description=(
            "Symbol to represent the constant. This will be used in the rest of the problem definition."
            " Notice that the elements of the tensor will be represented with the symbol followed by"
            " indices. E.g., the first element of the third element of a 2-dimensional tensor,"
            " is represented by 'x_1_3', where 'x' is the symbol given to the TensorVariable."
            " Note that indexing starts from 1."
        )
    )
    """
    Symbol to represent the constant. This will be used in the rest of the problem definition.
    Notice that the elements of the tensor will be represented with the symbol followed by
    indices. E.g., the first element of the third element of a 2-dimensional tensor,
    is represented by 'x_1_3', where 'x' is the symbol given to the TensorVariable.
    Note that indexing starts from 1.
    """
    shape: list[int] = Field(
        description=(
            "A list of the dimensions of the tensor, e.g., `[2, 3]` would indicate a matrix with 2 rows and 3 columns."
        )
    )
    """A list of the dimensions of the tensor, e.g., `[2, 3]` would indicate a matrix with 2 rows and 3 columns.
    """
    values: Tensor = Field(
        description=(
            "A list of lists, with the elements representing the values of each constant element in the tensor. "
            "E.g., `[[5, 22, 0], [14, 5, 44]]`."
        ),
    )
    """A list of lists, with the elements representing the initial values of each constant element in the tensor.
    E.g., `[[5, 22, 0], [14, 5, 44]]`."""

    _parse_list_to_mathjson = field_validator("values", mode="before")(parse_list_to_mathjson)

    def get_values(self) -> Iterable[VariableType | Iterable[VariableType]] | Iterable[None, Iterable[None]]:
        """Return the constant values as a Python iterable (e.g., list of list)."""
        values = get_tensor_values(self.values)
        if isinstance(values, VariableType | None):
            return np.full(self.shape, values).tolist()

        return values

    def to_constants(self) -> list[Constant]:
        """Flatten the tensor into a list of Constants.

        Returns:
            list[Constant]: a list of Constants.
        """
        constants = []
        for indices in list(product(*[range(1, dim + 1) for dim in self.shape])):
            constants.append(self[*indices])

        return constants

    def __getitem__(self, indices: int | tuple[int]) -> Constant:
        """Implements random access for TensorConstant.

        Note:
            Indexing is assumed to start at 1.

        Args:
            indices (int | Tuple[int]): a single integer or tuple of integers.

        Returns:
            Constant: A new instance of Constant that has been setup with
                information found at the specified indices in the TensorConstant.
        """
        if isinstance(indices, tuple):
            # multi-dimensional indexing
            name = f"{self.name} at position {[*indices]}"
            symbol = f"{self.symbol}_{'_'.join(map(str, indices))}"

            value = self.get_values()

            for idx in indices:
                value = value[idx - 1]

        else:
            # single indexing
            name = f"{self.name} at position [{indices}]"
            symbol = f"{self.symbol}_{indices}"
            value = self.get_values()[indices - 1]

        return Constant(name=name, symbol=symbol, value=value)
name class-attribute instance-attribute
name: str = Field(
    description="Descriptive name of the tensor representing the values. E.g., 'distances'"
)

Descriptive name of the tensor representing the values. E.g., 'distances'

shape class-attribute instance-attribute
shape: list[int] = Field(
    description="A list of the dimensions of the tensor, e.g., `[2, 3]` would indicate a matrix with 2 rows and 3 columns."
)

A list of the dimensions of the tensor, e.g., [2, 3] would indicate a matrix with 2 rows and 3 columns.

symbol class-attribute instance-attribute
symbol: str = Field(
    description="Symbol to represent the constant. This will be used in the rest of the problem definition. Notice that the elements of the tensor will be represented with the symbol followed by indices. E.g., the first element of the third element of a 2-dimensional tensor, is represented by 'x_1_3', where 'x' is the symbol given to the TensorVariable. Note that indexing starts from 1."
)

Symbol to represent the constant. This will be used in the rest of the problem definition. Notice that the elements of the tensor will be represented with the symbol followed by indices. E.g., the first element of the third element of a 2-dimensional tensor, is represented by 'x_1_3', where 'x' is the symbol given to the TensorVariable. Note that indexing starts from 1.

values class-attribute instance-attribute
values: Tensor = Field(
    description="A list of lists, with the elements representing the values of each constant element in the tensor. E.g., `[[5, 22, 0], [14, 5, 44]]`."
)

A list of lists, with the elements representing the initial values of each constant element in the tensor. E.g., [[5, 22, 0], [14, 5, 44]].

__getitem__
__getitem__(indices: int | tuple[int]) -> Constant

Implements random access for TensorConstant.

Note

Indexing is assumed to start at 1.

Parameters:

Name Type Description Default
indices int | Tuple[int]

a single integer or tuple of integers.

required

Returns:

Name Type Description
Constant Constant

A new instance of Constant that has been setup with information found at the specified indices in the TensorConstant.

Source code in desdeo/problem/schema.py
def __getitem__(self, indices: int | tuple[int]) -> Constant:
    """Implements random access for TensorConstant.

    Note:
        Indexing is assumed to start at 1.

    Args:
        indices (int | Tuple[int]): a single integer or tuple of integers.

    Returns:
        Constant: A new instance of Constant that has been setup with
            information found at the specified indices in the TensorConstant.
    """
    if isinstance(indices, tuple):
        # multi-dimensional indexing
        name = f"{self.name} at position {[*indices]}"
        symbol = f"{self.symbol}_{'_'.join(map(str, indices))}"

        value = self.get_values()

        for idx in indices:
            value = value[idx - 1]

    else:
        # single indexing
        name = f"{self.name} at position [{indices}]"
        symbol = f"{self.symbol}_{indices}"
        value = self.get_values()[indices - 1]

    return Constant(name=name, symbol=symbol, value=value)
get_values
get_values() -> (
    Iterable[VariableType | Iterable[VariableType]]
    | Iterable[None, Iterable[None]]
)

Return the constant values as a Python iterable (e.g., list of list).

Source code in desdeo/problem/schema.py
def get_values(self) -> Iterable[VariableType | Iterable[VariableType]] | Iterable[None, Iterable[None]]:
    """Return the constant values as a Python iterable (e.g., list of list)."""
    values = get_tensor_values(self.values)
    if isinstance(values, VariableType | None):
        return np.full(self.shape, values).tolist()

    return values
to_constants
to_constants() -> list[Constant]

Flatten the tensor into a list of Constants.

Returns:

Type Description
list[Constant]

list[Constant]: a list of Constants.

Source code in desdeo/problem/schema.py
def to_constants(self) -> list[Constant]:
    """Flatten the tensor into a list of Constants.

    Returns:
        list[Constant]: a list of Constants.
    """
    constants = []
    for indices in list(product(*[range(1, dim + 1) for dim in self.shape])):
        constants.append(self[*indices])

    return constants

TensorVariable

Bases: BaseModel

Model for a tensor, e.g., vector variable.

Source code in desdeo/problem/schema.py
class TensorVariable(BaseModel):
    """Model for a tensor, e.g., vector variable."""

    model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True, from_attributes=True, extra="forbid")

    name: str = Field(
        description="Descriptive name of the variable. This can be used in UI and visualizations. Example: 'velocity'."
    )
    """Descriptive name of the variable. This can be used in UI and visualizations. Example: 'velocity'."""
    symbol: str = Field(
        description=(
            "Symbol to represent the variable. This will be used in the rest of the problem definition."
            " Notice that the elements of the tensor will be represented with the symbol followed by"
            " indices. E.g., the first element of the third element of a 2-dimensional tensor,"
            " is represented by 'x_1_3', where 'x' is the symbol given to the TensorVariable."
            " Note that indexing starts from 1."
        )
    )
    """
    Symbol to represent the variable. This will be used in the rest of the problem definition.
    Notice that the elements of the tensor will be represented with the symbol followed by
    indices. E.g., the first element of the third element of a 2-dimensional tensor,
    is represented by 'x_1_3', where 'x' is the symbol given to the TensorVariable.
    Note that indexing starts from 1.
    """
    variable_type: VariableTypeEnum = Field(
        description=(
            "Type of the variable. Can be real, integer, or binary. "
            "Note that each element of a TensorVariable is assumed to be of the same type."
        )
    )
    """Type of the variable. Can be real, integer, or binary.
    Note that each element of a TensorVariable is assumed to be of the same type."""

    shape: list[int] = Field(
        description=(
            "A list of the dimensions of the tensor, e.g., `[2, 3]` would indicate a matrix with 2 rows and 3 columns."
        )
    )
    """A list of the dimensions of the tensor,
    e.g., `[2, 3]` would indicate a matrix with 2 rows and 3 columns.
    """
    lowerbounds: Tensor | None = Field(
        description=(
            "A list of lists, with the elements representing the lower bounds of each element. "
            "E.g., `[[1, 2, 3], [4, 5, 6]]`. If a single value is supplied, that value is assumed to be the lower "
            "bound of each element. Defaults to None."
        ),
        default=None,
    )
    """A list of lists, with the elements representing the lower bounds of each
    element.  E.g., `[[1, 2, 3], [4, 5, 6]]`. If a single value is supplied,
    that value is assumed to be the lower bound of each element. Defaults to
    None."""
    upperbounds: Tensor | VariableType | None = Field(
        description=(
            "A list of lists, with the elements representing the upper bounds of each "
            "element.  E.g., `[[1, 2, 3], [4, 5, 6]]`. If a single value is supplied, "
            "that value is assumed to be the upper bound of each element. Defaults to "
            "None."
        ),
        default=None,
    )
    """A list of lists, with the elements representing the upper bounds of each
    element.  E.g., `[[1, 2, 3], [4, 5, 6]]`. If a single value is supplied,
    that value is assumed to be the upper bound of each element. Defaults to
    None."""
    initial_values: Tensor | VariableType | None = Field(
        description=(
            "A list of lists, with the elements representing the initial values of "
            "each element.  E.g., `[[1, 2, 3], [4, 5, 6]]`. If a single value is "
            "supplied, that value is assumed to be the initial value of each element. "
            "Defaults to None."
        ),
        default=None,
    )
    """A list of lists, with the elements representing the initial values of
    each element.  E.g., `[[1, 2, 3], [4, 5, 6]]`. If a single value is
    supplied, that value is assumed to be the initial value of each element.
    Defaults to None."""

    _parse_list_to_mathjson = field_validator("lowerbounds", "upperbounds", "initial_values", mode="before")(
        parse_list_to_mathjson
    )

    def get_lowerbound_values(
        self,
    ) -> Iterable[VariableType | Iterable[VariableType]] | Iterable[None | Iterable[None]]:
        """Return the lowerbounds values, if any, as a Python iterable (list of list)."""
        lowerbounds = get_tensor_values(self.lowerbounds)
        if isinstance(lowerbounds, VariableType | None):
            # single value, construct list with the correct dimensions
            return np.full(self.shape, lowerbounds).tolist()

        return lowerbounds

    def get_upperbound_values(
        self,
    ) -> Iterable[VariableType | Iterable[VariableType]] | Iterable[None | Iterable[None]]:
        """Return the upperbounds values, if any, as a Python iterable (list of list)."""
        upperbounds = get_tensor_values(self.upperbounds)
        if isinstance(upperbounds, VariableType | None):
            # single value, construct list with the correct dimensions
            return np.full(self.shape, upperbounds).tolist()

        return upperbounds

    def get_initial_values(self) -> Iterable[VariableType | Iterable[VariableType]] | Iterable[None | Iterable[None]]:
        """Return the initial values, if any, as a Python iterable (list of list)."""
        values = get_tensor_values(self.initial_values)
        if isinstance(values, VariableType | None):
            # single value, construct list with the correct dimensions
            return np.full(self.shape, values).tolist()

        return values

    def to_variables(self) -> list[Variable]:
        """Flatten the tensor into a list of Variables.

        Returns:
            list[Variable]: a list of Variables.
        """
        variables = []
        for indices in list(product(*[range(1, dim + 1) for dim in self.shape])):
            variables.append(self[*indices])

        return variables

    def __getitem__(self, indices: int | tuple[int]) -> Variable:
        """Implements random access for TensorVariable.

        Note:
            Indexing is assumed to start at 1.

        Args:
            indices (int | Tuple[int]): a single integer or tuple of integers.

        Returns:
            Variable: A new instance of Variable that has been setup with
                information found at the specified indices in the TensorVariable.
        """
        if isinstance(indices, tuple):
            # multi-dimensional indexing
            name = f"{self.name} at position {[*indices]}"
            symbol = f"{self.symbol}_{'_'.join(map(str, indices))}"

            lowerbound = self.get_lowerbound_values()
            upperbound = self.get_upperbound_values()
            initial_value = self.get_initial_values()

            for idx in indices:
                lowerbound = lowerbound[idx - 1]
                upperbound = upperbound[idx - 1]
                initial_value = initial_value[idx - 1]

        else:
            # single indexing
            name = f"{self.name} at position [{indices}]"
            symbol = f"{self.symbol}_{indices}"

            lowerbound = self.get_lowerbound_values()[indices - 1]
            upperbound = self.get_upperbound_values()[indices - 1]
            initial_value = self.get_initial_values()[indices - 1]

        return Variable(
            name=name,
            symbol=symbol,
            variable_type=self.variable_type,
            lowerbound=lowerbound,
            upperbound=upperbound,
            initial_value=initial_value,
        )
initial_values class-attribute instance-attribute
initial_values: Tensor | VariableType | None = Field(
    description="A list of lists, with the elements representing the initial values of each element.  E.g., `[[1, 2, 3], [4, 5, 6]]`. If a single value is supplied, that value is assumed to be the initial value of each element. Defaults to None.",
    default=None,
)

A list of lists, with the elements representing the initial values of each element. E.g., [[1, 2, 3], [4, 5, 6]]. If a single value is supplied, that value is assumed to be the initial value of each element. Defaults to None.

lowerbounds class-attribute instance-attribute
lowerbounds: Tensor | None = Field(
    description="A list of lists, with the elements representing the lower bounds of each element. E.g., `[[1, 2, 3], [4, 5, 6]]`. If a single value is supplied, that value is assumed to be the lower bound of each element. Defaults to None.",
    default=None,
)

A list of lists, with the elements representing the lower bounds of each element. E.g., [[1, 2, 3], [4, 5, 6]]. If a single value is supplied, that value is assumed to be the lower bound of each element. Defaults to None.

name class-attribute instance-attribute
name: str = Field(
    description="Descriptive name of the variable. This can be used in UI and visualizations. Example: 'velocity'."
)

Descriptive name of the variable. This can be used in UI and visualizations. Example: 'velocity'.

shape class-attribute instance-attribute
shape: list[int] = Field(
    description="A list of the dimensions of the tensor, e.g., `[2, 3]` would indicate a matrix with 2 rows and 3 columns."
)

A list of the dimensions of the tensor, e.g., [2, 3] would indicate a matrix with 2 rows and 3 columns.

symbol class-attribute instance-attribute
symbol: str = Field(
    description="Symbol to represent the variable. This will be used in the rest of the problem definition. Notice that the elements of the tensor will be represented with the symbol followed by indices. E.g., the first element of the third element of a 2-dimensional tensor, is represented by 'x_1_3', where 'x' is the symbol given to the TensorVariable. Note that indexing starts from 1."
)

Symbol to represent the variable. This will be used in the rest of the problem definition. Notice that the elements of the tensor will be represented with the symbol followed by indices. E.g., the first element of the third element of a 2-dimensional tensor, is represented by 'x_1_3', where 'x' is the symbol given to the TensorVariable. Note that indexing starts from 1.

upperbounds class-attribute instance-attribute
upperbounds: Tensor | VariableType | None = Field(
    description="A list of lists, with the elements representing the upper bounds of each element.  E.g., `[[1, 2, 3], [4, 5, 6]]`. If a single value is supplied, that value is assumed to be the upper bound of each element. Defaults to None.",
    default=None,
)

A list of lists, with the elements representing the upper bounds of each element. E.g., [[1, 2, 3], [4, 5, 6]]. If a single value is supplied, that value is assumed to be the upper bound of each element. Defaults to None.

variable_type class-attribute instance-attribute
variable_type: VariableTypeEnum = Field(
    description="Type of the variable. Can be real, integer, or binary. Note that each element of a TensorVariable is assumed to be of the same type."
)

Type of the variable. Can be real, integer, or binary. Note that each element of a TensorVariable is assumed to be of the same type.

__getitem__
__getitem__(indices: int | tuple[int]) -> Variable

Implements random access for TensorVariable.

Note

Indexing is assumed to start at 1.

Parameters:

Name Type Description Default
indices int | Tuple[int]

a single integer or tuple of integers.

required

Returns:

Name Type Description
Variable Variable

A new instance of Variable that has been setup with information found at the specified indices in the TensorVariable.

Source code in desdeo/problem/schema.py
def __getitem__(self, indices: int | tuple[int]) -> Variable:
    """Implements random access for TensorVariable.

    Note:
        Indexing is assumed to start at 1.

    Args:
        indices (int | Tuple[int]): a single integer or tuple of integers.

    Returns:
        Variable: A new instance of Variable that has been setup with
            information found at the specified indices in the TensorVariable.
    """
    if isinstance(indices, tuple):
        # multi-dimensional indexing
        name = f"{self.name} at position {[*indices]}"
        symbol = f"{self.symbol}_{'_'.join(map(str, indices))}"

        lowerbound = self.get_lowerbound_values()
        upperbound = self.get_upperbound_values()
        initial_value = self.get_initial_values()

        for idx in indices:
            lowerbound = lowerbound[idx - 1]
            upperbound = upperbound[idx - 1]
            initial_value = initial_value[idx - 1]

    else:
        # single indexing
        name = f"{self.name} at position [{indices}]"
        symbol = f"{self.symbol}_{indices}"

        lowerbound = self.get_lowerbound_values()[indices - 1]
        upperbound = self.get_upperbound_values()[indices - 1]
        initial_value = self.get_initial_values()[indices - 1]

    return Variable(
        name=name,
        symbol=symbol,
        variable_type=self.variable_type,
        lowerbound=lowerbound,
        upperbound=upperbound,
        initial_value=initial_value,
    )
get_initial_values
get_initial_values() -> (
    Iterable[VariableType | Iterable[VariableType]]
    | Iterable[None | Iterable[None]]
)

Return the initial values, if any, as a Python iterable (list of list).

Source code in desdeo/problem/schema.py
def get_initial_values(self) -> Iterable[VariableType | Iterable[VariableType]] | Iterable[None | Iterable[None]]:
    """Return the initial values, if any, as a Python iterable (list of list)."""
    values = get_tensor_values(self.initial_values)
    if isinstance(values, VariableType | None):
        # single value, construct list with the correct dimensions
        return np.full(self.shape, values).tolist()

    return values
get_lowerbound_values
get_lowerbound_values() -> (
    Iterable[VariableType | Iterable[VariableType]]
    | Iterable[None | Iterable[None]]
)

Return the lowerbounds values, if any, as a Python iterable (list of list).

Source code in desdeo/problem/schema.py
def get_lowerbound_values(
    self,
) -> Iterable[VariableType | Iterable[VariableType]] | Iterable[None | Iterable[None]]:
    """Return the lowerbounds values, if any, as a Python iterable (list of list)."""
    lowerbounds = get_tensor_values(self.lowerbounds)
    if isinstance(lowerbounds, VariableType | None):
        # single value, construct list with the correct dimensions
        return np.full(self.shape, lowerbounds).tolist()

    return lowerbounds
get_upperbound_values
get_upperbound_values() -> (
    Iterable[VariableType | Iterable[VariableType]]
    | Iterable[None | Iterable[None]]
)

Return the upperbounds values, if any, as a Python iterable (list of list).

Source code in desdeo/problem/schema.py
def get_upperbound_values(
    self,
) -> Iterable[VariableType | Iterable[VariableType]] | Iterable[None | Iterable[None]]:
    """Return the upperbounds values, if any, as a Python iterable (list of list)."""
    upperbounds = get_tensor_values(self.upperbounds)
    if isinstance(upperbounds, VariableType | None):
        # single value, construct list with the correct dimensions
        return np.full(self.shape, upperbounds).tolist()

    return upperbounds
to_variables
to_variables() -> list[Variable]

Flatten the tensor into a list of Variables.

Returns:

Type Description
list[Variable]

list[Variable]: a list of Variables.

Source code in desdeo/problem/schema.py
def to_variables(self) -> list[Variable]:
    """Flatten the tensor into a list of Variables.

    Returns:
        list[Variable]: a list of Variables.
    """
    variables = []
    for indices in list(product(*[range(1, dim + 1) for dim in self.shape])):
        variables.append(self[*indices])

    return variables

Url

Bases: BaseModel

Model for a URL.

Source code in desdeo/problem/schema.py
class Url(BaseModel):
    """Model for a URL."""

    model_config = ConfigDict(frozen=True, from_attributes=True, extra="forbid")

    url: str = Field(
        description=(
            "A URL to the simulator. A GET request to this URL should be used to evaluate solutions in batches."
        )
    )
    """A URL to the simulator. A GET request to this URL should be used to evaluate solutions in batches."""

    auth: tuple[str, str] | None = Field(
        description=(
            "Optional. A tuple of username and password to be used for authentication when making requests to the URL."
        ),
        default=None,
    )
    """Optional. A tuple of username and password to be used for authentication when making requests to the URL."""
auth class-attribute instance-attribute
auth: tuple[str, str] | None = Field(
    description="Optional. A tuple of username and password to be used for authentication when making requests to the URL.",
    default=None,
)

Optional. A tuple of username and password to be used for authentication when making requests to the URL.

url class-attribute instance-attribute
url: str = Field(
    description="A URL to the simulator. A GET request to this URL should be used to evaluate solutions in batches."
)

A URL to the simulator. A GET request to this URL should be used to evaluate solutions in batches.

Variable

Bases: BaseModel

Model for a variable.

Source code in desdeo/problem/schema.py
class Variable(BaseModel):
    """Model for a variable."""

    model_config = ConfigDict(frozen=True, from_attributes=True, extra="forbid")

    name: str = Field(
        description="Descriptive name of the variable. This can be used in UI and visualizations. Example: 'velocity'."
    )
    """Descriptive name of the variable. This can be used in UI and visualizations. Example: 'velocity'."""
    symbol: str = Field(
        description=(
            "Symbol to represent the variable. This will be used in the rest of the problem definition."
            " It may also be used in UIs and visualizations. Example: 'v_1'."
        ),
    )
    """ Symbol to represent the variable. This will be used in the rest of the
    problem definition.  It may also be used in UIs and visualizations. Example:
    'v_1'."""
    variable_type: VariableTypeEnum = Field(description="Type of the variable. Can be real, integer or binary.")
    """Type of the variable. Can be real, integer or binary."""
    lowerbound: VariableType | None = Field(description="Lower bound of the variable.", default=None)
    """Lower bound of the variable. Defaults to `None`."""
    upperbound: VariableType | None = Field(description="Upper bound of the variable.", default=None)
    """Upper bound of the variable. Defaults to `None`."""
    initial_value: VariableType | None = Field(
        description="Initial value of the variable. This is optional.", default=None
    )
    """Initial value of the variable. This is optional. Defaults to `None`."""
initial_value class-attribute instance-attribute
initial_value: VariableType | None = Field(
    description="Initial value of the variable. This is optional.",
    default=None,
)

Initial value of the variable. This is optional. Defaults to None.

lowerbound class-attribute instance-attribute
lowerbound: VariableType | None = Field(
    description="Lower bound of the variable.", default=None
)

Lower bound of the variable. Defaults to None.

name class-attribute instance-attribute
name: str = Field(
    description="Descriptive name of the variable. This can be used in UI and visualizations. Example: 'velocity'."
)

Descriptive name of the variable. This can be used in UI and visualizations. Example: 'velocity'.

symbol class-attribute instance-attribute
symbol: str = Field(
    description="Symbol to represent the variable. This will be used in the rest of the problem definition. It may also be used in UIs and visualizations. Example: 'v_1'."
)

Symbol to represent the variable. This will be used in the rest of the problem definition. It may also be used in UIs and visualizations. Example: 'v_1'.

upperbound class-attribute instance-attribute
upperbound: VariableType | None = Field(
    description="Upper bound of the variable.", default=None
)

Upper bound of the variable. Defaults to None.

variable_type class-attribute instance-attribute
variable_type: VariableTypeEnum = Field(
    description="Type of the variable. Can be real, integer or binary."
)

Type of the variable. Can be real, integer or binary.

VariableDomainTypeEnum

Bases: str, Enum

An enumerator for the possible variable type domains of a problem.

Source code in desdeo/problem/schema.py
class VariableDomainTypeEnum(str, Enum):
    """An enumerator for the possible variable type domains of a problem."""

    continuous = "continuous"
    """All variables are real valued."""
    binary = "binary"
    """All variables are binary valued."""
    integer = "integer"
    """All variables are integer or binary valued."""
    mixed = "mixed"
    """Some variables are continuos, some are integer or binary."""
binary class-attribute instance-attribute
binary = 'binary'

All variables are binary valued.

continuous class-attribute instance-attribute
continuous = 'continuous'

All variables are real valued.

integer class-attribute instance-attribute
integer = 'integer'

All variables are integer or binary valued.

mixed class-attribute instance-attribute
mixed = 'mixed'

Some variables are continuos, some are integer or binary.

VariableTypeEnum

Bases: str, Enum

An enumerator for possible variable types.

Source code in desdeo/problem/schema.py
class VariableTypeEnum(str, Enum):
    """An enumerator for possible variable types."""

    real = "real"
    """A continuous variable."""
    integer = "integer"
    """An integer variable."""
    binary = "binary"
    """A binary variable."""
binary class-attribute instance-attribute
binary = 'binary'

A binary variable.

integer class-attribute instance-attribute
integer = 'integer'

An integer variable.

real class-attribute instance-attribute
real = 'real'

A continuous variable.

get_tensor_values

get_tensor_values(
    values: Iterable[VariableType | Iterable[VariableType]]
    | VariableType
    | None,
) -> (
    Iterable[VariableType | Iterable[VariableType]]
    | VariableType
    | None
)

Return the values for a given attribute as a nested Python list or single value.

Removes the 'List' entries from the JSON format to give a Python compatible list. If the values are a single value or None, then a single value or None is returned instead, respectively.

Parameters:

Name Type Description Default
values Iterable[VariableType | Iterable[VariableType]] | VariableType | None

the values that should be extracted as a Python list.

required

Returns:

Type Description
Iterable[VariableType | Iterable[VariableType]] | VariableType | None

list[VariableType] | Iterable[list[VariableType]] | VariableType| None: a list with shape self.shape with the values defined for the variable. If a single values consisted of a single value or None instead, then a single valuer or None are returned, respectively.

Source code in desdeo/problem/schema.py
def get_tensor_values(
    values: Iterable[VariableType | Iterable[VariableType]] | VariableType | None,
) -> Iterable[VariableType | Iterable[VariableType]] | VariableType | None:
    """Return the values for a given attribute as a nested Python list or single value.

    Removes the 'List' entries from the JSON format to give a Python compatible list.
    If the values are a single value or None, then a single value or None is returned
    instead, respectively.

    Arguments:
        values (Iterable[VariableType | Iterable[VariableType]] | VariableType | None):
            the values that should be extracted as a Python list.

    Returns:
        list[VariableType] | Iterable[list[VariableType]] | VariableType| None: a list with shape `self.shape` with the
            values defined for the variable. If a single values consisted of a single value or None instead, then
            a single valuer or None are returned, respectively.
    """
    if values is None or isinstance(values, VariableType):
        return values

    if isinstance(values, list) and len(values) > 1:
        if values[0] == "List" and isinstance(values[1], list):
            # recursive case, encountered list
            return [get_tensor_values(v_element) for v_element in values[1:]]
        if values[0] == "List":
            # terminal case, encountered a VariableType
            return [*values[1:]]

        # if anything else is encountered, raise an error
        msg = "Encountered value that is not a valid VariableType nor a list."
        raise ValueError(msg)

    msg = f"Values must be a valid MathJSON vector. Got {type(values)}."
    raise ValueError(msg)

parse_infix_to_func

parse_infix_to_func(cls: Problem, v: str | list) -> list

Validator that checks if the 'func' field is of type str or list.

If str, then it is assumed the string represents the func in infix notation. The string is parsed in the validator. If list, then the func is assumed to be represented in Math JSON format.

Parameters:

Name Type Description Default
cls Problem

the class of the pydantic model the validator is applied to.

required
v str | list

The func to be validated.

required

Raises:

Type Description
ValueError

v is neither an instance of str or a list.

Returns:

Name Type Description
list list

The func represented in Math JSON format.

Source code in desdeo/problem/schema.py
def parse_infix_to_func(cls: "Problem", v: str | list) -> list:
    """Validator that checks if the 'func' field is of type str or list.

    If str, then it is assumed the string represents the func in infix notation. The string
    is parsed in the validator. If list, then the func is assumed to be represented in Math JSON format.

    Args:
        cls: the class of the pydantic model the validator is applied to.
        v (str | list): The func to be validated.

    Raises:
        ValueError: v is neither an instance of str or a list.

    Returns:
        list: The func represented in Math JSON format.
    """
    if v is None:
        return v
    # Check if v is a string (infix expression), then parse it
    if isinstance(v, str):
        parser = InfixExpressionParser()
        return parser.parse(v)
    # If v is already in the correct format (a list), just return it
    if isinstance(v, list):
        return v

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

parse_list_to_mathjson

parse_list_to_mathjson(
    cls: TensorVariable, v: Tensor | VariableType | None
) -> list

Validator that makes sure a nested Python list is represented as tensor following the MathJSON convention.

Parameters:

Name Type Description Default
cls TensorVariable

the class of the pydantic model the validator is applied to.

required
v Tensor | VariableType | None

the nested lists to be validated.

required

Returns:

Name Type Description
list list

a tensor following the MathJSON conventions; or a single value or None, if v was assigned to one of these types.

Source code in desdeo/problem/schema.py
def parse_list_to_mathjson(cls: "TensorVariable", v: Tensor | VariableType | None) -> list:
    """Validator that makes sure a nested Python list is represented as tensor following the MathJSON convention.

    Args:
        cls (TensorVariable): the class of the pydantic model the validator is applied to.
        v (Tensor | VariableType | None): the nested lists to be validated.

    Returns:
        list: a tensor following the MathJSON conventions; or a single value or None,
            if v was assigned to one of these types.
    """
    if v is None or isinstance(v, VariableType):
        return v

    # Check if the input is already in MathJSON format
    if isinstance(v, list) and len(v) > 0 and v[0] == "List":
        return v

    # recursively parse into a MathJSON representation
    if isinstance(v, list) and len(v) > 0:
        if v[0] == "List":
            # assumed to be already in MathJson format, just return the list
            return v
        if isinstance(v[0], list):
            # recursive case, encountered list
            return ["List", *[parse_list_to_mathjson(TensorVariable, v_element) for v_element in v]]
        if isinstance(v[0], VariableType | None):
            # terminal case, encountered a VariableType
            return ["List", *v]

        # if anything else is encountered, raise an error
        msg = "Encountered value that is not a valid VariableType nor a list."
        raise ValueError(msg)

    msg = f"The tensor must a Python list (of lists) or a single value of type VariableType. Got {type(v)}."
    raise ValueError(msg)

parse_scenario_key_singleton_to_list

parse_scenario_key_singleton_to_list(
    cls: Problem, v: str | list[str]
) -> list[str] | None

Validator that checks the type of a scenario key.

If the type is a list, it will be returned as it is. If it is a string, then a list with the single string is returned. Else, a ValueError is raised.

Parameters:

Name Type Description Default
cls Problem

the class of the pydantic model the validator is applied to.

required
v str | list[str]

the scenario key, or keys, to be validated.

required

Raises:

Type Description
ValueError

raised when v it neither a string or a list.

Returns:

Type Description
list[str] | None

list[str]: a list with scenario keys.

Source code in desdeo/problem/schema.py
def parse_scenario_key_singleton_to_list(cls: "Problem", v: str | list[str]) -> list[str] | None:
    """Validator that checks the type of a scenario key.

    If the type is a list, it will be returned as it is. If it is a string,
    then a list with the single string is returned. Else, a ValueError is raised.

    Args:
        cls: the class of the pydantic model the validator is applied to.
        v (str | list[str]): the scenario key, or keys, to be validated.

    Raises:
        ValueError: raised when `v` it neither a string or a list.

    Returns:
        list[str]: a list with scenario keys.
    """
    if v is None:
        return v
    if isinstance(v, str):
        return [v]
    if isinstance(v, list):
        return v

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

tensor_custom_error_validator

tensor_custom_error_validator(
    value: Any,
    handler: ValidatorFunctionWrapHandler,
    _info: ValidationInfo,
) -> Any

Custom error handler to simplify error messages related to recursive tensor types.

Parameters:

Name Type Description Default
value Any

input value to be validated.

required
handler ValidatorFunctionWrapHandler

handler to check the values.

required
_info ValidationInfo

info related to the validation of the value.

required

Raises:

Type Description
PydanticCustomError

when the value is an invalid tensor type.

Returns:

Name Type Description
Any Any

a valid tensor.

Source code in desdeo/problem/schema.py
def tensor_custom_error_validator(value: Any, handler: ValidatorFunctionWrapHandler, _info: ValidationInfo) -> Any:
    """Custom error handler to simplify error messages related to recursive tensor types.

    Args:
        value (Any): input value to be validated.
        handler (ValidatorFunctionWrapHandler): handler to check the values.
        _info (ValidationInfo): info related to the validation of the value.

    Raises:
        PydanticCustomError: when the value is an invalid tensor type.

    Returns:
        Any: a valid tensor.
    """
    try:
        return handler(value)
    except ValidationError as exc:
        raise PydanticCustomError("invalid tensor", "Input is not a valid tensor") from exc

JSON parser

desdeo.problem.json_parser

Defines a parser to parse multiobjective optimziation problems defined in a JSON format.

FormatEnum

Bases: str, Enum

Enumerates the supported formats the JSON format may be parsed to.

Source code in desdeo/problem/json_parser.py
class FormatEnum(str, Enum):
    """Enumerates the supported formats the JSON format may be parsed to."""

    polars = "polars"
    pyomo = "pyomo"
    sympy = "sympy"
    gurobipy = "gurobipy"
    cvxpy = "cvxpy"

MathParser

A class to instantiate MathJSON parsers.

Currently only parses MathJSON to polars expressions. Pyomo WIP.

Source code in desdeo/problem/json_parser.py
  36
  37
  38
  39
  40
  41
  42
  43
  44
  45
  46
  47
  48
  49
  50
  51
  52
  53
  54
  55
  56
  57
  58
  59
  60
  61
  62
  63
  64
  65
  66
  67
  68
  69
  70
  71
  72
  73
  74
  75
  76
  77
  78
  79
  80
  81
  82
  83
  84
  85
  86
  87
  88
  89
  90
  91
  92
  93
  94
  95
  96
  97
  98
  99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
 128
 129
 130
 131
 132
 133
 134
 135
 136
 137
 138
 139
 140
 141
 142
 143
 144
 145
 146
 147
 148
 149
 150
 151
 152
 153
 154
 155
 156
 157
 158
 159
 160
 161
 162
 163
 164
 165
 166
 167
 168
 169
 170
 171
 172
 173
 174
 175
 176
 177
 178
 179
 180
 181
 182
 183
 184
 185
 186
 187
 188
 189
 190
 191
 192
 193
 194
 195
 196
 197
 198
 199
 200
 201
 202
 203
 204
 205
 206
 207
 208
 209
 210
 211
 212
 213
 214
 215
 216
 217
 218
 219
 220
 221
 222
 223
 224
 225
 226
 227
 228
 229
 230
 231
 232
 233
 234
 235
 236
 237
 238
 239
 240
 241
 242
 243
 244
 245
 246
 247
 248
 249
 250
 251
 252
 253
 254
 255
 256
 257
 258
 259
 260
 261
 262
 263
 264
 265
 266
 267
 268
 269
 270
 271
 272
 273
 274
 275
 276
 277
 278
 279
 280
 281
 282
 283
 284
 285
 286
 287
 288
 289
 290
 291
 292
 293
 294
 295
 296
 297
 298
 299
 300
 301
 302
 303
 304
 305
 306
 307
 308
 309
 310
 311
 312
 313
 314
 315
 316
 317
 318
 319
 320
 321
 322
 323
 324
 325
 326
 327
 328
 329
 330
 331
 332
 333
 334
 335
 336
 337
 338
 339
 340
 341
 342
 343
 344
 345
 346
 347
 348
 349
 350
 351
 352
 353
 354
 355
 356
 357
 358
 359
 360
 361
 362
 363
 364
 365
 366
 367
 368
 369
 370
 371
 372
 373
 374
 375
 376
 377
 378
 379
 380
 381
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
class MathParser:
    """A class to instantiate MathJSON parsers.

    Currently only parses MathJSON to polars expressions. Pyomo WIP.
    """

    def __init__(self, to_format: FormatEnum = "polars"):  # noqa: C901
        """Create a parser instance for parsing MathJSON notation into polars expressions.

        Args:
            to_format (FormatEnum, optional): to which format a JSON representation should be parsed to.
                Defaults to "polars".
        """
        # Define operator names. Change these when the name is altered in the JSON format.
        # Basic arithmetic operators
        self.NEGATE: str = "Negate"
        self.ADD: str = "Add"
        self.SUB: str = "Subtract"
        self.MUL: str = "Multiply"
        self.DIV: str = "Divide"

        # Vector and matrix operations
        self.MATMUL: str = "MatMul"
        self.SUM: str = "Sum"
        self.RANDOM_ACCESS = "At"

        # Exponentation and logarithms
        self.EXP: str = "Exp"
        self.LN: str = "Ln"
        self.LB: str = "Lb"
        self.LG: str = "Lg"
        self.LOP: str = "LogOnePlus"
        self.SQRT: str = "Sqrt"
        self.SQUARE: str = "Square"
        self.POW: str = "Power"

        # Rounding operators
        self.ABS: str = "Abs"
        self.CEIL: str = "Ceil"
        self.FLOOR: str = "Floor"

        # Trigonometric operations
        self.ARCCOS: str = "Arccos"
        self.ARCCOSH: str = "Arccosh"
        self.ARCSIN: str = "Arcsin"
        self.ARCSINH: str = "Arcsinh"
        self.ARCTAN: str = "Arctan"
        self.ARCTANH: str = "Arctanh"
        self.COS: str = "Cos"
        self.COSH: str = "Cosh"
        self.SIN: str = "Sin"
        self.SINH: str = "Sinh"
        self.TAN: str = "Tan"
        self.TANH: str = "Tanh"

        # Comparison operators
        self.EQUAL: str = "Equal"
        self.GREATER: str = "Greater"
        self.GE: str = "GreaterEqual"
        self.LESS: str = "Less"
        self.LE: str = "LessEqual"
        self.NE: str = "NotEqual"

        # Other operators
        self.MAX: str = "Max"
        self.MIN: str = "Min"
        self.RATIONAL: str = "Rational"

        self.literals = int | float

        def to_expr(x: self.literals | pl.Expr):
            """Helper function to convert literals to polars expressions."""
            return pl.lit(x) if isinstance(x, self.literals) else x

        def to_sympy_expr(x):
            return sp.sympify(x, evaluate=False) if isinstance(x, self.literals) else x

        def gp_error():
            msg = "The gurobipy model format only supports linear and quadratic expressions."
            ParserError(msg)

        def _polars_reduce(ufunc, exprs):
            def _reduce_function(acc, x, ufunc=ufunc):
                acc_numpy = acc.to_numpy()
                x_numpy = x.to_numpy()

                if acc_numpy.shape == x_numpy.shape:
                    return pl.Series(values=ufunc(acc_numpy, x_numpy))

                expanded_shape = acc_numpy.shape + (1,) * (x_numpy.ndim - acc_numpy.ndim)

                return pl.Series(values=ufunc(acc_numpy.reshape(expanded_shape), x_numpy))

            return pl.reduce(function=_reduce_function, exprs=exprs)

        def _polars_reduce_unary(expr, ufunc):
            def _reduce_function(acc, _, ufunc=ufunc):
                return pl.Series(values=ufunc(acc.to_numpy()))

            return pl.reduce(function=_reduce_function, exprs=[expr, None])

        def _polars_reduce_matmul(*exprs):
            def _reduce_function(acc, x):
                acc = acc.to_numpy()
                x = x.to_numpy()

                if len(acc.shape) == 2 and len(x.shape) == 2:  # noqa: PLR2004
                    # Row vectors, just return the dot product, polars does not handle
                    # "column" vectors anyway
                    return pl.Series(values=np.einsum("ij,ij->i", acc, x, optimize=True))

                # actual matrix product required
                return pl.Series(values=np.matmul(acc, x))

            return pl.reduce(function=_reduce_function, exprs=exprs)

        def _polars_summation(expr):
            """Polars matrix summation."""

            def _reduce_function(acc, _):
                acc_numpy = acc.to_numpy()
                return pl.Series(values=np.sum(acc_numpy, axis=tuple(range(1, acc_numpy.ndim))))

            return pl.reduce(function=_reduce_function, exprs=[expr, None])

        def _polars_random_access(expr, *indices):
            """Polars tensor random access."""
            for index in indices:
                expr = expr.arr.get(index - 1)  # 1 indexing assumed in JSON format

            return expr

        def _polars_generic_apply(a, b):
            pass

        polars_env = {
            # Define the operations for the different operators.
            # Basic arithmetic operations
            self.NEGATE: lambda x: _polars_reduce_unary(x, np.negative),
            self.ADD: lambda *args: _polars_reduce(np.add, args),
            self.SUB: lambda *args: _polars_reduce(np.subtract, args),
            self.MUL: lambda *args: _polars_reduce(np.multiply, args),
            self.DIV: lambda *args: _polars_reduce(np.divide, args),
            # Vector and matrix operations
            self.MATMUL: _polars_reduce_matmul,
            self.SUM: lambda x: _polars_summation(x),
            self.RANDOM_ACCESS: _polars_random_access,
            # Exponentiation and logarithms
            self.EXP: lambda x: _polars_reduce_unary(x, np.exp),
            self.LN: lambda x: _polars_reduce_unary(x, np.log),
            self.LB: lambda x: _polars_reduce_unary(x, np.log2),
            self.LG: lambda x: _polars_reduce_unary(x, np.log10),
            self.LOP: lambda x: _polars_reduce_unary(x, np.log1p),
            self.SQRT: lambda x: _polars_reduce_unary(x, np.sqrt),
            self.SQUARE: lambda x: _polars_reduce_unary(x, lambda y: np.power(y, 2)),
            self.POW: lambda *args: _polars_reduce(np.power, args),
            # Trigonometric operations
            self.ARCCOS: lambda x: _polars_reduce_unary(x, np.arccos),
            self.ARCCOSH: lambda x: _polars_reduce_unary(x, np.arccosh),
            self.ARCSIN: lambda x: _polars_reduce_unary(x, np.arcsin),
            self.ARCSINH: lambda x: _polars_reduce_unary(x, np.arcsinh),
            self.ARCTAN: lambda x: _polars_reduce_unary(x, np.arctan),
            self.ARCTANH: lambda x: _polars_reduce_unary(x, np.arctanh),
            self.COS: lambda x: _polars_reduce_unary(x, np.cos),
            self.COSH: lambda x: _polars_reduce_unary(x, np.cosh),
            self.SIN: lambda x: _polars_reduce_unary(x, np.sin),
            self.SINH: lambda x: _polars_reduce_unary(x, np.sinh),
            self.TAN: lambda x: _polars_reduce_unary(x, np.tan),
            self.TANH: lambda x: _polars_reduce_unary(x, np.tanh),
            # Rounding operations
            self.ABS: lambda x: _polars_reduce_unary(x, np.abs),
            self.CEIL: lambda x: _polars_reduce_unary(x, np.ceil),
            self.FLOOR: lambda x: _polars_reduce_unary(x, np.floor),
            # Other operations
            self.RATIONAL: lambda lst: reduce(lambda x, y: x / y, lst),  # Not supported
            self.MAX: lambda *args: reduce(lambda x, y: pl.max_horizontal(to_expr(x), to_expr(y)), args),
            self.MIN: lambda *args: reduce(lambda x, y: pl.min_horizontal(to_expr(x), to_expr(y)), args),
        }

        def _pyomo_negate(x):
            """Negates the given operand."""

            def _expr_negate_rule(x):
                def _inner(_, *indices):
                    return -x[indices]

                return _inner

            def _negate(x):
                # check if operand in indexed
                if hasattr(x, "index_set") and x.is_indexed():
                    # indexed, return new pyomo expression
                    expr = pyomo.Expression(x.index_set(), rule=_expr_negate_rule(x))
                    expr.construct()

                    return expr

                # not indexed, just regular negate
                return -x

            return _negate(x)

        def _pyomo_pow(base, exp):
            """Implements a power operator compatible with Pyomo expressions."""

            def _expr_pow_rule(base, exp):
                def _inner(_, *indices):
                    return base[indices] ** exp

                return _inner

            def _pow(base, exp=exp):
                # check if operand in indexed
                if hasattr(base, "index_set") and base.is_indexed():
                    # indexed, return new pyomo expression
                    expr = pyomo.Expression(base.index_set(), rule=_expr_pow_rule(base, exp))
                    expr.construct()

                    return expr

                # not indexed, just regular power
                return base**exp

            return _pow(base, exp)

        def _pyomo_unary(x, op):
            """Implements unary operators to work with indexed expressions."""

            def _expr_rule(x, op):
                def _inner(_, *indices, op=op):
                    return op(x[indices])

                return _inner

            def _op(x, op):
                # check if operand in indexed
                if hasattr(x, "index_set") and x.is_indexed():
                    # indexed, return new pyomo expression
                    expr = pyomo.Expression(x.index_set(), rule=_expr_rule(x, op))
                    expr.construct()

                    return expr

                # not indexed, just regular power
                return op(x)

            return _op(x, op)

        def _pyomo_addition(*args, subtraction=False):
            """Add (subtract) scalars or tensors to (from) each other."""

            def _expr_matrix_addition_rule(x, y, subtraction=subtraction):
                def _inner(_, *args):
                    return x[*args] + y[*args] if not subtraction else x[*args] - y[*args]

                return _inner

            def _expr_elementwise_with_single_indexed(indexed, not_indexed, subtraction=subtraction):
                def _inner(_, *indices):
                    return (indexed[indices] + not_indexed) if not subtraction else (indexed[indices] - not_indexed)

                return _inner

            def _add(x, y, subtraction=subtraction):
                # if both are indexed, try matrix addition
                if (hasattr(x, "index_set") and x.is_indexed()) and (hasattr(y, "index_set") and y.is_indexed()):
                    # try matrix addition
                    # check that the dimensions of x and y matches
                    if x.index_set().set_tuple != y.index_set().set_tuple:
                        msg = (
                            f"The dimensions of x {x.index_set().set_tuple} must match that"
                            f" of y {y.index_set().set_tuple} for matrix addition."
                        )
                        raise ParserError(msg)

                    expr = pyomo.Expression(
                        x.index_set(), rule=_expr_matrix_addition_rule(x, y, subtraction=subtraction)
                    )
                    expr.construct()

                    return expr

                # if neither is indexed, do normal addition
                if not (hasattr(x, "index_set") and x.is_indexed()) and not (
                    hasattr(y, "index_set") and y.is_indexed()
                ):
                    if not subtraction:
                        # try regular addition
                        return x + y
                    # try regular subtraction
                    return x - y

                # x is indexed, y is not
                if (hasattr(x, "index_set")) and x.is_indexed():
                    expr = pyomo.Expression(
                        x.index_set(), rule=_expr_elementwise_with_single_indexed(x, y, subtraction=subtraction)
                    )

                    expr.construct()
                    return expr

                # if y is indexed, x is not
                if (hasattr(y, "index_set")) and y.is_indexed():
                    expr = pyomo.Expression(
                        y.index_set(), rule=_expr_elementwise_with_single_indexed(y, x, subtraction=subtraction)
                    )

                    expr.construct()
                    return expr

                # if only one of the operands is indexed, then addition is not supported. Throw error.
                msg = "For addition, both operands must be either scalars or matrices with matching dimensions."
                raise ParserError(msg)

            return reduce(_add, args)

        def _pyomo_subtraction(*args):
            return _pyomo_addition(*args, subtraction=True)

        def _pyomo_multiply(*args, division=False):
            """Multiply tensor with a scalar."""

            def _expr_multiply_with_scalar_rule(scalar_value, to_multiply, division=division):
                def _inner(_, *indices):
                    return to_multiply[indices] * scalar_value if not division else to_multiply[indices] / scalar_value

                return _inner

            def _expr_matrix_multiply_rule(x, y, division=division):
                def _inner(_, *args):
                    return x[*args] * y[*args] if not division else x[*args] / y[*args]

                return _inner

            def _multiply(x, y, division=division):
                if not hasattr(x, "is_indexed") and not hasattr(y, "is_indexed"):
                    # x and y are scalars
                    return x * y if not division else x / y

                # check if x or y is scalar
                if (
                    hasattr(x, "is_indexed")
                    and x.is_indexed()
                    and x.dim() > 0
                    and (not hasattr(y, "is_indexed") or not y.is_indexed() or (y.is_indexed() and y.dim() == 0))
                ):
                    # x is a tensor, y is scalar
                    expr = pyomo.Expression(
                        x.index_set(), rule=_expr_multiply_with_scalar_rule(y, x, division=division)
                    )
                    expr.construct()
                    return expr
                if (
                    hasattr(y, "is_indexed")
                    and y.is_indexed()
                    and y.dim() > 0
                    and (not hasattr(x, "is_indexed") or not x.is_indexed() or (x.is_indexed() and x.dim() == 0))
                ):
                    # y is a tensor, x is scalar
                    expr = pyomo.Expression(
                        y.index_set(), rule=_expr_multiply_with_scalar_rule(x, y, division=division)
                    )
                    expr.construct()
                    return expr

                # check if both are indexed
                if hasattr(x, "index_set") and hasattr(y, "index_set"):
                    # both are indexed, neither is a scalar, check dims and sized, if match,
                    # multiply together element-wise
                    if x.index_set() != y.index_set():
                        msg = (
                            f"The dimensions of x {x.index_set().set_tuple} must match that"
                            f" of y {y.index_set().set_tuple} for element-wise matrix multiplication."
                        )
                        raise ParserError(msg)

                    expr = pyomo.Expression(x.index_set(), rule=_expr_matrix_multiply_rule(x, y, division=division))
                    expr.construct()

                    return expr

                # both are scalars
                return x * y if not division else x / y

            return reduce(_multiply, args)

        def _pyomo_division(*args):
            return _pyomo_multiply(*args, division=True)

        def _pyomo_matrix_multiplication(*args):
            """Multiply two matrices together."""

            def _expr_matmul_rule(mat_a, mat_b, j_indices):
                def _inner(_, i_index, k_index):
                    return sum(mat_a[i_index, j] * mat_b[j, k_index] for j in j_indices)

                return _inner

            def _matmul(mat_a, mat_b):
                if not (hasattr(mat_a, "is_indexed") and mat_a.is_indexed()) or not (
                    hasattr(mat_b, "is_indexed") and mat_b.is_indexed()
                ):
                    # either mat_a or mat_b is not tensor
                    msg = "Either mat_a or mat_b, or both, is not indexed. Cannot perform matrix multiplication."
                    raise ParserError(msg)

                # check for regular vectors, then do dot product
                if mat_a.dim() == 1 and mat_b.dim() == 1:
                    if (len_a := len(mat_a.index_set())) != (len_b := len(mat_b.index_set())):
                        msg = (
                            "For dot product, the sizes of the vectors must match."
                            f" Sizes mat_a = {len_a} and mat_b = {len_b}."
                        )
                        raise ParserError(msg)

                    return pyomo.sum_product(mat_a, mat_b, index=mat_a.index_set())

                # assuming mat_a has dimensions i,j; and mat_b j,k;
                # then the j dimension is squeezed and the i and k dimensions are kept.

                # check that we are dealing with matrices
                min_dimension = 2
                if (
                    not hasattr(mat_a.index_set(), "set_tuple") or len(mat_a.index_set().set_tuple) < min_dimension
                ) or (not hasattr(mat_b.index_set(), "set_tuple") and len(mat_b.index_set().set_tuple) < min_dimension):
                    msg = "Both mat_a and mat_b must have at least two dimensions."
                    raise ParserError(msg)

                # check that the outer dimensions (the one to be squeezed) matches
                if len(mat_a.index_set().set_tuple[-1]) != len(mat_b.index_set().set_tuple[0]):
                    msg = (
                        f"The last dimension size of mat_a ({mat_a.index_set().set_tuple[-1]}) must "
                        f"match the first dimension of mat_b ({mat_b.index_set().set_tuple[0]})"
                    )
                    raise ParserError(msg)

                expr = pyomo.Expression(
                    mat_a.index_set().set_tuple[0],
                    mat_b.index_set().set_tuple[1],
                    rule=_expr_matmul_rule(mat_a, mat_b, mat_a.index_set().set_tuple[1]),
                )
                expr.construct()

                return expr

            return reduce(_matmul, args)

        def _pyomo_summation(summand):
            """Sum an indexed Pyomo object."""
            return pyomo.sum_product(summand, index=summand.index_set())

        def _pyomo_random_access(indexed, *indices):
            return indexed[*indices]

        pyomo_env = {
            # Define the operations for the different operators.
            # Basic arithmetic operations
            self.NEGATE: _pyomo_negate,
            self.ADD: _pyomo_addition,
            self.SUB: _pyomo_subtraction,
            self.MUL: _pyomo_multiply,
            self.DIV: _pyomo_division,
            # Vector and matrix operations
            self.MATMUL: _pyomo_matrix_multiplication,
            self.SUM: _pyomo_summation,
            self.RANDOM_ACCESS: _pyomo_random_access,
            # Exponentiation and logarithms
            self.EXP: lambda x: _pyomo_unary(x, pyomo.exp),
            self.LN: lambda x: _pyomo_unary(x, pyomo.log),
            self.LB: lambda x: _pyomo_unary(
                x, lambda x: pyomo.log(x) / pyomo.log(2)
            ),  # change of base, pyomo has no log2
            self.LG: lambda x: _pyomo_unary(x, pyomo.log10),
            self.LOP: lambda x: _pyomo_unary(x, lambda x: pyomo.log(x + 1)),
            self.SQRT: lambda x: _pyomo_unary(x, pyomo.sqrt),
            self.SQUARE: lambda x: _pyomo_pow(x, 2),
            self.POW: lambda x, y: _pyomo_pow(x, y),
            # Trigonometric operations
            self.ARCCOS: lambda x: _pyomo_unary(x, pyomo.acos),
            self.ARCCOSH: lambda x: _pyomo_unary(x, pyomo.acosh),
            self.ARCSIN: lambda x: _pyomo_unary(x, pyomo.asin),
            self.ARCSINH: lambda x: _pyomo_unary(x, pyomo.asinh),
            self.ARCTAN: lambda x: _pyomo_unary(x, pyomo.atan),
            self.ARCTANH: lambda x: _pyomo_unary(x, pyomo.atanh),
            self.COS: lambda x: _pyomo_unary(x, pyomo.cos),
            self.COSH: lambda x: _pyomo_unary(x, pyomo.cosh),
            self.SIN: lambda x: _pyomo_unary(x, pyomo.sin),
            self.SINH: lambda x: _pyomo_unary(x, pyomo.sinh),
            self.TAN: lambda x: _pyomo_unary(x, pyomo.tan),
            self.TANH: lambda x: _pyomo_unary(x, pyomo.tanh),
            # Rounding operations
            self.ABS: lambda x: _pyomo_unary(x, abs),
            self.CEIL: lambda x: _pyomo_unary(x, pyomo.ceil),
            self.FLOOR: lambda x: _pyomo_unary(x, pyomo.floor),
            # Other operations
            self.RATIONAL: lambda lst: reduce(lambda x, y: x / y, lst),  # not supported
            # probably a better idea to reformulate expressions with a max when utilized with pyomo
            # self.MAX: lambda *args: reduce(lambda x, y: _PyomoMax((x, y)), args),
            self.MAX: lambda *args: _PyomoMax(args),
            self.MIN: lambda *args: _PyomoMin(args),
        }

        def _sympy_matmul(*args):
            """Sympy matrix multiplication."""
            msg = (
                "Matrix multiplication '@' has not been implemented for the Sympy parser yet. Feel free to contribute!"
            )
            raise NotImplementedError(msg)

        def _sympy_summation(summand):
            """Sympy matrix summation."""
            msg = "Matrix summation 'Sum' has not been implemented for the Sympy parser yet. Feel free to contribute!"
            raise NotImplementedError(msg)

        def _sympy_random_access(*args):
            msg = (
                "Tensor random access with 'At' has not been implemented for the Sympy parser yet. "
                "Feel free to contribute!"
            )
            raise NotImplementedError(msg)

        sympy_env = {
            # Basic arithmetic operations
            self.NEGATE: lambda x: -to_sympy_expr(x),
            self.ADD: lambda *args: reduce(lambda x, y: to_sympy_expr(x) + to_sympy_expr(y), args),
            self.SUB: lambda *args: reduce(lambda x, y: to_sympy_expr(x) - to_sympy_expr(y), args),
            self.MUL: lambda *args: reduce(lambda x, y: to_sympy_expr(x) * to_sympy_expr(y), args),
            self.DIV: lambda *args: reduce(lambda x, y: to_sympy_expr(x) / to_sympy_expr(y), args),
            # Vector and matrix operations
            self.MATMUL: _sympy_matmul,
            self.SUM: _sympy_summation,
            self.RANDOM_ACCESS: _sympy_random_access,
            # Exponentiation and logarithms
            self.EXP: lambda x: sp.exp(to_sympy_expr(x)),
            self.LN: lambda x: sp.log(to_sympy_expr(x)),
            self.LB: lambda x: sp.log(to_sympy_expr(x), 2),
            self.LG: lambda x: sp.log(to_sympy_expr(x), 10),
            self.LOP: lambda x: sp.log(1 + to_sympy_expr(x)),
            self.SQRT: lambda x: sp.sqrt(to_sympy_expr(x)),
            self.SQUARE: lambda x: to_sympy_expr(x) ** 2,
            self.POW: lambda x, y: to_sympy_expr(x) ** to_sympy_expr(y),
            # Trigonometric operations
            self.SIN: lambda x: sp.sin(to_sympy_expr(x)),
            self.COS: lambda x: sp.cos(to_sympy_expr(x)),
            self.TAN: lambda x: sp.tan(to_sympy_expr(x)),
            self.ARCSIN: lambda x: sp.asin(to_sympy_expr(x)),
            self.ARCCOS: lambda x: sp.acos(to_sympy_expr(x)),
            self.ARCTAN: lambda x: sp.atan(to_sympy_expr(x)),
            # Hyperbolic functions
            self.SINH: lambda x: sp.sinh(to_sympy_expr(x)),
            self.COSH: lambda x: sp.cosh(to_sympy_expr(x)),
            self.TANH: lambda x: sp.tanh(to_sympy_expr(x)),
            self.ARCSINH: lambda x: sp.asinh(to_sympy_expr(x)),
            self.ARCCOSH: lambda x: sp.acosh(to_sympy_expr(x)),
            self.ARCTANH: lambda x: sp.atanh(to_sympy_expr(x)),
            # Other
            self.ABS: lambda x: sp.Abs(to_sympy_expr(x)),
            self.CEIL: lambda x: sp.ceiling(to_sympy_expr(x)),
            self.FLOOR: lambda x: sp.floor(to_sympy_expr(x)),
            # Note: Max and Min in sympy take any number of arguments
            self.MAX: lambda *args: sp.Max(*args),
            self.MIN: lambda *args: sp.Min(*args),
            # Rational numbers, for now assuming two-element list for numerator and denominator
            self.RATIONAL: lambda x, y: sp.Rational(x, y),
        }

        def _gurobipy_matmul(*args):
            """Gurobipy matrix multiplication."""

            def _matmul(a, b):
                if isinstance(a, list):
                    a = np.array(a)
                if isinstance(b, list):
                    b = np.array(b)
                if len(np.shape(a @ b)) == 1:
                    return a @ b
                return (a @ b).sum()

            return reduce(_matmul, args)
            msg = (
                "Matrix multiplication '@' has not been implemented for the Gurobipy parser yet."
                " Feel free to contribute!"
            )
            raise NotImplementedError(msg)

        def _gurobipy_summation(summand):
            """Gurobipy matrix summation."""

            def _sum(summand):
                if isinstance(summand, list):
                    summand = np.array(summand)
                return summand.sum()

            return _sum(summand)
            msg = (
                "Matrix summation 'Sum' has not been implemented for the Gurobipy parser yet. Feel free to contribute!"
            )
            raise NotImplementedError(msg)

        def _gurobipy_random_access(*args):
            msg = (
                "Tensor random access with 'At' has not been implemented for the Gurobipy parser yet. "
                "Feel free to contribute!"
            )
            raise NotImplementedError(msg)

        gurobipy_env = {
            # Define the operations for the different operators.
            # Basic arithmetic operations
            self.NEGATE: lambda x: -x,
            self.ADD: lambda *args: reduce(lambda x, y: x + y, args),
            self.SUB: lambda *args: reduce(lambda x, y: x - y, args),
            self.MUL: lambda *args: reduce(lambda x, y: x * y, args),
            self.DIV: lambda *args: reduce(lambda x, y: x / y, args),
            # Vector and matrix operations
            self.MATMUL: _gurobipy_matmul,
            self.SUM: _gurobipy_summation,
            self.RANDOM_ACCESS: _gurobipy_random_access,
            # Exponentiation and logarithms
            # it would be possible to implement some of these with the special functions that
            # gurobi has to offer, but they would only work under specific circumstances
            self.EXP: lambda x: gp_error(),
            self.LN: lambda x: gp_error(),
            self.LB: lambda x: gp_error(),
            self.LG: lambda x: gp_error(),
            self.LOP: lambda x: gp_error(),
            self.SQRT: lambda x: gp_error(),
            self.SQUARE: lambda x: x**2,
            self.POW: lambda x, y: x**y,  # this will likely cause an error at some point for most y
            # Trigonometric operations
            # it would be possible to implement some of these with the special functions that
            # gurobi has to offer, but they would only work under specific circumstances
            self.ARCCOS: lambda x: gp_error(),
            self.ARCCOSH: lambda x: gp_error(),
            self.ARCSIN: lambda x: gp_error(),
            self.ARCSINH: lambda x: gp_error(),
            self.ARCTAN: lambda x: gp_error(),
            self.ARCTANH: lambda x: gp_error(),
            self.COS: lambda x: gp_error(),
            self.COSH: lambda x: gp_error(),
            self.SIN: lambda x: gp_error(),
            self.SINH: lambda x: gp_error(),
            self.TAN: lambda x: gp_error(),
            self.TANH: lambda x: gp_error(),
            # Rounding operations
            self.ABS: lambda x: gp.abs_(x),
            self.CEIL: lambda x: gp_error(),
            self.FLOOR: lambda x: gp_error(),
            # Other operations
            self.RATIONAL: lambda lst: reduce(lambda x, y: x / y, lst),  # not supported
            self.MAX: lambda *args: gp.max_(args),  ## OBS! max and min are unsupported, but left here for reasons
            self.MIN: lambda *args: gp.min_(args),
        }

        def _cvxpy_error():
            msg = "The cvxpy model format only supports linear and quadratic expressions."
            return lambda x: (_ for _ in ()).throw(ParserError(msg))

        def _cvxpy_matmul(*args):
            """CVXPY matrix multiplication."""

            def _matmul(a, b):
                if isinstance(a, list):
                    a = np.array(a)
                if isinstance(b, list):
                    b = np.array(b)
                if len(np.shape(a @ b)) == 1:
                    return a @ b
                return (a @ b).sum()

            return reduce(_matmul, args)

        def _cvxpy_summation(summand):
            """CVXPY matrix summation."""

            def _sum(summand):
                if isinstance(summand, list):
                    summand = np.array(summand)
                return cp.sum(summand)

            return _sum(summand)

        def _cvxpy_random_access(*args):
            msg = (
                "Tensor random access with 'At' has not been implemented for the CVXPY parser yet. "
                "Feel free to contribute!"
            )
            raise NotImplementedError(msg)

        cvxpy_env = {
            # Define the operations for the different operators.
            # Basic arithmetic operations
            self.NEGATE: lambda x: -x,
            self.ADD: lambda *args: reduce(lambda x, y: x + y, args),
            self.SUB: lambda *args: reduce(lambda x, y: x - y, args),
            self.MUL: lambda *args: reduce(lambda x, y: x * y, args),
            self.DIV: lambda *args: reduce(lambda x, y: x / y, args),
            # Vector and matrix operations
            self.MATMUL: _cvxpy_matmul,
            self.SUM: _cvxpy_summation,
            self.RANDOM_ACCESS: _cvxpy_random_access,
            # Exponentiation and logarithms
            # CVXPY supports some of these via special functions, but with restrictions
            self.EXP: lambda x: cp.exp(x),
            self.LN: lambda x: cp.log(x),
            self.LB: lambda x: cp.log(x) / np.log(2),
            self.LG: lambda x: cp.log(x) / np.log(10),
            self.LOP: lambda x: cp.log1p(x),
            self.SQRT: lambda x: cp.sqrt(x),
            self.SQUARE: lambda x: cp.square(x),
            self.POW: lambda x, y: x**y,  # may cause errors for non-integer powers
            # Trigonometric operations - CVXPY supports these
            self.ARCCOS: lambda x: cp.arccos(x),
            self.ARCCOSH: lambda x: cp.arccosh(x),
            self.ARCSIN: lambda x: cp.arcsin(x),
            self.ARCSINH: lambda x: cp.arcsinh(x),
            self.ARCTAN: lambda x: cp.arctan(x),
            self.ARCTANH: lambda x: cp.arctanh(x),
            self.COS: lambda x: cp.cos(x),
            self.COSH: lambda x: cp.cosh(x),
            self.SIN: lambda x: cp.sin(x),
            self.SINH: lambda x: cp.sinh(x),
            self.TAN: lambda x: cp.tan(x),
            self.TANH: lambda x: cp.tanh(x),
            # Rounding operations
            self.ABS: lambda x: cp.abs(x),
            self.CEIL: lambda x: cp.ceil(x),
            self.FLOOR: lambda x: cp.floor(x),
            # Other operations
            self.RATIONAL: lambda lst: reduce(lambda x, y: x / y, lst),
            self.MAX: lambda *args: cp.max(cp.stack(args, axis=0), axis=0),
            self.MIN: lambda *args: cp.min(cp.stack(args, axis=0), axis=0),
        }

        match to_format:
            case FormatEnum.polars:
                self.env = polars_env
                self.parse = self._parse_to_polars
            case FormatEnum.pyomo:
                self.env = pyomo_env
                self.parse = self._parse_to_pyomo
            case FormatEnum.sympy:
                self.env = sympy_env
                self.parse = self._parse_to_sympy
            case FormatEnum.gurobipy:
                self.env = gurobipy_env
                self.parse = self._parse_to_gurobipy
            case FormatEnum.cvxpy:
                self.env = cvxpy_env
                self.parse = self._parse_to_cvxpy
            case _:
                msg = f"Given target format {to_format} not supported. Must be one of {FormatEnum}."
                raise ParserError(msg)

    def _parse_to_polars(self, expr: list | str | int | float) -> pl.Expr:
        """Recursively parses JSON math expressions and returns a polars expression.

        Arguments:
            expr (list): A list with a Polish notation expression that describes a, e.g.,
                ["Multiply", ["Sqrt", 2], "x2"]

        Raises:
            ParserError: when a unsupported operator type is encountered.

        Returns:
            pl.Expr: A polars expression that may be evaluated further.

        """
        if isinstance(expr, pl.Expr):
            # Terminal case: polars expression
            return expr
        if isinstance(expr, str):
            # Terminal case: str expression (represents a column name)
            return pl.col(expr)
        if isinstance(expr, self.literals):
            # Terminal case: numeric literal
            return pl.lit(expr)

        if isinstance(expr, list):
            if len(expr) == 1 and isinstance(expr[0], str | self.literals):
                # Terminal case, single symbol expression or literal
                if isinstance(expr[0], str):
                    return pl.col(expr)
                # just a literal
                return pl.lit(expr[0])

            # Extract the operation name
            if isinstance(expr[0], str) and expr[0] in self.env:
                op_name = expr[0]
                # Parse the operands
                operands = [self.parse(e) for e in expr[1:]]

                if isinstance(operands, list) and len(operands) == 1:
                    # if the operands have redundant brackets, remove them
                    operands = operands[0]

                if isinstance(operands, list):
                    return self.env[op_name](*operands)

                return self.env[op_name](operands)

            # else, assume the list contents are parseable expressions
            return [self.parse(e) for e in expr]

        msg = f"Encountered unsupported type '{type(expr)}' during parsing."
        raise ParserError(msg)

    def _parse_to_pyomo(
        self, expr: list | str | int | float | pyomo.Expression, model: pyomo.Model
    ) -> pyomo.Expression:
        """Parses the MathJSON format recursively into a Pyomo expression.

        Args:
            expr (list | str | int | float): a list with a Polish notation expression that describes a, e.g.,
                ["Multiply", ["Sqrt", 2], "x2"]
            model (pyomo.Model): a pyomo model with the symbols defined appearing in the expression.
                E.g., "x2" -> model.x2 must be defined.

        Raises:
            ParserError: when a unsupported operator type is encountered.

        Returns:
            pyomo.Expression: returns a pyomo expression equivalent to the original expressions.
        """
        if isinstance(expr, pyomo.Expression):
            # Terminal case: pyomo expression
            return expr
        if isinstance(expr, str):
            # Terminal case: str expression, represent a variable or expression
            return getattr(model, expr)
        if isinstance(expr, self.literals):
            # Terminal case: numeric literal
            return expr

        if isinstance(expr, list):
            if len(expr) == 1 and isinstance(expr[0], str | self.literals):
                # Terminal case, single symbol expression or literal
                if isinstance(expr[0], str):
                    return getattr(model, expr[0])
                # just a literal
                return pyomo.Expression(expr=expr[0])

            # Extract the operation name
            if isinstance(expr[0], str) and expr[0] in self.env:
                op_name = expr[0]
                # Parse the operands
                operands = [self._parse_to_pyomo(e, model) for e in expr[1:]]

                if isinstance(operands, list) and len(operands) == 1:
                    # if the operands have redundant brackets, remove them
                    operands = operands[0]

                if isinstance(operands, list):
                    return self.env[op_name](*operands)

                return self.env[op_name](operands)

            # else, assume the list contents are parseable expressions
            return [self._parse_to_pyomo(e, model) for e in expr]

        msg = f"Encountered unsupported type '{type(expr)}' during parsing."
        raise ParserError(msg)

    def _parse_to_sympy(self, expr: list | str | int | float | sp.Basic) -> sp.Basic:
        """Parse the MathJSON format recursively into a sympy expression.

        Args:
            expr (list | str | int | float | sp.Basic): base call should be a list in Polish
                notation representing a mathematical expression. Recursion calls can be of various
                types.

        Raises:
            ParserError: when a unsupported operator type is encountered.

        Returns:
            sp.Basic: a sympy expression that represents the original mathematical
                expression in the supplied MathJSON format.
        """
        if isinstance(expr, sp.Basic):
            # Terminal case: sympy expression
            return expr
        if isinstance(expr, str):
            # Terminal case: represents a variable
            return sp.sympify(expr, evaluate=False)
        if isinstance(expr, self.literals):
            # Terminal case: numeric literal
            return sp.sympify(expr, evaluate=False)

        if isinstance(expr, list):
            if len(expr) == 1 and isinstance(expr[0], str | self.literals):
                # Terminal case, single symbol expression or literal
                return sp.sympify(expr[0], evaluate=False)

            # Extract the operation name
            if isinstance(expr[0], str) and expr[0] in self.env:
                op_name = expr[0]
                # Parse the operands
                operands = [self.parse(e) for e in expr[1:]]

                if isinstance(operands, list) and len(operands) == 1:
                    # if the operands have redundant brackets, remove them
                    operands = operands[0]

                if isinstance(operands, list):
                    return self.env[op_name](*operands)

                return self.env[op_name](operands)

            # else, assume the list contents are parseable expressions
            return [self.parse(e) for e in expr]

        msg = f"Encountered unsupported type '{type(expr)}' during parsing."
        raise ParserError(msg)

    def _parse_to_gurobipy(
        self, expr: list | str | int | float, callback: Callable[[str], gpexpression | int | float]
    ) -> gpexpression | int | float:
        """Parses the MathJSON format recursively into a gurobipy expression.

        Gurobi only fundamentally supports linear and quadratic expressions, and this parser
        does not check that the inputs are valid. If you try to input something else, you will
        likely encounter an error at some point.

        Args:
            expr (list | str | int | float): a list with a Polish notation expression that describes a, e.g.,
                ["Multiply", ["Sqrt", 2], "x2"]
            callback (Callable): A function that can return a gurobipy expression associated with the
                correct model when called with symbol str.

        Returns:
            Returns a gurobipy expression (that can belong into one of multiple types) equivalent to the original
            expressions.
            All possible output types should be supported as parts of gurobipy constraints. gurobipy.GenExpr at
            least isn't supported as an objective function.
        """
        if isinstance(expr, gpexpression):
            # Terminal case: gurobipy expression
            return expr
        if isinstance(expr, str):
            # Terminal case: str expression, represent a variable or expression
            return callback(expr)
        if isinstance(expr, self.literals):
            # Terminal case: numeric literal
            return expr

        if isinstance(expr, list):
            # Extract the operation name
            if isinstance(expr[0], str) and expr[0] in self.env:
                op_name = expr[0]
                # Parse the operands
                operands = [self._parse_to_gurobipy(e, callback) for e in expr[1:]]

                while isinstance(operands, list) and len(operands) == 1:
                    # if the operands have redundant brackets, remove them
                    operands = operands[0]

                if isinstance(operands, list):
                    return self.env[op_name](*operands)

                return self.env[op_name](operands)

            # else, assume the list contents are parseable expressions
            return [self._parse_to_gurobipy(e, callback) for e in expr]

        msg = f"Encountered unsupported type '{type(expr)}' during parsing."
        raise ParserError(msg)

    def _parse_to_cvxpy(
        self, expr: list | str | int | float, callback: Callable[[str], cvxpyexpression]
    ) -> cvxpyexpression:
        """Parses the MathJSON format recursively into a CVXPY expression.

        CVXPY supports a much broader range of expressions compared to gurobipy, including
        exponentials, logarithms, and trigonometric functions. However, some operations still have
        restrictions due to DCP (Disciplined Convex Programming) rules.

        Args:
            expr (list | str | int | float): a list with a Polish notation expression that describes a, e.g.,
                ["Multiply", ["Sqrt", 2], "x2"]
            callback (Callable): A function that can return a CVXPY expression associated with the
                correct model when called with symbol str.

        Returns:
            Returns a CVXPY expression equivalent to the original expression.
        """
        if isinstance(expr, (cp.Variable, cp.Parameter, cp.Expression)):
            # Terminal case: cvxpy expression
            return expr
        if isinstance(expr, str):
            # Terminal case: str expression, represent a variable or expression
            return callback(expr)
        if isinstance(expr, self.literals):
            # Terminal case: numeric literal
            return expr

        if isinstance(expr, list):
            # Extract the operation name
            if isinstance(expr[0], str) and expr[0] in self.env:
                op_name = expr[0]
                # Parse the operands
                operands = [self._parse_to_cvxpy(e, callback) for e in expr[1:]]

                while isinstance(operands, list) and len(operands) == 1:
                    # if the operands have redundant brackets, remove them
                    operands = operands[0]

                if isinstance(operands, list):
                    return self.env[op_name](*operands)

                return self.env[op_name](operands)

            # else, assume the list contents are parseable expressions
            return [self._parse_to_cvxpy(e, callback) for e in expr]

        msg = f"Encountered unsupported type '{type(expr)}' during parsing."
        raise ParserError(msg)
__init__
__init__(to_format: FormatEnum = 'polars')

Create a parser instance for parsing MathJSON notation into polars expressions.

Parameters:

Name Type Description Default
to_format FormatEnum

to which format a JSON representation should be parsed to. Defaults to "polars".

'polars'
Source code in desdeo/problem/json_parser.py
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
def __init__(self, to_format: FormatEnum = "polars"):  # noqa: C901
    """Create a parser instance for parsing MathJSON notation into polars expressions.

    Args:
        to_format (FormatEnum, optional): to which format a JSON representation should be parsed to.
            Defaults to "polars".
    """
    # Define operator names. Change these when the name is altered in the JSON format.
    # Basic arithmetic operators
    self.NEGATE: str = "Negate"
    self.ADD: str = "Add"
    self.SUB: str = "Subtract"
    self.MUL: str = "Multiply"
    self.DIV: str = "Divide"

    # Vector and matrix operations
    self.MATMUL: str = "MatMul"
    self.SUM: str = "Sum"
    self.RANDOM_ACCESS = "At"

    # Exponentation and logarithms
    self.EXP: str = "Exp"
    self.LN: str = "Ln"
    self.LB: str = "Lb"
    self.LG: str = "Lg"
    self.LOP: str = "LogOnePlus"
    self.SQRT: str = "Sqrt"
    self.SQUARE: str = "Square"
    self.POW: str = "Power"

    # Rounding operators
    self.ABS: str = "Abs"
    self.CEIL: str = "Ceil"
    self.FLOOR: str = "Floor"

    # Trigonometric operations
    self.ARCCOS: str = "Arccos"
    self.ARCCOSH: str = "Arccosh"
    self.ARCSIN: str = "Arcsin"
    self.ARCSINH: str = "Arcsinh"
    self.ARCTAN: str = "Arctan"
    self.ARCTANH: str = "Arctanh"
    self.COS: str = "Cos"
    self.COSH: str = "Cosh"
    self.SIN: str = "Sin"
    self.SINH: str = "Sinh"
    self.TAN: str = "Tan"
    self.TANH: str = "Tanh"

    # Comparison operators
    self.EQUAL: str = "Equal"
    self.GREATER: str = "Greater"
    self.GE: str = "GreaterEqual"
    self.LESS: str = "Less"
    self.LE: str = "LessEqual"
    self.NE: str = "NotEqual"

    # Other operators
    self.MAX: str = "Max"
    self.MIN: str = "Min"
    self.RATIONAL: str = "Rational"

    self.literals = int | float

    def to_expr(x: self.literals | pl.Expr):
        """Helper function to convert literals to polars expressions."""
        return pl.lit(x) if isinstance(x, self.literals) else x

    def to_sympy_expr(x):
        return sp.sympify(x, evaluate=False) if isinstance(x, self.literals) else x

    def gp_error():
        msg = "The gurobipy model format only supports linear and quadratic expressions."
        ParserError(msg)

    def _polars_reduce(ufunc, exprs):
        def _reduce_function(acc, x, ufunc=ufunc):
            acc_numpy = acc.to_numpy()
            x_numpy = x.to_numpy()

            if acc_numpy.shape == x_numpy.shape:
                return pl.Series(values=ufunc(acc_numpy, x_numpy))

            expanded_shape = acc_numpy.shape + (1,) * (x_numpy.ndim - acc_numpy.ndim)

            return pl.Series(values=ufunc(acc_numpy.reshape(expanded_shape), x_numpy))

        return pl.reduce(function=_reduce_function, exprs=exprs)

    def _polars_reduce_unary(expr, ufunc):
        def _reduce_function(acc, _, ufunc=ufunc):
            return pl.Series(values=ufunc(acc.to_numpy()))

        return pl.reduce(function=_reduce_function, exprs=[expr, None])

    def _polars_reduce_matmul(*exprs):
        def _reduce_function(acc, x):
            acc = acc.to_numpy()
            x = x.to_numpy()

            if len(acc.shape) == 2 and len(x.shape) == 2:  # noqa: PLR2004
                # Row vectors, just return the dot product, polars does not handle
                # "column" vectors anyway
                return pl.Series(values=np.einsum("ij,ij->i", acc, x, optimize=True))

            # actual matrix product required
            return pl.Series(values=np.matmul(acc, x))

        return pl.reduce(function=_reduce_function, exprs=exprs)

    def _polars_summation(expr):
        """Polars matrix summation."""

        def _reduce_function(acc, _):
            acc_numpy = acc.to_numpy()
            return pl.Series(values=np.sum(acc_numpy, axis=tuple(range(1, acc_numpy.ndim))))

        return pl.reduce(function=_reduce_function, exprs=[expr, None])

    def _polars_random_access(expr, *indices):
        """Polars tensor random access."""
        for index in indices:
            expr = expr.arr.get(index - 1)  # 1 indexing assumed in JSON format

        return expr

    def _polars_generic_apply(a, b):
        pass

    polars_env = {
        # Define the operations for the different operators.
        # Basic arithmetic operations
        self.NEGATE: lambda x: _polars_reduce_unary(x, np.negative),
        self.ADD: lambda *args: _polars_reduce(np.add, args),
        self.SUB: lambda *args: _polars_reduce(np.subtract, args),
        self.MUL: lambda *args: _polars_reduce(np.multiply, args),
        self.DIV: lambda *args: _polars_reduce(np.divide, args),
        # Vector and matrix operations
        self.MATMUL: _polars_reduce_matmul,
        self.SUM: lambda x: _polars_summation(x),
        self.RANDOM_ACCESS: _polars_random_access,
        # Exponentiation and logarithms
        self.EXP: lambda x: _polars_reduce_unary(x, np.exp),
        self.LN: lambda x: _polars_reduce_unary(x, np.log),
        self.LB: lambda x: _polars_reduce_unary(x, np.log2),
        self.LG: lambda x: _polars_reduce_unary(x, np.log10),
        self.LOP: lambda x: _polars_reduce_unary(x, np.log1p),
        self.SQRT: lambda x: _polars_reduce_unary(x, np.sqrt),
        self.SQUARE: lambda x: _polars_reduce_unary(x, lambda y: np.power(y, 2)),
        self.POW: lambda *args: _polars_reduce(np.power, args),
        # Trigonometric operations
        self.ARCCOS: lambda x: _polars_reduce_unary(x, np.arccos),
        self.ARCCOSH: lambda x: _polars_reduce_unary(x, np.arccosh),
        self.ARCSIN: lambda x: _polars_reduce_unary(x, np.arcsin),
        self.ARCSINH: lambda x: _polars_reduce_unary(x, np.arcsinh),
        self.ARCTAN: lambda x: _polars_reduce_unary(x, np.arctan),
        self.ARCTANH: lambda x: _polars_reduce_unary(x, np.arctanh),
        self.COS: lambda x: _polars_reduce_unary(x, np.cos),
        self.COSH: lambda x: _polars_reduce_unary(x, np.cosh),
        self.SIN: lambda x: _polars_reduce_unary(x, np.sin),
        self.SINH: lambda x: _polars_reduce_unary(x, np.sinh),
        self.TAN: lambda x: _polars_reduce_unary(x, np.tan),
        self.TANH: lambda x: _polars_reduce_unary(x, np.tanh),
        # Rounding operations
        self.ABS: lambda x: _polars_reduce_unary(x, np.abs),
        self.CEIL: lambda x: _polars_reduce_unary(x, np.ceil),
        self.FLOOR: lambda x: _polars_reduce_unary(x, np.floor),
        # Other operations
        self.RATIONAL: lambda lst: reduce(lambda x, y: x / y, lst),  # Not supported
        self.MAX: lambda *args: reduce(lambda x, y: pl.max_horizontal(to_expr(x), to_expr(y)), args),
        self.MIN: lambda *args: reduce(lambda x, y: pl.min_horizontal(to_expr(x), to_expr(y)), args),
    }

    def _pyomo_negate(x):
        """Negates the given operand."""

        def _expr_negate_rule(x):
            def _inner(_, *indices):
                return -x[indices]

            return _inner

        def _negate(x):
            # check if operand in indexed
            if hasattr(x, "index_set") and x.is_indexed():
                # indexed, return new pyomo expression
                expr = pyomo.Expression(x.index_set(), rule=_expr_negate_rule(x))
                expr.construct()

                return expr

            # not indexed, just regular negate
            return -x

        return _negate(x)

    def _pyomo_pow(base, exp):
        """Implements a power operator compatible with Pyomo expressions."""

        def _expr_pow_rule(base, exp):
            def _inner(_, *indices):
                return base[indices] ** exp

            return _inner

        def _pow(base, exp=exp):
            # check if operand in indexed
            if hasattr(base, "index_set") and base.is_indexed():
                # indexed, return new pyomo expression
                expr = pyomo.Expression(base.index_set(), rule=_expr_pow_rule(base, exp))
                expr.construct()

                return expr

            # not indexed, just regular power
            return base**exp

        return _pow(base, exp)

    def _pyomo_unary(x, op):
        """Implements unary operators to work with indexed expressions."""

        def _expr_rule(x, op):
            def _inner(_, *indices, op=op):
                return op(x[indices])

            return _inner

        def _op(x, op):
            # check if operand in indexed
            if hasattr(x, "index_set") and x.is_indexed():
                # indexed, return new pyomo expression
                expr = pyomo.Expression(x.index_set(), rule=_expr_rule(x, op))
                expr.construct()

                return expr

            # not indexed, just regular power
            return op(x)

        return _op(x, op)

    def _pyomo_addition(*args, subtraction=False):
        """Add (subtract) scalars or tensors to (from) each other."""

        def _expr_matrix_addition_rule(x, y, subtraction=subtraction):
            def _inner(_, *args):
                return x[*args] + y[*args] if not subtraction else x[*args] - y[*args]

            return _inner

        def _expr_elementwise_with_single_indexed(indexed, not_indexed, subtraction=subtraction):
            def _inner(_, *indices):
                return (indexed[indices] + not_indexed) if not subtraction else (indexed[indices] - not_indexed)

            return _inner

        def _add(x, y, subtraction=subtraction):
            # if both are indexed, try matrix addition
            if (hasattr(x, "index_set") and x.is_indexed()) and (hasattr(y, "index_set") and y.is_indexed()):
                # try matrix addition
                # check that the dimensions of x and y matches
                if x.index_set().set_tuple != y.index_set().set_tuple:
                    msg = (
                        f"The dimensions of x {x.index_set().set_tuple} must match that"
                        f" of y {y.index_set().set_tuple} for matrix addition."
                    )
                    raise ParserError(msg)

                expr = pyomo.Expression(
                    x.index_set(), rule=_expr_matrix_addition_rule(x, y, subtraction=subtraction)
                )
                expr.construct()

                return expr

            # if neither is indexed, do normal addition
            if not (hasattr(x, "index_set") and x.is_indexed()) and not (
                hasattr(y, "index_set") and y.is_indexed()
            ):
                if not subtraction:
                    # try regular addition
                    return x + y
                # try regular subtraction
                return x - y

            # x is indexed, y is not
            if (hasattr(x, "index_set")) and x.is_indexed():
                expr = pyomo.Expression(
                    x.index_set(), rule=_expr_elementwise_with_single_indexed(x, y, subtraction=subtraction)
                )

                expr.construct()
                return expr

            # if y is indexed, x is not
            if (hasattr(y, "index_set")) and y.is_indexed():
                expr = pyomo.Expression(
                    y.index_set(), rule=_expr_elementwise_with_single_indexed(y, x, subtraction=subtraction)
                )

                expr.construct()
                return expr

            # if only one of the operands is indexed, then addition is not supported. Throw error.
            msg = "For addition, both operands must be either scalars or matrices with matching dimensions."
            raise ParserError(msg)

        return reduce(_add, args)

    def _pyomo_subtraction(*args):
        return _pyomo_addition(*args, subtraction=True)

    def _pyomo_multiply(*args, division=False):
        """Multiply tensor with a scalar."""

        def _expr_multiply_with_scalar_rule(scalar_value, to_multiply, division=division):
            def _inner(_, *indices):
                return to_multiply[indices] * scalar_value if not division else to_multiply[indices] / scalar_value

            return _inner

        def _expr_matrix_multiply_rule(x, y, division=division):
            def _inner(_, *args):
                return x[*args] * y[*args] if not division else x[*args] / y[*args]

            return _inner

        def _multiply(x, y, division=division):
            if not hasattr(x, "is_indexed") and not hasattr(y, "is_indexed"):
                # x and y are scalars
                return x * y if not division else x / y

            # check if x or y is scalar
            if (
                hasattr(x, "is_indexed")
                and x.is_indexed()
                and x.dim() > 0
                and (not hasattr(y, "is_indexed") or not y.is_indexed() or (y.is_indexed() and y.dim() == 0))
            ):
                # x is a tensor, y is scalar
                expr = pyomo.Expression(
                    x.index_set(), rule=_expr_multiply_with_scalar_rule(y, x, division=division)
                )
                expr.construct()
                return expr
            if (
                hasattr(y, "is_indexed")
                and y.is_indexed()
                and y.dim() > 0
                and (not hasattr(x, "is_indexed") or not x.is_indexed() or (x.is_indexed() and x.dim() == 0))
            ):
                # y is a tensor, x is scalar
                expr = pyomo.Expression(
                    y.index_set(), rule=_expr_multiply_with_scalar_rule(x, y, division=division)
                )
                expr.construct()
                return expr

            # check if both are indexed
            if hasattr(x, "index_set") and hasattr(y, "index_set"):
                # both are indexed, neither is a scalar, check dims and sized, if match,
                # multiply together element-wise
                if x.index_set() != y.index_set():
                    msg = (
                        f"The dimensions of x {x.index_set().set_tuple} must match that"
                        f" of y {y.index_set().set_tuple} for element-wise matrix multiplication."
                    )
                    raise ParserError(msg)

                expr = pyomo.Expression(x.index_set(), rule=_expr_matrix_multiply_rule(x, y, division=division))
                expr.construct()

                return expr

            # both are scalars
            return x * y if not division else x / y

        return reduce(_multiply, args)

    def _pyomo_division(*args):
        return _pyomo_multiply(*args, division=True)

    def _pyomo_matrix_multiplication(*args):
        """Multiply two matrices together."""

        def _expr_matmul_rule(mat_a, mat_b, j_indices):
            def _inner(_, i_index, k_index):
                return sum(mat_a[i_index, j] * mat_b[j, k_index] for j in j_indices)

            return _inner

        def _matmul(mat_a, mat_b):
            if not (hasattr(mat_a, "is_indexed") and mat_a.is_indexed()) or not (
                hasattr(mat_b, "is_indexed") and mat_b.is_indexed()
            ):
                # either mat_a or mat_b is not tensor
                msg = "Either mat_a or mat_b, or both, is not indexed. Cannot perform matrix multiplication."
                raise ParserError(msg)

            # check for regular vectors, then do dot product
            if mat_a.dim() == 1 and mat_b.dim() == 1:
                if (len_a := len(mat_a.index_set())) != (len_b := len(mat_b.index_set())):
                    msg = (
                        "For dot product, the sizes of the vectors must match."
                        f" Sizes mat_a = {len_a} and mat_b = {len_b}."
                    )
                    raise ParserError(msg)

                return pyomo.sum_product(mat_a, mat_b, index=mat_a.index_set())

            # assuming mat_a has dimensions i,j; and mat_b j,k;
            # then the j dimension is squeezed and the i and k dimensions are kept.

            # check that we are dealing with matrices
            min_dimension = 2
            if (
                not hasattr(mat_a.index_set(), "set_tuple") or len(mat_a.index_set().set_tuple) < min_dimension
            ) or (not hasattr(mat_b.index_set(), "set_tuple") and len(mat_b.index_set().set_tuple) < min_dimension):
                msg = "Both mat_a and mat_b must have at least two dimensions."
                raise ParserError(msg)

            # check that the outer dimensions (the one to be squeezed) matches
            if len(mat_a.index_set().set_tuple[-1]) != len(mat_b.index_set().set_tuple[0]):
                msg = (
                    f"The last dimension size of mat_a ({mat_a.index_set().set_tuple[-1]}) must "
                    f"match the first dimension of mat_b ({mat_b.index_set().set_tuple[0]})"
                )
                raise ParserError(msg)

            expr = pyomo.Expression(
                mat_a.index_set().set_tuple[0],
                mat_b.index_set().set_tuple[1],
                rule=_expr_matmul_rule(mat_a, mat_b, mat_a.index_set().set_tuple[1]),
            )
            expr.construct()

            return expr

        return reduce(_matmul, args)

    def _pyomo_summation(summand):
        """Sum an indexed Pyomo object."""
        return pyomo.sum_product(summand, index=summand.index_set())

    def _pyomo_random_access(indexed, *indices):
        return indexed[*indices]

    pyomo_env = {
        # Define the operations for the different operators.
        # Basic arithmetic operations
        self.NEGATE: _pyomo_negate,
        self.ADD: _pyomo_addition,
        self.SUB: _pyomo_subtraction,
        self.MUL: _pyomo_multiply,
        self.DIV: _pyomo_division,
        # Vector and matrix operations
        self.MATMUL: _pyomo_matrix_multiplication,
        self.SUM: _pyomo_summation,
        self.RANDOM_ACCESS: _pyomo_random_access,
        # Exponentiation and logarithms
        self.EXP: lambda x: _pyomo_unary(x, pyomo.exp),
        self.LN: lambda x: _pyomo_unary(x, pyomo.log),
        self.LB: lambda x: _pyomo_unary(
            x, lambda x: pyomo.log(x) / pyomo.log(2)
        ),  # change of base, pyomo has no log2
        self.LG: lambda x: _pyomo_unary(x, pyomo.log10),
        self.LOP: lambda x: _pyomo_unary(x, lambda x: pyomo.log(x + 1)),
        self.SQRT: lambda x: _pyomo_unary(x, pyomo.sqrt),
        self.SQUARE: lambda x: _pyomo_pow(x, 2),
        self.POW: lambda x, y: _pyomo_pow(x, y),
        # Trigonometric operations
        self.ARCCOS: lambda x: _pyomo_unary(x, pyomo.acos),
        self.ARCCOSH: lambda x: _pyomo_unary(x, pyomo.acosh),
        self.ARCSIN: lambda x: _pyomo_unary(x, pyomo.asin),
        self.ARCSINH: lambda x: _pyomo_unary(x, pyomo.asinh),
        self.ARCTAN: lambda x: _pyomo_unary(x, pyomo.atan),
        self.ARCTANH: lambda x: _pyomo_unary(x, pyomo.atanh),
        self.COS: lambda x: _pyomo_unary(x, pyomo.cos),
        self.COSH: lambda x: _pyomo_unary(x, pyomo.cosh),
        self.SIN: lambda x: _pyomo_unary(x, pyomo.sin),
        self.SINH: lambda x: _pyomo_unary(x, pyomo.sinh),
        self.TAN: lambda x: _pyomo_unary(x, pyomo.tan),
        self.TANH: lambda x: _pyomo_unary(x, pyomo.tanh),
        # Rounding operations
        self.ABS: lambda x: _pyomo_unary(x, abs),
        self.CEIL: lambda x: _pyomo_unary(x, pyomo.ceil),
        self.FLOOR: lambda x: _pyomo_unary(x, pyomo.floor),
        # Other operations
        self.RATIONAL: lambda lst: reduce(lambda x, y: x / y, lst),  # not supported
        # probably a better idea to reformulate expressions with a max when utilized with pyomo
        # self.MAX: lambda *args: reduce(lambda x, y: _PyomoMax((x, y)), args),
        self.MAX: lambda *args: _PyomoMax(args),
        self.MIN: lambda *args: _PyomoMin(args),
    }

    def _sympy_matmul(*args):
        """Sympy matrix multiplication."""
        msg = (
            "Matrix multiplication '@' has not been implemented for the Sympy parser yet. Feel free to contribute!"
        )
        raise NotImplementedError(msg)

    def _sympy_summation(summand):
        """Sympy matrix summation."""
        msg = "Matrix summation 'Sum' has not been implemented for the Sympy parser yet. Feel free to contribute!"
        raise NotImplementedError(msg)

    def _sympy_random_access(*args):
        msg = (
            "Tensor random access with 'At' has not been implemented for the Sympy parser yet. "
            "Feel free to contribute!"
        )
        raise NotImplementedError(msg)

    sympy_env = {
        # Basic arithmetic operations
        self.NEGATE: lambda x: -to_sympy_expr(x),
        self.ADD: lambda *args: reduce(lambda x, y: to_sympy_expr(x) + to_sympy_expr(y), args),
        self.SUB: lambda *args: reduce(lambda x, y: to_sympy_expr(x) - to_sympy_expr(y), args),
        self.MUL: lambda *args: reduce(lambda x, y: to_sympy_expr(x) * to_sympy_expr(y), args),
        self.DIV: lambda *args: reduce(lambda x, y: to_sympy_expr(x) / to_sympy_expr(y), args),
        # Vector and matrix operations
        self.MATMUL: _sympy_matmul,
        self.SUM: _sympy_summation,
        self.RANDOM_ACCESS: _sympy_random_access,
        # Exponentiation and logarithms
        self.EXP: lambda x: sp.exp(to_sympy_expr(x)),
        self.LN: lambda x: sp.log(to_sympy_expr(x)),
        self.LB: lambda x: sp.log(to_sympy_expr(x), 2),
        self.LG: lambda x: sp.log(to_sympy_expr(x), 10),
        self.LOP: lambda x: sp.log(1 + to_sympy_expr(x)),
        self.SQRT: lambda x: sp.sqrt(to_sympy_expr(x)),
        self.SQUARE: lambda x: to_sympy_expr(x) ** 2,
        self.POW: lambda x, y: to_sympy_expr(x) ** to_sympy_expr(y),
        # Trigonometric operations
        self.SIN: lambda x: sp.sin(to_sympy_expr(x)),
        self.COS: lambda x: sp.cos(to_sympy_expr(x)),
        self.TAN: lambda x: sp.tan(to_sympy_expr(x)),
        self.ARCSIN: lambda x: sp.asin(to_sympy_expr(x)),
        self.ARCCOS: lambda x: sp.acos(to_sympy_expr(x)),
        self.ARCTAN: lambda x: sp.atan(to_sympy_expr(x)),
        # Hyperbolic functions
        self.SINH: lambda x: sp.sinh(to_sympy_expr(x)),
        self.COSH: lambda x: sp.cosh(to_sympy_expr(x)),
        self.TANH: lambda x: sp.tanh(to_sympy_expr(x)),
        self.ARCSINH: lambda x: sp.asinh(to_sympy_expr(x)),
        self.ARCCOSH: lambda x: sp.acosh(to_sympy_expr(x)),
        self.ARCTANH: lambda x: sp.atanh(to_sympy_expr(x)),
        # Other
        self.ABS: lambda x: sp.Abs(to_sympy_expr(x)),
        self.CEIL: lambda x: sp.ceiling(to_sympy_expr(x)),
        self.FLOOR: lambda x: sp.floor(to_sympy_expr(x)),
        # Note: Max and Min in sympy take any number of arguments
        self.MAX: lambda *args: sp.Max(*args),
        self.MIN: lambda *args: sp.Min(*args),
        # Rational numbers, for now assuming two-element list for numerator and denominator
        self.RATIONAL: lambda x, y: sp.Rational(x, y),
    }

    def _gurobipy_matmul(*args):
        """Gurobipy matrix multiplication."""

        def _matmul(a, b):
            if isinstance(a, list):
                a = np.array(a)
            if isinstance(b, list):
                b = np.array(b)
            if len(np.shape(a @ b)) == 1:
                return a @ b
            return (a @ b).sum()

        return reduce(_matmul, args)
        msg = (
            "Matrix multiplication '@' has not been implemented for the Gurobipy parser yet."
            " Feel free to contribute!"
        )
        raise NotImplementedError(msg)

    def _gurobipy_summation(summand):
        """Gurobipy matrix summation."""

        def _sum(summand):
            if isinstance(summand, list):
                summand = np.array(summand)
            return summand.sum()

        return _sum(summand)
        msg = (
            "Matrix summation 'Sum' has not been implemented for the Gurobipy parser yet. Feel free to contribute!"
        )
        raise NotImplementedError(msg)

    def _gurobipy_random_access(*args):
        msg = (
            "Tensor random access with 'At' has not been implemented for the Gurobipy parser yet. "
            "Feel free to contribute!"
        )
        raise NotImplementedError(msg)

    gurobipy_env = {
        # Define the operations for the different operators.
        # Basic arithmetic operations
        self.NEGATE: lambda x: -x,
        self.ADD: lambda *args: reduce(lambda x, y: x + y, args),
        self.SUB: lambda *args: reduce(lambda x, y: x - y, args),
        self.MUL: lambda *args: reduce(lambda x, y: x * y, args),
        self.DIV: lambda *args: reduce(lambda x, y: x / y, args),
        # Vector and matrix operations
        self.MATMUL: _gurobipy_matmul,
        self.SUM: _gurobipy_summation,
        self.RANDOM_ACCESS: _gurobipy_random_access,
        # Exponentiation and logarithms
        # it would be possible to implement some of these with the special functions that
        # gurobi has to offer, but they would only work under specific circumstances
        self.EXP: lambda x: gp_error(),
        self.LN: lambda x: gp_error(),
        self.LB: lambda x: gp_error(),
        self.LG: lambda x: gp_error(),
        self.LOP: lambda x: gp_error(),
        self.SQRT: lambda x: gp_error(),
        self.SQUARE: lambda x: x**2,
        self.POW: lambda x, y: x**y,  # this will likely cause an error at some point for most y
        # Trigonometric operations
        # it would be possible to implement some of these with the special functions that
        # gurobi has to offer, but they would only work under specific circumstances
        self.ARCCOS: lambda x: gp_error(),
        self.ARCCOSH: lambda x: gp_error(),
        self.ARCSIN: lambda x: gp_error(),
        self.ARCSINH: lambda x: gp_error(),
        self.ARCTAN: lambda x: gp_error(),
        self.ARCTANH: lambda x: gp_error(),
        self.COS: lambda x: gp_error(),
        self.COSH: lambda x: gp_error(),
        self.SIN: lambda x: gp_error(),
        self.SINH: lambda x: gp_error(),
        self.TAN: lambda x: gp_error(),
        self.TANH: lambda x: gp_error(),
        # Rounding operations
        self.ABS: lambda x: gp.abs_(x),
        self.CEIL: lambda x: gp_error(),
        self.FLOOR: lambda x: gp_error(),
        # Other operations
        self.RATIONAL: lambda lst: reduce(lambda x, y: x / y, lst),  # not supported
        self.MAX: lambda *args: gp.max_(args),  ## OBS! max and min are unsupported, but left here for reasons
        self.MIN: lambda *args: gp.min_(args),
    }

    def _cvxpy_error():
        msg = "The cvxpy model format only supports linear and quadratic expressions."
        return lambda x: (_ for _ in ()).throw(ParserError(msg))

    def _cvxpy_matmul(*args):
        """CVXPY matrix multiplication."""

        def _matmul(a, b):
            if isinstance(a, list):
                a = np.array(a)
            if isinstance(b, list):
                b = np.array(b)
            if len(np.shape(a @ b)) == 1:
                return a @ b
            return (a @ b).sum()

        return reduce(_matmul, args)

    def _cvxpy_summation(summand):
        """CVXPY matrix summation."""

        def _sum(summand):
            if isinstance(summand, list):
                summand = np.array(summand)
            return cp.sum(summand)

        return _sum(summand)

    def _cvxpy_random_access(*args):
        msg = (
            "Tensor random access with 'At' has not been implemented for the CVXPY parser yet. "
            "Feel free to contribute!"
        )
        raise NotImplementedError(msg)

    cvxpy_env = {
        # Define the operations for the different operators.
        # Basic arithmetic operations
        self.NEGATE: lambda x: -x,
        self.ADD: lambda *args: reduce(lambda x, y: x + y, args),
        self.SUB: lambda *args: reduce(lambda x, y: x - y, args),
        self.MUL: lambda *args: reduce(lambda x, y: x * y, args),
        self.DIV: lambda *args: reduce(lambda x, y: x / y, args),
        # Vector and matrix operations
        self.MATMUL: _cvxpy_matmul,
        self.SUM: _cvxpy_summation,
        self.RANDOM_ACCESS: _cvxpy_random_access,
        # Exponentiation and logarithms
        # CVXPY supports some of these via special functions, but with restrictions
        self.EXP: lambda x: cp.exp(x),
        self.LN: lambda x: cp.log(x),
        self.LB: lambda x: cp.log(x) / np.log(2),
        self.LG: lambda x: cp.log(x) / np.log(10),
        self.LOP: lambda x: cp.log1p(x),
        self.SQRT: lambda x: cp.sqrt(x),
        self.SQUARE: lambda x: cp.square(x),
        self.POW: lambda x, y: x**y,  # may cause errors for non-integer powers
        # Trigonometric operations - CVXPY supports these
        self.ARCCOS: lambda x: cp.arccos(x),
        self.ARCCOSH: lambda x: cp.arccosh(x),
        self.ARCSIN: lambda x: cp.arcsin(x),
        self.ARCSINH: lambda x: cp.arcsinh(x),
        self.ARCTAN: lambda x: cp.arctan(x),
        self.ARCTANH: lambda x: cp.arctanh(x),
        self.COS: lambda x: cp.cos(x),
        self.COSH: lambda x: cp.cosh(x),
        self.SIN: lambda x: cp.sin(x),
        self.SINH: lambda x: cp.sinh(x),
        self.TAN: lambda x: cp.tan(x),
        self.TANH: lambda x: cp.tanh(x),
        # Rounding operations
        self.ABS: lambda x: cp.abs(x),
        self.CEIL: lambda x: cp.ceil(x),
        self.FLOOR: lambda x: cp.floor(x),
        # Other operations
        self.RATIONAL: lambda lst: reduce(lambda x, y: x / y, lst),
        self.MAX: lambda *args: cp.max(cp.stack(args, axis=0), axis=0),
        self.MIN: lambda *args: cp.min(cp.stack(args, axis=0), axis=0),
    }

    match to_format:
        case FormatEnum.polars:
            self.env = polars_env
            self.parse = self._parse_to_polars
        case FormatEnum.pyomo:
            self.env = pyomo_env
            self.parse = self._parse_to_pyomo
        case FormatEnum.sympy:
            self.env = sympy_env
            self.parse = self._parse_to_sympy
        case FormatEnum.gurobipy:
            self.env = gurobipy_env
            self.parse = self._parse_to_gurobipy
        case FormatEnum.cvxpy:
            self.env = cvxpy_env
            self.parse = self._parse_to_cvxpy
        case _:
            msg = f"Given target format {to_format} not supported. Must be one of {FormatEnum}."
            raise ParserError(msg)
_parse_to_cvxpy
_parse_to_cvxpy(
    expr: list | str | int | float,
    callback: Callable[[str], cvxpyexpression],
) -> cvxpyexpression

Parses the MathJSON format recursively into a CVXPY expression.

CVXPY supports a much broader range of expressions compared to gurobipy, including exponentials, logarithms, and trigonometric functions. However, some operations still have restrictions due to DCP (Disciplined Convex Programming) rules.

Parameters:

Name Type Description Default
expr list | str | int | float

a list with a Polish notation expression that describes a, e.g., ["Multiply", ["Sqrt", 2], "x2"]

required
callback Callable

A function that can return a CVXPY expression associated with the correct model when called with symbol str.

required

Returns:

Type Description
cvxpyexpression

Returns a CVXPY expression equivalent to the original expression.

Source code in desdeo/problem/json_parser.py
def _parse_to_cvxpy(
    self, expr: list | str | int | float, callback: Callable[[str], cvxpyexpression]
) -> cvxpyexpression:
    """Parses the MathJSON format recursively into a CVXPY expression.

    CVXPY supports a much broader range of expressions compared to gurobipy, including
    exponentials, logarithms, and trigonometric functions. However, some operations still have
    restrictions due to DCP (Disciplined Convex Programming) rules.

    Args:
        expr (list | str | int | float): a list with a Polish notation expression that describes a, e.g.,
            ["Multiply", ["Sqrt", 2], "x2"]
        callback (Callable): A function that can return a CVXPY expression associated with the
            correct model when called with symbol str.

    Returns:
        Returns a CVXPY expression equivalent to the original expression.
    """
    if isinstance(expr, (cp.Variable, cp.Parameter, cp.Expression)):
        # Terminal case: cvxpy expression
        return expr
    if isinstance(expr, str):
        # Terminal case: str expression, represent a variable or expression
        return callback(expr)
    if isinstance(expr, self.literals):
        # Terminal case: numeric literal
        return expr

    if isinstance(expr, list):
        # Extract the operation name
        if isinstance(expr[0], str) and expr[0] in self.env:
            op_name = expr[0]
            # Parse the operands
            operands = [self._parse_to_cvxpy(e, callback) for e in expr[1:]]

            while isinstance(operands, list) and len(operands) == 1:
                # if the operands have redundant brackets, remove them
                operands = operands[0]

            if isinstance(operands, list):
                return self.env[op_name](*operands)

            return self.env[op_name](operands)

        # else, assume the list contents are parseable expressions
        return [self._parse_to_cvxpy(e, callback) for e in expr]

    msg = f"Encountered unsupported type '{type(expr)}' during parsing."
    raise ParserError(msg)
_parse_to_gurobipy
_parse_to_gurobipy(
    expr: list | str | int | float,
    callback: Callable[[str], gpexpression | int | float],
) -> gpexpression | int | float

Parses the MathJSON format recursively into a gurobipy expression.

Gurobi only fundamentally supports linear and quadratic expressions, and this parser does not check that the inputs are valid. If you try to input something else, you will likely encounter an error at some point.

Parameters:

Name Type Description Default
expr list | str | int | float

a list with a Polish notation expression that describes a, e.g., ["Multiply", ["Sqrt", 2], "x2"]

required
callback Callable

A function that can return a gurobipy expression associated with the correct model when called with symbol str.

required

Returns:

Type Description
gpexpression | int | float

Returns a gurobipy expression (that can belong into one of multiple types) equivalent to the original

gpexpression | int | float

expressions.

gpexpression | int | float

All possible output types should be supported as parts of gurobipy constraints. gurobipy.GenExpr at

gpexpression | int | float

least isn't supported as an objective function.

Source code in desdeo/problem/json_parser.py
def _parse_to_gurobipy(
    self, expr: list | str | int | float, callback: Callable[[str], gpexpression | int | float]
) -> gpexpression | int | float:
    """Parses the MathJSON format recursively into a gurobipy expression.

    Gurobi only fundamentally supports linear and quadratic expressions, and this parser
    does not check that the inputs are valid. If you try to input something else, you will
    likely encounter an error at some point.

    Args:
        expr (list | str | int | float): a list with a Polish notation expression that describes a, e.g.,
            ["Multiply", ["Sqrt", 2], "x2"]
        callback (Callable): A function that can return a gurobipy expression associated with the
            correct model when called with symbol str.

    Returns:
        Returns a gurobipy expression (that can belong into one of multiple types) equivalent to the original
        expressions.
        All possible output types should be supported as parts of gurobipy constraints. gurobipy.GenExpr at
        least isn't supported as an objective function.
    """
    if isinstance(expr, gpexpression):
        # Terminal case: gurobipy expression
        return expr
    if isinstance(expr, str):
        # Terminal case: str expression, represent a variable or expression
        return callback(expr)
    if isinstance(expr, self.literals):
        # Terminal case: numeric literal
        return expr

    if isinstance(expr, list):
        # Extract the operation name
        if isinstance(expr[0], str) and expr[0] in self.env:
            op_name = expr[0]
            # Parse the operands
            operands = [self._parse_to_gurobipy(e, callback) for e in expr[1:]]

            while isinstance(operands, list) and len(operands) == 1:
                # if the operands have redundant brackets, remove them
                operands = operands[0]

            if isinstance(operands, list):
                return self.env[op_name](*operands)

            return self.env[op_name](operands)

        # else, assume the list contents are parseable expressions
        return [self._parse_to_gurobipy(e, callback) for e in expr]

    msg = f"Encountered unsupported type '{type(expr)}' during parsing."
    raise ParserError(msg)
_parse_to_polars
_parse_to_polars(expr: list | str | int | float) -> pl.Expr

Recursively parses JSON math expressions and returns a polars expression.

Parameters:

Name Type Description Default
expr list

A list with a Polish notation expression that describes a, e.g., ["Multiply", ["Sqrt", 2], "x2"]

required

Raises:

Type Description
ParserError

when a unsupported operator type is encountered.

Returns:

Type Description
Expr

pl.Expr: A polars expression that may be evaluated further.

Source code in desdeo/problem/json_parser.py
def _parse_to_polars(self, expr: list | str | int | float) -> pl.Expr:
    """Recursively parses JSON math expressions and returns a polars expression.

    Arguments:
        expr (list): A list with a Polish notation expression that describes a, e.g.,
            ["Multiply", ["Sqrt", 2], "x2"]

    Raises:
        ParserError: when a unsupported operator type is encountered.

    Returns:
        pl.Expr: A polars expression that may be evaluated further.

    """
    if isinstance(expr, pl.Expr):
        # Terminal case: polars expression
        return expr
    if isinstance(expr, str):
        # Terminal case: str expression (represents a column name)
        return pl.col(expr)
    if isinstance(expr, self.literals):
        # Terminal case: numeric literal
        return pl.lit(expr)

    if isinstance(expr, list):
        if len(expr) == 1 and isinstance(expr[0], str | self.literals):
            # Terminal case, single symbol expression or literal
            if isinstance(expr[0], str):
                return pl.col(expr)
            # just a literal
            return pl.lit(expr[0])

        # Extract the operation name
        if isinstance(expr[0], str) and expr[0] in self.env:
            op_name = expr[0]
            # Parse the operands
            operands = [self.parse(e) for e in expr[1:]]

            if isinstance(operands, list) and len(operands) == 1:
                # if the operands have redundant brackets, remove them
                operands = operands[0]

            if isinstance(operands, list):
                return self.env[op_name](*operands)

            return self.env[op_name](operands)

        # else, assume the list contents are parseable expressions
        return [self.parse(e) for e in expr]

    msg = f"Encountered unsupported type '{type(expr)}' during parsing."
    raise ParserError(msg)
_parse_to_pyomo
_parse_to_pyomo(
    expr: list | str | int | float | Expression,
    model: Model,
) -> pyomo.Expression

Parses the MathJSON format recursively into a Pyomo expression.

Parameters:

Name Type Description Default
expr list | str | int | float

a list with a Polish notation expression that describes a, e.g., ["Multiply", ["Sqrt", 2], "x2"]

required
model Model

a pyomo model with the symbols defined appearing in the expression. E.g., "x2" -> model.x2 must be defined.

required

Raises:

Type Description
ParserError

when a unsupported operator type is encountered.

Returns:

Type Description
Expression

pyomo.Expression: returns a pyomo expression equivalent to the original expressions.

Source code in desdeo/problem/json_parser.py
def _parse_to_pyomo(
    self, expr: list | str | int | float | pyomo.Expression, model: pyomo.Model
) -> pyomo.Expression:
    """Parses the MathJSON format recursively into a Pyomo expression.

    Args:
        expr (list | str | int | float): a list with a Polish notation expression that describes a, e.g.,
            ["Multiply", ["Sqrt", 2], "x2"]
        model (pyomo.Model): a pyomo model with the symbols defined appearing in the expression.
            E.g., "x2" -> model.x2 must be defined.

    Raises:
        ParserError: when a unsupported operator type is encountered.

    Returns:
        pyomo.Expression: returns a pyomo expression equivalent to the original expressions.
    """
    if isinstance(expr, pyomo.Expression):
        # Terminal case: pyomo expression
        return expr
    if isinstance(expr, str):
        # Terminal case: str expression, represent a variable or expression
        return getattr(model, expr)
    if isinstance(expr, self.literals):
        # Terminal case: numeric literal
        return expr

    if isinstance(expr, list):
        if len(expr) == 1 and isinstance(expr[0], str | self.literals):
            # Terminal case, single symbol expression or literal
            if isinstance(expr[0], str):
                return getattr(model, expr[0])
            # just a literal
            return pyomo.Expression(expr=expr[0])

        # Extract the operation name
        if isinstance(expr[0], str) and expr[0] in self.env:
            op_name = expr[0]
            # Parse the operands
            operands = [self._parse_to_pyomo(e, model) for e in expr[1:]]

            if isinstance(operands, list) and len(operands) == 1:
                # if the operands have redundant brackets, remove them
                operands = operands[0]

            if isinstance(operands, list):
                return self.env[op_name](*operands)

            return self.env[op_name](operands)

        # else, assume the list contents are parseable expressions
        return [self._parse_to_pyomo(e, model) for e in expr]

    msg = f"Encountered unsupported type '{type(expr)}' during parsing."
    raise ParserError(msg)
_parse_to_sympy
_parse_to_sympy(
    expr: list | str | int | float | Basic,
) -> sp.Basic

Parse the MathJSON format recursively into a sympy expression.

Parameters:

Name Type Description Default
expr list | str | int | float | Basic

base call should be a list in Polish notation representing a mathematical expression. Recursion calls can be of various types.

required

Raises:

Type Description
ParserError

when a unsupported operator type is encountered.

Returns:

Type Description
Basic

sp.Basic: a sympy expression that represents the original mathematical expression in the supplied MathJSON format.

Source code in desdeo/problem/json_parser.py
def _parse_to_sympy(self, expr: list | str | int | float | sp.Basic) -> sp.Basic:
    """Parse the MathJSON format recursively into a sympy expression.

    Args:
        expr (list | str | int | float | sp.Basic): base call should be a list in Polish
            notation representing a mathematical expression. Recursion calls can be of various
            types.

    Raises:
        ParserError: when a unsupported operator type is encountered.

    Returns:
        sp.Basic: a sympy expression that represents the original mathematical
            expression in the supplied MathJSON format.
    """
    if isinstance(expr, sp.Basic):
        # Terminal case: sympy expression
        return expr
    if isinstance(expr, str):
        # Terminal case: represents a variable
        return sp.sympify(expr, evaluate=False)
    if isinstance(expr, self.literals):
        # Terminal case: numeric literal
        return sp.sympify(expr, evaluate=False)

    if isinstance(expr, list):
        if len(expr) == 1 and isinstance(expr[0], str | self.literals):
            # Terminal case, single symbol expression or literal
            return sp.sympify(expr[0], evaluate=False)

        # Extract the operation name
        if isinstance(expr[0], str) and expr[0] in self.env:
            op_name = expr[0]
            # Parse the operands
            operands = [self.parse(e) for e in expr[1:]]

            if isinstance(operands, list) and len(operands) == 1:
                # if the operands have redundant brackets, remove them
                operands = operands[0]

            if isinstance(operands, list):
                return self.env[op_name](*operands)

            return self.env[op_name](operands)

        # else, assume the list contents are parseable expressions
        return [self.parse(e) for e in expr]

    msg = f"Encountered unsupported type '{type(expr)}' during parsing."
    raise ParserError(msg)

ParserError

Bases: Exception

Raised when an error related to the MathParser class in encountered.

Source code in desdeo/problem/json_parser.py
class ParserError(Exception):
    """Raised when an error related to the MathParser class in encountered."""

replace_str

replace_str(
    lst: list | str,
    target: str,
    sub: list | str | float | int,
) -> list

Replace a target in list with a substitution recursively.

Parameters:

Name Type Description Default
lst list or str

The list where the substitution is to be made.

required
target str

The target of the substitution.

required
sub list or str

The content to substitute the target.

required
Return

list or str: The list or str with the substitution.

Example

replace_str("["Max", "g_i", ["Add","g_i","f_i"]]]", "_i", "_1") ---> ["Max", "g_1", ["Add","g_1","f_1"]]]

Source code in desdeo/problem/json_parser.py
def replace_str(lst: list | str, target: str, sub: list | str | float | int) -> list:
    """Replace a target in list with a substitution recursively.

    Arguments:
        lst (list or str): The list where the substitution is to be made.
        target (str): The target of the substitution.
        sub (list or str): The content to substitute the target.

    Return:
        list or str: The list or str with the substitution.

    Example:
        replace_str("["Max", "g_i", ["Add","g_i","f_i"]]]", "_i", "_1") --->
        ["Max", "g_1", ["Add","g_1","f_1"]]]
    """
    if isinstance(lst, list):
        return [replace_str(item, target, sub) for item in lst]
    if isinstance(lst, str):
        if target == lst:
            if isinstance(sub, str):
                return lst.replace(target, sub)
            return sub
        return lst
    return lst

Infix parser

desdeo.problem.infix_parser

Defines parsers for parsing mathematical expression in an infix format and expressed as string.

Currently, mostly parses to MathJSON, e.g., "n / (1 + n)" -> ['Divide', 'n', ['Add', 1, 'n']].

InfixExpressionParser

A class for defining infix notation parsers.

Source code in desdeo/problem/infix_parser.py
class InfixExpressionParser:
    """A class for defining infix notation parsers."""

    SUPPORTED_TARGETS: ClassVar[list] = ["MathJSON"]

    # Supported infix binary operators, i.e., '1+1'. The key is the notation of the operator in infix format,
    # and the value the notation in parsed format.
    BINARY_OPERATORS: ClassVar[dict] = {
        "+": "Add",
        "-": "Subtract",
        "*": "Multiply",
        "/": "Divide",
        "**": "Power",
        "@": "MatMul",
    }

    # Supported infix unary operators, i.e., 'Cos(90)'. The key is the notation of the operator in infix format,
    # and the value the notation in parsed format.
    UNARY_OPERATORS: ClassVar[dict] = {
        "Cos": "Cos",
        "Sin": "Sin",
        "Tan": "Tan",
        "Exp": "Exp",
        "Ln": "Ln",
        "Lb": "Lb",
        "Lg": "Lg",
        "LogOnePlus": "LogOnePlus",
        "Sqrt": "Sqrt",
        "Square": "Square",
        "Abs": "Abs",
        "Ceil": "Ceil",
        "Floor": "Floor",
        "Arccos": "Arccos",
        "Arccosh": "Arccosh",
        "Arcsin": "Arcsin",
        "Arcsinh": "Arcsinh",
        "Arctan": "Arctan",
        "Arctanh": "Arctanh",
        "Cosh": "Cosh",
        "Sinh": "Sinh",
        "Tanh": "Tanh",
        "Rational": "Rational",
        "-": "Negate",
        "Sum": "Sum",
    }

    # Supported infix variadic operators (operators that take one or more comma separated arguments),
    # i.e., 'Max(1,2, Cos(3)). The key is the notation of the operator in infix format,
    # and the value the notation in parsed format.
    VARIADIC_OPERATORS: ClassVar[dict] = {"Max": "Max", "Min": "Min"}

    def __init__(self, target="MathJSON"):
        """A parser for infix notation, e.g., the huma readable way of notating mathematical expressions.

        The parser can parse infix notation stored in a string to different formats. For instance,
        "Cos(2 + f_1) - 7.2 + Max(f_2, -f_3)" is first parsed to the list:
        ['Cos', [[2, '+', 'f_1']]], '-', 7.2, '+', ['Max', ['f_2', ['-', 'f_3']]. Then, if parsed to
        the MathJSON format, it will be parsed to the list:
        [
            [["Subtract", ["Cos", ["Add", 2, "f_1"]], ["Add", 7.2, ["Max", ["f_2", ["Negate", "f_3"]]]]]]
        ].


        Args:
            target (str, optional): The target format to parse an infix expression to.
                Currently only "MathJSON" is supported. Defaults to "MathJSON".
        """
        if target not in InfixExpressionParser.SUPPORTED_TARGETS:
            msg = f"The target '{target} is not supported. Should be one of {InfixExpressionParser.SUPPORTED_TARGETS}"
            raise ValueError(msg)
        self.target = target

        # Scope limiters
        lparen = Suppress("(")
        rparen = Suppress(")")
        lbracket = Suppress("[")
        rbracket = Suppress("]")

        # Define keywords (Note that binary operators must be defined manually)
        symbols_variadic = set(InfixExpressionParser.VARIADIC_OPERATORS)
        symbols_unary = set(InfixExpressionParser.UNARY_OPERATORS)

        # Define binary operation symbols (this is the manual part)
        # If new binary operators are to be added, they must be defined here.
        signop = one_of("+ -")
        multop = one_of("* / . @")
        plusop = one_of("+ -")
        expop = Literal("**")

        # Dynamically create Keyword objects for variadric functions
        variadic_pattern = r"\b(" + f"{'|'.join([*symbols_variadic])}" + r")\b"
        variadic_func_names = Regex(variadic_pattern)

        # Dynamically create Keyword objects for unary functions
        # unary_func_names = reduce(or_, (Keyword(k) for k in symbols_unary))
        unary_pattern = r"\b(" + f"{'|'.join([*symbols_unary])}" + r")\b"
        unary_func_names = Regex(unary_pattern)

        # Define operands
        integer = pyparsing_common.integer

        # Scientific notation
        scientific = pyparsing_common.sci_real

        # Complete regex pattern with exclusions and identifier pattern
        exclude = f"{'|'.join([*symbols_variadic, *symbols_unary])}"
        pattern = r"(?!\b(" + exclude + r")\b)(\b[a-zA-Z_][a-zA-Z0-9_]*\b)"
        variable = Regex(pattern)("variable")

        # Forward declarations of variadic, unary, and bracket function calls
        variadic_call = Forward()
        unary_call = Forward()
        bracket_access = Forward()

        operands = variable | scientific | integer

        # Define bracket access. Brackets following a variable may contain only integer values.
        index_list = Group(DelimitedList(integer))("bracket_indices")
        bracket_access <<= Group(variable + lbracket + index_list + rbracket)

        # The parsed expressions are assumed to follow a standard infix syntax. The operands
        # of the infix syntax can be either the literal 'operands' defined above (these are singletons),
        # or either a variadic function call or a unary function call. These latter two will be
        # defined to be recursive.
        #
        # Note that the order of the operators in the second argument (the list) of infixNotation matters!
        # The operation with the highest precedence is listed first.
        infix_expn = infix_notation(
            bracket_access | operands | variadic_call | unary_call,
            [
                (expop, 2, OpAssoc.LEFT),
                (signop, 1, OpAssoc.RIGHT),
                (multop, 2, OpAssoc.LEFT),
                (plusop, 2, OpAssoc.LEFT),
            ],
        )("binary_operator")

        # These are recursive definitions of the forward declarations of the two type of function calls.
        # In essence, the recursion continues until a singleton operand is encountered.
        variadic_call <<= Group(variadic_func_names + lparen + Group(DelimitedList(infix_expn)) + rparen)(
            "variadic_call"
        )
        unary_call <<= Group(unary_func_names + lparen + Group(infix_expn) + rparen)("unary_call")

        self.expn = infix_expn

        # The infix operations do not need to be in this list because they are handled by infixNotation() above.
        # If new binary operations are to be added, they must be updated in the infixNotation() call (the list).
        self.reserved_symbols: set[str] = symbols_unary | symbols_variadic

        # It is assumed that the dicts in the three class variables have unique keys.
        self.operator_mapping = {
            **InfixExpressionParser.BINARY_OPERATORS,
            **InfixExpressionParser.UNARY_OPERATORS,
            **InfixExpressionParser.VARIADIC_OPERATORS,
        }

        if self.target == "MathJSON":
            self.pre_parse = self._pre_parse
            self.parse_to_target = self._to_math_json
        else:
            self.pre_parse = None
            self.parse_to_target = None

    def _pre_parse(self, str_expr: str):
        return self.expn.parse_string(str_expr, parse_all=True)

    def _is_number_or_variable(self, c):
        return isinstance(c, int | float) or (isinstance(c, str) and c not in self.reserved_symbols)

    def _to_math_json(self, parsed: list | str) -> list:
        """Converts a list of expressions into a MathJSON compliant format.

        The conversion happens recursively. Each list of recursed until a terminal character is reached.
        Terminal characters are integers (int), floating point numbers (float), or non-keyword strings (str).
        Keyword strings are reserved for operations, such as 'Cos' or 'Max'.

        Args:
            parsed (list): A list possibly containing other lists. Represents a mathematical expression.

        Returns:
            list: A list representing a mathematical expression in a MathJSON compliant format.
        """
        # Directly return the input if it is an integer or a float
        if self._is_number_or_variable(parsed):
            return parsed

        # Handle bracket access
        # Assume that anything following variable in a bracket list is only integer.
        if "bracket_indices" in parsed:
            variable = parsed["variable"]
            indices = parsed["bracket_indices"]
            return ["At", variable, *indices]

        # Flatten binary operations like 1 + 2 + 3 into ["Add", 1, 2, 3]
        # Last check is to make sure that in cases like ["Max", ["x", "y", ...]] the 'y' is not confused to
        # be an operator.
        num_binary_elements = 3
        if (
            len(parsed) >= num_binary_elements
            and isinstance(parsed[1], str)
            and parsed[1] in InfixExpressionParser.BINARY_OPERATORS
        ):
            # Initialize the list to collect operands for the current operation
            operands = []

            # Check if the first operation is subtraction and handle it specially
            if parsed[1] == "-":
                current_operator = "Add"
                # Negate the operand immediately following the subtraction operator
                operands.append(self._to_math_json(parsed[0]))  # Add the first operand
                operands.append(["Negate", self._to_math_json(parsed[2])])  # Negate the second operand
                start_index = 3  # Start processing the rest of the expression from the next element
            else:
                current_operator = self.operator_mapping[parsed[1]]
                operands.append(self._to_math_json(parsed[0]))  # Add the first operand as is
                start_index = 1  # Start processing the rest of the expression from the second element

            i = start_index

            while i < len(parsed) - 1:
                op = parsed[i]

                if isinstance(parsed[i], str) and i + 2 < len(parsed) and parsed[i] == parsed[i + 2]:
                    next_operand = self._to_math_json(parsed[i + 1])  # Next operand

                    if op == "-":  # If subtraction, negate and add
                        operands.append(["Negate", next_operand])
                    else:
                        operands.append(next_operand)
                    i += 2
                else:
                    # Handle last expression, negate if needed.
                    if op == "-":
                        return [
                            [
                                current_operator,
                                *operands,
                                ["Negate", self._to_math_json(parsed[i + 1])],
                                *(self._to_math_json(parsed[(i + 1) + 2 :]) if parsed[(i + 1) + 2 :] else []),
                            ]
                        ]

                    return [[current_operator, *operands, *self._to_math_json(parsed[i + 1 :])]]

            return [[current_operator, *operands]]

        # Handle unary operations and functions
        if isinstance(parsed[0], str) and parsed[0] in self.reserved_symbols:
            if parsed[0] in self.operator_mapping:
                operator = self.operator_mapping.get(parsed[0], parsed[0])
                operands = [self._to_math_json(p) for p in parsed[1:]]

                while isinstance(operands, list) and len(operands) == 1 and isinstance(operands[0], list):
                    operands = operands[0]

                return [operator, [*operands]]

            operand = self._to_math_json(parsed[1])

            return [parsed[0], operand]

        # For lists and nested expressions
        return [self._to_math_json(part) for part in parsed]

    def _remove_extra_brackets(self, lst: list) -> list:
        """Removes recursively extra brackets from a nested list that may have been left when parsing an expression.

        Args:
            lst (list): A (nested) list that needs extra bracket removal.

        Returns:
            list: A list with extra brackets removed.
        """
        # Base case: if it's not a list, just return the item itself
        if not isinstance(lst, list):
            return lst

        # If the list has only one element and that element is a list, unpack it
        if len(lst) == 1 and (isinstance(lst[0], list) or self._is_number_or_variable(lst[0])):
            return self._remove_extra_brackets(lst[0])

        # Otherwise, process each element of the list
        return [self._remove_extra_brackets(item) for item in lst]

    def parse(self, str_expr: str) -> list:
        """The method to call when parsing an infix expression in represented by a string.

        Args:
            str_expr (str): A string expression to be parsed.

        Returns:
            list: A list representing the parsed expression.
        """
        expr = self._remove_extra_brackets(self.parse_to_target(self.pre_parse(str_expr)))

        # if the expression is just a string after removing extra brackets,
        # wrap it in a list to keep the return type consistent.
        # simple expressions, like 'x_1', are parsed into just a string after removing any extra
        # brackets, so we add them back there in case it is needed
        return expr if isinstance(expr, list) else [expr]
__init__
__init__(target='MathJSON')

A parser for infix notation, e.g., the huma readable way of notating mathematical expressions.

The parser can parse infix notation stored in a string to different formats. For instance, "Cos(2 + f_1) - 7.2 + Max(f_2, -f_3)" is first parsed to the list: ['Cos', [[2, '+', 'f_1']]], '-', 7.2, '+', ['Max', ['f_2', ['-', 'f_3']]. Then, if parsed to the MathJSON format, it will be parsed to the list: [ [["Subtract", ["Cos", ["Add", 2, "f_1"]], ["Add", 7.2, ["Max", ["f_2", ["Negate", "f_3"]]]]]] ].

Parameters:

Name Type Description Default
target str

The target format to parse an infix expression to. Currently only "MathJSON" is supported. Defaults to "MathJSON".

'MathJSON'
Source code in desdeo/problem/infix_parser.py
def __init__(self, target="MathJSON"):
    """A parser for infix notation, e.g., the huma readable way of notating mathematical expressions.

    The parser can parse infix notation stored in a string to different formats. For instance,
    "Cos(2 + f_1) - 7.2 + Max(f_2, -f_3)" is first parsed to the list:
    ['Cos', [[2, '+', 'f_1']]], '-', 7.2, '+', ['Max', ['f_2', ['-', 'f_3']]. Then, if parsed to
    the MathJSON format, it will be parsed to the list:
    [
        [["Subtract", ["Cos", ["Add", 2, "f_1"]], ["Add", 7.2, ["Max", ["f_2", ["Negate", "f_3"]]]]]]
    ].


    Args:
        target (str, optional): The target format to parse an infix expression to.
            Currently only "MathJSON" is supported. Defaults to "MathJSON".
    """
    if target not in InfixExpressionParser.SUPPORTED_TARGETS:
        msg = f"The target '{target} is not supported. Should be one of {InfixExpressionParser.SUPPORTED_TARGETS}"
        raise ValueError(msg)
    self.target = target

    # Scope limiters
    lparen = Suppress("(")
    rparen = Suppress(")")
    lbracket = Suppress("[")
    rbracket = Suppress("]")

    # Define keywords (Note that binary operators must be defined manually)
    symbols_variadic = set(InfixExpressionParser.VARIADIC_OPERATORS)
    symbols_unary = set(InfixExpressionParser.UNARY_OPERATORS)

    # Define binary operation symbols (this is the manual part)
    # If new binary operators are to be added, they must be defined here.
    signop = one_of("+ -")
    multop = one_of("* / . @")
    plusop = one_of("+ -")
    expop = Literal("**")

    # Dynamically create Keyword objects for variadric functions
    variadic_pattern = r"\b(" + f"{'|'.join([*symbols_variadic])}" + r")\b"
    variadic_func_names = Regex(variadic_pattern)

    # Dynamically create Keyword objects for unary functions
    # unary_func_names = reduce(or_, (Keyword(k) for k in symbols_unary))
    unary_pattern = r"\b(" + f"{'|'.join([*symbols_unary])}" + r")\b"
    unary_func_names = Regex(unary_pattern)

    # Define operands
    integer = pyparsing_common.integer

    # Scientific notation
    scientific = pyparsing_common.sci_real

    # Complete regex pattern with exclusions and identifier pattern
    exclude = f"{'|'.join([*symbols_variadic, *symbols_unary])}"
    pattern = r"(?!\b(" + exclude + r")\b)(\b[a-zA-Z_][a-zA-Z0-9_]*\b)"
    variable = Regex(pattern)("variable")

    # Forward declarations of variadic, unary, and bracket function calls
    variadic_call = Forward()
    unary_call = Forward()
    bracket_access = Forward()

    operands = variable | scientific | integer

    # Define bracket access. Brackets following a variable may contain only integer values.
    index_list = Group(DelimitedList(integer))("bracket_indices")
    bracket_access <<= Group(variable + lbracket + index_list + rbracket)

    # The parsed expressions are assumed to follow a standard infix syntax. The operands
    # of the infix syntax can be either the literal 'operands' defined above (these are singletons),
    # or either a variadic function call or a unary function call. These latter two will be
    # defined to be recursive.
    #
    # Note that the order of the operators in the second argument (the list) of infixNotation matters!
    # The operation with the highest precedence is listed first.
    infix_expn = infix_notation(
        bracket_access | operands | variadic_call | unary_call,
        [
            (expop, 2, OpAssoc.LEFT),
            (signop, 1, OpAssoc.RIGHT),
            (multop, 2, OpAssoc.LEFT),
            (plusop, 2, OpAssoc.LEFT),
        ],
    )("binary_operator")

    # These are recursive definitions of the forward declarations of the two type of function calls.
    # In essence, the recursion continues until a singleton operand is encountered.
    variadic_call <<= Group(variadic_func_names + lparen + Group(DelimitedList(infix_expn)) + rparen)(
        "variadic_call"
    )
    unary_call <<= Group(unary_func_names + lparen + Group(infix_expn) + rparen)("unary_call")

    self.expn = infix_expn

    # The infix operations do not need to be in this list because they are handled by infixNotation() above.
    # If new binary operations are to be added, they must be updated in the infixNotation() call (the list).
    self.reserved_symbols: set[str] = symbols_unary | symbols_variadic

    # It is assumed that the dicts in the three class variables have unique keys.
    self.operator_mapping = {
        **InfixExpressionParser.BINARY_OPERATORS,
        **InfixExpressionParser.UNARY_OPERATORS,
        **InfixExpressionParser.VARIADIC_OPERATORS,
    }

    if self.target == "MathJSON":
        self.pre_parse = self._pre_parse
        self.parse_to_target = self._to_math_json
    else:
        self.pre_parse = None
        self.parse_to_target = None
_remove_extra_brackets
_remove_extra_brackets(lst: list) -> list

Removes recursively extra brackets from a nested list that may have been left when parsing an expression.

Parameters:

Name Type Description Default
lst list

A (nested) list that needs extra bracket removal.

required

Returns:

Name Type Description
list list

A list with extra brackets removed.

Source code in desdeo/problem/infix_parser.py
def _remove_extra_brackets(self, lst: list) -> list:
    """Removes recursively extra brackets from a nested list that may have been left when parsing an expression.

    Args:
        lst (list): A (nested) list that needs extra bracket removal.

    Returns:
        list: A list with extra brackets removed.
    """
    # Base case: if it's not a list, just return the item itself
    if not isinstance(lst, list):
        return lst

    # If the list has only one element and that element is a list, unpack it
    if len(lst) == 1 and (isinstance(lst[0], list) or self._is_number_or_variable(lst[0])):
        return self._remove_extra_brackets(lst[0])

    # Otherwise, process each element of the list
    return [self._remove_extra_brackets(item) for item in lst]
_to_math_json
_to_math_json(parsed: list | str) -> list

Converts a list of expressions into a MathJSON compliant format.

The conversion happens recursively. Each list of recursed until a terminal character is reached. Terminal characters are integers (int), floating point numbers (float), or non-keyword strings (str). Keyword strings are reserved for operations, such as 'Cos' or 'Max'.

Parameters:

Name Type Description Default
parsed list

A list possibly containing other lists. Represents a mathematical expression.

required

Returns:

Name Type Description
list list

A list representing a mathematical expression in a MathJSON compliant format.

Source code in desdeo/problem/infix_parser.py
def _to_math_json(self, parsed: list | str) -> list:
    """Converts a list of expressions into a MathJSON compliant format.

    The conversion happens recursively. Each list of recursed until a terminal character is reached.
    Terminal characters are integers (int), floating point numbers (float), or non-keyword strings (str).
    Keyword strings are reserved for operations, such as 'Cos' or 'Max'.

    Args:
        parsed (list): A list possibly containing other lists. Represents a mathematical expression.

    Returns:
        list: A list representing a mathematical expression in a MathJSON compliant format.
    """
    # Directly return the input if it is an integer or a float
    if self._is_number_or_variable(parsed):
        return parsed

    # Handle bracket access
    # Assume that anything following variable in a bracket list is only integer.
    if "bracket_indices" in parsed:
        variable = parsed["variable"]
        indices = parsed["bracket_indices"]
        return ["At", variable, *indices]

    # Flatten binary operations like 1 + 2 + 3 into ["Add", 1, 2, 3]
    # Last check is to make sure that in cases like ["Max", ["x", "y", ...]] the 'y' is not confused to
    # be an operator.
    num_binary_elements = 3
    if (
        len(parsed) >= num_binary_elements
        and isinstance(parsed[1], str)
        and parsed[1] in InfixExpressionParser.BINARY_OPERATORS
    ):
        # Initialize the list to collect operands for the current operation
        operands = []

        # Check if the first operation is subtraction and handle it specially
        if parsed[1] == "-":
            current_operator = "Add"
            # Negate the operand immediately following the subtraction operator
            operands.append(self._to_math_json(parsed[0]))  # Add the first operand
            operands.append(["Negate", self._to_math_json(parsed[2])])  # Negate the second operand
            start_index = 3  # Start processing the rest of the expression from the next element
        else:
            current_operator = self.operator_mapping[parsed[1]]
            operands.append(self._to_math_json(parsed[0]))  # Add the first operand as is
            start_index = 1  # Start processing the rest of the expression from the second element

        i = start_index

        while i < len(parsed) - 1:
            op = parsed[i]

            if isinstance(parsed[i], str) and i + 2 < len(parsed) and parsed[i] == parsed[i + 2]:
                next_operand = self._to_math_json(parsed[i + 1])  # Next operand

                if op == "-":  # If subtraction, negate and add
                    operands.append(["Negate", next_operand])
                else:
                    operands.append(next_operand)
                i += 2
            else:
                # Handle last expression, negate if needed.
                if op == "-":
                    return [
                        [
                            current_operator,
                            *operands,
                            ["Negate", self._to_math_json(parsed[i + 1])],
                            *(self._to_math_json(parsed[(i + 1) + 2 :]) if parsed[(i + 1) + 2 :] else []),
                        ]
                    ]

                return [[current_operator, *operands, *self._to_math_json(parsed[i + 1 :])]]

        return [[current_operator, *operands]]

    # Handle unary operations and functions
    if isinstance(parsed[0], str) and parsed[0] in self.reserved_symbols:
        if parsed[0] in self.operator_mapping:
            operator = self.operator_mapping.get(parsed[0], parsed[0])
            operands = [self._to_math_json(p) for p in parsed[1:]]

            while isinstance(operands, list) and len(operands) == 1 and isinstance(operands[0], list):
                operands = operands[0]

            return [operator, [*operands]]

        operand = self._to_math_json(parsed[1])

        return [parsed[0], operand]

    # For lists and nested expressions
    return [self._to_math_json(part) for part in parsed]
parse
parse(str_expr: str) -> list

The method to call when parsing an infix expression in represented by a string.

Parameters:

Name Type Description Default
str_expr str

A string expression to be parsed.

required

Returns:

Name Type Description
list list

A list representing the parsed expression.

Source code in desdeo/problem/infix_parser.py
def parse(self, str_expr: str) -> list:
    """The method to call when parsing an infix expression in represented by a string.

    Args:
        str_expr (str): A string expression to be parsed.

    Returns:
        list: A list representing the parsed expression.
    """
    expr = self._remove_extra_brackets(self.parse_to_target(self.pre_parse(str_expr)))

    # if the expression is just a string after removing extra brackets,
    # wrap it in a list to keep the return type consistent.
    # simple expressions, like 'x_1', are parsed into just a string after removing any extra
    # brackets, so we add them back there in case it is needed
    return expr if isinstance(expr, list) else [expr]

Generic evaluator

desdeo.problem.evaluator

Defines a Polars-based evaluator.

PolarsEvaluator

A class for creating Polars-based evaluators for multiobjective optimization problems.

The evaluator is to be used with different optimizers. PolarsEvaluator is specifically for solvers that do not require an exact formulation of the problem, but rather work solely on the input and output values of the problem being solved. This evaluator might not be suitable for computationally expensive problems, or mixed-integer problems. This evaluator is suitable for many Python-based solvers.

Source code in desdeo/problem/evaluator.py
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
class PolarsEvaluator:
    """A class for creating Polars-based evaluators for multiobjective optimization problems.

    The evaluator is to be used with different optimizers. PolarsEvaluator is specifically
    for solvers that do not require an exact formulation of the problem, but rather work
    solely on the input and output values of the problem being solved. This evaluator might not
    be suitable for computationally expensive problems, or mixed-integer problems. This
    evaluator is suitable for many Python-based solvers.
    """

    ### Initialization (no need for decision variables yet)
    # 1. Create a math parser with parser type 'evaluator_type'. Defaults to 'polars'.
    # 2. Check for any constants in the definition of the problem. Replace the constants, if they exist,
    #    with their numerical values in all the function expressions found in problem.
    # 3. Parse the function expressions into a dataframe.

    ### Evaluating (we have decision variables to evaluate problem)
    # 1. Evaluate the extra functions (if any) in the dataframe with the decision variables. Store the results
    #    in new columns of the dataframe.
    # 2. Evaluate the objective functions based on the decision variables and the extra function values (if any).
    #    Store the results in the dataframe in their own columns.
    # 3. Evaluate the constraints (if any) based on the decision variables and extra function values (if any).
    #    Store the results in the dataframe in their own columns.
    # 4. Evalute the scalarization functions (if any) based on the objective function values and extra function values
    #    (if any). Store the results in the dataframe in their own columns.
    # 5. Return a pydantic dataclass with the results (decision variables, objective function values, constraint values,
    #    and scalarization function valeus).
    # 6. End.

    def __init__(self, problem: Problem, evaluator_mode: PolarsEvaluatorModesEnum = PolarsEvaluatorModesEnum.variables):
        """Create a Polars-based evaluator for a multiobjective optimization problem.

        By default, the evaluator expects a set of decision variables to
        evaluate the given problem.  However, if the problem is purely based on
        data (e.g., it represents an approximation of a Pareto optimal front),
        then the evaluator should be run in 'discrete' mode instead. In this
        mode, it will return the whole problem with all of its objectives,
        constraints, and scalarization functions evaluated with the current data
        representing the problem.

        Args:
            problem (Problem): The problem as a pydantic 'Problem' data class.
            evaluator_mode (str): The mode of evaluator used to parse the problem into a format
                that can be evaluated. Default 'variables'.
        """
        # Create a MathParser of type 'evaluator_type'.
        if evaluator_mode not in PolarsEvaluatorModesEnum:
            msg = (
                f"The provided 'evaluator_mode' '{evaluator_mode}' is not supported."
                f" Must be one of {PolarsEvaluatorModesEnum}."
            )
            raise PolarsEvaluatorError(msg)

        self.evaluator_mode = evaluator_mode

        self.problem = problem
        # Gather any constants of the problem definition.
        self.problem_constants = problem.constants
        # Gather the objective functions
        if evaluator_mode == PolarsEvaluatorModesEnum.mixed:
            self.problem_objectives = [
                objective
                for objective in problem.objectives
                if objective.objective_type in [ObjectiveTypeEnum.analytical, ObjectiveTypeEnum.data_based]
            ]
        else:
            self.problem_objectives = problem.objectives
        # Gather any constraints
        self.problem_constraints = problem.constraints
        # Gather any extra functions
        self.problem_extra = problem.extra_funcs
        # Gather any scalarization functions
        self.problem_scalarization = problem.scalarization_funcs
        # Gather the decision variable symbols defined in the problem
        self.problem_variable_symbols = [var.symbol for var in problem.variables]
        # The discrete definition of (some) objectives
        self.discrete_representation = problem.discrete_representation

        # The below 'expressions' are list of tuples with symbol and expressions pairs, as (symbol, expression)
        # These must be defined in a specialized initialization step, see further below for an example.
        # Symbol and expressions pairs of the objective functions
        self.objective_expressions = None
        # Symbol and expressions pairs of any constraints
        self.constraint_expressions = None
        # Symbol and expressions pairs of any extra functions
        self.extra_expressions = None
        # Symbol and expression pairs of any scalarization functions
        self.scalarization_expressions = None
        # Store TensorConstants in a dict
        self.tensor_constants = None

        # Note: `self.parser` is assumed to be set before continuing the initialization.
        self.parser = MathParser()
        self._polars_init()

        # Note, when calling an evaluate method, it is assumed the problem has been fully parsed.
        if self.evaluator_mode in [PolarsEvaluatorModesEnum.variables, PolarsEvaluatorModesEnum.mixed]:
            self.evaluate = self._polars_evaluate
            self.evaluate_flat = self._polars_evaluate_flat
        elif self.evaluator_mode == PolarsEvaluatorModesEnum.discrete:
            self.evaluate = self._from_discrete_data
        else:
            msg = (
                f"Provided 'evaluator_mode' {evaluator_mode} not supported. Must be one of {PolarsEvaluatorModesEnum}."
            )

    def _polars_init(self):  # noqa: C901
        """Initialization of the evaluator for parser type 'polars'."""
        # If any constants are defined in problem, replace their symbol with the defined numerical
        # value in all the function expressions found in the Problem.
        if self.problem_constants is not None:
            # Objectives are always defined, cannot be None
            parsed_obj_funcs = {}
            for obj in self.problem_objectives:
                if obj.objective_type == ObjectiveTypeEnum.analytical:
                    # if analytical proceed with replacing the symbols.
                    tmp = obj.func

                    # replace regular constants, skip TensorConstants
                    for c in self.problem_constants:
                        if isinstance(c, Constant):
                            tmp = replace_str(tmp, c.symbol, c.value)

                    parsed_obj_funcs[f"{obj.symbol}"] = tmp

                elif obj.objective_type == ObjectiveTypeEnum.data_based:
                    # data-based objective
                    parsed_obj_funcs[f"{obj.symbol}"] = None
                else:
                    msg = (
                        f"Incorrect objective-type {obj.objective_type} encountered. Must be one of {ObjectiveTypeEnum}"
                    )
                    raise PolarsEvaluatorError(msg)

            # Do the same for any constraint expressions as well.
            if self.problem_constraints is not None:
                parsed_cons_funcs: dict | None = {}
                for con in self.problem_constraints:
                    tmp = con.func

                    # replace regular constants, skip TensorConstants
                    for c in self.problem_constants:
                        if isinstance(c, Constant):
                            tmp = replace_str(tmp, c.symbol, c.value)

                    parsed_cons_funcs[f"{con.symbol}"] = tmp
            else:
                parsed_cons_funcs = None

            # Do the same for any extra functions
            parsed_extra_funcs: dict | None = {}
            if self.problem_extra is not None:
                for extra in self.problem_extra:
                    tmp = extra.func

                    # replace regular constants, skip TensorConstants
                    for c in self.problem_constants:
                        if isinstance(c, Constant):
                            tmp = replace_str(tmp, c.symbol, c.value)

                    parsed_extra_funcs[f"{extra.symbol}"] = tmp
            else:
                parsed_extra_funcs = None

            # Do the same for any scalarization functions
            parsed_scal_funcs: dict | None = {}
            if self.problem_scalarization is not None:
                for scal in self.problem_scalarization:
                    tmp = scal.func

                    # replace regular constants, skip TensorConstants
                    for c in self.problem_constants:
                        if isinstance(c, Constant):
                            tmp = replace_str(tmp, c.symbol, c.value)

                    parsed_scal_funcs[f"{scal.symbol}"] = tmp
            else:
                parsed_scal_funcs = None

            # Check for TensorConstants
            for c in self.problem_constants:
                if isinstance(c, TensorConstant):
                    if self.tensor_constants is None:
                        self.tensor_constants = {}
                    self.tensor_constants[c.symbol] = np.array(c.get_values())
        else:
            # no constants defined, just collect all expressions as they are
            parsed_obj_funcs = {f"{objective.symbol}": objective.func for objective in self.problem_objectives}

            if self.problem_constraints is not None:
                parsed_cons_funcs = {f"{constraint.symbol}": constraint.func for constraint in self.problem_constraints}
            else:
                parsed_cons_funcs = None

            if self.problem_extra is not None:
                parsed_extra_funcs = {f"{extra.symbol}": extra.func for extra in self.problem_extra}
            else:
                parsed_extra_funcs = None

            if self.problem_scalarization is not None:
                parsed_scal_funcs = {f"{scal.symbol}": scal.func for scal in self.problem_scalarization}
            else:
                parsed_scal_funcs = None

        # Parse all functions into expressions. These are stored as tuples, as (symbol, parsed expression)
        # parse objectives
        # If no expression is given (data-based objective, then the expression is set to be 'None')
        self.objective_expressions = [
            (symbol, self.parser.parse(expression)) if expression is not None else (symbol, None)
            for symbol, expression in parsed_obj_funcs.items()
        ]

        # parse constraints, if any
        # if a constraint is simulator or surrogate based (expression is None), set the "parsed" expression as None
        if parsed_cons_funcs is not None:
            self.constraint_expressions = [
                (symbol, self.parser.parse(expression)) if expression is not None else (symbol, None)
                for symbol, expression in parsed_cons_funcs.items()
            ]
        else:
            self.constraint_expressions = None

        # parse extra functions, if any
        # if an extra function is simulator or surrogate based (expression is None), set the "parsed" expression as None
        if parsed_extra_funcs is not None:
            self.extra_expressions = [
                (symbol, self.parser.parse(expression)) if expression is not None else (symbol, None)
                for symbol, expression in parsed_extra_funcs.items()
            ]
        else:
            self.extra_expressions = None

        # parse scalarization functions, if any
        if parsed_scal_funcs is not None:
            self.scalarization_expressions = [
                (symbol, self.parser.parse(expression)) for symbol, expression in parsed_scal_funcs.items()
            ]
        else:
            self.scalarization_expressions = None

        # store the symbol and min or max multiplier as well (symbol, min/max multiplier [1 | -1])
        self.objective_mix_max_mult = [
            (objective.symbol, -1 if objective.maximize else 1) for objective in self.problem_objectives
        ]

        # create dataframe with the discrete representation, if any exists
        if self.discrete_representation is not None:
            self.discrete_df = pl.DataFrame(
                {**self.discrete_representation.variable_values, **self.discrete_representation.objective_values}
            )
        else:
            self.discrete_df = None

    def _polars_evaluate(
        self,
        xs: pl.DataFrame | dict[str, list[float | int | bool]],
    ) -> pl.DataFrame:
        """Evaluate the problem with the given decision variable values utilizing a polars dataframe.

        Args:
            xs (pl.DataFrame | dict[str, list[float | int | bool]]): a Polars dataframe or
                dict with the decision variable symbols as the columns (keys)
                followed by the corresponding decision variable values stored in
                an array (list). The symbols must match the symbols defined for
                the decision variables defined in the `Problem` being solved.
                Each column (list) in the dataframe (dict) should contain the same number of values.

        Returns:
            pl.DataFrame: the polars dataframe with the computed results.

        Note:
            At least `self.objective_expressions` must be defined before calling this method.
        """
        # An aggregate dataframe to store intermediate evaluation results.
        # agg_df = pl.DataFrame({key: np.array(value) for key, value in xs.items()})
        agg_df = pl.DataFrame(
            xs,
            schema=[
                (var.symbol, pl.Float64 if isinstance(var, Variable) else pl.Array(pl.Float64, tuple(var.shape)))
                for var in self.problem.variables
            ],
        )  # need to make sure to provide schema for tensor variables of type Array

        # Deal with TensorConstant
        # agg_df.with_columns(pl.Series(np.array(2*[self.tensor_constants["W"]])).alias("W"))
        if self.tensor_constants is not None:
            for tc_symbol in self.tensor_constants:
                agg_df = agg_df.with_columns(
                    pl.Series(np.array(agg_df.height * [self.tensor_constants[tc_symbol]])).alias(tc_symbol)
                )

        # Evaluate any extra functions and put the results in the aggregate dataframe.
        # If an extra function is simulator or surrogate based (expression None), skip it here
        if self.extra_expressions is not None:
            for symbol, expr in self.extra_expressions:
                if expr is not None:
                    # expression given
                    extra_column = agg_df.select(expr.alias(symbol))
                    agg_df = agg_df.hstack(extra_column)

        # Evaluate the objective functions and put the results in the aggregate dataframe.
        # obj_columns = agg_df.select(*[expr.alias(symbol) for symbol, expr in self.objective_expressions])
        # agg_df = agg_df.hstack(obj_columns)

        for symbol, expr in self.objective_expressions:
            if expr is not None:
                # expression given
                obj_col = agg_df.select(expr.alias(symbol))
                agg_df = agg_df.hstack(obj_col)
            # elif self.evaluator_mode != PolarsEvaluatorModesEnum.mixed:
            else:
                # expr is None and there are no no simulator or surrogate based objectives,
                # therefore we must get the objective function's value somehow else, usually from data
                obj_col = find_closest_points(agg_df, self.discrete_df, self.problem_variable_symbols, symbol)
                agg_df = agg_df.hstack(obj_col)

        # Evaluate the minimization form of the objective functions
        # Note that the column name of these should be 'the objective function's symbol'_min
        # e.g., 'f_1' -> 'f_1_min'
        min_obj_columns = agg_df.select(
            *[
                (min_max_mult * pl.col(f"{symbol}")).alias(f"{symbol}_min")
                for symbol, min_max_mult in self.objective_mix_max_mult
            ]
        )
        agg_df = agg_df.hstack(min_obj_columns)

        # Evaluate any constraints and put the results in the aggregate dataframe
        # If a constraint is simulator or surrogate based (expression None), skip it here
        if self.constraint_expressions is not None:
            for symbol, expr in self.constraint_expressions:
                if expr is not None:
                    # expression given
                    cons_columns = agg_df.select(expr.alias(symbol))
                    agg_df = agg_df.hstack(cons_columns)

        # Evaluate any scalarization functions and put the result in the aggregate dataframe
        if self.scalarization_expressions is not None:
            scal_columns = agg_df.select(*[expr.alias(symbol) for symbol, expr in self.scalarization_expressions])
            agg_df = agg_df.hstack(scal_columns)

        # return the dataframe and let the solver figure it out
        return agg_df

    def _polars_evaluate_flat(
        self,
        xs: pl.DataFrame | dict[str, list[float | int | bool]],
    ) -> pl.DataFrame:
        """Evaluate the problem with flattened variables.

        Args:
            xs (pl.DataFrame | dict[str, list[float  |  int  |  bool]]): a polars dataframe
                or dict with flattened variables.
                E.g., if the original problem has a tensor variable 'X' with shape (2,2),
                then the input is expected to have entries with columns (keys) 'X_1_1', 'X_1_2',
                'X_2_1', and 'X_2_2'. The input is rebuilt and passed to
                `self._evaluate`.

        Note:
            Each flattened variable is assumed to contain the same number of samples.
                This means that if the entry 'X_1_1' of `xs` is, for example
                `[1,2,3]`, this means that 'X_1_1' and all the other flattened
                variables have three samples. This means also that the original
                problem will be evaluated with a tensor variable with shape (2,2)
                and three samples,
                e.g., 'X=[[[1, 1], [1,1]], [[2, 2], [2, 2]], [[3, 3], [3, 3]]]'.

        Returns:
            pl.DataFrame: a dataframe with the original problem's evaluated functions.
        """
        # Assume all variables have the same number of samples
        if isinstance(xs, dict):
            xs = pl.DataFrame(xs)

        unflattened_xs = pl.DataFrame()

        # iterate over the variables of the problem
        for var in self.problem.variables:
            if isinstance(var, TensorVariable):
                # construct the tensor variable

                unflattened_xs = unflattened_xs.with_columns(
                    xs.select(pl.concat_arr(f"^{var.symbol}_.*$").alias(var.symbol).reshape((1, *var.shape)))
                )

            else:
                # else, proceed normally
                unflattened_xs = unflattened_xs.with_columns(xs[var.symbol])

        # return result of regular evaluate
        return self.evaluate(unflattened_xs)

    def _from_discrete_data(self) -> pl.DataFrame:
        """Evaluates the problem based on its discrete representation only.

        Assumes that all the objective functions in the problem are of type 'data-based'.
        In this case, the problem is evaluated based on its current discrete representation. Therefore,
        no decision variable values are expected.

        Returns:
            pl.DataFrame: a polars dataframe with the evaluation results.
        """
        agg_df = self.discrete_df.clone()

        # Evaluate any extra functions and put the results in the aggregate dataframe.
        if self.extra_expressions is not None:
            extra_columns = agg_df.select(*[expr.alias(symbol) for symbol, expr in self.extra_expressions])
            agg_df = agg_df.hstack(extra_columns)

        # Evaluate the minimization form of the objective functions
        # Note that the column name of these should be 'the objective function's symbol'_min
        # e.g., 'f_1' -> 'f_1_min'
        min_obj_columns = agg_df.select(
            *[
                (min_max_mult * pl.col(f"{symbol}")).alias(f"{symbol}_min")
                for symbol, min_max_mult in self.objective_mix_max_mult
            ]
        )

        agg_df = agg_df.hstack(min_obj_columns)

        # Evaluate any constraints and put the results in the aggregate dataframe
        if self.constraint_expressions is not None:
            cons_columns = agg_df.select(*[expr.alias(symbol) for symbol, expr in self.constraint_expressions])
            agg_df = agg_df.hstack(cons_columns)

        # Evaluate any scalarization functions and put the result in the aggregate dataframe
        if self.scalarization_expressions is not None:
            scal_columns = agg_df.select(*[expr.alias(symbol) for symbol, expr in self.scalarization_expressions])
            agg_df = agg_df.hstack(scal_columns)

        # no more processing needed, it is assumed a solver will handle the rest
        return agg_df
__init__
__init__(
    problem: Problem,
    evaluator_mode: PolarsEvaluatorModesEnum = PolarsEvaluatorModesEnum.variables,
)

Create a Polars-based evaluator for a multiobjective optimization problem.

By default, the evaluator expects a set of decision variables to evaluate the given problem. However, if the problem is purely based on data (e.g., it represents an approximation of a Pareto optimal front), then the evaluator should be run in 'discrete' mode instead. In this mode, it will return the whole problem with all of its objectives, constraints, and scalarization functions evaluated with the current data representing the problem.

Parameters:

Name Type Description Default
problem Problem

The problem as a pydantic 'Problem' data class.

required
evaluator_mode str

The mode of evaluator used to parse the problem into a format that can be evaluated. Default 'variables'.

variables
Source code in desdeo/problem/evaluator.py
def __init__(self, problem: Problem, evaluator_mode: PolarsEvaluatorModesEnum = PolarsEvaluatorModesEnum.variables):
    """Create a Polars-based evaluator for a multiobjective optimization problem.

    By default, the evaluator expects a set of decision variables to
    evaluate the given problem.  However, if the problem is purely based on
    data (e.g., it represents an approximation of a Pareto optimal front),
    then the evaluator should be run in 'discrete' mode instead. In this
    mode, it will return the whole problem with all of its objectives,
    constraints, and scalarization functions evaluated with the current data
    representing the problem.

    Args:
        problem (Problem): The problem as a pydantic 'Problem' data class.
        evaluator_mode (str): The mode of evaluator used to parse the problem into a format
            that can be evaluated. Default 'variables'.
    """
    # Create a MathParser of type 'evaluator_type'.
    if evaluator_mode not in PolarsEvaluatorModesEnum:
        msg = (
            f"The provided 'evaluator_mode' '{evaluator_mode}' is not supported."
            f" Must be one of {PolarsEvaluatorModesEnum}."
        )
        raise PolarsEvaluatorError(msg)

    self.evaluator_mode = evaluator_mode

    self.problem = problem
    # Gather any constants of the problem definition.
    self.problem_constants = problem.constants
    # Gather the objective functions
    if evaluator_mode == PolarsEvaluatorModesEnum.mixed:
        self.problem_objectives = [
            objective
            for objective in problem.objectives
            if objective.objective_type in [ObjectiveTypeEnum.analytical, ObjectiveTypeEnum.data_based]
        ]
    else:
        self.problem_objectives = problem.objectives
    # Gather any constraints
    self.problem_constraints = problem.constraints
    # Gather any extra functions
    self.problem_extra = problem.extra_funcs
    # Gather any scalarization functions
    self.problem_scalarization = problem.scalarization_funcs
    # Gather the decision variable symbols defined in the problem
    self.problem_variable_symbols = [var.symbol for var in problem.variables]
    # The discrete definition of (some) objectives
    self.discrete_representation = problem.discrete_representation

    # The below 'expressions' are list of tuples with symbol and expressions pairs, as (symbol, expression)
    # These must be defined in a specialized initialization step, see further below for an example.
    # Symbol and expressions pairs of the objective functions
    self.objective_expressions = None
    # Symbol and expressions pairs of any constraints
    self.constraint_expressions = None
    # Symbol and expressions pairs of any extra functions
    self.extra_expressions = None
    # Symbol and expression pairs of any scalarization functions
    self.scalarization_expressions = None
    # Store TensorConstants in a dict
    self.tensor_constants = None

    # Note: `self.parser` is assumed to be set before continuing the initialization.
    self.parser = MathParser()
    self._polars_init()

    # Note, when calling an evaluate method, it is assumed the problem has been fully parsed.
    if self.evaluator_mode in [PolarsEvaluatorModesEnum.variables, PolarsEvaluatorModesEnum.mixed]:
        self.evaluate = self._polars_evaluate
        self.evaluate_flat = self._polars_evaluate_flat
    elif self.evaluator_mode == PolarsEvaluatorModesEnum.discrete:
        self.evaluate = self._from_discrete_data
    else:
        msg = (
            f"Provided 'evaluator_mode' {evaluator_mode} not supported. Must be one of {PolarsEvaluatorModesEnum}."
        )
_from_discrete_data
_from_discrete_data() -> pl.DataFrame

Evaluates the problem based on its discrete representation only.

Assumes that all the objective functions in the problem are of type 'data-based'. In this case, the problem is evaluated based on its current discrete representation. Therefore, no decision variable values are expected.

Returns:

Type Description
DataFrame

pl.DataFrame: a polars dataframe with the evaluation results.

Source code in desdeo/problem/evaluator.py
def _from_discrete_data(self) -> pl.DataFrame:
    """Evaluates the problem based on its discrete representation only.

    Assumes that all the objective functions in the problem are of type 'data-based'.
    In this case, the problem is evaluated based on its current discrete representation. Therefore,
    no decision variable values are expected.

    Returns:
        pl.DataFrame: a polars dataframe with the evaluation results.
    """
    agg_df = self.discrete_df.clone()

    # Evaluate any extra functions and put the results in the aggregate dataframe.
    if self.extra_expressions is not None:
        extra_columns = agg_df.select(*[expr.alias(symbol) for symbol, expr in self.extra_expressions])
        agg_df = agg_df.hstack(extra_columns)

    # Evaluate the minimization form of the objective functions
    # Note that the column name of these should be 'the objective function's symbol'_min
    # e.g., 'f_1' -> 'f_1_min'
    min_obj_columns = agg_df.select(
        *[
            (min_max_mult * pl.col(f"{symbol}")).alias(f"{symbol}_min")
            for symbol, min_max_mult in self.objective_mix_max_mult
        ]
    )

    agg_df = agg_df.hstack(min_obj_columns)

    # Evaluate any constraints and put the results in the aggregate dataframe
    if self.constraint_expressions is not None:
        cons_columns = agg_df.select(*[expr.alias(symbol) for symbol, expr in self.constraint_expressions])
        agg_df = agg_df.hstack(cons_columns)

    # Evaluate any scalarization functions and put the result in the aggregate dataframe
    if self.scalarization_expressions is not None:
        scal_columns = agg_df.select(*[expr.alias(symbol) for symbol, expr in self.scalarization_expressions])
        agg_df = agg_df.hstack(scal_columns)

    # no more processing needed, it is assumed a solver will handle the rest
    return agg_df
_polars_evaluate
_polars_evaluate(
    xs: DataFrame | dict[str, list[float | int | bool]],
) -> pl.DataFrame

Evaluate the problem with the given decision variable values utilizing a polars dataframe.

Parameters:

Name Type Description Default
xs DataFrame | dict[str, list[float | int | bool]]

a Polars dataframe or dict with the decision variable symbols as the columns (keys) followed by the corresponding decision variable values stored in an array (list). The symbols must match the symbols defined for the decision variables defined in the Problem being solved. Each column (list) in the dataframe (dict) should contain the same number of values.

required

Returns:

Type Description
DataFrame

pl.DataFrame: the polars dataframe with the computed results.

Note

At least self.objective_expressions must be defined before calling this method.

Source code in desdeo/problem/evaluator.py
def _polars_evaluate(
    self,
    xs: pl.DataFrame | dict[str, list[float | int | bool]],
) -> pl.DataFrame:
    """Evaluate the problem with the given decision variable values utilizing a polars dataframe.

    Args:
        xs (pl.DataFrame | dict[str, list[float | int | bool]]): a Polars dataframe or
            dict with the decision variable symbols as the columns (keys)
            followed by the corresponding decision variable values stored in
            an array (list). The symbols must match the symbols defined for
            the decision variables defined in the `Problem` being solved.
            Each column (list) in the dataframe (dict) should contain the same number of values.

    Returns:
        pl.DataFrame: the polars dataframe with the computed results.

    Note:
        At least `self.objective_expressions` must be defined before calling this method.
    """
    # An aggregate dataframe to store intermediate evaluation results.
    # agg_df = pl.DataFrame({key: np.array(value) for key, value in xs.items()})
    agg_df = pl.DataFrame(
        xs,
        schema=[
            (var.symbol, pl.Float64 if isinstance(var, Variable) else pl.Array(pl.Float64, tuple(var.shape)))
            for var in self.problem.variables
        ],
    )  # need to make sure to provide schema for tensor variables of type Array

    # Deal with TensorConstant
    # agg_df.with_columns(pl.Series(np.array(2*[self.tensor_constants["W"]])).alias("W"))
    if self.tensor_constants is not None:
        for tc_symbol in self.tensor_constants:
            agg_df = agg_df.with_columns(
                pl.Series(np.array(agg_df.height * [self.tensor_constants[tc_symbol]])).alias(tc_symbol)
            )

    # Evaluate any extra functions and put the results in the aggregate dataframe.
    # If an extra function is simulator or surrogate based (expression None), skip it here
    if self.extra_expressions is not None:
        for symbol, expr in self.extra_expressions:
            if expr is not None:
                # expression given
                extra_column = agg_df.select(expr.alias(symbol))
                agg_df = agg_df.hstack(extra_column)

    # Evaluate the objective functions and put the results in the aggregate dataframe.
    # obj_columns = agg_df.select(*[expr.alias(symbol) for symbol, expr in self.objective_expressions])
    # agg_df = agg_df.hstack(obj_columns)

    for symbol, expr in self.objective_expressions:
        if expr is not None:
            # expression given
            obj_col = agg_df.select(expr.alias(symbol))
            agg_df = agg_df.hstack(obj_col)
        # elif self.evaluator_mode != PolarsEvaluatorModesEnum.mixed:
        else:
            # expr is None and there are no no simulator or surrogate based objectives,
            # therefore we must get the objective function's value somehow else, usually from data
            obj_col = find_closest_points(agg_df, self.discrete_df, self.problem_variable_symbols, symbol)
            agg_df = agg_df.hstack(obj_col)

    # Evaluate the minimization form of the objective functions
    # Note that the column name of these should be 'the objective function's symbol'_min
    # e.g., 'f_1' -> 'f_1_min'
    min_obj_columns = agg_df.select(
        *[
            (min_max_mult * pl.col(f"{symbol}")).alias(f"{symbol}_min")
            for symbol, min_max_mult in self.objective_mix_max_mult
        ]
    )
    agg_df = agg_df.hstack(min_obj_columns)

    # Evaluate any constraints and put the results in the aggregate dataframe
    # If a constraint is simulator or surrogate based (expression None), skip it here
    if self.constraint_expressions is not None:
        for symbol, expr in self.constraint_expressions:
            if expr is not None:
                # expression given
                cons_columns = agg_df.select(expr.alias(symbol))
                agg_df = agg_df.hstack(cons_columns)

    # Evaluate any scalarization functions and put the result in the aggregate dataframe
    if self.scalarization_expressions is not None:
        scal_columns = agg_df.select(*[expr.alias(symbol) for symbol, expr in self.scalarization_expressions])
        agg_df = agg_df.hstack(scal_columns)

    # return the dataframe and let the solver figure it out
    return agg_df
_polars_evaluate_flat
_polars_evaluate_flat(
    xs: DataFrame | dict[str, list[float | int | bool]],
) -> pl.DataFrame

Evaluate the problem with flattened variables.

Parameters:

Name Type Description Default
xs DataFrame | dict[str, list[float | int | bool]]

a polars dataframe or dict with flattened variables. E.g., if the original problem has a tensor variable 'X' with shape (2,2), then the input is expected to have entries with columns (keys) 'X_1_1', 'X_1_2', 'X_2_1', and 'X_2_2'. The input is rebuilt and passed to self._evaluate.

required
Note

Each flattened variable is assumed to contain the same number of samples. This means that if the entry 'X_1_1' of xs is, for example [1,2,3], this means that 'X_1_1' and all the other flattened variables have three samples. This means also that the original problem will be evaluated with a tensor variable with shape (2,2) and three samples, e.g., 'X=[[[1, 1], [1,1]], [[2, 2], [2, 2]], [[3, 3], [3, 3]]]'.

Returns:

Type Description
DataFrame

pl.DataFrame: a dataframe with the original problem's evaluated functions.

Source code in desdeo/problem/evaluator.py
def _polars_evaluate_flat(
    self,
    xs: pl.DataFrame | dict[str, list[float | int | bool]],
) -> pl.DataFrame:
    """Evaluate the problem with flattened variables.

    Args:
        xs (pl.DataFrame | dict[str, list[float  |  int  |  bool]]): a polars dataframe
            or dict with flattened variables.
            E.g., if the original problem has a tensor variable 'X' with shape (2,2),
            then the input is expected to have entries with columns (keys) 'X_1_1', 'X_1_2',
            'X_2_1', and 'X_2_2'. The input is rebuilt and passed to
            `self._evaluate`.

    Note:
        Each flattened variable is assumed to contain the same number of samples.
            This means that if the entry 'X_1_1' of `xs` is, for example
            `[1,2,3]`, this means that 'X_1_1' and all the other flattened
            variables have three samples. This means also that the original
            problem will be evaluated with a tensor variable with shape (2,2)
            and three samples,
            e.g., 'X=[[[1, 1], [1,1]], [[2, 2], [2, 2]], [[3, 3], [3, 3]]]'.

    Returns:
        pl.DataFrame: a dataframe with the original problem's evaluated functions.
    """
    # Assume all variables have the same number of samples
    if isinstance(xs, dict):
        xs = pl.DataFrame(xs)

    unflattened_xs = pl.DataFrame()

    # iterate over the variables of the problem
    for var in self.problem.variables:
        if isinstance(var, TensorVariable):
            # construct the tensor variable

            unflattened_xs = unflattened_xs.with_columns(
                xs.select(pl.concat_arr(f"^{var.symbol}_.*$").alias(var.symbol).reshape((1, *var.shape)))
            )

        else:
            # else, proceed normally
            unflattened_xs = unflattened_xs.with_columns(xs[var.symbol])

    # return result of regular evaluate
    return self.evaluate(unflattened_xs)
_polars_init
_polars_init()

Initialization of the evaluator for parser type 'polars'.

Source code in desdeo/problem/evaluator.py
def _polars_init(self):  # noqa: C901
    """Initialization of the evaluator for parser type 'polars'."""
    # If any constants are defined in problem, replace their symbol with the defined numerical
    # value in all the function expressions found in the Problem.
    if self.problem_constants is not None:
        # Objectives are always defined, cannot be None
        parsed_obj_funcs = {}
        for obj in self.problem_objectives:
            if obj.objective_type == ObjectiveTypeEnum.analytical:
                # if analytical proceed with replacing the symbols.
                tmp = obj.func

                # replace regular constants, skip TensorConstants
                for c in self.problem_constants:
                    if isinstance(c, Constant):
                        tmp = replace_str(tmp, c.symbol, c.value)

                parsed_obj_funcs[f"{obj.symbol}"] = tmp

            elif obj.objective_type == ObjectiveTypeEnum.data_based:
                # data-based objective
                parsed_obj_funcs[f"{obj.symbol}"] = None
            else:
                msg = (
                    f"Incorrect objective-type {obj.objective_type} encountered. Must be one of {ObjectiveTypeEnum}"
                )
                raise PolarsEvaluatorError(msg)

        # Do the same for any constraint expressions as well.
        if self.problem_constraints is not None:
            parsed_cons_funcs: dict | None = {}
            for con in self.problem_constraints:
                tmp = con.func

                # replace regular constants, skip TensorConstants
                for c in self.problem_constants:
                    if isinstance(c, Constant):
                        tmp = replace_str(tmp, c.symbol, c.value)

                parsed_cons_funcs[f"{con.symbol}"] = tmp
        else:
            parsed_cons_funcs = None

        # Do the same for any extra functions
        parsed_extra_funcs: dict | None = {}
        if self.problem_extra is not None:
            for extra in self.problem_extra:
                tmp = extra.func

                # replace regular constants, skip TensorConstants
                for c in self.problem_constants:
                    if isinstance(c, Constant):
                        tmp = replace_str(tmp, c.symbol, c.value)

                parsed_extra_funcs[f"{extra.symbol}"] = tmp
        else:
            parsed_extra_funcs = None

        # Do the same for any scalarization functions
        parsed_scal_funcs: dict | None = {}
        if self.problem_scalarization is not None:
            for scal in self.problem_scalarization:
                tmp = scal.func

                # replace regular constants, skip TensorConstants
                for c in self.problem_constants:
                    if isinstance(c, Constant):
                        tmp = replace_str(tmp, c.symbol, c.value)

                parsed_scal_funcs[f"{scal.symbol}"] = tmp
        else:
            parsed_scal_funcs = None

        # Check for TensorConstants
        for c in self.problem_constants:
            if isinstance(c, TensorConstant):
                if self.tensor_constants is None:
                    self.tensor_constants = {}
                self.tensor_constants[c.symbol] = np.array(c.get_values())
    else:
        # no constants defined, just collect all expressions as they are
        parsed_obj_funcs = {f"{objective.symbol}": objective.func for objective in self.problem_objectives}

        if self.problem_constraints is not None:
            parsed_cons_funcs = {f"{constraint.symbol}": constraint.func for constraint in self.problem_constraints}
        else:
            parsed_cons_funcs = None

        if self.problem_extra is not None:
            parsed_extra_funcs = {f"{extra.symbol}": extra.func for extra in self.problem_extra}
        else:
            parsed_extra_funcs = None

        if self.problem_scalarization is not None:
            parsed_scal_funcs = {f"{scal.symbol}": scal.func for scal in self.problem_scalarization}
        else:
            parsed_scal_funcs = None

    # Parse all functions into expressions. These are stored as tuples, as (symbol, parsed expression)
    # parse objectives
    # If no expression is given (data-based objective, then the expression is set to be 'None')
    self.objective_expressions = [
        (symbol, self.parser.parse(expression)) if expression is not None else (symbol, None)
        for symbol, expression in parsed_obj_funcs.items()
    ]

    # parse constraints, if any
    # if a constraint is simulator or surrogate based (expression is None), set the "parsed" expression as None
    if parsed_cons_funcs is not None:
        self.constraint_expressions = [
            (symbol, self.parser.parse(expression)) if expression is not None else (symbol, None)
            for symbol, expression in parsed_cons_funcs.items()
        ]
    else:
        self.constraint_expressions = None

    # parse extra functions, if any
    # if an extra function is simulator or surrogate based (expression is None), set the "parsed" expression as None
    if parsed_extra_funcs is not None:
        self.extra_expressions = [
            (symbol, self.parser.parse(expression)) if expression is not None else (symbol, None)
            for symbol, expression in parsed_extra_funcs.items()
        ]
    else:
        self.extra_expressions = None

    # parse scalarization functions, if any
    if parsed_scal_funcs is not None:
        self.scalarization_expressions = [
            (symbol, self.parser.parse(expression)) for symbol, expression in parsed_scal_funcs.items()
        ]
    else:
        self.scalarization_expressions = None

    # store the symbol and min or max multiplier as well (symbol, min/max multiplier [1 | -1])
    self.objective_mix_max_mult = [
        (objective.symbol, -1 if objective.maximize else 1) for objective in self.problem_objectives
    ]

    # create dataframe with the discrete representation, if any exists
    if self.discrete_representation is not None:
        self.discrete_df = pl.DataFrame(
            {**self.discrete_representation.variable_values, **self.discrete_representation.objective_values}
        )
    else:
        self.discrete_df = None

PolarsEvaluatorError

Bases: Exception

Error raised when exceptions are encountered in an PolarsEvaluator.

Source code in desdeo/problem/evaluator.py
class PolarsEvaluatorError(Exception):
    """Error raised when exceptions are encountered in an PolarsEvaluator."""

PolarsEvaluatorModesEnum

Bases: str, Enum

Defines the supported modes for the PolarsEvaluator.

Source code in desdeo/problem/evaluator.py
class PolarsEvaluatorModesEnum(str, Enum):
    """Defines the supported modes for the PolarsEvaluator."""

    variables = "variables"
    """Indicates that the evaluator should expect decision variables vectors and
    evaluate the problem with them."""
    discrete = "discrete"
    """Indicates that the problem is defined by discrete decision variable
    vector and objective vector pairs and those should be evaluated. In this
    mode, the evaluator does not expect any decision variables as arguments when
    evaluating."""
    mixed = "mixed"
    """Indicates that the problem has analytical and simulator and/or surrogate
    based objectives, constraints and extra functions. In this mode, the evaluator
    only handles data-based and analytical functions. For data-based objectives,
    it assumes that the variables are to be evaluated by finding the closest
    variables values in the data compare to the input, and evaluating the result
    to be the matching objective function values that match to the closest
    variable values found.  The evaluator should expect decision variables
    vectors and evaluate the problem with them."""
discrete class-attribute instance-attribute
discrete = 'discrete'

Indicates that the problem is defined by discrete decision variable vector and objective vector pairs and those should be evaluated. In this mode, the evaluator does not expect any decision variables as arguments when evaluating.

mixed class-attribute instance-attribute
mixed = 'mixed'

Indicates that the problem has analytical and simulator and/or surrogate based objectives, constraints and extra functions. In this mode, the evaluator only handles data-based and analytical functions. For data-based objectives, it assumes that the variables are to be evaluated by finding the closest variables values in the data compare to the input, and evaluating the result to be the matching objective function values that match to the closest variable values found. The evaluator should expect decision variables vectors and evaluate the problem with them.

variables class-attribute instance-attribute
variables = 'variables'

Indicates that the evaluator should expect decision variables vectors and evaluate the problem with them.

VariableDimensionEnum

Bases: str, Enum

An enumerator for the possible dimensions of the variables of a problem.

Source code in desdeo/problem/evaluator.py
class VariableDimensionEnum(str, Enum):
    """An enumerator for the possible dimensions of the variables of a problem."""

    scalar = "scalar"
    """All variables are scalar valued."""
    vector = "vector"
    """Highest dimensional variable is a vector."""
    tensor = "tensor"
    """Some variable has more dimensions."""
scalar class-attribute instance-attribute
scalar = 'scalar'

All variables are scalar valued.

tensor class-attribute instance-attribute
tensor = 'tensor'

Some variable has more dimensions.

vector class-attribute instance-attribute
vector = 'vector'

Highest dimensional variable is a vector.

find_closest_points

find_closest_points(
    xs: DataFrame,
    discrete_df: DataFrame,
    variable_symbols: list[str],
    objective_symbol: list[str],
) -> pl.DataFrame

Finds the closest points between the variable columns in xs and discrete_df.

For each row in xs, compares the variable_symbols columns and find the closest point in discrete_df. Returns the objective value in the objective_symbol column in discrete_df for each variable defined in xs, where the objective value corresponds to the closest point of each variable in xs compared to discrete_df.

Both xs and discrete_df must have the columns variable_symbols. discrete_df must also have the column objective_symbol.

Parameters:

Name Type Description Default
xs DataFrame

a polars dataframe with the variable values we are interested in finding the closest corresponding variable values in discrete_df.

required
discrete_df DataFrame

a polars dataframe to compare the rows in xs to.

required
variable_symbols list[str]

the names of the columns with decision variable values.

required
objective_symbol str

the name of the column in discrete_df that has the objective function values.

required

Returns:

Type Description
DataFrame

pl.DataFrame: a dataframe with the columns objective_symbol with the objective function value that corresponds to each decision variable vector in xs.

Source code in desdeo/problem/evaluator.py
def find_closest_points(
    xs: pl.DataFrame, discrete_df: pl.DataFrame, variable_symbols: list[str], objective_symbol: list[str]
) -> pl.DataFrame:
    """Finds the closest points between the variable columns in xs and discrete_df.

    For each row in xs, compares the `variable_symbols` columns and find the closest
    point in `discrete_df`. Returns the objective value in the `objective_symbol` column in
    `discrete_df` for each variable defined in `xs`, where the objective value
    corresponds to the closest point of each variable in `xs` compared to `discrete_df`.

    Both `xs` and `discrete_df` must have the columns `variable_symbols`. `discrete_df` must
    also have the column `objective_symbol`.

    Args:
        xs (pl.DataFrame): a polars dataframe with the variable values we are
            interested in finding the closest corresponding variable values in
            `discrete_df`.
        discrete_df (pl.DataFrame): a polars dataframe to compare the rows in `xs` to.
        variable_symbols (list[str]): the names of the columns with decision variable values.
        objective_symbol (str): the name of the column in `discrete_df` that has the objective function values.

    Returns:
        pl.DataFrame: a dataframe with the columns `objective_symbol` with the
            objective function value that corresponds to each decision variable
            vector in `xs`.
    """
    xs_vars_only = xs[variable_symbols]

    results = []

    for row in xs_vars_only.rows(named=True):
        distance_expr = (
            sum((pl.col(var_symbol) - row[var_symbol]) ** 2 for var_symbol in variable_symbols).sqrt().alias("distance")
        )

        combined_df = discrete_df.with_columns(distance_expr)

        closest = combined_df.sort("distance").head(1)

        results.append(closest[f"{objective_symbol}"][0])

    return pl.DataFrame({f"{objective_symbol}": results})

variable_dimension_enumerate

variable_dimension_enumerate(
    problem: Problem,
) -> VariableDimensionEnum

Return a VariableDimensionEnum based on the problems variables' dimensions.

This is needed as different evaluators and solvers can handle different dimensional variables.

If there are no TensorVariables in the problem, will return scalar. If there are, at the highest, one dimensional TensorVariables, will return vector. Else, there is at least a TensorVariable with a higher dimension, will return tensor.

Parameters:

Name Type Description Default
problem Problem

The problem being solved or evaluated.

required

Returns:

Name Type Description
VariableDimensionEnum VariableDimensionEnum

The enumeration of the problems variable dimensions.

Source code in desdeo/problem/evaluator.py
def variable_dimension_enumerate(problem: Problem) -> VariableDimensionEnum:
    """Return a VariableDimensionEnum based on the problems variables' dimensions.

    This is needed as different evaluators and solvers can handle different dimensional variables.

    If there are no TensorVariables in the problem, will return scalar.
    If there are, at the highest, one dimensional TensorVariables, will return vector.
    Else, there is at least a TensorVariable with a higher dimension, will return tensor.

    Args:
        problem (Problem): The problem being solved or evaluated.

    Returns:
        VariableDimensionEnum: The enumeration of the problems variable dimensions.
    """
    enum = VariableDimensionEnum.scalar
    for var in problem.variables:
        if isinstance(var, TensorVariable):
            if len(var.shape) == 1 or (len(var.shape) == 2 and not (var.shape[0] > 1 and var.shape[1] > 1)):  # noqa: PLR2004
                enum = VariableDimensionEnum.vector
            else:
                return VariableDimensionEnum.tensor
    return enum

Pyomo evaluator

desdeo.problem.pyomo_evaluator

Defines an evaluator compatible with the Problem JSON format and transforms it into a Pyomo model.

PyomoEvaluator

Defines an evaluator that transforms an instance of Problem into a pyomo model.

Source code in desdeo/problem/pyomo_evaluator.py
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
class PyomoEvaluator:
    """Defines an evaluator that transforms an instance of Problem into a pyomo model."""

    def __init__(self, problem: Problem):
        """Initializes the evaluator.

        Args:
            problem (Problem): the problem to be transformed in a pyomo model.
        """
        model = pyomo.ConcreteModel()

        # set the parser
        self.parse = MathParser(to_format=FormatEnum.pyomo).parse

        # Add variables
        model = self.init_variables(problem, model)

        # Add constants, if any
        if problem.constants is not None:
            model = self.init_constants(problem, model)

        # Add extra expressions, if any
        if problem.extra_funcs is not None:
            model = self.init_extras(problem, model)

        # Add objective function expressions
        model = self.init_objectives(problem, model)

        # Add constraints, if any
        if problem.constraints is not None:
            model = self.init_constraints(problem, model)

        # Add scalarization functions, if any
        if problem.scalarization_funcs is not None:
            model = self.init_scalarizations(problem, model)

        self.model = model
        self.problem = problem

    @classmethod
    def _bounds_rule(cls, lowerbounds, upperbounds):
        def bounds_rule(model, *args) -> tuple:
            indices = tuple(arg - 1 for arg in args)

            lower_value = lowerbounds
            upper_value = upperbounds

            for index in indices:
                lower_value = lower_value[index]
                upper_value = upper_value[index]

            return (lower_value, upper_value)

        return bounds_rule

    @classmethod
    def _init_rule(cls, initial_values):
        def init_rule(model, *args):
            indices = tuple(arg - 1 for arg in args)

            initial_value = initial_values

            for index in indices:
                initial_value = initial_value[index]

            return initial_value

        return init_rule

    def init_variables(self, problem: Problem, model: pyomo.Model) -> pyomo.Model:
        """Add variables to the pyomo model.

        Args:
            problem (Problem): problem from which to extract the variables.
            model (pyomo.Model): the pyomo model to add the variables to.

        Raises:
            PyomoEvaluator: when a problem in extracting the variables is encountered.
                I.e., the bounds of the variables are incorrect or of a non supported type.

        Returns:
            pyomo.Model: the pyomo model with the variables added as attributes.
        """
        for var in problem.variables:
            if isinstance(var, Variable):
                # handle regular variables
                lowerbound = var.lowerbound if var.lowerbound is not None else float("-inf")
                upperbound = var.upperbound if var.upperbound is not None else float("inf")

                # figure out the variable type
                match (lowerbound >= 0, upperbound >= 0, var.variable_type):
                    case (True, True, VariableTypeEnum.integer):
                        # variable is positive integer
                        domain = pyomo.NonNegativeIntegers
                    case (False, False, VariableTypeEnum.integer):
                        # variable is negative integer
                        domain = pyomo.NegativeIntegers
                    case (False, True, VariableTypeEnum.integer):
                        # variable can be both negative an positive integer
                        domain = pyomo.Integers
                    case (True, False, VariableTypeEnum.integer):
                        # error! lower bound is greater than upper bound
                        msg = (
                            f"The lower bound {var.lowerbound} for variable {var.symbol} is greater than the "
                            f"upper bound {var.upperbound}"
                        )
                        raise PyomoEvaluatorError(msg)
                    case (True, True, VariableTypeEnum.real):
                        # variable is positive real
                        domain = pyomo.NonNegativeReals
                    case (False, False, VariableTypeEnum.real):
                        # variable is negative real
                        domain = pyomo.NegativeReals
                    case (False, True, VariableTypeEnum.real):
                        # variable can be both negative and positive real
                        domain = pyomo.Reals
                    case (True, False, VariableTypeEnum.real):
                        # error! lower bound is greater than upper bound
                        msg = (
                            f"The lower bound {var.lowerbound} for variable {var.symbol} is greater than the "
                            f"upper bound {var.upperbound}"
                        )
                        raise PyomoEvaluatorError(msg)
                    # TODO: check binary type!
                    case _:
                        msg = f"Could not figure out the type for variable {var}."
                        raise PyomoEvaluatorError(msg)

                # if a variable's initial value is set, use it. Otherwise, check if the lower and upper bounds
                # are defined, if they are, use the mid-point of the bounds, otherwise use the initial value, which is
                # None.
                if var.initial_value is not None:
                    initial_value = var.initial_value
                else:
                    initial_value = (
                        var.initial_value
                        if var.lowerbound is None or var.upperbound is None
                        else (var.lowerbound + var.upperbound) / 2
                    )

                pyomo_var = pyomo.Var(
                    name=var.name,
                    initialize=initial_value,
                    bounds=(var.lowerbound, var.upperbound),
                    domain=domain,
                )

            elif isinstance(var, TensorVariable):
                # handle tensor variables, i.e., vectors etc..
                # create the needed range sets
                index_sets = [pyomo.RangeSet(1, dim_size) for dim_size in var.shape]

                # TODO: check domain properly
                if var.variable_type == VariableTypeEnum.binary:
                    domain = pyomo.Binary
                elif var.variable_type == VariableTypeEnum.integer:
                    domain = pyomo.Integers
                else:
                    domain = pyomo.Reals

                # create the Var
                pyomo_var = pyomo.Var(
                    *index_sets,
                    name=var.name,
                    initialize=self._init_rule(var.get_initial_values()),
                    bounds=self._bounds_rule(var.get_lowerbound_values(), var.get_upperbound_values()),
                    domain=domain,
                )

            else:
                msg = f"Unsupported variable type '{type(var)} encountered."
                raise PyomoEvaluatorError(msg)

            # add and then construct the variable
            setattr(model, var.symbol, pyomo_var)
            getattr(model, var.symbol).construct()

        return model

    def init_constants(self, problem: Problem, model: pyomo.Model) -> pyomo.Model:
        """Add constants to a pyomo model.

        Args:
            problem (Problem): problem from which to extract the constants.
            model (pyomo.Model): the pyomo model to add the constants to.

        Raises:
            PyomoEvaluatorError: when the domain of a constant cannot be figured out.

        Returns:
            pyomo.Model: the pyomo model with the constants added as attributes.
        """
        for con in problem.constants:
            # Handle regular constnants
            if isinstance(con, Constant):
                # figure out the domain of the constant
                match (
                    isinstance(con.value, int),
                    isinstance(con.value, float),
                    con.value >= 0,
                ):
                    case (True, False, True):
                        # positive integer
                        domain = pyomo.NonNegativeIntegers
                    case (True, False, False):
                        # negative integer
                        domain = pyomo.NegativeIntegers
                    case (False, True, True):
                        # positive real
                        domain = pyomo.NonNegativeReals
                    case (False, True, False):
                        # negative real
                        domain = pyomo.NegativeReals
                    case _:
                        # not possible, something went wrong
                        msg = f"Failed to figure out the domain for the constant {con.symbol}."
                        raise PyomoEvaluatorError(msg)

                pyomo_param = pyomo.Param(name=con.name, default=con.value, domain=domain)

            elif isinstance(con, TensorConstant):
                # handle TensorConstants, like vectors
                # create the needed range sets
                index_sets = [pyomo.RangeSet(1, dim_size) for dim_size in con.shape]

                # TODO: check domain properly
                # for now, constants are always assumed to be real (which is quite safe to do...)
                domain = pyomo.Reals

                # create the Con
                pyomo_param = pyomo.Param(
                    *index_sets,
                    name=con.name,
                    initialize=self._init_rule(con.get_values()),
                    domain=domain,
                )
            else:
                msg = f"Unsupported constant type '{type(con)}' encountered."
                raise PyomoEvaluatorError(msg)

            setattr(model, con.symbol, pyomo_param)

        return model

    def init_extras(self, problem: Problem, model: pyomo.Model) -> pyomo.Model:
        """Add extra function expressions to a pyomo model.

        Args:
            problem (Problem): problem from which the extract the extra function expressions.
            model (pyomo.Model): the pyomo model to add the extra function expressions to.

        Returns:
            pyomo.Model: the pyomo model with the expressions added as attributes.
        """
        for extra in problem.extra_funcs:
            pyomo_expr = self.parse(extra.func, model)

            setattr(model, extra.symbol, pyomo.Expression(expr=pyomo_expr))

        return model

    def init_objectives(self, problem: Problem, model: pyomo.Model) -> pyomo.Model:
        """Add objective function expressions to a pyomo model.

        Does not yet add any actual pyomo objectives, only the expressions of the objectives.
        A pyomo solved must add the appropiate pyomo objective before solving.

        Args:
            problem (Problem): problem from which to extract the objective function expresions.
            model (pyomo.Model): the pyomo model to add the expressions to.

        Returns:
            pyomo.Model: the pyomo model with the objective expressions added as pyomo Objectives.
                The objectives are deactivated by default.
        """
        for obj in problem.objectives:
            pyomo_expr = self.parse(obj.func, model)

            setattr(model, obj.symbol, pyomo.Expression(expr=pyomo_expr))

            # the obj.symbol_min objectives are used when optimizing and building scalarizations etc...
            new_expr = (-1) * pyomo_expr if obj.maximize else pyomo_expr
            setattr(model, f"{obj.symbol}_min", pyomo.Expression(expr=new_expr))

        return model

    def init_constraints(self, problem: Problem, model: pyomo.Model) -> pyomo.Model:
        """Add constraint expressions to a pyomo model.

        Args:
            problem (Problem): the problem from which to extract the constraint function expressions.
            model (pyomo.Model): the pyomo model to add the exprssions to.

        Raises:
            PyomoEvaluatorError: when an unsupported constraint type is encountered.

        Returns:
            pyomo.Model: the pyomo model with the constraint expressions added as pyomo Constraints.
        """
        for cons in problem.constraints:
            pyomo_expr = self.parse(cons.func, model)

            match con_type := cons.cons_type:
                case ConstraintTypeEnum.LTE:
                    # constraints in DESDEO are defined such that they must be less than zero
                    pyomo_expr = _le(pyomo_expr, 0, cons.name)
                case ConstraintTypeEnum.EQ:
                    # if these constraints start acting up, check how indexed
                    # stuff is implemented in the local function _le
                    pyomo_expr = pyomo.Constraint(expr=_eq(pyomo_expr, 0), name=cons.name)
                    pyomo_expr.construct()
                case _:
                    msg = f"Constraint type of {con_type} not supported. Must be one of {ConstraintTypeEnum}."
                    raise PyomoEvaluatorError(msg)

            # cons_expr = pyomo.Constraint(expr=pyomo_expr, name=cons.name)

            setattr(model, cons.symbol, pyomo_expr)
            # getattr(model, cons.symbol).construct()

        return model

    def init_scalarizations(self, problem: Problem, model: pyomo.Model) -> pyomo.Model:
        """Add scalrization expressions to a pyomo model.

        Args:
            problem (Problem): the problem from which to extract thescalarization function expressions.
            model (pyomo.Model): the pyomo model to add the expressions to.

        Returns:
            pyomo.Model: the pyomo model with the scalarization expressions addedd as pyomo Objectives.
                The objectives are deactivated by default. Scalarization functions are always minimized.
        """
        for scal in problem.scalarization_funcs:
            pyomo_expr = self.parse(scal.func, model)

            setattr(model, scal.symbol, pyomo.Expression(expr=pyomo_expr))

        return model

    def evaluate(
        self, xs: dict[str, float | int | bool | Iterable[float | int | bool]]
    ) -> dict[str, float | int | bool | list[dict[str, float | int | bool]]]:
        """Evaluate the current pyomo model with the given decision variable values.

        Warning:
            This should not be used for actually solving the pyomo model! For debugging mostly.

        Args:
            xs (dict[str, float | int | bool | Iterable[float | int | bool]]): a dict with the decision variable symbols
                as the keys followed by the corresponding decision variable values, which can also
                be represented by a list for multiple values. The symbols
                must match the symbols defined for the decision variables defined in the `Problem` being solved.
                Each list in the dict should contain the same number of values.

        Returns:
            dict | list[dict]: the results of evaluating the pyomo model with its variable values set to the values
                found in xs.
        """
        res = []
        n_samples = len(next(iter(xs.values()))) if isinstance(next(iter(xs.values())), list) else 1
        for i in range(n_samples):
            for var in self.problem.variables:
                x = xs[var.symbol][i]
                if isinstance(var, Variable):
                    # scalar variable
                    setattr(self.model, var.symbol, x)
                else:
                    # tensor variable
                    indices = itertools.product(*[range(1, dim + 1) for dim in var.shape])  # 1-based indexing
                    for idx in indices:
                        elem = x
                        for j in idx:
                            elem = elem[j - 1]  # 0-based indexing

                        getattr(self.model, var.symbol)[idx] = elem  # 1-based indexing

            res.append(self.get_values())

        return res if len(res) > 1 else res[0]

    def get_values(self) -> dict[str, float | int | bool]:  # noqa: C901
        """Get the values from the pyomo model in dict.

        The keys of the dict will be the symbols defined in the problem utilized to initialize the evaluator.

        Returns:
            dict[str, float | int | bool]: a dict with keys equivalent to the symbols defined in self.problem.
        """
        result_dict = {}

        for var in self.problem.variables:
            if isinstance(var, Variable):
                result_dict[var.symbol] = pyomo.value(getattr(self.model, var.symbol))
            elif isinstance(var, TensorVariable):
                result_dict[var.symbol] = getattr(self.model, var.symbol).get_values()
            else:
                msg = f"Unsupported variable type {type(var)} encountered."
                raise PyomoEvaluatorError(msg)

        for obj in self.problem.objectives:
            result_dict[obj.symbol] = pyomo.value(getattr(self.model, obj.symbol))

        if self.problem.constants is not None:
            for con in self.problem.constants:
                if isinstance(con, Constant):
                    result_dict[con.symbol] = pyomo.value(getattr(self.model, con.symbol))
                elif isinstance(con, TensorConstant):
                    result_dict[con.symbol] = getattr(self.model, con.symbol).extract_values()
                else:
                    msg = f"Unsupported variable type {type(var)} encountered."
                    raise PyomoEvaluatorError(msg)

        if self.problem.extra_funcs is not None:
            for extra in self.problem.extra_funcs:
                result_dict[extra.symbol] = pyomo.value(getattr(self.model, extra.symbol))

        # TODO: after implementing TensorConstraint, fix this
        if self.problem.constraints is not None:
            for const in self.problem.constraints:
                obj = getattr(self.model, const.symbol)

                if obj.is_indexed():
                    result_dict[const.symbol] = {k: pyomo.value(obj[k]) for k in obj}
                else:
                    result_dict[const.symbol] = pyomo.value(obj)

        if self.problem.scalarization_funcs is not None:
            for scal in self.problem.scalarization_funcs:
                result_dict[scal.symbol] = pyomo.value(getattr(self.model, scal.symbol))

        return result_dict

    def set_optimization_target(self, target: str):
        """Creates a minimization objective from the target attribute of the pyomo model.

        The attribute name of the pyomo objective will be target + _objective, e.g.,
        'f_1' will become 'f_1_objective'. This is done so that the original f_1 expressions
        attribute does not get reassigned.

        Args:
            target (str): an str representing a symbol.

        Raises:
            PyomoEvaluatorError: the given target was not an attribute of the pyomo model.
        """
        if not hasattr(self.model, target):
            msg = f"The pyomo model has no attribute {target}."
            raise PyomoEvaluatorError(msg)

        # delete any existing objectives, if any
        for obj in self.model.component_objects(pyomo.Objective, active=True):
            obj.deactivate()

        obj_expr = getattr(self.model, target)

        objective = pyomo.Objective(expr=obj_expr, sense=pyomo.minimize, name=target)

        # add the postfix '_objective' to the attribute name of the pyomo objective
        setattr(self.model, f"{target}_objective", objective)
__init__
__init__(problem: Problem)

Initializes the evaluator.

Parameters:

Name Type Description Default
problem Problem

the problem to be transformed in a pyomo model.

required
Source code in desdeo/problem/pyomo_evaluator.py
def __init__(self, problem: Problem):
    """Initializes the evaluator.

    Args:
        problem (Problem): the problem to be transformed in a pyomo model.
    """
    model = pyomo.ConcreteModel()

    # set the parser
    self.parse = MathParser(to_format=FormatEnum.pyomo).parse

    # Add variables
    model = self.init_variables(problem, model)

    # Add constants, if any
    if problem.constants is not None:
        model = self.init_constants(problem, model)

    # Add extra expressions, if any
    if problem.extra_funcs is not None:
        model = self.init_extras(problem, model)

    # Add objective function expressions
    model = self.init_objectives(problem, model)

    # Add constraints, if any
    if problem.constraints is not None:
        model = self.init_constraints(problem, model)

    # Add scalarization functions, if any
    if problem.scalarization_funcs is not None:
        model = self.init_scalarizations(problem, model)

    self.model = model
    self.problem = problem
evaluate
evaluate(
    xs: dict[
        str,
        float | int | bool | Iterable[float | int | bool],
    ],
) -> dict[
    str,
    float
    | int
    | bool
    | list[dict[str, float | int | bool]],
]

Evaluate the current pyomo model with the given decision variable values.

Warning

This should not be used for actually solving the pyomo model! For debugging mostly.

Parameters:

Name Type Description Default
xs dict[str, float | int | bool | Iterable[float | int | bool]]

a dict with the decision variable symbols as the keys followed by the corresponding decision variable values, which can also be represented by a list for multiple values. The symbols must match the symbols defined for the decision variables defined in the Problem being solved. Each list in the dict should contain the same number of values.

required

Returns:

Type Description
dict[str, float | int | bool | list[dict[str, float | int | bool]]]

dict | list[dict]: the results of evaluating the pyomo model with its variable values set to the values found in xs.

Source code in desdeo/problem/pyomo_evaluator.py
def evaluate(
    self, xs: dict[str, float | int | bool | Iterable[float | int | bool]]
) -> dict[str, float | int | bool | list[dict[str, float | int | bool]]]:
    """Evaluate the current pyomo model with the given decision variable values.

    Warning:
        This should not be used for actually solving the pyomo model! For debugging mostly.

    Args:
        xs (dict[str, float | int | bool | Iterable[float | int | bool]]): a dict with the decision variable symbols
            as the keys followed by the corresponding decision variable values, which can also
            be represented by a list for multiple values. The symbols
            must match the symbols defined for the decision variables defined in the `Problem` being solved.
            Each list in the dict should contain the same number of values.

    Returns:
        dict | list[dict]: the results of evaluating the pyomo model with its variable values set to the values
            found in xs.
    """
    res = []
    n_samples = len(next(iter(xs.values()))) if isinstance(next(iter(xs.values())), list) else 1
    for i in range(n_samples):
        for var in self.problem.variables:
            x = xs[var.symbol][i]
            if isinstance(var, Variable):
                # scalar variable
                setattr(self.model, var.symbol, x)
            else:
                # tensor variable
                indices = itertools.product(*[range(1, dim + 1) for dim in var.shape])  # 1-based indexing
                for idx in indices:
                    elem = x
                    for j in idx:
                        elem = elem[j - 1]  # 0-based indexing

                    getattr(self.model, var.symbol)[idx] = elem  # 1-based indexing

        res.append(self.get_values())

    return res if len(res) > 1 else res[0]
get_values
get_values() -> dict[str, float | int | bool]

Get the values from the pyomo model in dict.

The keys of the dict will be the symbols defined in the problem utilized to initialize the evaluator.

Returns:

Type Description
dict[str, float | int | bool]

dict[str, float | int | bool]: a dict with keys equivalent to the symbols defined in self.problem.

Source code in desdeo/problem/pyomo_evaluator.py
def get_values(self) -> dict[str, float | int | bool]:  # noqa: C901
    """Get the values from the pyomo model in dict.

    The keys of the dict will be the symbols defined in the problem utilized to initialize the evaluator.

    Returns:
        dict[str, float | int | bool]: a dict with keys equivalent to the symbols defined in self.problem.
    """
    result_dict = {}

    for var in self.problem.variables:
        if isinstance(var, Variable):
            result_dict[var.symbol] = pyomo.value(getattr(self.model, var.symbol))
        elif isinstance(var, TensorVariable):
            result_dict[var.symbol] = getattr(self.model, var.symbol).get_values()
        else:
            msg = f"Unsupported variable type {type(var)} encountered."
            raise PyomoEvaluatorError(msg)

    for obj in self.problem.objectives:
        result_dict[obj.symbol] = pyomo.value(getattr(self.model, obj.symbol))

    if self.problem.constants is not None:
        for con in self.problem.constants:
            if isinstance(con, Constant):
                result_dict[con.symbol] = pyomo.value(getattr(self.model, con.symbol))
            elif isinstance(con, TensorConstant):
                result_dict[con.symbol] = getattr(self.model, con.symbol).extract_values()
            else:
                msg = f"Unsupported variable type {type(var)} encountered."
                raise PyomoEvaluatorError(msg)

    if self.problem.extra_funcs is not None:
        for extra in self.problem.extra_funcs:
            result_dict[extra.symbol] = pyomo.value(getattr(self.model, extra.symbol))

    # TODO: after implementing TensorConstraint, fix this
    if self.problem.constraints is not None:
        for const in self.problem.constraints:
            obj = getattr(self.model, const.symbol)

            if obj.is_indexed():
                result_dict[const.symbol] = {k: pyomo.value(obj[k]) for k in obj}
            else:
                result_dict[const.symbol] = pyomo.value(obj)

    if self.problem.scalarization_funcs is not None:
        for scal in self.problem.scalarization_funcs:
            result_dict[scal.symbol] = pyomo.value(getattr(self.model, scal.symbol))

    return result_dict
init_constants
init_constants(
    problem: Problem, model: Model
) -> pyomo.Model

Add constants to a pyomo model.

Parameters:

Name Type Description Default
problem Problem

problem from which to extract the constants.

required
model Model

the pyomo model to add the constants to.

required

Raises:

Type Description
PyomoEvaluatorError

when the domain of a constant cannot be figured out.

Returns:

Type Description
Model

pyomo.Model: the pyomo model with the constants added as attributes.

Source code in desdeo/problem/pyomo_evaluator.py
def init_constants(self, problem: Problem, model: pyomo.Model) -> pyomo.Model:
    """Add constants to a pyomo model.

    Args:
        problem (Problem): problem from which to extract the constants.
        model (pyomo.Model): the pyomo model to add the constants to.

    Raises:
        PyomoEvaluatorError: when the domain of a constant cannot be figured out.

    Returns:
        pyomo.Model: the pyomo model with the constants added as attributes.
    """
    for con in problem.constants:
        # Handle regular constnants
        if isinstance(con, Constant):
            # figure out the domain of the constant
            match (
                isinstance(con.value, int),
                isinstance(con.value, float),
                con.value >= 0,
            ):
                case (True, False, True):
                    # positive integer
                    domain = pyomo.NonNegativeIntegers
                case (True, False, False):
                    # negative integer
                    domain = pyomo.NegativeIntegers
                case (False, True, True):
                    # positive real
                    domain = pyomo.NonNegativeReals
                case (False, True, False):
                    # negative real
                    domain = pyomo.NegativeReals
                case _:
                    # not possible, something went wrong
                    msg = f"Failed to figure out the domain for the constant {con.symbol}."
                    raise PyomoEvaluatorError(msg)

            pyomo_param = pyomo.Param(name=con.name, default=con.value, domain=domain)

        elif isinstance(con, TensorConstant):
            # handle TensorConstants, like vectors
            # create the needed range sets
            index_sets = [pyomo.RangeSet(1, dim_size) for dim_size in con.shape]

            # TODO: check domain properly
            # for now, constants are always assumed to be real (which is quite safe to do...)
            domain = pyomo.Reals

            # create the Con
            pyomo_param = pyomo.Param(
                *index_sets,
                name=con.name,
                initialize=self._init_rule(con.get_values()),
                domain=domain,
            )
        else:
            msg = f"Unsupported constant type '{type(con)}' encountered."
            raise PyomoEvaluatorError(msg)

        setattr(model, con.symbol, pyomo_param)

    return model
init_constraints
init_constraints(
    problem: Problem, model: Model
) -> pyomo.Model

Add constraint expressions to a pyomo model.

Parameters:

Name Type Description Default
problem Problem

the problem from which to extract the constraint function expressions.

required
model Model

the pyomo model to add the exprssions to.

required

Raises:

Type Description
PyomoEvaluatorError

when an unsupported constraint type is encountered.

Returns:

Type Description
Model

pyomo.Model: the pyomo model with the constraint expressions added as pyomo Constraints.

Source code in desdeo/problem/pyomo_evaluator.py
def init_constraints(self, problem: Problem, model: pyomo.Model) -> pyomo.Model:
    """Add constraint expressions to a pyomo model.

    Args:
        problem (Problem): the problem from which to extract the constraint function expressions.
        model (pyomo.Model): the pyomo model to add the exprssions to.

    Raises:
        PyomoEvaluatorError: when an unsupported constraint type is encountered.

    Returns:
        pyomo.Model: the pyomo model with the constraint expressions added as pyomo Constraints.
    """
    for cons in problem.constraints:
        pyomo_expr = self.parse(cons.func, model)

        match con_type := cons.cons_type:
            case ConstraintTypeEnum.LTE:
                # constraints in DESDEO are defined such that they must be less than zero
                pyomo_expr = _le(pyomo_expr, 0, cons.name)
            case ConstraintTypeEnum.EQ:
                # if these constraints start acting up, check how indexed
                # stuff is implemented in the local function _le
                pyomo_expr = pyomo.Constraint(expr=_eq(pyomo_expr, 0), name=cons.name)
                pyomo_expr.construct()
            case _:
                msg = f"Constraint type of {con_type} not supported. Must be one of {ConstraintTypeEnum}."
                raise PyomoEvaluatorError(msg)

        # cons_expr = pyomo.Constraint(expr=pyomo_expr, name=cons.name)

        setattr(model, cons.symbol, pyomo_expr)
        # getattr(model, cons.symbol).construct()

    return model
init_extras
init_extras(problem: Problem, model: Model) -> pyomo.Model

Add extra function expressions to a pyomo model.

Parameters:

Name Type Description Default
problem Problem

problem from which the extract the extra function expressions.

required
model Model

the pyomo model to add the extra function expressions to.

required

Returns:

Type Description
Model

pyomo.Model: the pyomo model with the expressions added as attributes.

Source code in desdeo/problem/pyomo_evaluator.py
def init_extras(self, problem: Problem, model: pyomo.Model) -> pyomo.Model:
    """Add extra function expressions to a pyomo model.

    Args:
        problem (Problem): problem from which the extract the extra function expressions.
        model (pyomo.Model): the pyomo model to add the extra function expressions to.

    Returns:
        pyomo.Model: the pyomo model with the expressions added as attributes.
    """
    for extra in problem.extra_funcs:
        pyomo_expr = self.parse(extra.func, model)

        setattr(model, extra.symbol, pyomo.Expression(expr=pyomo_expr))

    return model
init_objectives
init_objectives(
    problem: Problem, model: Model
) -> pyomo.Model

Add objective function expressions to a pyomo model.

Does not yet add any actual pyomo objectives, only the expressions of the objectives. A pyomo solved must add the appropiate pyomo objective before solving.

Parameters:

Name Type Description Default
problem Problem

problem from which to extract the objective function expresions.

required
model Model

the pyomo model to add the expressions to.

required

Returns:

Type Description
Model

pyomo.Model: the pyomo model with the objective expressions added as pyomo Objectives. The objectives are deactivated by default.

Source code in desdeo/problem/pyomo_evaluator.py
def init_objectives(self, problem: Problem, model: pyomo.Model) -> pyomo.Model:
    """Add objective function expressions to a pyomo model.

    Does not yet add any actual pyomo objectives, only the expressions of the objectives.
    A pyomo solved must add the appropiate pyomo objective before solving.

    Args:
        problem (Problem): problem from which to extract the objective function expresions.
        model (pyomo.Model): the pyomo model to add the expressions to.

    Returns:
        pyomo.Model: the pyomo model with the objective expressions added as pyomo Objectives.
            The objectives are deactivated by default.
    """
    for obj in problem.objectives:
        pyomo_expr = self.parse(obj.func, model)

        setattr(model, obj.symbol, pyomo.Expression(expr=pyomo_expr))

        # the obj.symbol_min objectives are used when optimizing and building scalarizations etc...
        new_expr = (-1) * pyomo_expr if obj.maximize else pyomo_expr
        setattr(model, f"{obj.symbol}_min", pyomo.Expression(expr=new_expr))

    return model
init_scalarizations
init_scalarizations(
    problem: Problem, model: Model
) -> pyomo.Model

Add scalrization expressions to a pyomo model.

Parameters:

Name Type Description Default
problem Problem

the problem from which to extract thescalarization function expressions.

required
model Model

the pyomo model to add the expressions to.

required

Returns:

Type Description
Model

pyomo.Model: the pyomo model with the scalarization expressions addedd as pyomo Objectives. The objectives are deactivated by default. Scalarization functions are always minimized.

Source code in desdeo/problem/pyomo_evaluator.py
def init_scalarizations(self, problem: Problem, model: pyomo.Model) -> pyomo.Model:
    """Add scalrization expressions to a pyomo model.

    Args:
        problem (Problem): the problem from which to extract thescalarization function expressions.
        model (pyomo.Model): the pyomo model to add the expressions to.

    Returns:
        pyomo.Model: the pyomo model with the scalarization expressions addedd as pyomo Objectives.
            The objectives are deactivated by default. Scalarization functions are always minimized.
    """
    for scal in problem.scalarization_funcs:
        pyomo_expr = self.parse(scal.func, model)

        setattr(model, scal.symbol, pyomo.Expression(expr=pyomo_expr))

    return model
init_variables
init_variables(
    problem: Problem, model: Model
) -> pyomo.Model

Add variables to the pyomo model.

Parameters:

Name Type Description Default
problem Problem

problem from which to extract the variables.

required
model Model

the pyomo model to add the variables to.

required

Raises:

Type Description
PyomoEvaluator

when a problem in extracting the variables is encountered. I.e., the bounds of the variables are incorrect or of a non supported type.

Returns:

Type Description
Model

pyomo.Model: the pyomo model with the variables added as attributes.

Source code in desdeo/problem/pyomo_evaluator.py
def init_variables(self, problem: Problem, model: pyomo.Model) -> pyomo.Model:
    """Add variables to the pyomo model.

    Args:
        problem (Problem): problem from which to extract the variables.
        model (pyomo.Model): the pyomo model to add the variables to.

    Raises:
        PyomoEvaluator: when a problem in extracting the variables is encountered.
            I.e., the bounds of the variables are incorrect or of a non supported type.

    Returns:
        pyomo.Model: the pyomo model with the variables added as attributes.
    """
    for var in problem.variables:
        if isinstance(var, Variable):
            # handle regular variables
            lowerbound = var.lowerbound if var.lowerbound is not None else float("-inf")
            upperbound = var.upperbound if var.upperbound is not None else float("inf")

            # figure out the variable type
            match (lowerbound >= 0, upperbound >= 0, var.variable_type):
                case (True, True, VariableTypeEnum.integer):
                    # variable is positive integer
                    domain = pyomo.NonNegativeIntegers
                case (False, False, VariableTypeEnum.integer):
                    # variable is negative integer
                    domain = pyomo.NegativeIntegers
                case (False, True, VariableTypeEnum.integer):
                    # variable can be both negative an positive integer
                    domain = pyomo.Integers
                case (True, False, VariableTypeEnum.integer):
                    # error! lower bound is greater than upper bound
                    msg = (
                        f"The lower bound {var.lowerbound} for variable {var.symbol} is greater than the "
                        f"upper bound {var.upperbound}"
                    )
                    raise PyomoEvaluatorError(msg)
                case (True, True, VariableTypeEnum.real):
                    # variable is positive real
                    domain = pyomo.NonNegativeReals
                case (False, False, VariableTypeEnum.real):
                    # variable is negative real
                    domain = pyomo.NegativeReals
                case (False, True, VariableTypeEnum.real):
                    # variable can be both negative and positive real
                    domain = pyomo.Reals
                case (True, False, VariableTypeEnum.real):
                    # error! lower bound is greater than upper bound
                    msg = (
                        f"The lower bound {var.lowerbound} for variable {var.symbol} is greater than the "
                        f"upper bound {var.upperbound}"
                    )
                    raise PyomoEvaluatorError(msg)
                # TODO: check binary type!
                case _:
                    msg = f"Could not figure out the type for variable {var}."
                    raise PyomoEvaluatorError(msg)

            # if a variable's initial value is set, use it. Otherwise, check if the lower and upper bounds
            # are defined, if they are, use the mid-point of the bounds, otherwise use the initial value, which is
            # None.
            if var.initial_value is not None:
                initial_value = var.initial_value
            else:
                initial_value = (
                    var.initial_value
                    if var.lowerbound is None or var.upperbound is None
                    else (var.lowerbound + var.upperbound) / 2
                )

            pyomo_var = pyomo.Var(
                name=var.name,
                initialize=initial_value,
                bounds=(var.lowerbound, var.upperbound),
                domain=domain,
            )

        elif isinstance(var, TensorVariable):
            # handle tensor variables, i.e., vectors etc..
            # create the needed range sets
            index_sets = [pyomo.RangeSet(1, dim_size) for dim_size in var.shape]

            # TODO: check domain properly
            if var.variable_type == VariableTypeEnum.binary:
                domain = pyomo.Binary
            elif var.variable_type == VariableTypeEnum.integer:
                domain = pyomo.Integers
            else:
                domain = pyomo.Reals

            # create the Var
            pyomo_var = pyomo.Var(
                *index_sets,
                name=var.name,
                initialize=self._init_rule(var.get_initial_values()),
                bounds=self._bounds_rule(var.get_lowerbound_values(), var.get_upperbound_values()),
                domain=domain,
            )

        else:
            msg = f"Unsupported variable type '{type(var)} encountered."
            raise PyomoEvaluatorError(msg)

        # add and then construct the variable
        setattr(model, var.symbol, pyomo_var)
        getattr(model, var.symbol).construct()

    return model
set_optimization_target
set_optimization_target(target: str)

Creates a minimization objective from the target attribute of the pyomo model.

The attribute name of the pyomo objective will be target + _objective, e.g., 'f_1' will become 'f_1_objective'. This is done so that the original f_1 expressions attribute does not get reassigned.

Parameters:

Name Type Description Default
target str

an str representing a symbol.

required

Raises:

Type Description
PyomoEvaluatorError

the given target was not an attribute of the pyomo model.

Source code in desdeo/problem/pyomo_evaluator.py
def set_optimization_target(self, target: str):
    """Creates a minimization objective from the target attribute of the pyomo model.

    The attribute name of the pyomo objective will be target + _objective, e.g.,
    'f_1' will become 'f_1_objective'. This is done so that the original f_1 expressions
    attribute does not get reassigned.

    Args:
        target (str): an str representing a symbol.

    Raises:
        PyomoEvaluatorError: the given target was not an attribute of the pyomo model.
    """
    if not hasattr(self.model, target):
        msg = f"The pyomo model has no attribute {target}."
        raise PyomoEvaluatorError(msg)

    # delete any existing objectives, if any
    for obj in self.model.component_objects(pyomo.Objective, active=True):
        obj.deactivate()

    obj_expr = getattr(self.model, target)

    objective = pyomo.Objective(expr=obj_expr, sense=pyomo.minimize, name=target)

    # add the postfix '_objective' to the attribute name of the pyomo objective
    setattr(self.model, f"{target}_objective", objective)

PyomoEvaluatorError

Bases: Exception

Raised when an error within the PyomoEvaluator class is encountered.

Source code in desdeo/problem/pyomo_evaluator.py
class PyomoEvaluatorError(Exception):
    """Raised when an error within the PyomoEvaluator class is encountered."""

Simulator evaluator

desdeo.problem.simulator_evaluator

Evaluators are defined to evaluate simulator based and surrogate based objectives, constraints and extras.

EvaluatorError

Bases: Exception

Error raised when exceptions are encountered in an Evaluator.

Source code in desdeo/problem/simulator_evaluator.py
class EvaluatorError(Exception):
    """Error raised when exceptions are encountered in an Evaluator."""

SimulatorEvaluator

A class for creating evaluators for simulator based and surrogate based objectives, constraints and extras.

Source code in desdeo/problem/simulator_evaluator.py
class SimulatorEvaluator:
    """A class for creating evaluators for simulator based and surrogate based objectives, constraints and extras."""

    def __init__(  # noqa: PLR0912
        self,
        problem: Problem,
        params: dict[str, dict] | ProviderParams | None = None,
        surrogate_paths: dict[str, Path] | None = None,
    ):
        """Creating an evaluator for simulator based and surrogate based objectives, constraints and extras.

        Args:
            problem (Problem): The problem as a pydantic 'Problem' data class.
            params (dict[str, dict], optional): Parameters for the different simulators used in the problem.
                Given as dict with the simulators' symbols as keys and the corresponding simulator parameters
                as a dict as values. Defaults to None.
            surrogate_paths (dict[str, Path], optional): A dictionary where the keys are the names of the objectives,
                constraints and extra functions and the values are the paths to the surrogate models saved on disk.
                The names of the objectives, constraints and extra functions should match the names of the objectives,
                constraints and extra functions in the problem JSON. Defaults to None.
        """
        self.problem = problem
        # store the symbol and min or max multiplier as well (symbol, min/max multiplier [1 | -1])
        self.objective_mix_max_mult = [
            (objective.symbol, -1 if objective.maximize else 1) for objective in problem.objectives
        ]
        # Gather symbols objectives of different types into their own lists
        self.analytical_symbols = [
            obj.symbol
            for obj in list(filter(lambda x: x.objective_type == ObjectiveTypeEnum.analytical, problem.objectives))
        ]
        self.data_based_symbols = [
            obj.symbol for obj in problem.objectives if obj.objective_type == ObjectiveTypeEnum.data_based
        ]
        self.simulator_symbols = [
            obj.symbol
            for obj in list(filter(lambda x: x.objective_type == ObjectiveTypeEnum.simulator, problem.objectives))
        ]
        self.surrogate_symbols = [
            obj.symbol
            for obj in list(filter(lambda x: x.objective_type == ObjectiveTypeEnum.surrogate, problem.objectives))
        ]
        if problem.scalarization_funcs is not None:
            parser = MathParser()
            self.scalarization_funcs = [
                (func.symbol, parser.parse(func.func))
                for func in problem.scalarization_funcs
                if func.symbol is not None
            ]
        else:
            self.scalarization_funcs = []
        # Gather any constraints' symbols
        if problem.constraints is not None:
            self.analytical_symbols = self.analytical_symbols + [
                con.symbol for con in list(filter(lambda x: x.func is not None, problem.constraints))
            ]
            self.simulator_symbols = self.simulator_symbols + [
                con.symbol for con in list(filter(lambda x: x.simulator_path is not None, problem.constraints))
            ]
            self.surrogate_symbols = self.surrogate_symbols + [
                con.symbol for con in list(filter(lambda x: x.surrogates is not None, problem.constraints))
            ]

        # Gather any extra functions' symbols
        if problem.extra_funcs is not None:
            self.analytical_symbols = self.analytical_symbols + [
                extra.symbol for extra in list(filter(lambda x: x.func is not None, problem.extra_funcs))
            ]
            self.simulator_symbols = self.simulator_symbols + [
                extra.symbol for extra in list(filter(lambda x: x.simulator_path is not None, problem.extra_funcs))
            ]
            self.surrogate_symbols = self.surrogate_symbols + [
                extra.symbol for extra in list(filter(lambda x: x.surrogates is not None, problem.extra_funcs))
            ]

        # Gather all the symbols of objectives, constraints and extra functions
        self.problem_symbols = (
            self.analytical_symbols + self.data_based_symbols + self.simulator_symbols + self.surrogate_symbols
        )

        # Gather the possible simulators
        self.simulators = problem.simulators if problem.simulators is not None else []

        # Gather the possibly given parameters
        if params and not isinstance(params, dict):
            params = params.model_dump()
        self.params = {}
        for sim in self.simulators:
            sim_params = params.get(sim.name, {}) if params is not None else {}
            if sim.parameter_options is not None:
                for key in sim.parameter_options:
                    sim_params[key] = sim.parameter_options[key]
            self.params[sim.name] = sim_params

        self.surrogates = {}
        if surrogate_paths is not None:
            self._load_surrogates(surrogate_paths)
        else:
            self._load_surrogates()

        if len(self.surrogate_symbols) > 0:
            missing_surrogates = []
            for symbol in self.surrogate_symbols:
                if symbol not in self.surrogates:
                    missing_surrogates.append(symbol)

            if len(missing_surrogates) > 0:
                raise EvaluatorError(f"Some surrogates missing: {missing_surrogates}.")

    def _evaluate_simulator(self, xs: dict[str, list[int | float]]) -> pl.DataFrame:
        """Evaluate the problem for the given decision variables using the problem's simulators.

        Args:
            xs (dict[str, list[int | float]]): The decision variables for which the functions are to be evaluated.
                Given as a dictionary with the decision variable symbols as keys and a list of decision variable values
                as the values. The length of the lists is the number of samples and each list should have the same
                length (same number of samples).

        Returns:
            pl.DataFrame: The objective, constraint and extra function values for the given decision variables as
                a polars dataframe. The symbols of the objectives, constraints and extra functions are the column names
                and the length of the columns is the number of samples. Will return those objective, constraint and
                extra function values that are gained from simulators listed in the problem object.
        """
        res_df = pl.DataFrame()
        for sim in self.simulators:
            # gather the possible parameters for the simulator
            params = self.params.get(sim.name, {})
            if sim.file is not None:
                # call the simulator with the decision variable values and parameters as dicts
                res = subprocess.run(
                    [sys.executable, sim.file, "-d", str(xs), "-p", str(params)], capture_output=True, text=True
                )
                if res.returncode == 0:
                    # gather the simulation results (a dict) into the results dataframe
                    res_df = res_df.hstack(pl.DataFrame(json.loads(res.stdout)))
                else:
                    raise EvaluatorError(res.stderr)
            elif sim.url is not None:
                # call the endpoint
                try:
                    if isinstance(xs, pl.DataFrame):
                        # if xs is a polars dataframe, convert it to a dict
                        xs = xs.to_dict(as_series=False)
                    scheme = urlparse(sim.url.url).scheme
                    if scheme in supported_schemes:
                        # desdeo
                        res = _external_resolver.evaluate(sim.url.url, params, xs)
                        res_df = res_df.hstack(pl.DataFrame(res))
                        # parse res
                    else:
                        # http, https, etc...
                        res = requests.get(sim.url.url, auth=sim.url.auth, json={"d": xs, "p": params})
                        res.raise_for_status()  # raise an error if the request failed
                        res_df = res_df.hstack(pl.DataFrame(res.json()))
                except requests.RequestException as e:
                    raise EvaluatorError(
                        f"Failed to call the simulator at {sim.url}. Is the simulator server running?"
                    ) from e

        # Evaluate the minimization form of the objective functions
        min_obj_columns = pl.DataFrame()
        for symbol, min_max_mult in self.objective_mix_max_mult:
            if symbol in res_df.columns:
                min_obj_columns = min_obj_columns.hstack(
                    res_df.select((min_max_mult * pl.col(f"{symbol}")).alias(f"{symbol}_min"))
                )

        res_df = res_df.hstack(min_obj_columns)
        # If there are scalarization functions, evaluate them as well
        scalarization_columns = res_df.select(*[expr.alias(symbol) for symbol, expr in self.scalarization_funcs])
        return res_df.hstack(scalarization_columns)

    def _evaluate_surrogates(self, xs: dict[str, list[int | float]]) -> pl.DataFrame:
        """Evaluate the problem for the given decision variables using the surrogate models.

        Args:
            xs (dict[str, list[int | float]]): The decision variables for which the functions are to be evaluated.
                Given as a dictionary with the decision variable symbols as keys and a list of decision variable values
                as the values. The length of the lists is the number of samples and each list should have the same
                length (same number of samples).

        Returns:
            pl.DataFrame: The values of the evaluated objectives, constraints and extra functions as a polars
                dataframe. The uncertainty prediction values are also returned. If a model does not provide
                uncertainty predictions, then they are set as NaN.
        """
        res = pl.DataFrame()
        var = np.array([value for _, value in xs.items()]).T  # has to be transpose (at least for sklearn models)
        for symbol in self.surrogates:
            # get a list of args accepted by the model's predict function
            accepted_args = getfullargspec(self.surrogates[symbol].predict).args
            # if "return_std" accepted, gather the uncertainty predictions as well
            if "return_std" in accepted_args:
                value, uncertainty = self.surrogates[symbol].predict(var, return_std=True)
            # otherwise, set the uncertainties as NaN
            else:
                value = self.surrogates[symbol].predict(var)
                uncertainty = np.full(np.shape(value), np.nan)
            # add the objects, constraints and extra functions into the polars dataframe
            # values go into columns with the symbol as the column names
            res = res.with_columns(pl.Series(value).alias(symbol))
            # uncertainties go into columns with {symbol}_uncert as the column names
            res = res.with_columns(pl.Series(uncertainty).alias(f"{symbol}_uncert"))

        # Evaluate the minimization form of the objective functions
        min_obj_columns = pl.DataFrame()
        for symbol, min_max_mult in self.objective_mix_max_mult:
            if symbol in res.columns:
                min_obj_columns = min_obj_columns.hstack(
                    res.select((min_max_mult * pl.col(f"{symbol}")).alias(f"{symbol}_min"))
                )
        res_df = res.hstack(min_obj_columns)
        # If there are scalarization functions, evaluate them as well
        scalarization_columns = res_df.select(*[expr.alias(symbol) for symbol, expr in self.scalarization_funcs])
        return res_df.hstack(scalarization_columns)

    def _load_surrogates(self, surrogate_paths: dict[str, Path] | None = None):
        """Load the surrogate models from disk and store them within the evaluator.

        This is used during initialization of the evaluator or when the analyst wants to replace the current surrogate
        models with other models. However if a new model is trained after initialization of the evaluator, the problem
        JSON should be updated with the new model paths and the evaluator should be re-initialized. This can happen
        with any solver that does model management.

        Args:
            surrogate_paths (dict[str, Path]): A dictionary where the keys are the names of the objectives, constraints
                and extra functions and the values are the paths to the surrogate models saved on disk. The names of
                the objectives should match the names of the objectives in the problem JSON. At the moment the supported
                file format is .skops (through skops.io). TODO: if skops.io used, should be added to pyproject.toml.
        """
        if surrogate_paths is not None:
            for symbol in surrogate_paths:
                with Path.open(f"{surrogate_paths[symbol]}", "rb") as file:
                    self.surrogates[symbol] = joblib.load(file)
                    """unknown_types = sio.get_untrusted_types(file=file)
                    if len(unknown_types) == 0:
                        self.surrogates[symbol] = sio.load(file, unknown_types)
                    else: # TODO: if there are unknown types they should be checked
                        self.surrogates[symbol] = sio.load(file, unknown_types)
                        #raise EvaluatorError(f"Untrusted types found in the model of {obj.symbol}: {unknown_types}")"""
        else:
            # check each surrogate based objective, constraint and extra function for surrogate path
            for obj in self.problem.objectives:
                if obj.surrogates is not None:
                    with Path.open(f"{obj.surrogates[0]}", "rb") as file:
                        self.surrogates[obj.symbol] = joblib.load(file)
                        """unknown_types = sio.get_untrusted_types(file=file)
                        if len(unknown_types) == 0:
                            self.surrogates[obj.symbol] = sio.load(file, unknown_types)
                        else: # TODO: if there are unknown types they should be checked
                            self.surrogates[obj.symbol] = sio.load(file, unknown_types)
                            #raise EvaluatorError(f"Untrusted types found in the model of {obj.symbol}: {unknown_types}")"""
            for con in self.problem.constraints or []:  # if there are no constraints, an empty list is used
                if con.surrogates is not None:
                    with Path.open(f"{con.surrogates[0]}", "rb") as file:
                        self.surrogates[con.symbol] = joblib.load(file)
                        """unknown_types = sio.get_untrusted_types(file=file)
                        if len(unknown_types) == 0:
                            self.surrogates[con.symbol] = sio.load(file, unknown_types)
                        else: # TODO: if there are unknown types they should be checked
                            self.surrogates[con.symbol] = sio.load(file, unknown_types)
                            #raise EvaluatorError(f"Untrusted types found in the model of {obj.symbol}: {unknown_types}")"""
            for extra in self.problem.extra_funcs or []:  # if there are no extra functions, an empty list is used
                if extra.surrogates is not None:
                    with Path.open(f"{extra.surrogates[0]}", "rb") as file:
                        self.surrogates[extra.symbol] = joblib.load(file)
                        """unknown_types = sio.get_untrusted_types(file=file)
                        if len(unknown_types) == 0:
                            self.surrogates[extra.symbol] = sio.load(file, unknown_types)
                        else: # TODO: if there are unknown types they should be checked
                            self.surrogates[extra.symbol] = sio.load(file, unknown_types)
                            #raise EvaluatorError(f"Untrusted types found in the model of {obj.symbol}: {unknown_types}")"""

    def evaluate(self, xs: dict[str, list[int | float]], flat: bool = False) -> pl.DataFrame:
        """Evaluate the functions for the given decision variables.

        Evaluates analytical, simulation based and surrogate based functions. For now, the evaluator assumes that there
        are no data based objectives.

        Args:
            xs (dict[str, list[int | float]]): The decision variables for which the functions are to be evaluated.
                Given as a dictionary with the decision variable symbols as keys and a list of decision variable values
                as the values. The length of the lists is the number of samples and each list should have the same
                length (same number of samples).
            flat (bool, optional): whether the valuation is done using flattened variables or not. Defaults to False.

        Returns:
            pl.DataFrame: polars dataframe with the evaluated function values.
        """
        # TODO (@gialmisi): Make work with polars dataframes as well in addition to dict.
        # See, e.g., PolarsEvaluator._polars_evaluate. Then, remove the arg `flat`.
        res = pl.DataFrame()

        # Evaluate the analytical functions
        if len(self.analytical_symbols + self.data_based_symbols) > 0:
            polars_evaluator = PolarsEvaluator(self.problem, evaluator_mode=PolarsEvaluatorModesEnum.mixed)
            analytical_values = (
                polars_evaluator._polars_evaluate(xs) if not flat else polars_evaluator._polars_evaluate_flat(xs)
            )
            res = res.hstack(analytical_values)

        # Evaluate the simulator based functions
        if len(self.simulator_symbols) > 0:
            simulator_values = self._evaluate_simulator(xs)
            res = res.hstack(simulator_values)

        # Evaluate the surrogate based functions
        if len(self.surrogate_symbols) > 0:
            surrogate_values = self._evaluate_surrogates(xs)
            res = res.hstack(surrogate_values)

        # Check that everything is evaluated
        for symbol in self.problem_symbols:
            if symbol not in res.columns:
                raise EvaluatorError(f"{symbol} not evaluated.")
        return res
__init__
__init__(
    problem: Problem,
    params: dict[str, dict] | ProviderParams | None = None,
    surrogate_paths: dict[str, Path] | None = None,
)

Creating an evaluator for simulator based and surrogate based objectives, constraints and extras.

Parameters:

Name Type Description Default
problem Problem

The problem as a pydantic 'Problem' data class.

required
params dict[str, dict]

Parameters for the different simulators used in the problem. Given as dict with the simulators' symbols as keys and the corresponding simulator parameters as a dict as values. Defaults to None.

None
surrogate_paths dict[str, Path]

A dictionary where the keys are the names of the objectives, constraints and extra functions and the values are the paths to the surrogate models saved on disk. The names of the objectives, constraints and extra functions should match the names of the objectives, constraints and extra functions in the problem JSON. Defaults to None.

None
Source code in desdeo/problem/simulator_evaluator.py
def __init__(  # noqa: PLR0912
    self,
    problem: Problem,
    params: dict[str, dict] | ProviderParams | None = None,
    surrogate_paths: dict[str, Path] | None = None,
):
    """Creating an evaluator for simulator based and surrogate based objectives, constraints and extras.

    Args:
        problem (Problem): The problem as a pydantic 'Problem' data class.
        params (dict[str, dict], optional): Parameters for the different simulators used in the problem.
            Given as dict with the simulators' symbols as keys and the corresponding simulator parameters
            as a dict as values. Defaults to None.
        surrogate_paths (dict[str, Path], optional): A dictionary where the keys are the names of the objectives,
            constraints and extra functions and the values are the paths to the surrogate models saved on disk.
            The names of the objectives, constraints and extra functions should match the names of the objectives,
            constraints and extra functions in the problem JSON. Defaults to None.
    """
    self.problem = problem
    # store the symbol and min or max multiplier as well (symbol, min/max multiplier [1 | -1])
    self.objective_mix_max_mult = [
        (objective.symbol, -1 if objective.maximize else 1) for objective in problem.objectives
    ]
    # Gather symbols objectives of different types into their own lists
    self.analytical_symbols = [
        obj.symbol
        for obj in list(filter(lambda x: x.objective_type == ObjectiveTypeEnum.analytical, problem.objectives))
    ]
    self.data_based_symbols = [
        obj.symbol for obj in problem.objectives if obj.objective_type == ObjectiveTypeEnum.data_based
    ]
    self.simulator_symbols = [
        obj.symbol
        for obj in list(filter(lambda x: x.objective_type == ObjectiveTypeEnum.simulator, problem.objectives))
    ]
    self.surrogate_symbols = [
        obj.symbol
        for obj in list(filter(lambda x: x.objective_type == ObjectiveTypeEnum.surrogate, problem.objectives))
    ]
    if problem.scalarization_funcs is not None:
        parser = MathParser()
        self.scalarization_funcs = [
            (func.symbol, parser.parse(func.func))
            for func in problem.scalarization_funcs
            if func.symbol is not None
        ]
    else:
        self.scalarization_funcs = []
    # Gather any constraints' symbols
    if problem.constraints is not None:
        self.analytical_symbols = self.analytical_symbols + [
            con.symbol for con in list(filter(lambda x: x.func is not None, problem.constraints))
        ]
        self.simulator_symbols = self.simulator_symbols + [
            con.symbol for con in list(filter(lambda x: x.simulator_path is not None, problem.constraints))
        ]
        self.surrogate_symbols = self.surrogate_symbols + [
            con.symbol for con in list(filter(lambda x: x.surrogates is not None, problem.constraints))
        ]

    # Gather any extra functions' symbols
    if problem.extra_funcs is not None:
        self.analytical_symbols = self.analytical_symbols + [
            extra.symbol for extra in list(filter(lambda x: x.func is not None, problem.extra_funcs))
        ]
        self.simulator_symbols = self.simulator_symbols + [
            extra.symbol for extra in list(filter(lambda x: x.simulator_path is not None, problem.extra_funcs))
        ]
        self.surrogate_symbols = self.surrogate_symbols + [
            extra.symbol for extra in list(filter(lambda x: x.surrogates is not None, problem.extra_funcs))
        ]

    # Gather all the symbols of objectives, constraints and extra functions
    self.problem_symbols = (
        self.analytical_symbols + self.data_based_symbols + self.simulator_symbols + self.surrogate_symbols
    )

    # Gather the possible simulators
    self.simulators = problem.simulators if problem.simulators is not None else []

    # Gather the possibly given parameters
    if params and not isinstance(params, dict):
        params = params.model_dump()
    self.params = {}
    for sim in self.simulators:
        sim_params = params.get(sim.name, {}) if params is not None else {}
        if sim.parameter_options is not None:
            for key in sim.parameter_options:
                sim_params[key] = sim.parameter_options[key]
        self.params[sim.name] = sim_params

    self.surrogates = {}
    if surrogate_paths is not None:
        self._load_surrogates(surrogate_paths)
    else:
        self._load_surrogates()

    if len(self.surrogate_symbols) > 0:
        missing_surrogates = []
        for symbol in self.surrogate_symbols:
            if symbol not in self.surrogates:
                missing_surrogates.append(symbol)

        if len(missing_surrogates) > 0:
            raise EvaluatorError(f"Some surrogates missing: {missing_surrogates}.")
_evaluate_simulator
_evaluate_simulator(
    xs: dict[str, list[int | float]],
) -> pl.DataFrame

Evaluate the problem for the given decision variables using the problem's simulators.

Parameters:

Name Type Description Default
xs dict[str, list[int | float]]

The decision variables for which the functions are to be evaluated. Given as a dictionary with the decision variable symbols as keys and a list of decision variable values as the values. The length of the lists is the number of samples and each list should have the same length (same number of samples).

required

Returns:

Type Description
DataFrame

pl.DataFrame: The objective, constraint and extra function values for the given decision variables as a polars dataframe. The symbols of the objectives, constraints and extra functions are the column names and the length of the columns is the number of samples. Will return those objective, constraint and extra function values that are gained from simulators listed in the problem object.

Source code in desdeo/problem/simulator_evaluator.py
def _evaluate_simulator(self, xs: dict[str, list[int | float]]) -> pl.DataFrame:
    """Evaluate the problem for the given decision variables using the problem's simulators.

    Args:
        xs (dict[str, list[int | float]]): The decision variables for which the functions are to be evaluated.
            Given as a dictionary with the decision variable symbols as keys and a list of decision variable values
            as the values. The length of the lists is the number of samples and each list should have the same
            length (same number of samples).

    Returns:
        pl.DataFrame: The objective, constraint and extra function values for the given decision variables as
            a polars dataframe. The symbols of the objectives, constraints and extra functions are the column names
            and the length of the columns is the number of samples. Will return those objective, constraint and
            extra function values that are gained from simulators listed in the problem object.
    """
    res_df = pl.DataFrame()
    for sim in self.simulators:
        # gather the possible parameters for the simulator
        params = self.params.get(sim.name, {})
        if sim.file is not None:
            # call the simulator with the decision variable values and parameters as dicts
            res = subprocess.run(
                [sys.executable, sim.file, "-d", str(xs), "-p", str(params)], capture_output=True, text=True
            )
            if res.returncode == 0:
                # gather the simulation results (a dict) into the results dataframe
                res_df = res_df.hstack(pl.DataFrame(json.loads(res.stdout)))
            else:
                raise EvaluatorError(res.stderr)
        elif sim.url is not None:
            # call the endpoint
            try:
                if isinstance(xs, pl.DataFrame):
                    # if xs is a polars dataframe, convert it to a dict
                    xs = xs.to_dict(as_series=False)
                scheme = urlparse(sim.url.url).scheme
                if scheme in supported_schemes:
                    # desdeo
                    res = _external_resolver.evaluate(sim.url.url, params, xs)
                    res_df = res_df.hstack(pl.DataFrame(res))
                    # parse res
                else:
                    # http, https, etc...
                    res = requests.get(sim.url.url, auth=sim.url.auth, json={"d": xs, "p": params})
                    res.raise_for_status()  # raise an error if the request failed
                    res_df = res_df.hstack(pl.DataFrame(res.json()))
            except requests.RequestException as e:
                raise EvaluatorError(
                    f"Failed to call the simulator at {sim.url}. Is the simulator server running?"
                ) from e

    # Evaluate the minimization form of the objective functions
    min_obj_columns = pl.DataFrame()
    for symbol, min_max_mult in self.objective_mix_max_mult:
        if symbol in res_df.columns:
            min_obj_columns = min_obj_columns.hstack(
                res_df.select((min_max_mult * pl.col(f"{symbol}")).alias(f"{symbol}_min"))
            )

    res_df = res_df.hstack(min_obj_columns)
    # If there are scalarization functions, evaluate them as well
    scalarization_columns = res_df.select(*[expr.alias(symbol) for symbol, expr in self.scalarization_funcs])
    return res_df.hstack(scalarization_columns)
_evaluate_surrogates
_evaluate_surrogates(
    xs: dict[str, list[int | float]],
) -> pl.DataFrame

Evaluate the problem for the given decision variables using the surrogate models.

Parameters:

Name Type Description Default
xs dict[str, list[int | float]]

The decision variables for which the functions are to be evaluated. Given as a dictionary with the decision variable symbols as keys and a list of decision variable values as the values. The length of the lists is the number of samples and each list should have the same length (same number of samples).

required

Returns:

Type Description
DataFrame

pl.DataFrame: The values of the evaluated objectives, constraints and extra functions as a polars dataframe. The uncertainty prediction values are also returned. If a model does not provide uncertainty predictions, then they are set as NaN.

Source code in desdeo/problem/simulator_evaluator.py
def _evaluate_surrogates(self, xs: dict[str, list[int | float]]) -> pl.DataFrame:
    """Evaluate the problem for the given decision variables using the surrogate models.

    Args:
        xs (dict[str, list[int | float]]): The decision variables for which the functions are to be evaluated.
            Given as a dictionary with the decision variable symbols as keys and a list of decision variable values
            as the values. The length of the lists is the number of samples and each list should have the same
            length (same number of samples).

    Returns:
        pl.DataFrame: The values of the evaluated objectives, constraints and extra functions as a polars
            dataframe. The uncertainty prediction values are also returned. If a model does not provide
            uncertainty predictions, then they are set as NaN.
    """
    res = pl.DataFrame()
    var = np.array([value for _, value in xs.items()]).T  # has to be transpose (at least for sklearn models)
    for symbol in self.surrogates:
        # get a list of args accepted by the model's predict function
        accepted_args = getfullargspec(self.surrogates[symbol].predict).args
        # if "return_std" accepted, gather the uncertainty predictions as well
        if "return_std" in accepted_args:
            value, uncertainty = self.surrogates[symbol].predict(var, return_std=True)
        # otherwise, set the uncertainties as NaN
        else:
            value = self.surrogates[symbol].predict(var)
            uncertainty = np.full(np.shape(value), np.nan)
        # add the objects, constraints and extra functions into the polars dataframe
        # values go into columns with the symbol as the column names
        res = res.with_columns(pl.Series(value).alias(symbol))
        # uncertainties go into columns with {symbol}_uncert as the column names
        res = res.with_columns(pl.Series(uncertainty).alias(f"{symbol}_uncert"))

    # Evaluate the minimization form of the objective functions
    min_obj_columns = pl.DataFrame()
    for symbol, min_max_mult in self.objective_mix_max_mult:
        if symbol in res.columns:
            min_obj_columns = min_obj_columns.hstack(
                res.select((min_max_mult * pl.col(f"{symbol}")).alias(f"{symbol}_min"))
            )
    res_df = res.hstack(min_obj_columns)
    # If there are scalarization functions, evaluate them as well
    scalarization_columns = res_df.select(*[expr.alias(symbol) for symbol, expr in self.scalarization_funcs])
    return res_df.hstack(scalarization_columns)
_load_surrogates
_load_surrogates(
    surrogate_paths: dict[str, Path] | None = None,
)

Load the surrogate models from disk and store them within the evaluator.

This is used during initialization of the evaluator or when the analyst wants to replace the current surrogate models with other models. However if a new model is trained after initialization of the evaluator, the problem JSON should be updated with the new model paths and the evaluator should be re-initialized. This can happen with any solver that does model management.

Parameters:

Name Type Description Default
surrogate_paths dict[str, Path]

A dictionary where the keys are the names of the objectives, constraints and extra functions and the values are the paths to the surrogate models saved on disk. The names of the objectives should match the names of the objectives in the problem JSON. At the moment the supported file format is .skops (through skops.io). TODO: if skops.io used, should be added to pyproject.toml.

None
Source code in desdeo/problem/simulator_evaluator.py
def _load_surrogates(self, surrogate_paths: dict[str, Path] | None = None):
    """Load the surrogate models from disk and store them within the evaluator.

    This is used during initialization of the evaluator or when the analyst wants to replace the current surrogate
    models with other models. However if a new model is trained after initialization of the evaluator, the problem
    JSON should be updated with the new model paths and the evaluator should be re-initialized. This can happen
    with any solver that does model management.

    Args:
        surrogate_paths (dict[str, Path]): A dictionary where the keys are the names of the objectives, constraints
            and extra functions and the values are the paths to the surrogate models saved on disk. The names of
            the objectives should match the names of the objectives in the problem JSON. At the moment the supported
            file format is .skops (through skops.io). TODO: if skops.io used, should be added to pyproject.toml.
    """
    if surrogate_paths is not None:
        for symbol in surrogate_paths:
            with Path.open(f"{surrogate_paths[symbol]}", "rb") as file:
                self.surrogates[symbol] = joblib.load(file)
                """unknown_types = sio.get_untrusted_types(file=file)
                if len(unknown_types) == 0:
                    self.surrogates[symbol] = sio.load(file, unknown_types)
                else: # TODO: if there are unknown types they should be checked
                    self.surrogates[symbol] = sio.load(file, unknown_types)
                    #raise EvaluatorError(f"Untrusted types found in the model of {obj.symbol}: {unknown_types}")"""
    else:
        # check each surrogate based objective, constraint and extra function for surrogate path
        for obj in self.problem.objectives:
            if obj.surrogates is not None:
                with Path.open(f"{obj.surrogates[0]}", "rb") as file:
                    self.surrogates[obj.symbol] = joblib.load(file)
                    """unknown_types = sio.get_untrusted_types(file=file)
                    if len(unknown_types) == 0:
                        self.surrogates[obj.symbol] = sio.load(file, unknown_types)
                    else: # TODO: if there are unknown types they should be checked
                        self.surrogates[obj.symbol] = sio.load(file, unknown_types)
                        #raise EvaluatorError(f"Untrusted types found in the model of {obj.symbol}: {unknown_types}")"""
        for con in self.problem.constraints or []:  # if there are no constraints, an empty list is used
            if con.surrogates is not None:
                with Path.open(f"{con.surrogates[0]}", "rb") as file:
                    self.surrogates[con.symbol] = joblib.load(file)
                    """unknown_types = sio.get_untrusted_types(file=file)
                    if len(unknown_types) == 0:
                        self.surrogates[con.symbol] = sio.load(file, unknown_types)
                    else: # TODO: if there are unknown types they should be checked
                        self.surrogates[con.symbol] = sio.load(file, unknown_types)
                        #raise EvaluatorError(f"Untrusted types found in the model of {obj.symbol}: {unknown_types}")"""
        for extra in self.problem.extra_funcs or []:  # if there are no extra functions, an empty list is used
            if extra.surrogates is not None:
                with Path.open(f"{extra.surrogates[0]}", "rb") as file:
                    self.surrogates[extra.symbol] = joblib.load(file)
                    """unknown_types = sio.get_untrusted_types(file=file)
                    if len(unknown_types) == 0:
                        self.surrogates[extra.symbol] = sio.load(file, unknown_types)
                    else: # TODO: if there are unknown types they should be checked
                        self.surrogates[extra.symbol] = sio.load(file, unknown_types)
                        #raise EvaluatorError(f"Untrusted types found in the model of {obj.symbol}: {unknown_types}")"""
evaluate
evaluate(
    xs: dict[str, list[int | float]], flat: bool = False
) -> pl.DataFrame

Evaluate the functions for the given decision variables.

Evaluates analytical, simulation based and surrogate based functions. For now, the evaluator assumes that there are no data based objectives.

Parameters:

Name Type Description Default
xs dict[str, list[int | float]]

The decision variables for which the functions are to be evaluated. Given as a dictionary with the decision variable symbols as keys and a list of decision variable values as the values. The length of the lists is the number of samples and each list should have the same length (same number of samples).

required
flat bool

whether the valuation is done using flattened variables or not. Defaults to False.

False

Returns:

Type Description
DataFrame

pl.DataFrame: polars dataframe with the evaluated function values.

Source code in desdeo/problem/simulator_evaluator.py
def evaluate(self, xs: dict[str, list[int | float]], flat: bool = False) -> pl.DataFrame:
    """Evaluate the functions for the given decision variables.

    Evaluates analytical, simulation based and surrogate based functions. For now, the evaluator assumes that there
    are no data based objectives.

    Args:
        xs (dict[str, list[int | float]]): The decision variables for which the functions are to be evaluated.
            Given as a dictionary with the decision variable symbols as keys and a list of decision variable values
            as the values. The length of the lists is the number of samples and each list should have the same
            length (same number of samples).
        flat (bool, optional): whether the valuation is done using flattened variables or not. Defaults to False.

    Returns:
        pl.DataFrame: polars dataframe with the evaluated function values.
    """
    # TODO (@gialmisi): Make work with polars dataframes as well in addition to dict.
    # See, e.g., PolarsEvaluator._polars_evaluate. Then, remove the arg `flat`.
    res = pl.DataFrame()

    # Evaluate the analytical functions
    if len(self.analytical_symbols + self.data_based_symbols) > 0:
        polars_evaluator = PolarsEvaluator(self.problem, evaluator_mode=PolarsEvaluatorModesEnum.mixed)
        analytical_values = (
            polars_evaluator._polars_evaluate(xs) if not flat else polars_evaluator._polars_evaluate_flat(xs)
        )
        res = res.hstack(analytical_values)

    # Evaluate the simulator based functions
    if len(self.simulator_symbols) > 0:
        simulator_values = self._evaluate_simulator(xs)
        res = res.hstack(simulator_values)

    # Evaluate the surrogate based functions
    if len(self.surrogate_symbols) > 0:
        surrogate_values = self._evaluate_surrogates(xs)
        res = res.hstack(surrogate_values)

    # Check that everything is evaluated
    for symbol in self.problem_symbols:
        if symbol not in res.columns:
            raise EvaluatorError(f"{symbol} not evaluated.")
    return res

Utilities

desdeo.problem.utils

Various utilities used across the framework related to the Problem formulation.

ProblemUtilsError

Bases: Exception

Raised when an exception occurs in one of the utils function.

Raised when an exception occurs in one of the utils functions defined in the Problem module.

Source code in desdeo/problem/utils.py
class ProblemUtilsError(Exception):
    """Raised when an exception occurs in one of the utils function.

    Raised when an exception occurs in one of the utils functions defined in the Problem module.
    """

flatten_variable_dict

flatten_variable_dict(
    problem: Problem, variable_dict: dict[str, float | list]
) -> np.ndarray

Flatten a dictionary representing variable values of an instance of Problem into a numpy array.

Flattens a dictionary representing variable values of an instance of Problem into a numpy array. The flattening follows a C-like order. Support the flattening of both Variable and TensorVariable types. Note that it is assumed that no more than one value is defined for each symbol in variable_dict that correspond to the required shape of the underlying variable type.

Parameters:

Name Type Description Default
problem Problem

the problem instance the decision variables are associated with.

required
variable_dict dict[str, float | list]

a dictionary with its keys being the symbols of the variables defined in the instance of Problem, and the values corresponding to the variables' values.

required

Raises:

Type Description
ValueError

the variable_dict does not contain as its keys one or more of the symbols defined for the variables in the instance of Problem.

TypeError

unsupported variable type encountered in the variables defined in the instance of Problem.

Returns:

Type Description
ndarray

np.ndarray: a 1D numpy array with the variable values unflattened in C-like order.

Source code in desdeo/problem/utils.py
def flatten_variable_dict(problem: Problem, variable_dict: dict[str, float | list]) -> np.ndarray:
    """Flatten a dictionary representing variable values of an instance of `Problem` into a numpy array.

    Flattens a dictionary representing variable values of an instance of `Problem` into a numpy array.
    The flattening follows a C-like order. Support the flattening of both `Variable` and `TensorVariable`
    types. Note that it is assumed that no more than one value is defined for each symbol in `variable_dict`
    that correspond to the required shape of the underlying variable type.

    Args:
        problem (Problem): the problem instance the decision variables are associated with.
        variable_dict (dict[str, float  |  list]): a dictionary with its keys being the symbols
            of the variables defined in the instance of `Problem`, and the values corresponding
            to the variables' values.

    Raises:
        ValueError: the `variable_dict` does not contain as its keys one or more of the symbols
            defined for the variables in the instance of `Problem`.
        TypeError: unsupported variable type encountered in the variables defined in the
            instance of `Problem`.

    Returns:
        np.ndarray: a 1D numpy array with the variable values unflattened in C-like order.
    """
    tmp = []
    for var in problem.variables:
        if isinstance(var, Variable):
            # just a regular variable
            if var.symbol not in variable_dict:
                msg = f"The variable_dict is missing values for the variable {var.symbol}."
                raise ValueError(msg)
            tmp.append([variable_dict[var.symbol]])
            continue

        if isinstance(var, TensorVariable):
            # tensor variable
            if var.symbol in variable_dict:
                # tensor variable is defined in the dict as a tensor
                tmp = [*tmp, np.array(variable_dict[var.symbol]).flatten(order="C")]
                continue
            if any(key.startswith(f"{var.symbol}_") for key in variable_dict):
                # tensor variable flattened in the dict
                indices = itertools.product(*[range(1, s + 1) for s in var.shape])
                flat_symbols = [f"{var.symbol}_{'_'.join(map(str, index))}" for index in indices]
                tmp = [*tmp, np.array([variable_dict[s] for s in flat_symbols])]
                continue

            msg = f"The variable dict is missing values for the variable {var.symbol}."
            raise ValueError(msg)

        msg = f"Unsupported variable type {type(var)} encountered."
        raise TypeError(msg)

    return np.concatenate(tmp)

get_ideal_dict

get_ideal_dict(problem: Problem) -> dict[str, float]

Return a dict representing a problem's ideal point.

Parameters:

Name Type Description Default
problem Problem

the problem with the ideal point.

required

Returns:

Type Description
dict[str, float]

dict[str, float]: key are objective funciton symbols, values are ideal values.

Source code in desdeo/problem/utils.py
def get_ideal_dict(problem: Problem) -> dict[str, float]:
    """Return a dict representing a problem's ideal point.

    Args:
        problem (Problem): the problem with the ideal point.

    Returns:
        dict[str, float]: key are objective funciton symbols, values are ideal values.
    """
    return {objective.symbol: objective.ideal for objective in problem.objectives}

get_nadir_dict

get_nadir_dict(problem: Problem) -> dict[str, float]

Return a dict representing a problem's nadir point.

Parameters:

Name Type Description Default
problem Problem

the problem with the nadir point.

required

Returns:

Type Description
dict[str, float]

dict[str, float]: key are objective funciton symbols, values are nadir values.

Source code in desdeo/problem/utils.py
def get_nadir_dict(problem: Problem) -> dict[str, float]:
    """Return a dict representing a problem's nadir point.

    Args:
        problem (Problem): the problem with the nadir point.

    Returns:
        dict[str, float]: key are objective funciton symbols, values are nadir values.
    """
    return {objective.symbol: objective.nadir for objective in problem.objectives}

numpy_array_to_objective_dict

numpy_array_to_objective_dict(
    problem: Problem, numpy_array: ndarray
) -> dict[str, float]

Takes a numpy array with objective function values and return a dict.

The reverse of objective_dict_to_numpy_array.

Parameters:

Name Type Description Default
problem Problem

the problem the numpy array represents an objective vector of.

required
numpy_array ndarray

the objective vector as a numpy array. The array is squeezed, i.e., axes or length one are removed: [[42]] -> [42].

required

Returns:

Type Description
dict[str, float]

dict[str, float]: a dict with keys being objective function symbols and value being objective function values.

Source code in desdeo/problem/utils.py
def numpy_array_to_objective_dict(problem: Problem, numpy_array: np.ndarray) -> dict[str, float]:
    """Takes a numpy array with objective function values and return a dict.

    The reverse of objective_dict_to_numpy_array.

    Args:
        problem (Problem): the problem the numpy array represents an objective vector of.
        numpy_array (np.ndarray): the objective vector as a numpy array. The
            array is squeezed, i.e., axes or length one are removed: [[42]] -> [42].

    Returns:
        dict[str, float]: a dict with keys being objective function symbols and value being
            objective function values.
    """
    return {objective.symbol: np.squeeze(numpy_array).tolist()[i] for i, objective in enumerate(problem.objectives)}

objective_dict_to_numpy_array

objective_dict_to_numpy_array(
    problem: Problem, objective_dict: dict[str, float]
) -> np.ndarray

Takes a dict with an objective vector and returns a numpy array.

Takes a dict with the keys being objective function symbols and the values being the corresponding objective function values. Returns a numpy array with the objective function values in the same order they have been defined in the original problem.

Parameters:

Name Type Description Default
problem Problem

the problem the objective dict belongs to.

required
objective_dict dict[str, float]

the dict with the objective function values.

required

Returns:

Type Description
ndarray

np.ndarray: a numpy array with the objective function values in the order they are present in problem.

Source code in desdeo/problem/utils.py
def objective_dict_to_numpy_array(problem: Problem, objective_dict: dict[str, float]) -> np.ndarray:
    """Takes a dict with an objective vector and returns a numpy array.

    Takes a dict with the keys being objective function symbols and the values
    being the corresponding objective function values. Returns a numpy array
    with the objective function values in the same order they have been defined
    in the original problem.

    Args:
        problem (Problem): the problem the objective dict belongs to.
        objective_dict (dict[str, float]): the dict with the objective function values.

    Returns:
        np.ndarray: a numpy array with the objective function values in the order they are
            present in problem.
    """
    if isinstance(objective_dict[problem.objectives[0].symbol], list):
        if len(objective_dict[problem.objectives[0].symbol]) != 1:
            raise ValueError("The objective_dict has multiple values for an objective function")
        return np.array([objective_dict[objective.symbol][0] for objective in problem.objectives])
    return np.array([objective_dict[objective.symbol] for objective in problem.objectives])

tensor_constant_from_dataframe

tensor_constant_from_dataframe(
    df: DataFrame,
    name: str,
    symbol: str,
    n_rows: int,
    column_names: list[str],
) -> TensorConstant

Create a TensorConstant from a Polars dataframe.

Parameters:

Name Type Description Default
df DataFrame

a Polars dataframe with at least the columns in column_names

required
name str

name attribute of the created TensorConstant.

required
symbol str

symbol attribute of the created TensorConstant.

required
n_rows int

the number of rows to read from the dataframe.

required
column_names list[str]

the column names in the dataframe from which the constant values will be picked.

required

Returns:

Name Type Description
TensorConstant TensorConstant

A TensorConstant instance with values taken from the a given Polars dataframe. The shape of the TensorConstant will be (n_rows, len(column_names)).

Note

In the argument shape the first element must be either less or equal to the number of rows in df. The second element in shape must be equal to the number of element in column_names.

Source code in desdeo/problem/utils.py
def tensor_constant_from_dataframe(
    df: pl.DataFrame, name: str, symbol: str, n_rows: int, column_names: list[str]
) -> TensorConstant:
    """Create a TensorConstant from a Polars dataframe.

    Args:
        df (pl.DataFrame): a Polars dataframe with at least the columns in `column_names`
        name (str): name attribute of the created TensorConstant.
        symbol (str): symbol attribute of the created TensorConstant.
        n_rows (int): the number of rows to read from the dataframe.
        column_names (list[str]): the column names in the dataframe from which
            the constant values will be picked.

    Returns:
        TensorConstant: A TensorConstant instance with values taken from the a given
            Polars dataframe. The shape of the TensorConstant will be
            (`n_rows`, len(`column_names`)).

    Note:
        In the argument `shape` the first element must be either less or equal to the
            number of rows in `df`. The second element in `shape` must be equal
            to the number of element in `column_names`.
    """
    if n_rows > df.shape[0]:
        # not enough rows in df
        msg = f"Requested {n_rows} rows, but the dataframe has only {df.shape[0]} rows."
        raise ProblemUtilsError(msg)

    if len(column_names) > df.shape[1]:
        # not enough cols in df
        msg = f"Requested {len(column_names)} columns, but the dataframe has only {df.shape[1]} columns."
        raise ProblemUtilsError(msg)

    for col in column_names:
        if col not in df.columns:
            msg = f"The requested column '{col}' is not found in the given dataframe with columns {df.columns}."
            raise ProblemUtilsError(msg)

    selected_df = df.select(column_names).head(n_rows)
    selected_values = [selected_df.to_dict()[col_name].to_list() for col_name in column_names]

    return TensorConstant(name=name, symbol=symbol, shape=[n_rows, len(column_names)], values=selected_values)

unflatten_variable_array

unflatten_variable_array(
    problem: Problem, var_array: ndarray
) -> dict[str, float | list]

Unflatten a numpy array representing decision variable values.

Unflatten a numpy array that represent decision variable values. It is assumed that the unflattened values follow a C-like order when it comes to unflattening values for TensorVariables. Note that var_array must be of dimension 1.

Parameters:

Name Type Description Default
problem Problem

the problem instance the decision variables are associated with.

required
var_array ndarray

a flat 1D array of numerical values representing decision variable values.

required

Raises:

Type Description
ValueError

var_array is of some other dimension that 1.

IndexError

var_array has too few elements given the variables defined in the instance of Problem.

TypeError

unsupported variable type encountered in the variables defined in the instance of Problem.

Returns:

Type Description
dict[str, float | list]

dict[str, float | list]: a dict with keys equal to the symbols of the variables defined in the Problem instance, and values equal to the decision variable values as they were defined in var_array.

Source code in desdeo/problem/utils.py
def unflatten_variable_array(problem: Problem, var_array: np.ndarray) -> dict[str, float | list]:
    """Unflatten a numpy array representing decision variable values.

    Unflatten a numpy array that represent decision variable values. It is assumed
    that the unflattened values follow a C-like order when it comes to unflattening
    values for `TensorVariable`s. Note that `var_array` must be of dimension 1.

    Args:
        problem (Problem): the problem instance the decision variables are associated with.
        var_array (np.ndarray): a flat 1D array of numerical values representing
            decision variable values.

    Raises:
        ValueError: `var_array` is of some other dimension that 1.
        IndexError: `var_array` has too few elements given the variables defined in
            the instance of `Problem`.
        TypeError: unsupported variable type encountered in the variables defined in the
            instance of `Problem`.

    Returns:
        dict[str, float | list]: a dict with keys equal to the symbols of the variables
            defined in the `Problem` instance, and values equal to the decision variable
            values as they were defined in `var_array`.
    """
    if (dimension := var_array.ndim) != 1:
        msg = f"The given variable array must have a dimension of 1. Current {dimension=}"
        raise ValueError(msg)

    var_dict = {}
    array_i = 0
    for var in problem.variables:
        if array_i >= len(var_array):
            msg = (
                "End of variable array reached before all variables in the problem were iterated over. "
                f"The variable array is too short with length={len(var_array)}."
            )
            raise IndexError(msg)

        if isinstance(var, Variable):
            # regular variable, just pick it
            var_dict[var.symbol] = var_array[array_i].item()
            array_i += 1
            continue

        if isinstance(var, TensorVariable):
            # tensor variable, pick row-wise from var_array
            slice_length = reduce(lambda x1, x2: x1 * x2, var.shape)  # product of dimensions
            flat_values = var_array[array_i : array_i + slice_length]
            var_dict[var.symbol] = np.reshape(flat_values, var.shape, order="C").tolist()
            array_i += slice_length
            continue

        msg = f"Unsupported variable type {type(var)} encountered."
        raise TypeError(msg)

    # check if values remain in var_array
    if array_i < len(var_array):
        # some values remain, warn user, but do not raise an error
        msg = f"Warning, the variable array had some values that were not unflattened: f{['...', *var_array[array_i:]]}"
        warnings.warn(msg, stacklevel=2)

    # return the variable dict
    return var_dict