Skip to content

problem

Test problems

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 the 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)

Returns:

Name Type Description
Problem Problem

A problem instance representing the Binh and Korn problem.

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 the 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).

    Returns:
        Problem: A problem instance representing the Binh and Korn problem.

    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,
    )

car_side_impact

car_side_impact(three_obj: bool = True) -> Problem

The Car-side impact problem with two possible version of the problem with three or four objectives.

The constraints and objective functions for the car side-impact problem are defined as follows:

\[\begin{align} \min\quad f_1(\mathbf{x}) &= 1.98 + 4.9x_1 + 6.67x_2 + 6.98x_3 + 4.01x_4 + 1.78x_5 + 0.00001x_6 + 2.73x_7 \\ \min\quad f_2(\mathbf{x}) &= 4.72 - 0.5x_4 - 0.19x_2x_3 \\ \min\quad f_3(\mathbf{x}) &= 0.5\,(V_{MBP} + V_{FD}) \\ \min\quad f_4(\mathbf{x}) &= \max(1.16 - 0.3717x_2x_4 - 0.0092928x_3 - 1.0, 0) \\ &+ \max(0.261 - 0.0159x_1x_2 - 0.06486x_1 - 0.019x_2x_7 + 0.0144x_3x_5 + 0.0154464x_6 - 0.32, 0) \\ &+ \max(0.214 + 0.00817x_5 - 0.045195x_1 - 0.0135168x_1 + 0.03099x_2x_6 - 0.018x_2x_7 + 0.007176x_3 + 0.023232x_3 - 0.00364x_5x_6 - 0.018x_2^2 - 0.32, 0) \\ &+ \max(0.74 - 0.61x_2 - 0.031296x_3 - 0.031872x_7 + 0.227x_2^2 - 0.32,0) \\ &+ \max(28.98 + 3.818x_3 - 4.2x_1x_2 + 1.27296x_6 - 2.68065x_7 - 32.0,0) \\ &+ \max(33.86 + 2.95x_3 - 5.057x_1x_2 - 3.795x_2 - 3.4431x_7 + 1.45728 - 32.0,0) \\ &+ \max(46.36 - 9.9x_2 - 4.4505x_1 - 32.0,0) \\ &+ \max(4.72 - 0.5x_4 - 0.19x_2x_3 - 4.0, 0) \\ &+ \max(10.58 - 0.674x_1x_2 - 0.67275x_2 - 9.9, 0) \\ &+ \max(16.45 - 0.489x_3x_7 - 0.843x_5x_6 - 15.7, 0) \\ g_1(\mathbf{x}) &= 1.16 - 0.3717x_2x_4 - 0.0092928x_3 \leq 1.0 \\ g_2(\mathbf{x}) &= 0.261 - 0.0159x_1x_2 - 0.06486x_1 - 0.019x_2x_7 + 0.0144x_3x_5 + 0.0154464x_6 \leq 0.32 \\ g_3(\mathbf{x}) &= 0.214 + 0.00817x_5 - 0.045195x_1 - 0.0135168x_1 + 0.03099x_2x_6 - 0.018x_2x_7 + 0.007176x_3 + 0.023232x_3 - 0.00364x_5x_6 - 0.018x_2^2 \leq 0.32 \\ g_4(\mathbf{x}) &= 0.74 - 0.61x_2 - 0.031296x_3 - 0.031872x_7 + 0.227x_2^2 \leq 0.32 \\ g_5(\mathbf{x}) &= 28.98 + 3.818x_3 - 4.2x_1x_2 + 1.27296x_6 - 2.68065x_7 \leq 32.0 \\ g_6(\mathbf{x}) &= 33.86 + 2.95x_3 - 5.057x_1x_2 - 3.795x_2 - 3.4431x_7 + 1.45728 \leq 32.0 \\ g_7(\mathbf{x}) &= 46.36 - 9.9x_2 - 4.4505x_1 \leq 32.0 \\ g_8(\mathbf{x}) &= 4.72 - 0.5x_4 - 0.19x_2x_3 \leq 4.0 \\ g_9(\mathbf{x}) &= 10.58 - 0.674x_1x_2 - 0.67275x_2 \leq 9.9 \\ g_{10}(\mathbf{x}) &= 16.45 - 0.489x_3x_7 - 0.843x_5x_6 \leq 15.7 \\ &\mathbf{x}^{L} \leq \mathbf{x} \leq \mathbf{x}^{U}, \end{align}\]

where

\[ V_{MBP} = 10.58 - 0.674x_1x_2 - 0.67275x_2, \]
\[ V_{FD} = 16.45 - 0.489x_3x_7 - 0.843x_5x_6, \]
\[ \mathbf{x}^{L} = (0.5,\,0.45,\,0.5,\,0.5,\,0.875,\,0.4,\,0.4), \]
\[ \mathbf{x}^{U} = (1.5,\,1.35,\,1.5,\,1.5,\,2.625,\,1.2,\,1.2). \]

Parameters:

Name Type Description Default
three_obj bool

If true, utilize the three objectives version (objectives \(f_1\)-\(f_3\)). If false, utilizes the fourth objective (\(f_4\)) as well. Defaults to true (three objectives).

True

Returns:

Name Type Description
Problem Problem

A problem instance representing the Car-side impact problem.

References

Jain, H., & Deb, K. (2014). An evolutionary many-objective optimization algorithm using reference-point based nondominated sorting approach, part II. IEEE TEVC, 18(4), 602-622.

Three objective problem from:

Jain, H. & Deb, K. (2014). An Evolutionary Many-Objective Optimization Algorithm Using Reference-Point Based Nondominated Sorting Approach, Part II: Handling Constraints and Extending to an Adaptive Approach. IEEE transactions on evolutionary computation, 18(4), 602-622.

Optional fourth objective from:

Tanabe, R. & Ishibuchi, H. (2020). An easy-to-use real-world multi-objective optimization problem suite. Applied soft computing, 89, 106078.

Variable names from:

Deb, K., Gupta, S., Daum, D., Branke, J., Mall, A. & Padmanabhan, D. (2009). Reliability-Based Optimization Using Evolutionary Algorithms. IEEE transactions on evolutionary computation, 13(5), 1054-1074.

Source code in desdeo/problem/testproblems/car_side_impact_problem.py
def car_side_impact(three_obj: bool = True) -> Problem:
    r"""The Car-side impact problem with two possible version of the problem with three or four objectives.

    The constraints and objective functions for the car side-impact problem are defined as follows:

    \begin{align}
        \min\quad f_1(\mathbf{x})
        &= 1.98 + 4.9x_1 + 6.67x_2 + 6.98x_3 + 4.01x_4
        + 1.78x_5 + 0.00001x_6 + 2.73x_7 \\
        \min\quad f_2(\mathbf{x})
        &= 4.72 - 0.5x_4 - 0.19x_2x_3 \\
        \min\quad f_3(\mathbf{x})
        &= 0.5\,(V_{MBP} + V_{FD}) \\
        \min\quad f_4(\mathbf{x})
        &= \max(1.16 - 0.3717x_2x_4 - 0.0092928x_3 - 1.0, 0) \\
        &+ \max(0.261 - 0.0159x_1x_2 - 0.06486x_1 - 0.019x_2x_7
        + 0.0144x_3x_5 + 0.0154464x_6 - 0.32, 0) \\
        &+ \max(0.214 + 0.00817x_5 - 0.045195x_1 - 0.0135168x_1 + 0.03099x_2x_6 - 0.018x_2x_7
        + 0.007176x_3 + 0.023232x_3 - 0.00364x_5x_6 - 0.018x_2^2 - 0.32, 0) \\
        &+ \max(0.74 - 0.61x_2 - 0.031296x_3 - 0.031872x_7 + 0.227x_2^2 - 0.32,0) \\
        &+ \max(28.98 + 3.818x_3 - 4.2x_1x_2 + 1.27296x_6 - 2.68065x_7 - 32.0,0) \\
        &+ \max(33.86 + 2.95x_3 - 5.057x_1x_2 - 3.795x_2 - 3.4431x_7 + 1.45728 - 32.0,0) \\
        &+ \max(46.36 - 9.9x_2 - 4.4505x_1 - 32.0,0) \\
        &+ \max(4.72 - 0.5x_4 - 0.19x_2x_3 - 4.0, 0) \\
        &+ \max(10.58 - 0.674x_1x_2 - 0.67275x_2 - 9.9, 0) \\
        &+ \max(16.45 - 0.489x_3x_7 - 0.843x_5x_6 - 15.7, 0) \\
        g_1(\mathbf{x})
        &= 1.16 - 0.3717x_2x_4 - 0.0092928x_3 \leq 1.0 \\
        g_2(\mathbf{x})
        &= 0.261 - 0.0159x_1x_2 - 0.06486x_1
        - 0.019x_2x_7 + 0.0144x_3x_5
        + 0.0154464x_6 \leq 0.32 \\
        g_3(\mathbf{x})
        &= 0.214 + 0.00817x_5 - 0.045195x_1
        - 0.0135168x_1 + 0.03099x_2x_6
        - 0.018x_2x_7 + 0.007176x_3
        + 0.023232x_3 - 0.00364x_5x_6
        - 0.018x_2^2 \leq 0.32 \\
        g_4(\mathbf{x})
        &= 0.74 - 0.61x_2 - 0.031296x_3
        - 0.031872x_7 + 0.227x_2^2
        \leq 0.32 \\
        g_5(\mathbf{x})
        &= 28.98 + 3.818x_3
        - 4.2x_1x_2
        + 1.27296x_6
        - 2.68065x_7
        \leq 32.0 \\
        g_6(\mathbf{x})
        &= 33.86 + 2.95x_3
        - 5.057x_1x_2
        - 3.795x_2
        - 3.4431x_7
        + 1.45728
        \leq 32.0 \\
        g_7(\mathbf{x})
        &= 46.36 - 9.9x_2 - 4.4505x_1
        \leq 32.0 \\
        g_8(\mathbf{x})
        &= 4.72 - 0.5x_4 - 0.19x_2x_3
        \leq 4.0 \\
        g_9(\mathbf{x})
        &= 10.58 - 0.674x_1x_2 - 0.67275x_2
        \leq 9.9 \\
        g_{10}(\mathbf{x})
        &= 16.45 - 0.489x_3x_7 - 0.843x_5x_6
        \leq 15.7 \\
        &\mathbf{x}^{L} \leq \mathbf{x} \leq \mathbf{x}^{U},
    \end{align}

    where

    \[
    V_{MBP} = 10.58 - 0.674x_1x_2 - 0.67275x_2,
    \]

    \[
    V_{FD} = 16.45 - 0.489x_3x_7 - 0.843x_5x_6,
    \]

    \[
    \mathbf{x}^{L} = (0.5,\,0.45,\,0.5,\,0.5,\,0.875,\,0.4,\,0.4),
    \]

    \[
    \mathbf{x}^{U} = (1.5,\,1.35,\,1.5,\,1.5,\,2.625,\,1.2,\,1.2).
    \]

    Arguments:
        three_obj (bool): If true, utilize the three objectives version (objectives $f_1$-$f_3$).
            If false, utilizes the fourth objective ($f_4$) as well. Defaults to true (three objectives).


    Returns:
        Problem: A problem instance representing the Car-side impact problem.

    References:
        Jain, H., & Deb, K. (2014). An evolutionary many-objective optimization algorithm using
            reference-point based nondominated sorting approach, part II. IEEE TEVC, 18(4), 602-622.

        Three objective problem from:

        Jain, H. & Deb, K. (2014). An Evolutionary Many-Objective Optimization Algorithm
            Using Reference-Point Based Nondominated Sorting Approach, Part II: Handling Constraints
            and Extending to an Adaptive Approach. IEEE transactions on evolutionary computation,
            18(4), 602-622.

        Optional fourth objective from:

        Tanabe, R. & Ishibuchi, H. (2020). An easy-to-use real-world
            multi-objective optimization problem suite.
            Applied soft computing, 89, 106078.

        Variable names from:

        Deb, K., Gupta, S., Daum, D., Branke, J., Mall, A. & Padmanabhan, D. (2009).
            Reliability-Based Optimization Using Evolutionary Algorithms.
            IEEE transactions on evolutionary computation, 13(5), 1054-1074.
    """
    x_1 = Variable(
        name="x_1",
        symbol="x_1",
        variable_type=VariableTypeEnum.real,
        lowerbound=0.5,
        upperbound=1.5,
    )

    x_2 = Variable(
        name="x_2",
        symbol="x_2",
        variable_type=VariableTypeEnum.real,
        lowerbound=0.45,
        upperbound=1.35,
    )

    x_3 = Variable(
        name="x_3",
        symbol="x_3",
        variable_type=VariableTypeEnum.real,
        lowerbound=0.5,
        upperbound=1.5,
    )

    x_4 = Variable(
        name="x_4",
        symbol="x_4",
        variable_type=VariableTypeEnum.real,
        lowerbound=0.5,
        upperbound=1.5,
    )

    x_5 = Variable(
        name="x_5",
        symbol="x_5",
        variable_type=VariableTypeEnum.real,
        lowerbound=0.875,
        upperbound=2.625,
    )

    x_6 = Variable(
        name="x_6",
        symbol="x_6",
        variable_type=VariableTypeEnum.real,
        lowerbound=0.4,
        upperbound=1.2,
    )

    x_7 = Variable(
        name="x_7",
        symbol="x_7",
        variable_type=VariableTypeEnum.real,
        lowerbound=0.4,
        upperbound=1.2,
    )

    g_1 = Constraint(
        name="g_1",
        symbol="g_1",
        cons_type=ConstraintTypeEnum.LTE,
        func="1.16 - 0.3717 * x_2 * x_4 - 0.0092928 * x_3 - 1.0",
        is_linear=False,
        is_convex=False,
        is_twice_differentiable=True,
    )

    g_2 = Constraint(
        name="g_2",
        symbol="g_2",
        cons_type=ConstraintTypeEnum.LTE,
        func=(
            "0.261"
            " - 0.0159 * x_1 * x_2"
            " - 0.06486 * x_1"
            " - 0.019 * x_2 * x_7"
            " + 0.0144 * x_3 * x_5"
            " + 0.0154464 * x_6"
            " - 0.32"
        ),
        is_linear=False,
        is_convex=False,
        is_twice_differentiable=True,
    )

    g_3 = Constraint(
        name="g_3",
        symbol="g_3",
        cons_type=ConstraintTypeEnum.LTE,
        func=(
            "0.214"
            " + 0.00817 * x_5"
            " - 0.045195 * x_1"
            " - 0.0135168 * x_1"
            " + 0.03099 * x_2 * x_6"
            " - 0.018 * x_2 * x_7"
            " + 0.007176 * x_3"
            " + 0.023232 * x_3"
            " - 0.00364 * x_5 * x_6"
            " - 0.018 * x_2**2"
            " - 0.32"
        ),
        is_linear=False,
        is_convex=False,
        is_twice_differentiable=True,
    )

    g_4 = Constraint(
        name="g_4",
        symbol="g_4",
        cons_type=ConstraintTypeEnum.LTE,
        func="0.74 - 0.61 * x_2 - 0.031296 * x_3 - 0.031872 * x_7 + 0.227 * x_2**2 - 0.32",
        is_linear=False,
        is_convex=False,
        is_twice_differentiable=True,
    )

    g_5 = Constraint(
        name="g_5",
        symbol="g_5",
        cons_type=ConstraintTypeEnum.LTE,
        func="28.98 + 3.818 * x_3 - 4.2 * x_1 * x_2 + 1.27296 * x_6 - 2.68065 * x_7 - 32.0",
        is_linear=False,
        is_convex=False,
        is_twice_differentiable=True,
    )

    g_6 = Constraint(
        name="g_6",
        symbol="g_6",
        cons_type=ConstraintTypeEnum.LTE,
        func="33.86 + 2.95 * x_3 - 5.057 * x_1 * x_2 - 3.795 * x_2 - 3.4431 * x_7 + 1.45728 - 32.0",
        is_linear=False,
        is_convex=False,
        is_twice_differentiable=True,
    )

    g_7 = Constraint(
        name="g_7",
        symbol="g_7",
        cons_type=ConstraintTypeEnum.LTE,
        func="46.36 - 9.9 * x_2 - 4.4505 * x_1 - 32.0",
        is_linear=False,
        is_convex=False,
        is_twice_differentiable=True,
    )

    g_8 = Constraint(
        name="g_8",
        symbol="g_8",
        cons_type=ConstraintTypeEnum.LTE,
        func="4.72 - 0.5 * x_4 - 0.19 * x_2 * x_3 - 4.0",
        is_linear=False,
        is_convex=False,
        is_twice_differentiable=True,
    )

    g_9 = Constraint(
        name="g_9",
        symbol="g_9",
        cons_type=ConstraintTypeEnum.LTE,
        func="10.58 - 0.674 * x_1 * x_2 - 0.67275 * x_2 - 9.9",
        is_linear=False,
        is_convex=False,
        is_twice_differentiable=True,
    )

    g_10 = Constraint(
        name="g_10",
        symbol="g_10",
        cons_type=ConstraintTypeEnum.LTE,
        func="16.45 - 0.489 * x_3 * x_7 - 0.843 * x_5 * x_6 - 15.7",
        is_linear=False,
        is_convex=False,
        is_twice_differentiable=True,
    )

    f_1 = Objective(
        name="f_1",
        symbol="f_1",
        func="1.98 + 4.9 * x_1 + 6.67 * x_2 + 6.98 * x_3 + 4.01 * x_4 + 1.78 * x_5 + 0.00001 * x_6 + 2.73 * x_7",
        objective_type=ObjectiveTypeEnum.analytical,
        is_linear=True,
        is_convex=True,
        is_twice_differentiable=True,
    )

    f_2 = Objective(
        name="f_2",
        symbol="f_2",
        func="4.72 - 0.5 * x_4 - 0.19 * x_2 * x_3",
        objective_type=ObjectiveTypeEnum.analytical,
        is_linear=False,
        is_convex=False,
        is_twice_differentiable=True,
    )

    f_3 = Objective(
        name="f_3",
        symbol="f_3",
        func="0.5 * ((10.58 - 0.674 * x_1 * x_2 - 0.67275 * x_2) + (16.45 - 0.489 * x_3 * x_7 - 0.843 * x_5 * x_6))",
        objective_type=ObjectiveTypeEnum.analytical,
        is_linear=False,
        is_convex=False,
        is_twice_differentiable=True,
    )

    if three_obj:
        objectives = [f_1, f_2, f_3]

    else:
        # If three_obj is false, then problem is with 4 objectives.

        f_4 = Objective(
            name="f_4",
            symbol="f_4",
            func=(
                "Max(1.16 - 0.3717 * x_2 * x_4 - 0.0092928 * x_3 - 1.0, 0)"
                " + Max(0.261 - 0.0159 * x_1 * x_2 - 0.06486 * x_1 - 0.019 * x_2 * x_7"
                " + 0.0144 * x_3 * x_5 + 0.0154464 * x_6 - 0.32, 0)"
                " + Max(0.214 + 0.00817 * x_5 - 0.045195 * x_1 - 0.0135168 * x_1 + 0.03099 * x_2 * x_6"
                " - 0.018 * x_2 * x_7 + 0.007176 * x_3 + 0.023232 * x_3 - 0.00364 * x_5 * x_6"
                " - 0.018 * x_2**2 - 0.32, 0)"
                " + Max(0.74 - 0.61 * x_2 - 0.031296 * x_3 - 0.031872 * x_7 + 0.227 * x_2**2 - 0.32, 0)"
                " + Max(28.98 + 3.818 * x_3 - 4.2*x_1 * x_2 + 1.27296 * x_6 - 2.68065 * x_7 - 32.0, 0)"
                " + Max(33.86 + 2.95 * x_3 - 5.057 * x_1 * x_2 - 3.795 * x_2 - 3.4431 * x_7"
                " + 1.45728 - 32.0, 0)"
                " + Max(46.36 - 9.9 * x_2 - 4.4505 * x_1 - 32.0, 0)"
                " + Max(4.72 - 0.5 * x_4 - 0.19 * x_2 * x_3 - 4.0, 0)"
                " + Max(10.58 - 0.674 * x_1 * x_2 - 0.67275 * x_2 - 9.9, 0)"
                " + Max(16.45 - 0.489 * x_3 * x_7 - 0.843 * x_5 * x_6 - 15.7, 0)"
            ),
            objective_type=ObjectiveTypeEnum.analytical,
            is_linear=False,
            is_convex=False,
            is_twice_differentiable=False,
        )

        objectives = [f_1, f_2, f_3, f_4]

    return Problem(
        name="The car-side impact",
        description="The car-side impact problem",
        variables=[x_1, x_2, x_3, x_4, x_5, x_6, x_7],
        objectives=objectives,
        constraints=[g_1, g_2, g_3, g_4, g_5, g_6, g_7, g_8, g_9, g_10],
    )

ctp1

ctp1(n_variables: int) -> Problem

Defines the CTP1 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 \\[1ex] \min\quad f_2(\textbf{x}) &= g(\textbf{x}) \exp\!\left( -\frac{f_1(\textbf{x})}{g(\textbf{x})} \right) \\[1ex] g(\textbf{x}) &= 1 + \frac{9}{n-1}\sum_{i=2}^{n} x_i \\[1ex] g_1(\mathbf{x}) &= 0.858\,\exp(-0.541\,x_1) - f_2(\textbf{x}) \le 0 \\[1ex] g_2(\mathbf{x}) &= 0.728\,\exp(-0.295\,x_1) - f_2(\textbf{x}) \le 0 \\[1ex] 0 &\le x_i \le 1, \qquad i = 1,\ldots,n. \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_1\) and \(g_2\) are auxiliary functions.

Source code in desdeo/problem/testproblems/ctp_problems.py
def ctp1(n_variables: int) -> Problem:
    r"""Defines the CTP1 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 \\[1ex]
        \min\quad f_2(\textbf{x})
        &= g(\textbf{x})
        \exp\!\left(
        -\frac{f_1(\textbf{x})}{g(\textbf{x})}
        \right) \\[1ex]
        g(\textbf{x})
        &= 1 + \frac{9}{n-1}\sum_{i=2}^{n} x_i \\[1ex]
        g_1(\mathbf{x})
        &= 0.858\,\exp(-0.541\,x_1) - f_2(\textbf{x})
        \le 0 \\[1ex]
        g_2(\mathbf{x})
        &= 0.728\,\exp(-0.295\,x_1) - f_2(\textbf{x})
        \le 0 \\[1ex] 0
        &\le x_i \le 1,
        \qquad i = 1,\ldots,n.
    \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_1$ and $g_2$ are auxiliary functions.
    """
    n = n_variables

    # function f_1
    f1_expr = "x_1"

    # function 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 f_2
    f_2_expr = f"{g_expr} * Exp(-{f1_expr} / ({g_expr}))"

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

    objective_1 = Objective(
        name="f_1",
        symbol="f_1",
        func="x_1",
        objective_type=ObjectiveTypeEnum.analytical,
        is_linear=True,
        is_convex=True,
        is_twice_differentiable=True,
    )

    objective_2 = Objective(
        name="f_2",
        symbol="f_2",
        func=f_2_expr,
        objective_type=ObjectiveTypeEnum.analytical,
        is_linear=False,
        is_convex=False,
        is_twice_differentiable=True,
    )

    constraint_1 = Constraint(
        name="g_1",
        symbol="g_1",
        cons_type=ConstraintTypeEnum.LTE,
        func=f"0.858 * Exp(-0.541 * x_1) - ({f_2_expr})",
        is_linear=False,
        is_convex=False,
        is_twice_differentiable=True,
    )

    constraint_2 = Constraint(
        name="g_2",
        symbol="g_2",
        cons_type=ConstraintTypeEnum.LTE,
        func=f"0.728 * Exp(-0.295 * x_1) - ({f_2_expr})",
        is_linear=False,
        is_convex=False,
        is_twice_differentiable=True,
    )

    return Problem(
        name="ctp1",
        description="The CTP1 test problem.",
        variables=variables,
        objectives=[objective_1, objective_2],
        constraints=[constraint_1, constraint_2],
        is_convex=True,
        is_linear=False,
        is_twice_differentiable=True,
    )

ctp2

ctp2(n_variables: int) -> Problem

Defines the CTP2 test problem.

A variable number of decision variables and two objective functions to be minimized:

\[\begin{align*} \min\quad f_1(\mathbf{x}) &= x_1 \\[1ex] \min\quad f_2(\mathbf{x}) &= g(\mathbf{x})\left(1 - \frac{f_1(\mathbf{x})}{g(\mathbf{x})}\right) \\[1ex] g(\mathbf{x}) &= 1 + \frac{9}{n - 1}\sum_{i=2}^{n} x_i \\[1ex] \text{s.t.}\quad &\cos(\theta)(f_2 - e) - \sin(\theta) f_1 \ge a\left|\sin\!\big(b\pi(\sin(\theta)(f_2 - e) + \cos(\theta) f_1)^c\big)\right|^d \\[1ex] &0 \le x_i \le 1, \qquad i = 1,\ldots,n, \end{align*}\]

with parameters \(\theta = -0.2\pi\), \(a = 0.2\), \(b = 10\), \(c = 1\), \(d = 6\), \(e = 1\). The single constraint makes the unconstrained Pareto front infeasible except in a number of disconnected regions. The g function is not specified in the original chapter; the ZDT-style g above is used for consistency across the DESDEO CTP suite.

Source code in desdeo/problem/testproblems/ctp_problems.py
def ctp2(n_variables: int) -> Problem:
    r"""Defines the CTP2 test problem.

    A variable number of decision variables and two objective functions to be minimized:

    \begin{align*}
        \min\quad f_1(\mathbf{x}) &= x_1 \\[1ex]
        \min\quad f_2(\mathbf{x}) &= g(\mathbf{x})\left(1 - \frac{f_1(\mathbf{x})}{g(\mathbf{x})}\right) \\[1ex]
        g(\mathbf{x}) &= 1 + \frac{9}{n - 1}\sum_{i=2}^{n} x_i \\[1ex]
        \text{s.t.}\quad
        &\cos(\theta)(f_2 - e) - \sin(\theta) f_1
        \ge a\left|\sin\!\big(b\pi(\sin(\theta)(f_2 - e) + \cos(\theta) f_1)^c\big)\right|^d \\[1ex]
        &0 \le x_i \le 1, \qquad i = 1,\ldots,n,
    \end{align*}

    with parameters $\theta = -0.2\pi$, $a = 0.2$, $b = 10$, $c = 1$, $d = 6$, $e = 1$. The single
    constraint makes the unconstrained Pareto front infeasible except in a number of disconnected
    regions. The g function is not specified in the original chapter; the ZDT-style g above is used
    for consistency across the DESDEO CTP suite.
    """
    return _ctp_problem(
        name="ctp2",
        description="The CTP2 test problem.",
        n_variables=n_variables,
        constraint_params=[(-0.2 * math.pi, 0.2, 10.0, 1.0, 6.0, 1.0)],
    )

ctp3

ctp3(n_variables: int) -> Problem

Defines the CTP3 test problem.

Same structure as :func:ctp2 but with constraint parameters \(\theta = -0.2\pi\), \(a = 0.1\), \(b = 10\), \(c = 1\), \(d = 0.5\), \(e = 1\). The smaller \(d\) shrinks each disconnected feasible region so that each one degenerates towards a single Pareto-optimal solution.

Source code in desdeo/problem/testproblems/ctp_problems.py
def ctp3(n_variables: int) -> Problem:
    r"""Defines the CTP3 test problem.

    Same structure as :func:`ctp2` but with constraint parameters $\theta = -0.2\pi$, $a = 0.1$,
    $b = 10$, $c = 1$, $d = 0.5$, $e = 1$. The smaller $d$ shrinks each disconnected feasible region
    so that each one degenerates towards a single Pareto-optimal solution.
    """
    return _ctp_problem(
        name="ctp3",
        description="The CTP3 test problem.",
        n_variables=n_variables,
        constraint_params=[(-0.2 * math.pi, 0.1, 10.0, 1.0, 0.5, 1.0)],
    )

ctp4

ctp4(n_variables: int) -> Problem

Defines the CTP4 test problem.

Same structure as :func:ctp2 but with constraint parameters \(\theta = -0.2\pi\), \(a = 0.75\), \(b = 10\), \(c = 1\), \(d = 0.5\), \(e = 1\). The larger \(a\) stretches the infeasible "tunnels" far away from the Pareto-optimal region, making it the hardest of the front-difficulty CTP problems.

Source code in desdeo/problem/testproblems/ctp_problems.py
def ctp4(n_variables: int) -> Problem:
    r"""Defines the CTP4 test problem.

    Same structure as :func:`ctp2` but with constraint parameters $\theta = -0.2\pi$, $a = 0.75$,
    $b = 10$, $c = 1$, $d = 0.5$, $e = 1$. The larger $a$ stretches the infeasible "tunnels" far away
    from the Pareto-optimal region, making it the hardest of the front-difficulty CTP problems.
    """
    return _ctp_problem(
        name="ctp4",
        description="The CTP4 test problem.",
        n_variables=n_variables,
        constraint_params=[(-0.2 * math.pi, 0.75, 10.0, 1.0, 0.5, 1.0)],
    )

ctp5

ctp5(n_variables: int) -> Problem

Defines the CTP5 test problem.

Same structure as :func:ctp2 but with constraint parameters \(\theta = -0.2\pi\), \(a = 0.1\), \(b = 10\), \(c = 2\), \(d = 0.5\), \(e = 1\). With \(c \ne 1\) the discrete Pareto-optimal solutions are scattered non-uniformly along the front.

Source code in desdeo/problem/testproblems/ctp_problems.py
def ctp5(n_variables: int) -> Problem:
    r"""Defines the CTP5 test problem.

    Same structure as :func:`ctp2` but with constraint parameters $\theta = -0.2\pi$, $a = 0.1$,
    $b = 10$, $c = 2$, $d = 0.5$, $e = 1$. With $c \ne 1$ the discrete Pareto-optimal solutions are
    scattered non-uniformly along the front.
    """
    return _ctp_problem(
        name="ctp5",
        description="The CTP5 test problem.",
        n_variables=n_variables,
        constraint_params=[(-0.2 * math.pi, 0.1, 10.0, 2.0, 0.5, 1.0)],
    )

ctp6

ctp6(n_variables: int) -> Problem

Defines the CTP6 test problem.

Same structure as :func:ctp2 but with constraint parameters \(\theta = 0.1\pi\), \(a = 40\), \(b = 0.5\), \(c = 1\), \(d = 2\), \(e = -2\). Here the infeasibility spans the entire search space as a series of infeasible bands; an algorithm must cross several of them to reach the feasible island holding the Pareto-optimal front.

Source code in desdeo/problem/testproblems/ctp_problems.py
def ctp6(n_variables: int) -> Problem:
    r"""Defines the CTP6 test problem.

    Same structure as :func:`ctp2` but with constraint parameters $\theta = 0.1\pi$, $a = 40$,
    $b = 0.5$, $c = 1$, $d = 2$, $e = -2$. Here the infeasibility spans the entire search space as a
    series of infeasible bands; an algorithm must cross several of them to reach the feasible island
    holding the Pareto-optimal front.
    """
    return _ctp_problem(
        name="ctp6",
        description="The CTP6 test problem.",
        n_variables=n_variables,
        constraint_params=[(0.1 * math.pi, 40.0, 0.5, 1.0, 2.0, -2.0)],
    )

ctp7

ctp7(n_variables: int) -> Problem

Defines the CTP7 test problem.

Same structure as :func:ctp2 but with constraint parameters \(\theta = -0.05\pi\), \(a = 40\), \(b = 5\), \(c = 1\), \(d = 6\), \(e = 0\). The infeasible bands run along the Pareto-optimal region, splitting the unconstrained front into a disconnected set of continuous regions.

Source code in desdeo/problem/testproblems/ctp_problems.py
def ctp7(n_variables: int) -> Problem:
    r"""Defines the CTP7 test problem.

    Same structure as :func:`ctp2` but with constraint parameters $\theta = -0.05\pi$, $a = 40$,
    $b = 5$, $c = 1$, $d = 6$, $e = 0$. The infeasible bands run along the Pareto-optimal region,
    splitting the unconstrained front into a disconnected set of continuous regions.
    """
    return _ctp_problem(
        name="ctp7",
        description="The CTP7 test problem.",
        n_variables=n_variables,
        constraint_params=[(-0.05 * math.pi, 40.0, 5.0, 1.0, 6.0, 0.0)],
    )

ctp8

ctp8(n_variables: int) -> Problem

Defines the CTP8 test problem.

Same structure as :func:ctp2 but with two simultaneous constraints, combining the entire-search-space difficulties of :func:ctp6 and :func:ctp7:

  • constraint 1: \(\theta = 0.1\pi\), \(a = 40\), \(b = 0.5\), \(c = 1\), \(d = 2\), \(e = -2\);
  • constraint 2: \(\theta = -0.05\pi\), \(a = 40\), \(b = 2\), \(c = 1\), \(d = 6\), \(e = 0\).

A solution must satisfy both constraints, leaving the feasible Pareto-optimal region as a small number of disconnected patches. Note: the original chapter only sketches CTP8 as a "combination of two or more" constraints without listing parameters; the values above are the standard ones from Deb (2001).

Source code in desdeo/problem/testproblems/ctp_problems.py
def ctp8(n_variables: int) -> Problem:
    r"""Defines the CTP8 test problem.

    Same structure as :func:`ctp2` but with two simultaneous constraints, combining the
    entire-search-space difficulties of :func:`ctp6` and :func:`ctp7`:

    - constraint 1: $\theta = 0.1\pi$, $a = 40$, $b = 0.5$, $c = 1$, $d = 2$, $e = -2$;
    - constraint 2: $\theta = -0.05\pi$, $a = 40$, $b = 2$, $c = 1$, $d = 6$, $e = 0$.

    A solution must satisfy both constraints, leaving the feasible Pareto-optimal region as a small
    number of disconnected patches. Note: the original chapter only sketches CTP8 as a "combination
    of two or more" constraints without listing parameters; the values above are the standard ones
    from Deb (2001).
    """
    return _ctp_problem(
        name="ctp8",
        description="The CTP8 test problem.",
        n_variables=n_variables,
        constraint_params=[
            (0.1 * math.pi, 40.0, 0.5, 1.0, 2.0, -2.0),
            (-0.05 * math.pi, 40.0, 2.0, 1.0, 6.0, 0.0),
        ],
    )

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,
    )

dtlz1

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

Defines the DTLZ1 test problem.

The objective functions for DTLZ1 are defined as follows:

\[\begin{align*} &\min_{\mathbf{x}} \quad & f_1(\mathbf{x}) &= \frac{1}{2}(1+g(\mathbf{x}_M)) \prod_{j=1}^{M-1} x_j \\ &\min_{\mathbf{x}} \quad & f_m(\mathbf{x}) &= \frac{1}{2}(1+g(\mathbf{x}_M)) \prod_{j=1}^{M-m} x_j (1 - x_{M-m+1}), \quad m=2,\dots,M-1 \\ &\min_{\mathbf{x}} \quad & f_M(\mathbf{x}) &= \frac{1}{2}(1+g(\mathbf{x}_M))(1 - x_1) \end{align*}\]

where

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

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

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 DTLZ1 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/dtlz_problems.py
def dtlz1(n_variables: int, n_objectives: int) -> Problem:
    r"""Defines the DTLZ1 test problem.

    The objective functions for DTLZ1 are defined as follows:

    \begin{align*}
        &\min_{\mathbf{x}} \quad & f_1(\mathbf{x}) &= \frac{1}{2}(1+g(\mathbf{x}_M))
        \prod_{j=1}^{M-1} x_j \\
        &\min_{\mathbf{x}} \quad & f_m(\mathbf{x}) &= \frac{1}{2}(1+g(\mathbf{x}_M))
        \prod_{j=1}^{M-m} x_j (1 - x_{M-m+1}), \quad m=2,\dots,M-1 \\
        &\min_{\mathbf{x}} \quad & f_M(\mathbf{x}) &= \frac{1}{2}(1+g(\mathbf{x}_M))(1 - x_1)
    \end{align*}

    where

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

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

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

    Returns:
        Problem: an instance of the DTLZ1 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.
    """
    if n_objectives <= 1:
        raise ValueError("DTLZ1 requires at least 2 objectives.")

    # function g
    g_symbol = "g"
    g_sum = " + ".join(
        [f"((x_{i} - 0.5)**2 - Cos(20*{np.pi}*(x_{i} - 0.5)))" for i in range(n_objectives, n_variables + 1)]
    )
    k = n_variables - n_objectives + 1
    g_expr = f"(100 * ({k} + {g_sum}))"

    objectives = []
    for m in range(1, n_objectives + 1):
        # function f_m
        terms = [f"x_{i}" for i in range(1, n_objectives - m + 1)]

        if m > 1:
            terms.append(f"(1 - x_{n_objectives - m + 1})")

        prod_expr = " * ".join(terms)
        f_m_expr = f"0.5 * (1 + {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="dtlz1",
        description="The DTLZ1 test problem.",
        variables=variables,
        objectives=objectives,
        extra_funcs=extras,
    )

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/dtlz_problems.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)])

    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"(1 + {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,
    )

dtlz4

dtlz4(
    n_variables: int,
    n_objectives: int,
    alpha: float = 100.0,
) -> Problem

Defines the DTLZ4 test problem.

The objective functions for DTLZ4 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^{\alpha} \frac{\pi}{2}\right) \times \begin{cases} 1 & \text{if } i=1 \\ \sin\left(x_{(M-i+1)}^{\alpha}\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 DTLZ4 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
alpha float

exponent

100.0

Returns:

Name Type Description
Problem Problem

an instance of a DTLZ4 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/dtlz_problems.py
def dtlz4(n_variables: int, n_objectives: int, alpha: float = 100.0) -> Problem:
    r"""Defines the DTLZ4 test problem.

    The objective functions for DTLZ4 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^{\alpha} \frac{\pi}{2}\right) \times
        \begin{cases}
        1 & \text{if } i=1 \\
        \sin\left(x_{(M-i+1)}^{\alpha}\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 DTLZ4 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: number of variables
        n_objectives: number of objective functions
        alpha: exponent

    Returns:
        Problem: an instance of a DTLZ4 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)])

    objectives = []
    for m in range(1, n_objectives + 1):
        # function f_m
        prod_expr = " * ".join([f"Cos(0.5 * {np.pi} * x_{i}**{alpha})" 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}**{alpha})"
        if prod_expr == "":
            prod_expr = "1"  # Only reached when n_objectives == 1: no trig terms, so f_1 = 1 + g
        f_m_expr = f"(1 + {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="dtlz4",
        description="The DTLZ4 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,
    )

gaa

gaa(*, fold_constraint: bool = False) -> Problem

Defines the General Aviation Aircraft product family design problem.

Three aircraft -- a 2-, 4- and 6-seat variant -- are designed as one family. Each has nine design variables, giving 27 in total, and nine performance responses predicted by second-order polynomial response surfaces in those variables. Writing \(R_{r,s}\) for response \(r\) of the \(s\)-seat aircraft, and \(z_{i,s}\) for its \(i\)-th design variable scaled to \([-1, 1]\):

\[\begin{align} &\min_{\mathbf{x}} & f_1 \ldots f_6 & = \max_{s \in \{2,4,6\}} R_{r,s}, & r & \in \{\text{noise}, \text{wemp}, \text{doc}, \text{rough}, \text{wfuel}, \text{purch}\} \\ &\max_{\mathbf{x}} & f_7 \ldots f_9 & = \min_{s \in \{2,4,6\}} R_{r,s}, & r & \in \{\text{range}, \text{ldmax}, \text{vcmax}\} \\ &\min_{\mathbf{x}} & f_{10} & = \sqrt{\sum_{i=1}^{9}\sum_{s \in \{2,4,6\}} \left(z_{i,s} - \bar{z_i}\right)^2} \end{align}\]

The tenth objective is the product family penalty function: the spread of the three aircraft around their common mean design. Driving it to zero makes the three variants identical, which is the cheapest family to build and the worst-performing one, so it trades against all nine performance objectives at once. That trade is what the problem exists to study.

The constraint is the total violation of eighteen performance limits -- six responses on each of the three aircraft, each normalised by its limit and clamped at zero:

\[\begin{align} g(\mathbf{x}) = \sum_{s \in \{2,4,6\}} \left( \sum_{r} \max\left\{\frac{R_{r,s} - L_{r,s}}{L_{r,s}}, 0\right\} + \max\left\{\frac{L_{\text{range},s} - R_{\text{range},s}}{L_{\text{range},s}}, 0\right\} \right) \leq 0 \end{align}\]

Every term is clamped at zero, so \(g \geq 0\) always and a solution is feasible exactly when it meets all eighteen limits.

Note

The three variants have different response surface coefficients -- 27 fitted surfaces in all, not 9 reused three times.

The running maxima of the first six objectives start at 0 and the running minima of the next three start at 200 000, exactly as in the reference implementation. Those bounds never bind on the design box, but they are kept so that the two implementations agree bit for bit.

The reference implementation also computes a set of *_DEV goal deviations that nothing then reads. They are dead code there and are not reproduced here.

Parameters:

Name Type Description Default
fold_constraint bool

if True, return the constraint violation as an eleventh objective and no constraints, in the manner of the RE suite, which makes the problem bound-constrained. If False, return the ten objectives and the constraint as published. Defaults to False.

False

Returns:

Name Type Description
Problem Problem

an instance of the General Aviation Aircraft product family design problem.

Source code in desdeo/problem/testproblems/gaa_problem.py
def gaa(*, fold_constraint: bool = False) -> Problem:
    r"""Defines the General Aviation Aircraft product family design problem.

    Three aircraft -- a 2-, 4- and 6-seat variant -- are designed as one family. Each has nine design
    variables, giving 27 in total, and nine performance responses predicted by second-order polynomial
    response surfaces in those variables. Writing $R_{r,s}$ for response $r$ of the $s$-seat aircraft,
    and $z_{i,s}$ for its $i$-th design variable scaled to $[-1, 1]$:

    \begin{align}
        &\min_{\mathbf{x}} & f_1 \ldots f_6 & = \max_{s \in \{2,4,6\}} R_{r,s},
            & r & \in \{\text{noise}, \text{wemp}, \text{doc}, \text{rough}, \text{wfuel},
            \text{purch}\} \\
        &\max_{\mathbf{x}} & f_7 \ldots f_9 & = \min_{s \in \{2,4,6\}} R_{r,s},
            & r & \in \{\text{range}, \text{ldmax}, \text{vcmax}\} \\
        &\min_{\mathbf{x}} & f_{10} & = \sqrt{\sum_{i=1}^{9}\sum_{s \in \{2,4,6\}}
            \left(z_{i,s} - \bar{z_i}\right)^2}
    \end{align}

    The tenth objective is the *product family penalty function*: the spread of the three aircraft
    around their common mean design. Driving it to zero makes the three variants identical, which is
    the cheapest family to build and the worst-performing one, so it trades against all nine
    performance objectives at once. That trade is what the problem exists to study.

    The constraint is the total violation of eighteen performance limits -- six responses on each of
    the three aircraft, each normalised by its limit and clamped at zero:

    \begin{align}
        g(\mathbf{x}) = \sum_{s \in \{2,4,6\}} \left(
            \sum_{r} \max\left\{\frac{R_{r,s} - L_{r,s}}{L_{r,s}}, 0\right\}
            + \max\left\{\frac{L_{\text{range},s} - R_{\text{range},s}}{L_{\text{range},s}}, 0\right\}
        \right) \leq 0
    \end{align}

    Every term is clamped at zero, so $g \geq 0$ always and a solution is feasible exactly when it
    meets all eighteen limits.

    Note:
        The three variants have **different** response surface coefficients -- 27 fitted surfaces in
        all, not 9 reused three times.

        The running maxima of the first six objectives start at 0 and the running minima of the next
        three start at 200 000, exactly as in the reference implementation. Those bounds never bind on
        the design box, but they are kept so that the two implementations agree bit for bit.

        The reference implementation also computes a set of `*_DEV` goal deviations that nothing then
        reads. They are dead code there and are not reproduced here.

    Args:
        fold_constraint (bool, optional): if `True`, return the constraint violation as an eleventh
            objective and no constraints, in the manner of the RE suite, which makes the problem
            bound-constrained. If `False`, return the ten objectives and the constraint as published.
            Defaults to `False`.

    Returns:
        Problem: an instance of the General Aviation Aircraft product family design problem.
    """
    variables = []
    extras = []

    for aircraft, seats in enumerate(_SEATS):
        for i, (design, description, centre, half_range) in enumerate(_DESIGN):
            symbol = f"x_{aircraft * len(_DESIGN) + i + 1}"
            variables.append(
                Variable(
                    name=f"{description} ({seats}-seat)",
                    symbol=symbol,
                    variable_type=VariableTypeEnum.real,
                    lowerbound=centre - half_range,
                    upperbound=centre + half_range,
                    initial_value=centre,
                )
            )
            extras.append(
                ExtraFunction(
                    name=f"scaled {design} ({seats}-seat)",
                    symbol=_scaled_symbol(design, seats),
                    func=f"({symbol} - {centre!r}) / {half_range!r}",
                    is_linear=True,
                    is_convex=True,
                    is_twice_differentiable=True,
                )
            )

    for response, description, _ in _RESPONSES:
        for seats in _SEATS:
            extras.append(
                ExtraFunction(
                    name=f"{description} ({seats}-seat)",
                    symbol=f"{response}_{seats}",
                    func=_surface_expression(response, seats),
                    is_linear=False,
                    is_convex=False,  # Not checked
                    is_twice_differentiable=True,
                )
            )

    objectives = []
    for index, (response, description, maximize) in enumerate(_RESPONSES, start=1):
        across = ", ".join(f"{response}_{seats}" for seats in _SEATS)
        # The starting value of the reference implementation's running extremum comes first.
        aggregate = f"Min(200000, {across})" if maximize else f"Max(0, {across})"
        objectives.append(
            Objective(
                # Both aggregations take the worst case: the largest of the six that are minimised,
                # the smallest of the three that are maximised.
                name=f"worst {description} across the family",
                symbol=f"f_{index}",
                func=aggregate,
                objective_type=ObjectiveTypeEnum.analytical,
                maximize=maximize,
                is_linear=False,
                is_convex=False,  # Not checked
                is_twice_differentiable=False,
            )
        )

    for design, description, _, _ in _DESIGN:
        across = " + ".join(_scaled_symbol(design, seats) for seats in _SEATS)
        extras.append(
            ExtraFunction(
                name=f"family mean of the scaled {description}",
                symbol=f"{design}_mean",
                func=f"({across}) / {len(_SEATS)}",
                is_linear=True,
                is_convex=True,
                is_twice_differentiable=True,
            )
        )

    spread = " + ".join(
        f"({_scaled_symbol(design, seats)} - {design}_mean)**2" for design, *_ in _DESIGN for seats in _SEATS
    )
    objectives.append(
        Objective(
            name="product family penalty function",
            symbol=f"f_{len(_RESPONSES) + 1}",
            func=f"Sqrt({spread})",
            objective_type=ObjectiveTypeEnum.analytical,
            is_linear=False,
            is_convex=False,  # Not checked
            is_twice_differentiable=False,
        )
    )

    violations = []
    for response, limits in _CONSTRAINT_LIMITS.items():
        for seats in _SEATS:
            limit = limits[seats]
            symbol = f"{response}_{seats}"
            # The range must exceed its limit; every other response must stay below it.
            excess = f"({limit!r} - {symbol})" if response == "range" else f"({symbol} - {limit!r})"
            violations.append(f"Max({excess} / {limit!r}, 0)")
    total_violation = " + ".join(violations)

    if fold_constraint:
        objectives.append(
            Objective(
                name="total performance limit violation",
                symbol=f"f_{len(_RESPONSES) + 2}",
                func=total_violation,
                objective_type=ObjectiveTypeEnum.analytical,
                is_linear=False,
                is_convex=False,  # Not checked
                is_twice_differentiable=False,
            )
        )
        constraints = None
    else:
        constraints = [
            Constraint(
                name="total performance limit violation",
                symbol="g_1",
                cons_type=ConstraintTypeEnum.LTE,
                func=total_violation,
                is_linear=False,
                is_convex=False,  # Not checked
                is_twice_differentiable=False,
            )
        ]

    return Problem(
        name="gaa",
        description="The General Aviation Aircraft product family design problem",
        variables=variables,
        objectives=objectives,
        constraints=constraints,
        extra_funcs=extras,
    )

generate_solar_profile

generate_solar_profile() -> np.ndarray

Generate hourly solar production per 160 W panel from June 1 to August 31.

Uses a clear-sky sine curve based on approximate sunrise/sunset times for 60°N latitude, scaled by a daily cloudiness factor sampled uniformly from [0.2, 1.0] (up to 80% reduction). On a clear summer day the panel produces its nominal 0.16 kWh/h at solar noon.

Returns:

Type Description
ndarray

A numpy array of shape (2208,) with kWh output per panel per hour.

Source code in desdeo/problem/testproblems/summer_cabin_electricity.py
def generate_solar_profile() -> np.ndarray:
    """Generate hourly solar production per 160 W panel from June 1 to August 31.

    Uses a clear-sky sine curve based on approximate sunrise/sunset times for 60°N latitude,
    scaled by a daily cloudiness factor sampled uniformly from [0.2, 1.0] (up to 80% reduction).
    On a clear summer day the panel produces its nominal 0.16 kWh/h at solar noon.

    Returns:
        A numpy array of shape (2208,) with kWh output per panel per hour.
    """
    rng = np.random.default_rng(42)
    time_index = pd.date_range("2025-06-01", "2025-08-31 23:00", freq="h")
    panel_peak_kw = 0.16  # kW nominal per panel
    solar_noon = 13.0  # hour of peak irradiance (local time)
    lat = np.radians(60.0)

    days = pd.date_range("2025-06-01", "2025-08-31", freq="D")
    cloudiness = {d.date(): rng.uniform(0.2, 1.0) for d in days}

    profile = []
    for ts in time_index:
        doy = ts.day_of_year
        dec = np.radians(23.45 * np.sin(np.radians(360.0 / 365.0 * (doy - 80))))
        cos_ha = np.clip(-np.tan(lat) * np.tan(dec), -1.0, 1.0)
        day_len = 2.0 * np.degrees(np.arccos(cos_ha)) / 15.0
        sunrise = solar_noon - day_len / 2.0
        sunset = solar_noon + day_len / 2.0

        hour = ts.hour
        if sunrise <= hour <= sunset:
            clear_sky = panel_peak_kw * np.sin(np.pi * (hour - sunrise) / (sunset - sunrise))
        else:
            clear_sky = 0.0

        profile.append(max(0.0, clear_sky * cloudiness[ts.date()]))

    return np.array(profile)

generate_summer_cabin_electricity_data

generate_summer_cabin_electricity_data()

Generates synthetic hourly electricity load and temperature data for a summer cabin from June 1 to August 31.

Source code in desdeo/problem/testproblems/summer_cabin_electricity.py
def generate_summer_cabin_electricity_data():  # noqa: C901
    """Generates synthetic hourly electricity load and temperature data for a summer cabin from June 1 to August 31."""
    rng = np.random.default_rng(6969)

    time_index = pd.date_range("2025-06-01", "2025-08-31 23:00", freq="h")

    # --- Temperature model ---
    def seasonal_temp(ts):
        match ts.month:
            case 6:
                return 14
            case 7:
                return 18
            case _:
                return 15

    def daily_temp(hour):
        # peak at 15:00, trough at 04:00
        return 5 * np.sin((hour - 15) / 24 * 2 * np.pi)

    def temperature(ts):
        return seasonal_temp(ts) + daily_temp(ts.hour) + rng.normal(0, 1.5)

    # --- Heating ---
    def heating_load(temp):
        threshold = 18
        k = 0.2
        return max(0, (threshold - temp) * k)

    # --- Base load ---
    def base_load():
        return rng.uniform(0.2, 0.35)

    # --- Daily pattern ---
    def daily_usage(hour):
        if hour < 6:
            return 0.1
        if hour < 9:
            return 0.8
        if hour < 17:
            return 0.4
        if hour < 22:
            return 1.0
        return 0.3

    # --- Cooking spikes ---
    def cooking_spike(ts):
        if ts.hour in [7, 12, 18] and rng.random() < 0.5:
            return rng.uniform(0.8, 1.8)
        return 0

    # --- Sauna ---
    def sauna_spike(ts):
        if ts.weekday() in [4, 5, 6]:  # Fri-Sun  # noqa: SIM102
            if 18 <= ts.hour <= 21 and rng.random() < 0.25:
                return rng.uniform(5, 8)
        return 0

    loads = []
    temps = []

    for ts in time_index:
        temps.append(temperature(ts))

        load = 0
        load += base_load()
        load += daily_usage(ts.hour)
        load += heating_load(temperature(ts))
        load += cooking_spike(ts)
        # load += sauna_spike(ts) # no one uses electric sauna in the summer cabin
        load += rng.normal(0, 0.05)

        loads.append(max(load, 0.15))

    prices_eur_kwh = np.load(_PRICES_PATH)["prices"] / 1000.0

    return pd.DataFrame(
        {"load_kWh": loads, "temperature_C": temps, "price_EUR_kWh": prices_eur_kwh},
        index=time_index,
    )

lame_superspheres

lame_superspheres(
    n_variables: int, n_objectives: int, gamma: float = 2.0
) -> Problem

Defines the Lamé superspheres test problem.

The objective functions for the Lamé superspheres problem are defined as follows, for \(i = 1\) to \(M\):

\[\begin{equation} f_i(\mathbf{x}) = (1 + g(\mathbf{x}_M)) \tilde{p}_i(\mathbf{x}), \end{equation}\]

where the hypersphere parametrization follows Eq. (10)

\[\begin{equation} p_1 = \cos(\theta_1), \end{equation}\]
\[\begin{equation} p_2 = \sin(\theta_1)\cos(\theta_2), \end{equation}\]
\[\begin{equation} p_3 = \sin(\theta_1)\sin(\theta_2)\cos(\theta_3), \end{equation}\]
\[\begin{equation} \vdots \end{equation}\]
\[\begin{equation} p_M = \prod_{j=1}^{M-1} \sin(\theta_j). \end{equation}\]

The Lamé supersphere coordinates are then obtained as

\[\begin{equation} \tilde{p}_i = p_i^{2/\gamma}. \end{equation}\]

The distance function is

\[\begin{equation} g(\mathbf{x}_M) = \sqrt{ \sum_{x_i \in \mathbf{x}_M} x_i^2 }. \end{equation}\]

The parameter \(\gamma\) controls the curvature of the Pareto front:

  • \(\gamma < 1\) gives a convex Pareto front.
  • \(\gamma = 1\) gives a linear Pareto front.
  • \(\gamma = 2\) gives a spherical (concave) Pareto front.
  • \(\gamma > 2\) gives a more boxy, strongly concave Pareto front.

Parameters:

Name Type Description Default
n_variables int

Number of variables.

required
n_objectives int

Number of objectives.

required
gamma float

Lamé exponent controlling front curvature.

2.0

Returns:

Name Type Description
Problem Problem

A Lamé superspheres test problem.

References

Emmerich, M. T. M., & Deutz, A. H. (2007). Test Problems Based on Lamé Superspheres. EMO 2007, LNCS 4403, 922-936.

Source code in desdeo/problem/testproblems/lame_superspheres_problem.py
def lame_superspheres(
    n_variables: int,
    n_objectives: int,
    gamma: float = 2.0,
) -> Problem:
    r"""Defines the Lamé superspheres test problem.

    The objective functions for the Lamé superspheres problem are defined
    as follows, for $i = 1$ to $M$:

    \begin{equation}
    f_i(\mathbf{x})
    =
    (1 + g(\mathbf{x}_M))
    \tilde{p}_i(\mathbf{x}),
    \end{equation}

    where the hypersphere parametrization follows Eq. (10)

    \begin{equation}
    p_1 = \cos(\theta_1),
    \end{equation}

    \begin{equation}
    p_2 = \sin(\theta_1)\cos(\theta_2),
    \end{equation}

    \begin{equation}
    p_3 = \sin(\theta_1)\sin(\theta_2)\cos(\theta_3),
    \end{equation}

    \begin{equation}
    \vdots
    \end{equation}

    \begin{equation}
    p_M =
    \prod_{j=1}^{M-1}
    \sin(\theta_j).
    \end{equation}

    The Lamé supersphere coordinates are then obtained as

    \begin{equation}
    \tilde{p}_i = p_i^{2/\gamma}.
    \end{equation}

    The distance function is

    \begin{equation}
    g(\mathbf{x}_M)
    =
    \sqrt{
    \sum_{x_i \in \mathbf{x}_M}
    x_i^2
    }.
    \end{equation}

    The parameter $\gamma$ controls the curvature of the Pareto front:

    - $\gamma < 1$ gives a convex Pareto front.
    - $\gamma = 1$ gives a linear Pareto front.
    - $\gamma = 2$ gives a spherical (concave) Pareto front.
    - $\gamma > 2$ gives a more boxy, strongly concave Pareto front.

    Args:
        n_variables: Number of variables.
        n_objectives: Number of objectives.
        gamma: Lamé exponent controlling front curvature.

    Returns:
        Problem: A Lamé superspheres test problem.

    References:
        Emmerich, M. T. M., & Deutz, A. H. (2007).  Test Problems Based on Lamé
        Superspheres.  EMO 2007, LNCS 4403, 922-936.
    """
    if n_objectives < 2:  # noqa: PLR2004
        raise ValueError(f"n_objectives must be at least 2, got {n_objectives}.")

    if n_variables < n_objectives:
        raise ValueError(
            "n_variables must be greater than or equal to n_objectives "
            f"(need at least {n_objectives} variables), got {n_variables}."
        )

    g_symbol = "g"

    sum_expr = " + ".join([f"(x_{i})**2" for i in range(n_objectives, n_variables + 1)])

    g_expr = f"Sqrt({sum_expr})"
    objectives = []

    for m in range(1, n_objectives + 1):
        if m == 1:
            angular_expr = f"Cos(0.5 * {np.pi} * x_1)"

        elif m < n_objectives:
            factors = [f"Sin(0.5 * {np.pi} * x_{i})" for i in range(1, m)]

            factors.append(f"Cos(0.5 * {np.pi} * x_{m})")

            angular_expr = " * ".join(factors)

        else:
            angular_expr = " * ".join([f"Sin(0.5 * {np.pi} * x_{i})" for i in range(1, n_objectives)])

        f_m_expr = f"(1 + {g_symbol}) * (({angular_expr})**({2 / gamma}))"

        objectives.append(
            Objective(
                name=f"f_{m}",
                symbol=f"f_{m}",
                func=f_m_expr,
                maximize=False,
                ideal=0,
                nadir=1,
                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="lame_superspheres",
        description="Lamé superspheres test problem.",
        variables=variables,
        objectives=objectives,
        extra_funcs=extras,
    )

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,
    )

metallurgical_application

metallurgical_application(
    objectives: tuple[str, ...] | list[str] = MOP_II,
    *,
    random_state: int = 0,
    charpy_temperature: float = DEFAULT_CHARPY_TEMPERATURE,
    data_dir: Path | None = None,
    cache_dir: Path | None = None,
    download: bool = True,
) -> Problem

Defines the microalloyed steel design problem of Saini et al. (2023).

A steel is designed by choosing the weight per cent of 17 alloying elements. Four of the six objectives -- yield strength, ultimate tensile strength, elongation and Charpy impact energy -- are predicted from the composition by surrogate models fitted to measurement data and are maximised. The other two are analytical and minimised: the carbon equivalent, which stands for how hard the alloy is to weld, and the material cost.

\[\begin{align} \text{CE} &= C + \frac{Mn + Si}{6} + \frac{Cr + Mo + V}{5} + \frac{Cu + Ni}{15} \\ \text{COST} &= \sum_i \text{price}_i \cdot c_i \end{align}\]

The paper's own two formulations are available as MOP_I, all six objectives, and MOP_II, which drops the Charpy objective. MOP_II is the default, following the paper, which introduces it because "the Charpy surrogate model had a worse accuracy than the other surrogate models".

Objective subsets

Pass any subset of OBJECTIVES to get a different version of the problem. Only the surrogates for the chosen properties are trained, so asking for ("CE", "YS") does not pay for the models it will not use.

The decision variable bounds depend on which objectives are chosen. They are the intersection of the composition ranges of the datasets behind the selected surrogate objectives, so dropping a property widens the box, as it does between the paper's MOP-I and MOP-II: "removing this objective also led to an expansion of the bounds of the decision variables (calculated as the bounds of the intersection of the remaining datasets)". The intersection is what keeps every point of the box inside the range each model was fitted on, so no model is asked to extrapolate. When no surrogate objective is selected at all, the intersection of all four datasets is used.

A study that compares subsets against each other should know that they therefore do not share a decision space, and that the comparison is between problems, not between objective counts on one problem.

Note

Three deviations from the paper, and one from the how-to guide, all deliberate.

The paper selects XGBoost for the tensile strength by cross-validation, at a median \(R^2\) of 0.8440 against gradient boosting's 0.8437. Gradient boosting is used here instead: the gap is two parts in ten thousand of a quantity whose standard deviation across folds is 0.07, and xgboost is not a DESDEO dependency.

The paper's Table 3 lists the bounds it used. Five of the seventeen upper bounds computed from the datasets in this repository differ from it -- manganese, nickel, aluminium, boron and copper -- so the data shipped here is not exactly the data the paper's numbers came from. The bounds are computed from the shipped data rather than hard-coded from the table, so that the box always matches the models actually fitted.

The paper reports an ideal cost of about 43 USD per kg, which the cost formula above does not reproduce: read as weight per cent times USD per kg, the cheapest point of the box costs well under 1. The paper does not state the unit convention, so the formula is implemented as written and the discrepancy is left visible rather than tuned away.

The how-to guide omits the silicon term from the carbon equivalent, uses gradient boosting for the tensile strength, fits every model on all 17 elements, and has neither the Charpy nor the cost objective. This module follows the paper.

The training data contains repeated compositions with different measured values -- the same alloy behaves differently after different processing -- so the models cannot interpolate their own training data exactly. The paper's cross-validated \(R^2\) is 0.75 for yield strength, 0.84 for tensile strength, 0.67 for elongation and 0.44 for the Charpy energy, which is the one to keep in mind when reading results.

Data and models

The datasets ship with the DESDEO repository but not with the installed package. They are looked for in data_dir, then in the repository checkout this module lives in, then in the cache; only if none of those has them are they downloaded from GitHub.

Trained models are cached under $XDG_CACHE_HOME/desdeo/MetallApplication (or ~/.cache/desdeo/MetallApplication), keyed by the scikit-learn version, the seed, the Charpy temperature and a digest of the training data. Training them takes a few seconds; later calls load the cache. A cached file that fails to load is discarded and retrained rather than raised, which is what happens when scikit-learn is upgraded under an old pickle.

random_state is passed to every regressor, so the problem is reproducible for a given seed and scikit-learn version. The how-to guide leaves it unset, and its models therefore differ from run to run.

Each model is wrapped in a CompositionModel, which selects the elements that model was fitted on and, for the Charpy energy, appends the fixed test temperature. The wrapper is what lets every objective share one 17-element decision vector.

Parameters:

Name Type Description Default
objectives tuple[str, ...] | list[str]

which objectives to include, as symbols from OBJECTIVES. The order given is the order they appear in. Defaults to MOP_II.

MOP_II
random_state int

the seed the regressors are fitted with. Defaults to 0.

0
charpy_temperature float

the temperature in degrees Celsius the Charpy impact energy is evaluated at. Ignored unless CHARPY is among the objectives. Defaults to DEFAULT_CHARPY_TEMPERATURE.

DEFAULT_CHARPY_TEMPERATURE
data_dir Path | None

a directory to look for the datasets in before anywhere else. Defaults to None.

None
cache_dir Path | None

where to cache datasets and models. Defaults to None, meaning the user cache directory.

None
download bool

whether the datasets may be fetched from the DESDEO repository when they are not available locally. Defaults to True.

True

Raises:

Type Description
ValueError

if objectives is empty, names an unknown objective, or repeats one.

FileNotFoundError

if a dataset is missing locally and download is False.

RuntimeError

if a dataset has to be downloaded and the download fails.

Returns:

Name Type Description
Problem Problem

an instance of the microalloyed steel design problem.

Source code in desdeo/problem/testproblems/metallurgical_application_problem.py
def metallurgical_application(
    objectives: tuple[str, ...] | list[str] = MOP_II,
    *,
    random_state: int = 0,
    charpy_temperature: float = DEFAULT_CHARPY_TEMPERATURE,
    data_dir: Path | None = None,
    cache_dir: Path | None = None,
    download: bool = True,
) -> Problem:
    r"""Defines the microalloyed steel design problem of Saini et al. (2023).

    A steel is designed by choosing the weight per cent of 17 alloying elements. Four of the six
    objectives -- yield strength, ultimate tensile strength, elongation and Charpy impact energy --
    are predicted from the composition by surrogate models fitted to measurement data and are
    maximised. The other two are analytical and minimised: the carbon equivalent, which stands for how
    hard the alloy is to weld, and the material cost.

    \begin{align}
        \text{CE} &= C + \frac{Mn + Si}{6} + \frac{Cr + Mo + V}{5} + \frac{Cu + Ni}{15} \\
        \text{COST} &= \sum_i \text{price}_i \cdot c_i
    \end{align}

    The paper's own two formulations are available as `MOP_I`, all six objectives, and `MOP_II`, which
    drops the Charpy objective. `MOP_II` is the default, following the paper, which introduces it
    because "the Charpy surrogate model had a worse accuracy than the other surrogate models".

    Objective subsets:
        Pass any subset of `OBJECTIVES` to get a different version of the problem. Only the surrogates
        for the chosen properties are trained, so asking for `("CE", "YS")` does not pay for the
        models it will not use.

        **The decision variable bounds depend on which objectives are chosen.** They are the
        intersection of the composition ranges of the datasets behind the selected surrogate
        objectives, so dropping a property widens the box, as it does between the paper's MOP-I and
        MOP-II: "removing this objective also led to an expansion of the bounds of the decision
        variables (calculated as the bounds of the intersection of the remaining datasets)". The
        intersection is what keeps every point of the box inside the range each model was fitted on,
        so no model is asked to extrapolate. When no surrogate objective is selected at all, the
        intersection of all four datasets is used.

        A study that compares subsets against each other should know that they therefore do not share
        a decision space, and that the comparison is between problems, not between objective counts on
        one problem.

    Note:
        **Three deviations from the paper, and one from the how-to guide, all deliberate.**

        The paper selects XGBoost for the tensile strength by cross-validation, at a median $R^2$ of
        0.8440 against gradient boosting's 0.8437. Gradient boosting is used here instead: the gap is
        two parts in ten thousand of a quantity whose standard deviation across folds is 0.07, and
        `xgboost` is not a DESDEO dependency.

        The paper's Table 3 lists the bounds it used. Five of the seventeen upper bounds computed from
        the datasets in this repository differ from it -- manganese, nickel, aluminium, boron and
        copper -- so the data shipped here is not exactly the data the paper's numbers came from. The
        bounds are computed from the shipped data rather than hard-coded from the table, so that the
        box always matches the models actually fitted.

        The paper reports an ideal cost of about 43 USD per kg, which the cost formula above does not
        reproduce: read as weight per cent times USD per kg, the cheapest point of the box costs well
        under 1. The paper does not state the unit convention, so the formula is implemented as
        written and the discrepancy is left visible rather than tuned away.

        The how-to guide omits the silicon term from the carbon equivalent, uses gradient boosting for
        the tensile strength, fits every model on all 17 elements, and has neither the Charpy nor the
        cost objective. This module follows the paper.

        The training data contains repeated compositions with different measured values -- the same
        alloy behaves differently after different processing -- so the models cannot interpolate their
        own training data exactly. The paper's cross-validated $R^2$ is 0.75 for yield strength, 0.84
        for tensile strength, 0.67 for elongation and 0.44 for the Charpy energy, which is the one to
        keep in mind when reading results.

    Data and models:
        The datasets ship with the DESDEO repository but not with the installed package. They are
        looked for in `data_dir`, then in the repository checkout this module lives in, then in the
        cache; only if none of those has them are they downloaded from GitHub.

        Trained models are cached under `$XDG_CACHE_HOME/desdeo/MetallApplication` (or
        `~/.cache/desdeo/MetallApplication`), keyed by the scikit-learn version, the seed, the Charpy
        temperature and a digest of the training data. Training them takes a few seconds; later calls
        load the cache. A cached file that fails to load is discarded and retrained rather than
        raised, which is what happens when scikit-learn is upgraded under an old pickle.

        `random_state` is passed to every regressor, so the problem is reproducible for a given seed
        and scikit-learn version. The how-to guide leaves it unset, and its models therefore differ
        from run to run.

        Each model is wrapped in a `CompositionModel`, which selects the elements that model was
        fitted on and, for the Charpy energy, appends the fixed test temperature. The wrapper is what
        lets every objective share one 17-element decision vector.

    Args:
        objectives (tuple[str, ...] | list[str], optional): which objectives to include, as symbols
            from `OBJECTIVES`. The order given is the order they appear in. Defaults to `MOP_II`.
        random_state (int, optional): the seed the regressors are fitted with. Defaults to 0.
        charpy_temperature (float, optional): the temperature in degrees Celsius the Charpy impact
            energy is evaluated at. Ignored unless `CHARPY` is among the objectives. Defaults to
            `DEFAULT_CHARPY_TEMPERATURE`.
        data_dir (Path | None, optional): a directory to look for the datasets in before anywhere
            else. Defaults to None.
        cache_dir (Path | None, optional): where to cache datasets and models. Defaults to None,
            meaning the user cache directory.
        download (bool, optional): whether the datasets may be fetched from the DESDEO repository when
            they are not available locally. Defaults to True.

    Raises:
        ValueError: if `objectives` is empty, names an unknown objective, or repeats one.
        FileNotFoundError: if a dataset is missing locally and `download` is False.
        RuntimeError: if a dataset has to be downloaded and the download fails.

    Returns:
        Problem: an instance of the microalloyed steel design problem.
    """
    chosen = list(objectives)
    if not chosen:
        raise ValueError("At least one objective is needed; `objectives` was empty.")
    if unknown := [symbol for symbol in chosen if symbol not in OBJECTIVES]:
        raise ValueError(f"Unknown objective(s) {unknown}. Available objectives are {sorted(OBJECTIVES)}.")
    if len(set(chosen)) != len(chosen):
        raise ValueError(f"`objectives` repeats an objective: {chosen}.")

    surrogate_targets = [symbol for symbol in chosen if symbol in _SURROGATE_SPEC]
    # With no surrogate objective there is no data to derive a box from, so all four datasets are used
    # and the box is the narrowest of the ones any selection could give.
    bound_targets = surrogate_targets or list(_SURROGATE_SPEC)
    datasets = {
        target: _read_dataset(target, data_dir, cache_dir, download=download)
        for target in dict.fromkeys([*bound_targets, *surrogate_targets])
    }
    lower, upper = _variable_bounds({target: datasets[target] for target in bound_targets})

    variables = [
        Variable(
            name=element,
            symbol=element,
            variable_type=VariableTypeEnum.real,
            lowerbound=lower[element],
            upperbound=upper[element],
            initial_value=(lower[element] + upper[element]) / 2,
        )
        for element in _ELEMENTS
    ]

    analytical = {"CE": _CARBON_EQUIVALENT, "COST": _cost_expression()}

    problem_objectives = []
    for symbol in chosen:
        description, maximize = OBJECTIVES[symbol]

        if symbol in analytical:
            problem_objectives.append(
                Objective(
                    name=description,
                    symbol=symbol,
                    func=analytical[symbol],
                    objective_type=ObjectiveTypeEnum.analytical,
                    maximize=maximize,
                    is_linear=True,
                    is_convex=True,
                    is_twice_differentiable=True,
                )
            )
            continue

        temperature = charpy_temperature if _SURROGATE_SPEC[symbol].needs_temperature else None
        problem_objectives.append(
            Objective(
                name=description,
                symbol=symbol,
                surrogates=[_ensure_surrogate(symbol, datasets[symbol], random_state, temperature, cache_dir)],
                objective_type=ObjectiveTypeEnum.surrogate,
                maximize=maximize,
                is_linear=False,
                is_convex=False,
                is_twice_differentiable=False,
            )
        )

    charpy_note = f", with the Charpy energy at {charpy_temperature} degrees Celsius" if "CHARPY" in chosen else ""
    return Problem(
        name="metallurgical application",
        description=(
            f"Microalloyed steel design from 17 alloying elements, with {len(chosen)} objective(s): "
            f"{', '.join(chosen)}{charpy_note}. The mechanical properties come from surrogate models "
            "fitted to measurement data; the carbon equivalent and the material cost are analytical."
        ),
        variables=variables,
        objectives=problem_objectives,
    )

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],
    )

re31

re31() -> Problem

The two bar truss design problem.

Two bars carry a load between two fixed points. The first two objectives are the structural weight and the resultant displacement of the joint; the third is the sum of the three constraint violations, which is how the RE suite turns the original constrained problem into a bound-constrained one:

\[\begin{align} &\min_{\mathbf{x}} & f_1(\mathbf{x}) & = x_1\sqrt{16 + x_3^2} + x_2\sqrt{1 + x_3^2} \\ &\min_{\mathbf{x}} & f_2(\mathbf{x}) & = \frac{20\sqrt{16 + x_3^2}}{x_3 x_1} \\ &\min_{\mathbf{x}} & f_3(\mathbf{x}) & = \sum_{i=1}^3 \max\{-g_i(\mathbf{x}), 0\} \\ &\text{where} & g_1(\mathbf{x}) & = 0.1 - f_1(\mathbf{x}) \geq 0,\\ & & g_2(\mathbf{x}) & = 10^5 - f_2(\mathbf{x}) \geq 0, \\ & & g_3(\mathbf{x}) & = 10^5 - \frac{80\sqrt{1 + x_3^2}}{x_3 x_2} \geq 0, \end{align}\]

where \(x_1, x_2 \in [10^{-5}, 100]\) and \(x_3 \in [1, 3]\). \(x_1\) and \(x_2\) are the lengths of the two bars and \(x_3\) is the vertical distance from the second bar. The original problem leaves \(x_1\) and \(x_2\) unbounded above; the RE suite adds bounds to make the problem bound-constrained.

Note

Following the RE suite, the returned problem has no constraints — the constraint violations are folded into f_3. The constrained variant is CRE31, which is not implemented here.

References

Coello Coello, C. A., & Pulido, G. T. (2005). Multiobjective structural optimization using a microgenetic algorithm. Structural and Multidisciplinary Optimization, 30(5), 388-403. https://doi.org/10.1007/s00158-005-0527-z.

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.

Returns:

Name Type Description
Problem Problem

an instance of the two bar truss design problem.

Source code in desdeo/problem/testproblems/re_problem.py
def re31() -> Problem:
    r"""The two bar truss design problem.

    Two bars carry a load between two fixed points. The first two objectives are the structural weight
    and the resultant displacement of the joint; the third is the sum of the three constraint
    violations, which is how the RE suite turns the original constrained problem into a
    bound-constrained one:

    \begin{align}
        &\min_{\mathbf{x}} & f_1(\mathbf{x}) & = x_1\sqrt{16 + x_3^2} + x_2\sqrt{1 + x_3^2} \\
        &\min_{\mathbf{x}} & f_2(\mathbf{x}) & = \frac{20\sqrt{16 + x_3^2}}{x_3 x_1} \\
        &\min_{\mathbf{x}} & f_3(\mathbf{x}) & = \sum_{i=1}^3 \max\{-g_i(\mathbf{x}), 0\} \\
        &\text{where}   & g_1(\mathbf{x}) & = 0.1 - f_1(\mathbf{x}) \geq 0,\\
        & & g_2(\mathbf{x}) & = 10^5 - f_2(\mathbf{x}) \geq 0, \\
        & & g_3(\mathbf{x}) & = 10^5 - \frac{80\sqrt{1 + x_3^2}}{x_3 x_2} \geq 0,
    \end{align}

    where $x_1, x_2 \in [10^{-5}, 100]$ and $x_3 \in [1, 3]$. $x_1$ and $x_2$ are the lengths of the two
    bars and $x_3$ is the vertical distance from the second bar. The original problem leaves $x_1$ and
    $x_2$ unbounded above; the RE suite adds bounds to make the problem bound-constrained.

    Note:
        Following the RE suite, the returned problem has **no constraints** — the constraint violations
        are folded into `f_3`. The constrained variant is CRE31, which is not implemented here.

    References:
        Coello Coello, C. A., & Pulido, G. T. (2005). Multiobjective structural optimization using a
            microgenetic algorithm. Structural and Multidisciplinary Optimization, 30(5), 388-403.
            https://doi.org/10.1007/s00158-005-0527-z.

        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.

    Returns:
        Problem: an instance of the two bar truss design problem.
    """
    x_1 = Variable(
        name="x_1", symbol="x_1", variable_type=VariableTypeEnum.real, lowerbound=1e-5, upperbound=100, initial_value=50
    )
    x_2 = Variable(
        name="x_2", symbol="x_2", variable_type=VariableTypeEnum.real, lowerbound=1e-5, upperbound=100, initial_value=50
    )
    x_3 = Variable(
        name="x_3", symbol="x_3", variable_type=VariableTypeEnum.real, lowerbound=1, upperbound=3, initial_value=2
    )

    f_1_expr = "x_1 * Sqrt(16 + x_3**2) + x_2 * Sqrt(1 + x_3**2)"
    f_2_expr = "(20 * Sqrt(16 + x_3**2)) / (x_3 * x_1)"

    # The g_i are written in the paper's ">= 0" orientation, so a negative value is a violation.
    extras = [
        ExtraFunction(
            name="g_1",
            symbol="g_1",
            func=f"0.1 - ({f_1_expr})",
            is_linear=False,
            is_convex=False,  # Not checked
            is_twice_differentiable=True,
        ),
        ExtraFunction(
            name="g_2",
            symbol="g_2",
            func=f"100000 - ({f_2_expr})",
            is_linear=False,
            is_convex=False,  # Not checked
            is_twice_differentiable=True,
        ),
        ExtraFunction(
            name="g_3",
            symbol="g_3",
            func="100000 - (80 * Sqrt(1 + x_3**2)) / (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_1_expr,
        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_2_expr,
        objective_type=ObjectiveTypeEnum.analytical,
        is_linear=False,
        is_convex=False,  # Not checked
        is_twice_differentiable=True,
    )
    f_3 = Objective(
        name="f_3",
        symbol="f_3",
        func=_violation_objective(["g_1", "g_2", "g_3"]),
        objective_type=ObjectiveTypeEnum.analytical,
        is_linear=False,
        is_convex=False,  # Not checked
        is_twice_differentiable=False,
    )

    return Problem(
        name="re31",
        description="The two bar truss design problem",
        variables=[x_1, x_2, x_3],
        objectives=[f_1, f_2, f_3],
        extra_funcs=extras,
    )

re32

re32(
    p: float = 6000.0,
    l: float = 14.0,
    e: float = 30.0 * 1000000.0,
    g: float = 12.0 * 1000000.0,
    tau_max: float = 13600.0,
    sigma_max: float = 30000.0,
) -> Problem

The welded beam design problem.

A beam is welded to a rigid support and carries a load at its free end. The first two objectives are the fabrication cost and the end deflection; the third is the sum of the four constraint violations:

\[\begin{align} &\min_{\mathbf{x}} & f_1(\mathbf{x}) & = 1.10471x_1^2x_2 + 0.04811x_3x_4(14 + x_2) \\ &\min_{\mathbf{x}} & f_2(\mathbf{x}) & = \frac{4PL^3}{Ex_4x_3^3} \\ &\min_{\mathbf{x}} & f_3(\mathbf{x}) & = \sum_{i=1}^4 \max\{-g_i(\mathbf{x}), 0\} \\ &\text{where} & g_1(\mathbf{x}) & = \tau_{max} - \tau(\mathbf{x}) \geq 0,\\ & & g_2(\mathbf{x}) & = \sigma_{max} - \sigma(\mathbf{x}) \geq 0, \\ & & g_3(\mathbf{x}) & = x_4 - x_1 \geq 0, \\ & & g_4(\mathbf{x}) & = P_C(\mathbf{x}) - P \geq 0, \end{align}\]

with the stress terms

\[\begin{align} \tau(\mathbf{x}) & = \sqrt{(\tau')^2 + \frac{2\tau'\tau''x_2}{2R} + (\tau'')^2}, & \tau' & = \frac{P}{\sqrt{2}x_1x_2}, & \tau'' & = \frac{MR}{J}, \\ M & = P\left(L + \frac{x_2}{2}\right), & R & = \sqrt{\frac{x_2^2}{4} + \left(\frac{x_1 + x_3}{2}\right)^2}, & J & = 2\left(\sqrt{2}x_1x_2\left(\frac{x_2^2}{12} + \left(\frac{x_1 + x_3}{2}\right)^2\right)\right), \\ \sigma(\mathbf{x}) & = \frac{6PL}{x_4x_3^2}, & P_C(\mathbf{x}) & = \frac{4.013E\sqrt{x_3^2x_4^6/36}}{L^2} \left(1 - \frac{x_3}{2L}\sqrt{\frac{E}{4G}}\right). & \end{align}\]

where \(x_1, x_4 \in [0.125, 5]\) and \(x_2, x_3 \in [0.1, 10]\). The four variables adjust the size of the beam: the weld thickness, the weld length, the beam depth and the beam width.

Note

Following the RE suite, the returned problem has no constraints — the constraint violations are folded into f_3. The constrained variant is CRE32, which is not implemented here.

References

Ray, T., & Liew, K. M. (2002). A swarm metaphor for multiobjective design optimization. Engineering Optimization, 34(2), 141-153. https://doi.org/10.1080/03052150210915.

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.

Parameters:

Name Type Description Default
p float

the load on the beam (lb). Defaults to 6000.0.

6000.0
l float

the beam overhang length (in). Defaults to 14.0.

14.0
e float

Young's modulus (psi). Defaults to 30.0e6.

30.0 * 1000000.0
g float

the shear modulus (psi). Defaults to 12.0e6.

12.0 * 1000000.0
tau_max float

the maximum allowable shear stress (psi). Defaults to 13600.0.

13600.0
sigma_max float

the maximum allowable normal stress (psi). Defaults to 30000.0.

30000.0

Returns:

Name Type Description
Problem Problem

an instance of the welded beam design problem.

Source code in desdeo/problem/testproblems/re_problem.py
def re32(
    p: float = 6000.0,
    l: float = 14.0,
    e: float = 30.0 * 1e6,
    g: float = 12.0 * 1e6,
    tau_max: float = 13600.0,
    sigma_max: float = 30000.0,
) -> Problem:
    r"""The welded beam design problem.

    A beam is welded to a rigid support and carries a load at its free end. The first two objectives are
    the fabrication cost and the end deflection; the third is the sum of the four constraint violations:

    \begin{align}
        &\min_{\mathbf{x}} & f_1(\mathbf{x}) & = 1.10471x_1^2x_2 + 0.04811x_3x_4(14 + x_2) \\
        &\min_{\mathbf{x}} & f_2(\mathbf{x}) & = \frac{4PL^3}{Ex_4x_3^3} \\
        &\min_{\mathbf{x}} & f_3(\mathbf{x}) & = \sum_{i=1}^4 \max\{-g_i(\mathbf{x}), 0\} \\
        &\text{where}   & g_1(\mathbf{x}) & = \tau_{max} - \tau(\mathbf{x}) \geq 0,\\
        & & g_2(\mathbf{x}) & = \sigma_{max} - \sigma(\mathbf{x}) \geq 0, \\
        & & g_3(\mathbf{x}) & = x_4 - x_1 \geq 0, \\
        & & g_4(\mathbf{x}) & = P_C(\mathbf{x}) - P \geq 0,
    \end{align}

    with the stress terms

    \begin{align}
        \tau(\mathbf{x}) & = \sqrt{(\tau')^2 + \frac{2\tau'\tau''x_2}{2R} + (\tau'')^2}, &
        \tau' & = \frac{P}{\sqrt{2}x_1x_2}, &
        \tau'' & = \frac{MR}{J}, \\
        M & = P\left(L + \frac{x_2}{2}\right), &
        R & = \sqrt{\frac{x_2^2}{4} + \left(\frac{x_1 + x_3}{2}\right)^2}, &
        J & = 2\left(\sqrt{2}x_1x_2\left(\frac{x_2^2}{12} +
            \left(\frac{x_1 + x_3}{2}\right)^2\right)\right), \\
        \sigma(\mathbf{x}) & = \frac{6PL}{x_4x_3^2}, &
        P_C(\mathbf{x}) & = \frac{4.013E\sqrt{x_3^2x_4^6/36}}{L^2}
            \left(1 - \frac{x_3}{2L}\sqrt{\frac{E}{4G}}\right). &
    \end{align}

    where $x_1, x_4 \in [0.125, 5]$ and $x_2, x_3 \in [0.1, 10]$. The four variables adjust the size of
    the beam: the weld thickness, the weld length, the beam depth and the beam width.

    Note:
        Following the RE suite, the returned problem has **no constraints** — the constraint violations
        are folded into `f_3`. The constrained variant is CRE32, which is not implemented here.

    References:
        Ray, T., & Liew, K. M. (2002). A swarm metaphor for multiobjective design optimization.
            Engineering Optimization, 34(2), 141-153. https://doi.org/10.1080/03052150210915.

        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.

    Args:
        p (float, optional): the load on the beam (lb). Defaults to 6000.0.
        l (float, optional): the beam overhang length (in). Defaults to 14.0.
        e (float, optional): Young's modulus (psi). Defaults to 30.0e6.
        g (float, optional): the shear modulus (psi). Defaults to 12.0e6.
        tau_max (float, optional): the maximum allowable shear stress (psi). Defaults to 13600.0.
        sigma_max (float, optional): the maximum allowable normal stress (psi). Defaults to 30000.0.

    Returns:
        Problem: an instance of the welded beam design problem.
    """
    x_1 = Variable(
        name="x_1", symbol="x_1", variable_type=VariableTypeEnum.real, lowerbound=0.125, upperbound=5, initial_value=1.0
    )
    x_2 = Variable(
        name="x_2", symbol="x_2", variable_type=VariableTypeEnum.real, lowerbound=0.1, upperbound=10, initial_value=5.0
    )
    x_3 = Variable(
        name="x_3", symbol="x_3", variable_type=VariableTypeEnum.real, lowerbound=0.1, upperbound=10, initial_value=5.0
    )
    x_4 = Variable(
        name="x_4", symbol="x_4", variable_type=VariableTypeEnum.real, lowerbound=0.125, upperbound=5, initial_value=1.0
    )

    # The half-sum (x_1 + x_3) / 2 appears in both R and J, so it is written once.
    half_sum_sq = "((x_1 + x_3) / 2)**2"
    tau_prime = f"({p} / ({np.sqrt(2.0)} * x_1 * x_2))"
    moment = f"({p} * ({l} + x_2 / 2))"
    radius = f"(Sqrt(x_2**2 / 4 + {half_sum_sq}))"
    polar_moment = f"(2 * ({np.sqrt(2.0)} * x_1 * x_2 * (x_2**2 / 12 + {half_sum_sq})))"
    tau_double_prime = f"(({moment} * {radius}) / {polar_moment})"
    tau = (
        f"Sqrt({tau_prime}**2 + (2 * {tau_prime} * {tau_double_prime} * x_2) / (2 * {radius}) + {tau_double_prime}**2)"
    )
    sigma = f"((6 * {p} * {l}) / (x_4 * x_3**2))"
    buckling_load = (
        f"(((4.013 * {e} * Sqrt(x_3**2 * x_4**6 / 36)) / {l}**2) * (1 - (x_3 / (2 * {l})) * {np.sqrt(e / (4.0 * g))}))"
    )

    extras = [
        ExtraFunction(
            name="g_1",
            symbol="g_1",
            func=f"{tau_max} - {tau}",
            is_linear=False,
            is_convex=False,  # Not checked
            is_twice_differentiable=True,
        ),
        ExtraFunction(
            name="g_2",
            symbol="g_2",
            func=f"{sigma_max} - {sigma}",
            is_linear=False,
            is_convex=False,  # Not checked
            is_twice_differentiable=True,
        ),
        ExtraFunction(
            name="g_3",
            symbol="g_3",
            func="x_4 - x_1",
            is_linear=True,
            is_convex=True,
            is_twice_differentiable=True,
        ),
        ExtraFunction(
            name="g_4",
            symbol="g_4",
            func=f"{buckling_load} - {p}",
            is_linear=False,
            is_convex=False,  # Not checked
            is_twice_differentiable=True,
        ),
    ]

    f_1 = Objective(
        name="f_1",
        symbol="f_1",
        func="1.10471 * x_1**2 * x_2 + 0.04811 * x_3 * x_4 * (14 + x_2)",
        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"({4 * p * l**3 / e}) / (x_4 * x_3**3)",
        objective_type=ObjectiveTypeEnum.analytical,
        is_linear=False,
        is_convex=False,  # Not checked
        is_twice_differentiable=True,
    )
    f_3 = Objective(
        name="f_3",
        symbol="f_3",
        func=_violation_objective(["g_1", "g_2", "g_3", "g_4"]),
        objective_type=ObjectiveTypeEnum.analytical,
        is_linear=False,
        is_convex=False,  # Not checked
        is_twice_differentiable=False,
    )

    return Problem(
        name="re32",
        description="The welded beam design problem",
        variables=[x_1, x_2, x_3, x_4],
        objectives=[f_1, f_2, f_3],
        extra_funcs=extras,
    )

re33

re33() -> Problem

The disc brake design problem.

The first two objectives are the mass of the brake and the minimum stopping time; the third is the sum of the four constraint violations:

\[\begin{align} &\min_{\mathbf{x}} & f_1(\mathbf{x}) & = 4.9 \times 10^{-5}(x_2^2 - x_1^2)(x_4 - 1) \\ &\min_{\mathbf{x}} & f_2(\mathbf{x}) & = \frac{9.82 \times 10^6 (x_2^2 - x_1^2)} {x_3x_4(x_2^3 - x_1^3)} \\ &\min_{\mathbf{x}} & f_3(\mathbf{x}) & = \sum_{i=1}^4 \max\{-g_i(\mathbf{x}), 0\} \\ &\text{where} & g_1(\mathbf{x}) & = (x_2 - x_1) - 20 \geq 0,\\ & & g_2(\mathbf{x}) & = 0.4 - \frac{x_3}{3.14(x_2^2 - x_1^2)} \geq 0, \\ & & g_3(\mathbf{x}) & = 1 - \frac{2.22 \times 10^{-3}x_3(x_2^3 - x_1^3)} {(x_2^2 - x_1^2)^2} \geq 0, \\ & & g_4(\mathbf{x}) & = \frac{2.66 \times 10^{-2}x_3x_4(x_2^3 - x_1^3)}{x_2^2 - x_1^2} - 900 \geq 0, \end{align}\]

where \(x_1 \in [55, 80]\), \(x_2 \in [75, 110]\), \(x_3 \in [1000, 3000]\) and \(x_4 \in [11, 20]\). The four variables are the inner radius of the discs, the outer radius of the discs, the engaging force, and the number of friction surfaces. The number of friction surfaces is continuous here: the original range is \([2, 20]\), but the original problem also constrains \(x_4 \geq 11\), so the RE suite folds that bound into the variable range instead of into \(f_3\).

Note

Following the RE suite, the returned problem has no constraints — the constraint violations are folded into f_3. The constrained variant is CRE33, which is not implemented here.

References

Ray, T., & Liew, K. M. (2002). A swarm metaphor for multiobjective design optimization. Engineering Optimization, 34(2), 141-153. https://doi.org/10.1080/03052150210915.

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.

Returns:

Name Type Description
Problem Problem

an instance of the disc brake design problem.

Source code in desdeo/problem/testproblems/re_problem.py
def re33() -> Problem:
    r"""The disc brake design problem.

    The first two objectives are the mass of the brake and the minimum stopping time; the third is the
    sum of the four constraint violations:

    \begin{align}
        &\min_{\mathbf{x}} & f_1(\mathbf{x}) & = 4.9 \times 10^{-5}(x_2^2 - x_1^2)(x_4 - 1) \\
        &\min_{\mathbf{x}} & f_2(\mathbf{x}) & = \frac{9.82 \times 10^6 (x_2^2 - x_1^2)}
            {x_3x_4(x_2^3 - x_1^3)} \\
        &\min_{\mathbf{x}} & f_3(\mathbf{x}) & = \sum_{i=1}^4 \max\{-g_i(\mathbf{x}), 0\} \\
        &\text{where}   & g_1(\mathbf{x}) & = (x_2 - x_1) - 20 \geq 0,\\
        & & g_2(\mathbf{x}) & = 0.4 - \frac{x_3}{3.14(x_2^2 - x_1^2)} \geq 0, \\
        & & g_3(\mathbf{x}) & = 1 - \frac{2.22 \times 10^{-3}x_3(x_2^3 - x_1^3)}
            {(x_2^2 - x_1^2)^2} \geq 0, \\
        & & g_4(\mathbf{x}) & = \frac{2.66 \times 10^{-2}x_3x_4(x_2^3 - x_1^3)}{x_2^2 - x_1^2}
            - 900 \geq 0,
    \end{align}

    where $x_1 \in [55, 80]$, $x_2 \in [75, 110]$, $x_3 \in [1000, 3000]$ and $x_4 \in [11, 20]$. The four
    variables are the inner radius of the discs, the outer radius of the discs, the engaging force, and
    the number of friction surfaces. The number of friction surfaces is continuous here: the original
    range is $[2, 20]$, but the original problem also constrains $x_4 \geq 11$, so the RE suite folds
    that bound into the variable range instead of into $f_3$.

    Note:
        Following the RE suite, the returned problem has **no constraints** — the constraint violations
        are folded into `f_3`. The constrained variant is CRE33, which is not implemented here.

    References:
        Ray, T., & Liew, K. M. (2002). A swarm metaphor for multiobjective design optimization.
            Engineering Optimization, 34(2), 141-153. https://doi.org/10.1080/03052150210915.

        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.

    Returns:
        Problem: an instance of the disc brake design problem.
    """
    x_1 = Variable(
        name="x_1", symbol="x_1", variable_type=VariableTypeEnum.real, lowerbound=55, upperbound=80, initial_value=60
    )
    x_2 = Variable(
        name="x_2", symbol="x_2", variable_type=VariableTypeEnum.real, lowerbound=75, upperbound=110, initial_value=90
    )
    x_3 = Variable(
        name="x_3",
        symbol="x_3",
        variable_type=VariableTypeEnum.real,
        lowerbound=1000,
        upperbound=3000,
        initial_value=2000,
    )
    x_4 = Variable(
        name="x_4", symbol="x_4", variable_type=VariableTypeEnum.real, lowerbound=11, upperbound=20, initial_value=15
    )

    # Squared and cubed radius differences recur in every function below.
    sq_diff = "(x_2**2 - x_1**2)"
    cube_diff = "(x_2**3 - x_1**3)"

    extras = [
        ExtraFunction(
            name="g_1",
            symbol="g_1",
            func="(x_2 - x_1) - 20",
            is_linear=True,
            is_convex=True,
            is_twice_differentiable=True,
        ),
        ExtraFunction(
            name="g_2",
            symbol="g_2",
            func=f"0.4 - x_3 / (3.14 * {sq_diff})",
            is_linear=False,
            is_convex=False,  # Not checked
            is_twice_differentiable=True,
        ),
        ExtraFunction(
            name="g_3",
            symbol="g_3",
            func=f"1 - (2.22e-3 * x_3 * {cube_diff}) / {sq_diff}**2",
            is_linear=False,
            is_convex=False,  # Not checked
            is_twice_differentiable=True,
        ),
        ExtraFunction(
            name="g_4",
            symbol="g_4",
            func=f"(2.66e-2 * x_3 * x_4 * {cube_diff}) / {sq_diff} - 900",
            is_linear=False,
            is_convex=False,  # Not checked
            is_twice_differentiable=True,
        ),
    ]

    f_1 = Objective(
        name="f_1",
        symbol="f_1",
        func=f"4.9e-5 * {sq_diff} * (x_4 - 1)",
        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"(9.82e6 * {sq_diff}) / (x_3 * x_4 * {cube_diff})",
        objective_type=ObjectiveTypeEnum.analytical,
        is_linear=False,
        is_convex=False,  # Not checked
        is_twice_differentiable=True,
    )
    f_3 = Objective(
        name="f_3",
        symbol="f_3",
        func=_violation_objective(["g_1", "g_2", "g_3", "g_4"]),
        objective_type=ObjectiveTypeEnum.analytical,
        is_linear=False,
        is_convex=False,  # Not checked
        is_twice_differentiable=False,
    )

    return Problem(
        name="re33",
        description="The disc brake design problem",
        variables=[x_1, x_2, x_3, x_4],
        objectives=[f_1, f_2, f_3],
        extra_funcs=extras,
    )

re34

re34() -> Problem

The vehicle crash worthiness design problem.

The implementation of this problem is taken from the vehicle_crashworthiness_problem.py file.

Source code in desdeo/problem/testproblems/re_problem.py
def re34() -> Problem:
    """The vehicle crash worthiness design problem.

    The implementation of this problem is taken from the vehicle_crashworthiness_problem.py file.
    """
    return vehicle_crashworthiness()

re37

re37() -> Problem

The rocket injector design problem.

The implementation of this problem is taken from the rocket_injector_design_problem.py file.

Source code in desdeo/problem/testproblems/re_problem.py
def re37() -> Problem:
    """The rocket injector design problem.

    The implementation of this problem is taken from the rocket_injector_design_problem.py file.
    """
    return rocket_injector_design()

re41

re41() -> Problem

The car side impact design problem.

The implementation of this problem is taken from the car_side_impact_problem.py file. Removes the constraints from the problem.

Source code in desdeo/problem/testproblems/re_problem.py
def re41() -> Problem:
    """The car side impact design problem.

    The implementation of this problem is taken from the car_side_impact_problem.py file. Removes the constraints from
    the problem.
    """
    return car_side_impact(three_obj=False).model_copy(update={"constraints": None})

re42

re42(
    round_trip_miles: float = 5000.0,
    fuel_price: float = 100.0,
    handling_rate: float = 8000.0,
    gravity: float = 9.8065,
) -> Problem

The conceptual marine design problem.

A bulk carrier is sized by its length, beam, depth, draft, speed and block coefficient. The first three objectives are the transportation cost, the light ship weight and the (negated) annual cargo transport capacity; the fourth is the sum of the nine constraint violations:

\[\begin{align} &\min_{\mathbf{x}} & f_1(\mathbf{x}) & = \frac{\text{annual costs}}{\text{annual cargo}} \\ &\min_{\mathbf{x}} & f_2(\mathbf{x}) & = W_s + W_o + W_m \\ &\min_{\mathbf{x}} & f_3(\mathbf{x}) & = -\text{cargo DWT} \times \text{RTPA} \\ &\min_{\mathbf{x}} & f_4(\mathbf{x}) & = \sum_{i=1}^9 \max\{-g_i(\mathbf{x}), 0\} \\ &\text{where} & g_1 & = L/B - 6 \geq 0, & g_2 & = 15 - L/D \geq 0, \\ & & g_3 & = 19 - L/T \geq 0, & g_4 & = 0.45\,\text{DWT}^{0.31} - T \geq 0, \\ & & g_5 & = 0.7D + 0.7 - T \geq 0, & g_6 & = \text{DWT} - 3\,000 \geq 0, \\ & & g_7 & = 500\,000 - \text{DWT} \geq 0, & g_8 & = 0.32 - F_n \geq 0, \\ & & g_9 & = K_B + BM_T - K_G - 0.07B \geq 0. & \end{align}\]

The intermediate quantities — displacement, power, Froude number, the weight and cost breakdowns, the round-trip schedule and the stability terms — are defined as extra functions on the returned problem, one per equation of the source, so they can be inspected alongside the objectives.

The decision variables \(L, B, D, T, V_k, C_B\) are the length, beam, depth, draft, speed and block coefficient, with \(L \in [150, 274.32]\), \(B \in [20, 32.31]\), \(D \in [13, 25]\), \(T \in [10, 11.71]\), \(V_k \in [14, 18]\) and \(C_B \in [0.63, 0.75]\). The annual cargo capacity is maximised in the original formulation and is negated here so that every objective is minimised.

Note

Following the RE suite, the returned problem has no constraints — the constraint violations are folded into f_4. The constrained variant is CRE42, which is not implemented here.

The upper bound on the deadweight in \(g_7\) is 500 000, not the 50 000 that the RE reference implementation shipped until it was corrected in 2021.

Warning

The number of sea days is \(\text{round trip miles} / 24 \times V_k\), not \(\text{round trip miles} / (24 V_k)\). The second is the dimensionally sensible reading — days at sea should fall as the ship goes faster — but the first is what defines RE42, and it is reproduced here deliberately. Verified against the suite's published ideal point: the objective minima over the box agree to eight significant figures with \((-2756.259, 3962.558, 1947.881, 0)\) under this reading, and are wrong by a factor of 400 in \(f_3\) under the other. Any comparison with published RE42 results needs this equation.

A consequence worth knowing before interpreting results: at 3 333 sea days the ship carries more fuel than its deadweight, so the cargo deadweight is negative across the whole reference front. The annual cargo is therefore negative and \(f_3 = -\text{annual cargo}\) is positive, which is why the published front has \(f_3 \in [1948, 5195]\) rather than the large negative values a physically-sensible schedule would give.

References

Parsons, M. G., & Scott, R. L. (2004). Formulation of multicriterion design optimization problems for solution with scalar numerical optimization methods. Journal of Ship Research, 48(1), 61-76. https://doi.org/10.5957/jsr.2004.48.1.61.

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.

Parameters:

Name Type Description Default
round_trip_miles float

the length of one round trip (nautical miles). Defaults to 5000.0.

5000.0
fuel_price float

the price of fuel per ton. Defaults to 100.0.

100.0
handling_rate float

the cargo handling rate in port (tons per day). Defaults to 8000.0.

8000.0
gravity float

gravitational acceleration (m/s^2), used in the Froude number. Defaults to 9.8065.

9.8065

Returns:

Name Type Description
Problem Problem

an instance of the conceptual marine design problem.

Source code in desdeo/problem/testproblems/re_problem.py
def re42(
    round_trip_miles: float = 5000.0,
    fuel_price: float = 100.0,
    handling_rate: float = 8000.0,
    gravity: float = 9.8065,
) -> Problem:
    r"""The conceptual marine design problem.

    A bulk carrier is sized by its length, beam, depth, draft, speed and block coefficient. The first
    three objectives are the transportation cost, the light ship weight and the (negated) annual cargo
    transport capacity; the fourth is the sum of the nine constraint violations:

    \begin{align}
        &\min_{\mathbf{x}} & f_1(\mathbf{x}) & = \frac{\text{annual costs}}{\text{annual cargo}} \\
        &\min_{\mathbf{x}} & f_2(\mathbf{x}) & = W_s + W_o + W_m \\
        &\min_{\mathbf{x}} & f_3(\mathbf{x}) & = -\text{cargo DWT} \times \text{RTPA} \\
        &\min_{\mathbf{x}} & f_4(\mathbf{x}) & = \sum_{i=1}^9 \max\{-g_i(\mathbf{x}), 0\} \\
        &\text{where}   & g_1 & = L/B - 6 \geq 0, & g_2 & = 15 - L/D \geq 0, \\
        & & g_3 & = 19 - L/T \geq 0, & g_4 & = 0.45\,\text{DWT}^{0.31} - T \geq 0, \\
        & & g_5 & = 0.7D + 0.7 - T \geq 0, & g_6 & = \text{DWT} - 3\,000 \geq 0, \\
        & & g_7 & = 500\,000 - \text{DWT} \geq 0, & g_8 & = 0.32 - F_n \geq 0, \\
        & & g_9 & = K_B + BM_T - K_G - 0.07B \geq 0. &
    \end{align}

    The intermediate quantities — displacement, power, Froude number, the weight and cost breakdowns,
    the round-trip schedule and the stability terms — are defined as extra functions on the returned
    problem, one per equation of the source, so they can be inspected alongside the objectives.

    The decision variables $L, B, D, T, V_k, C_B$ are the length, beam, depth, draft, speed and block
    coefficient, with $L \in [150, 274.32]$, $B \in [20, 32.31]$, $D \in [13, 25]$, $T \in [10, 11.71]$,
    $V_k \in [14, 18]$ and $C_B \in [0.63, 0.75]$. The annual cargo capacity is maximised in the original
    formulation and is negated here so that every objective is minimised.

    Note:
        Following the RE suite, the returned problem has **no constraints** — the constraint violations
        are folded into `f_4`. The constrained variant is CRE42, which is not implemented here.

        The upper bound on the deadweight in $g_7$ is 500 000, not the 50 000 that the RE reference
        implementation shipped until it was corrected in 2021.

    Warning:
        **The number of sea days is $\text{round trip miles} / 24 \times V_k$, not
        $\text{round trip miles} / (24 V_k)$.** The second is the dimensionally sensible reading — days
        at sea should fall as the ship goes faster — but the first is what defines RE42, and it is
        reproduced here deliberately. Verified against the suite's published ideal point: the objective
        minima over the box agree to eight significant figures with
        $(-2756.259, 3962.558, 1947.881, 0)$ under this reading, and are wrong by a factor of 400 in
        $f_3$ under the other. Any comparison with published RE42 results needs this equation.

        A consequence worth knowing before interpreting results: at 3 333 sea days the ship carries
        more fuel than its deadweight, so the cargo deadweight is negative across the whole reference
        front. The annual cargo is therefore negative and $f_3 = -\text{annual cargo}$ is positive,
        which is why the published front has $f_3 \in [1948, 5195]$ rather than the large negative
        values a physically-sensible schedule would give.

    References:
        Parsons, M. G., & Scott, R. L. (2004). Formulation of multicriterion design optimization
            problems for solution with scalar numerical optimization methods. Journal of Ship Research,
            48(1), 61-76. https://doi.org/10.5957/jsr.2004.48.1.61.

        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.

    Args:
        round_trip_miles (float, optional): the length of one round trip (nautical miles).
            Defaults to 5000.0.
        fuel_price (float, optional): the price of fuel per ton. Defaults to 100.0.
        handling_rate (float, optional): the cargo handling rate in port (tons per day).
            Defaults to 8000.0.
        gravity (float, optional): gravitational acceleration (m/s^2), used in the Froude number.
            Defaults to 9.8065.

    Returns:
        Problem: an instance of the conceptual marine design problem.
    """
    # x_1 = L, x_2 = B, x_3 = D, x_4 = T, x_5 = V_k, x_6 = C_B.
    bounds = [
        ("x_1", "length", 150.0, 274.32),
        ("x_2", "beam", 20.0, 32.31),
        ("x_3", "depth", 13.0, 25.0),
        ("x_4", "draft", 10.0, 11.71),
        ("x_5", "speed", 14.0, 18.0),
        ("x_6", "block coefficient", 0.63, 0.75),
    ]
    variables = [
        Variable(
            name=name,
            symbol=symbol,
            variable_type=VariableTypeEnum.real,
            lowerbound=lower,
            upperbound=upper,
            initial_value=(lower + upper) / 2,
        )
        for symbol, name, lower, upper in bounds
    ]

    def extra(symbol: str, func: str, *, is_linear: bool = False) -> ExtraFunction:
        return ExtraFunction(
            name=symbol,
            symbol=symbol,
            func=func,
            is_linear=is_linear,
            is_convex=False,  # Not checked
            is_twice_differentiable=True,
        )

    # One extra function per equation of the source, in dependency order: each may refer to the ones
    # above it. Written this way rather than as one substituted expression because the source defines
    # the ship this way, and because the intermediate values are worth reading off a solution.
    extras = [
        extra("displacement", "1.025 * x_1 * x_2 * x_4 * x_6"),
        extra("froude", f"(0.5144 * x_5) / Sqrt({gravity} * x_1)"),
        # The admiralty coefficient is a quadratic in the block coefficient.
        extra("coef_a", "4977.06 * x_6**2 - 8105.61 * x_6 + 4456.51"),
        extra("coef_b", "-10847.2 * x_6**2 + 12817 * x_6 - 6960.32"),
        extra("power", f"(displacement**{2 / 3} * x_5**3) / (coef_a + coef_b * froude)"),
        extra("weight_steel", "0.034 * x_1**1.7 * x_2**0.7 * x_3**0.4 * x_6**0.5"),
        extra("weight_outfit", "1.0 * x_1**0.8 * x_2**0.6 * x_3**0.3 * x_6**0.1"),
        extra("weight_machinery", "0.17 * power**0.9"),
        extra("light_ship", "weight_steel + weight_outfit + weight_machinery", is_linear=True),
        extra("deadweight", "displacement - light_ship", is_linear=True),
        extra("ship_cost", "1.3 * (2000 * weight_steel**0.85 + 3500 * weight_outfit + 2400 * power**0.8)"),
        extra("capital_costs", "0.2 * ship_cost", is_linear=True),
        extra("running_costs", "40000 * deadweight**0.3"),
        extra("daily_consumption", "(0.19 * power * 24) / 1000 + 0.2", is_linear=True),
        # Transcribed exactly as the RE suite states it: (round trip miles / 24) * speed. Dividing by
        # the speed instead would be the dimensionally sensible reading, but it is not this problem —
        # see the note in the docstring. The parentheses are load-bearing: the expression parser reads
        # "a / b * c" as "a / (b * c)", which is the sensible reading and the wrong problem.
        extra("sea_days", f"({round_trip_miles} / 24) * x_5"),
        extra("fuel_cost", f"1.05 * daily_consumption * sea_days * {fuel_price}"),
        extra("port_cost", "6.3 * deadweight**0.8"),
        extra("fuel_carried", "daily_consumption * (sea_days + 5)"),
        extra("miscellaneous_deadweight", "2 * deadweight**0.5"),
        extra("cargo_deadweight", "deadweight - fuel_carried - miscellaneous_deadweight", is_linear=True),
        extra("port_days", f"2 * (cargo_deadweight / {handling_rate} + 0.5)", is_linear=True),
        extra("round_trips_per_year", "350 / (sea_days + port_days)"),
        extra("voyage_costs", "(fuel_cost + port_cost) * round_trips_per_year"),
        extra("annual_costs", "capital_costs + running_costs + voyage_costs", is_linear=True),
        extra("annual_cargo", "cargo_deadweight * round_trips_per_year"),
        # Stability: centre of buoyancy, metacentric radius and centre of gravity.
        extra("centre_of_buoyancy", "0.53 * x_4", is_linear=True),
        extra("metacentric_radius", "((0.085 * x_6 - 0.002) * x_2**2) / (x_4 * x_6)"),
        extra("centre_of_gravity", "1 + 0.52 * x_3", is_linear=True),
        extra("g_1", "x_1 / x_2 - 6"),
        extra("g_2", "15 - x_1 / x_3"),
        extra("g_3", "19 - x_1 / x_4"),
        extra("g_4", "0.45 * deadweight**0.31 - x_4"),
        extra("g_5", "0.7 * x_3 + 0.7 - x_4", is_linear=True),
        extra("g_6", "deadweight - 3000", is_linear=True),
        extra("g_7", "500000 - deadweight", is_linear=True),
        extra("g_8", "0.32 - froude"),
        extra("g_9", "centre_of_buoyancy + metacentric_radius - centre_of_gravity - 0.07 * x_2", is_linear=True),
    ]

    f_1 = Objective(
        name="f_1",
        symbol="f_1",
        func="annual_costs / annual_cargo",
        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="light_ship",
        objective_type=ObjectiveTypeEnum.analytical,
        is_linear=False,
        is_convex=False,  # Not checked
        is_twice_differentiable=True,
    )
    f_3 = Objective(
        name="f_3",
        symbol="f_3",
        func="-annual_cargo",
        objective_type=ObjectiveTypeEnum.analytical,
        is_linear=False,
        is_convex=False,  # Not checked
        is_twice_differentiable=True,
    )
    f_4 = Objective(
        name="f_4",
        symbol="f_4",
        func=_violation_objective([f"g_{i}" for i in range(1, 10)]),
        objective_type=ObjectiveTypeEnum.analytical,
        is_linear=False,
        is_convex=False,  # Not checked
        is_twice_differentiable=False,
    )

    return Problem(
        name="re42",
        description="The conceptual marine design problem",
        variables=variables,
        objectives=[f_1, f_2, f_3, f_4],
        extra_funcs=extras,
    )

re61

re61() -> Problem

The water management design problem.

The implementation of this problem is taken from the water_management_problem.py file. Removes the constraints from the problem.

Source code in desdeo/problem/testproblems/re_problem.py
def re61() -> Problem:
    """The water management design problem.

    The implementation of this problem is taken from the water_management_problem.py file. Removes the constraints from
    the problem.
    """
    return water_management(six_obj=True).model_copy(update={"constraints": None})

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 the 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 the 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 the 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 the 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
ScenarioModel 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:
        ScenarioModel: 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]

    f4_expr = "-0.96 + 0.96/(1.09 - x_2**2)"
    f4 = Objective(
        name="Addition to city tax",
        symbol="f4",
        func=f4_expr,
        objective_type=ObjectiveTypeEnum.analytical,
        maximize=False,
        is_linear=False,
        is_convex=False,
        is_twice_differentiable=True,
    )

    base_problem = Problem(
        name="Scenario-based river pollution problem",
        description="The scenario-based river pollution problem",
        constants=constants,
        variables=variables,
        objectives=[f4],
    )

    # Build per-scenario objective pool and scenario definitions
    pool_objectives = []
    scenarios = {}

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

        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 = Objective(
            name="DO level city",
            symbol=f"f1_{i + 1}",
            func=f1_expr,
            objective_type=ObjectiveTypeEnum.analytical,
            maximize=True,
            is_linear=False,
            is_convex=False,
            is_twice_differentiable=True,
        )
        f2 = Objective(
            name="DO level fishery",
            symbol=f"f2_{i + 1}",
            func=f2_expr,
            objective_type=ObjectiveTypeEnum.analytical,
            maximize=True,
            is_linear=False,
            is_convex=False,
            is_twice_differentiable=True,
        )
        f3 = Objective(
            name="Return of investment",
            symbol=f"f3_{i + 1}",
            func=f3_expr,
            objective_type=ObjectiveTypeEnum.analytical,
            maximize=True,
            is_linear=False,
            is_convex=False,
            is_twice_differentiable=True,
        )

        base_idx = i * 3
        pool_objectives.extend([f1, f2, f3])
        scenarios[scenario_key] = Scenario(
            objectives={f1.symbol: base_idx, f2.symbol: base_idx + 1, f3.symbol: base_idx + 2}
        )

    return ScenarioModel(
        scenario_tree=[f"{scenario_key_stub}_{i + 1}" for i in range(num_scenarios)],
        base_problem=base_problem,
        objectives=pool_objectives,
        scenarios=scenarios,
    )

rocket_injector_design

rocket_injector_design(original_version=False) -> Problem

The rocket 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 rocket 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,
        is_linear=True,
        is_convex=True,
        is_twice_differentiable=True,
    )

simple_scenario_model

simple_scenario_model() -> ScenarioModel

Returns a ScenarioModel for testing scenario-based problem construction.

The base problem contains elements shared across all scenarios (f_3, con_3). The pool contains scenario-specific elements. Scenario s_1: objectives f_1, f_2 and constraints con_1, con_4. Scenario s_2: objectives f_2, f_4, f_5, constraints con_2, con_4, and extra_func extra_1.

Source code in desdeo/problem/testproblems/simple_problem.py
def simple_scenario_model() -> ScenarioModel:
    """Returns a ScenarioModel for testing scenario-based problem construction.

    The base problem contains elements shared across all scenarios (f_3, con_3).
    The pool contains scenario-specific elements.
    Scenario s_1: objectives f_1, f_2 and constraints con_1, con_4.
    Scenario s_2: objectives f_2, f_4, f_5, constraints con_2, con_4, and extra_func extra_1.
    """
    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,
        ),
    ]

    base_problem = Problem(
        name="Simple scenario base problem",
        description="Base problem for scenario testing; contains elements shared by all scenarios.",
        variables=variables,
        constants=constants,
        objectives=[
            Objective(
                name="f_3",
                symbol="f_3",
                func="(x_1 - 3)**2 + x_2",
                maximize=False,
                ideal=-100,
                nadir=100,
                objective_type=ObjectiveTypeEnum.analytical,
                is_linear=True,
                is_convex=True,
                is_twice_differentiable=True,
            ),
        ],
        constraints=[
            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,
            ),
        ],
    )

    return ScenarioModel(
        scenario_tree={"ROOT": ["s_1", "s_2", "s_3"], "s_1": [], "s_2": [], "s_3": []},
        scenario_probabilities={"s_1": 0.2, "s_2": 0.3, "s_3": 0.5},
        anticipation_stop={"ROOT": ["x_1"]},
        base_problem=base_problem,
        objectives=[
            # index 0: f_1 used by s_1
            Objective(
                name="f_1 (s_1)",
                symbol="f_1",
                func="x_1 + x_2",
                maximize=False,
                ideal=-100,
                nadir=100,
                objective_type=ObjectiveTypeEnum.analytical,
                is_linear=True,
                is_convex=True,
                is_twice_differentiable=True,
            ),
            # index 1: f_2 used by s_1 and s_3
            Objective(
                name="f_2 (s_1/s_3)",
                symbol="f_2",
                func="x_1 - x_2",
                maximize=False,
                ideal=-100,
                nadir=100,
                objective_type=ObjectiveTypeEnum.analytical,
                is_linear=True,
                is_convex=True,
                is_twice_differentiable=True,
            ),
            # index 2: f_1 used by s_2 and s_3
            Objective(
                name="f_1 (s_2/s_3)",
                symbol="f_1",
                func="c_1 + x_2**2 - x_1",
                maximize=False,
                ideal=-100,
                nadir=100,
                objective_type=ObjectiveTypeEnum.analytical,
                is_linear=False,
                is_convex=True,
                is_twice_differentiable=True,
            ),
            # index 3: f_2 used by s_2
            Objective(
                name="f_2 (s_2)",
                symbol="f_2",
                func="-x_1 - x_2",
                maximize=False,
                ideal=-100,
                nadir=100,
                objective_type=ObjectiveTypeEnum.analytical,
                is_linear=True,
                is_convex=True,
                is_twice_differentiable=True,
            ),
        ],
        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,
            ),
            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,
            ),
            Constraint(
                name="con_4",
                symbol="con_4",
                cons_type=ConstraintTypeEnum.LTE,
                func="extra_1 - 5",
                is_linear=True,
                is_convex=True,
                is_twice_differentiable=True,
            ),
        ],
        extra_funcs=[
            # index 0: used by s_2
            ExtraFunction(
                name="extra_1 (s_2)",
                symbol="extra_1",
                func="5*x_1",
                is_linear=True,
                is_convex=True,
                is_twice_differentiable=True,
            ),
            # index 1: used by s_1
            ExtraFunction(
                name="extra_1 (s_1)",
                symbol="extra_1",
                func="2*x_1",
                is_linear=True,
                is_convex=True,
                is_twice_differentiable=True,
            ),
        ],
        constants=[
            Constant(name="c_1 (s_1)", symbol="c_1", value=1.0),  # index 0
            Constant(name="c_1 (s_2)", symbol="c_1", value=5.0),  # index 1
            Constant(name="c_1 (s_3)", symbol="c_1", value=10.0),  # index 2
        ],
        scenarios={
            # constants: c_1→0/1/2 per scenario
            # objectives: f_1→0(s_1),2(s_2/s_3) | f_2→1(s_1/s_3),3(s_2)
            # constraints: con_1=0, con_2=1, con_4=2 | extra_funcs: extra_1→1(s_1),0(s_2)
            "s_1": Scenario(
                constants={"c_1": 0},
                objectives={"f_1": 0, "f_2": 1},
                constraints={"con_1": 0, "con_4": 2},
                extra_funcs={"extra_1": 1},
            ),
            "s_2": Scenario(
                constants={"c_1": 1},
                objectives={"f_1": 2, "f_2": 3},
                constraints={"con_2": 1, "con_4": 2},
                extra_funcs={"extra_1": 0},
            ),
            "s_3": Scenario(
                constants={"c_1": 2},
                objectives={"f_1": 2, "f_2": 1},
                constraints={"con_1": 0, "con_2": 1},
            ),
        },
    )

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,
        ),
        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,
        ),
        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,
        ),
        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,
        ),
    ]

    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,
        ),
        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,
        ),
        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,
        ),
        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,
        ),
        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,
        ),
    ]

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

    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,
    )

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,
    )

summer_cabin_battery_problem

summer_cabin_battery_problem(
    initial_soc: float = 0.0, n_panels_max: int = 50
) -> Problem

Build a bi-objective MILP for battery + solar investment and scheduling at the summer cabin.

Decision variables: - y ∈ {0,1}: whether the battery is installed - E ∈ [0, 42] kWh: battery capacity (14-42 kWh if installed, 0 otherwise) - n ∈ {0,...,n_panels_max}: number of 160 W solar panels - c_t ∈ [0, 10] kW: hourly charging power (vector of length T) - d_t ∈ [0, 10] kW: hourly discharging power (vector of length T) - soc_t ∈ [0, 42] kWh: state of charge (vector of length T) - buy_t ≥ 0 kWh/h: electricity purchased from the grid (vector of length T) - sell_t ≥ 0 kWh/h: electricity sold to the grid (vector of length T)

Objectives: - f_1: total electricity cost (EUR) = Σ q_t·buy_t - Σ p_t·sell_t where q = p + 0.05 EUR/kWh (spot + transmission) for buying, and p is the spot price for selling (no transmission). - f_2: total investment cost (EUR) = 2000·y + 310·E + 200·n

Parameters:

Name Type Description Default
initial_soc float

initial state of charge in kWh. Defaults to 0.0 (empty).

0.0
n_panels_max int

upper bound on number of solar panels. Defaults to 50.

50

Returns:

Type Description
Problem

A DESDEO Problem instance.

Source code in desdeo/problem/testproblems/summer_cabin_electricity.py
def summer_cabin_battery_problem(initial_soc: float = 0.0, n_panels_max: int = 50) -> "Problem":
    """Build a bi-objective MILP for battery + solar investment and scheduling at the summer cabin.

    Decision variables:
    - y ∈ {0,1}: whether the battery is installed
    - E ∈ [0, 42] kWh: battery capacity (14-42 kWh if installed, 0 otherwise)
    - n ∈ {0,...,n_panels_max}: number of 160 W solar panels
    - c_t ∈ [0, 10] kW: hourly charging power (vector of length T)
    - d_t ∈ [0, 10] kW: hourly discharging power (vector of length T)
    - soc_t ∈ [0, 42] kWh: state of charge (vector of length T)
    - buy_t ≥ 0 kWh/h: electricity purchased from the grid (vector of length T)
    - sell_t ≥ 0 kWh/h: electricity sold to the grid (vector of length T)

    Objectives:
    - f_1: total electricity cost (EUR) = Σ q_t·buy_t - Σ p_t·sell_t
      where q = p + 0.05 EUR/kWh (spot + transmission) for buying,
      and p is the spot price for selling (no transmission).
    - f_2: total investment cost (EUR) = 2000·y + 310·E + 200·n

    Args:
        initial_soc: initial state of charge in kWh. Defaults to 0.0 (empty).
        n_panels_max: upper bound on number of solar panels. Defaults to 50.

    Returns:
        A DESDEO Problem instance.
    """
    df = generate_summer_cabin_electricity_data()
    prices = df["price_EUR_kWh"].to_numpy()
    loads = df["load_kWh"].to_numpy()
    solar = generate_solar_profile()
    T = len(prices)

    # --- Variables ---
    installed = Variable(
        name="Battery installed",
        symbol="y",
        variable_type=VariableTypeEnum.binary,
        lowerbound=0,
        upperbound=1,
        initial_value=0,
    )
    capacity = Variable(
        name="Battery capacity (kWh)",
        symbol="E",
        variable_type=VariableTypeEnum.real,
        lowerbound=0.0,
        upperbound=42.0,
        initial_value=0.0,
    )
    n_panels = Variable(
        name="Number of solar panels",
        symbol="n",
        variable_type=VariableTypeEnum.integer,
        lowerbound=0,
        upperbound=n_panels_max,
        initial_value=0,
    )
    charge = TensorVariable(
        name="Charging power (kW)",
        symbol="c",
        shape=[T],
        variable_type=VariableTypeEnum.real,
        lowerbounds=0.0,
        upperbounds=10.0,
        initial_values=0.0,
    )
    discharge = TensorVariable(
        name="Discharging power (kW)",
        symbol="d",
        shape=[T],
        variable_type=VariableTypeEnum.real,
        lowerbounds=0.0,
        upperbounds=10.0,
        initial_values=0.0,
    )
    soc = TensorVariable(
        name="State of charge (kWh)",
        symbol="soc",
        shape=[T],
        variable_type=VariableTypeEnum.real,
        lowerbounds=0.0,
        upperbounds=42.0,
        initial_values=initial_soc,
    )
    buy = TensorVariable(
        name="Grid electricity purchased (kWh/h)",
        symbol="buy",
        shape=[T],
        variable_type=VariableTypeEnum.real,
        lowerbounds=0.0,
        upperbounds=None,
        initial_values=0.0,
    )
    sell = TensorVariable(
        name="Grid electricity sold (kWh/h)",
        symbol="sell",
        shape=[T],
        variable_type=VariableTypeEnum.real,
        lowerbounds=0.0,
        upperbounds=None,
        initial_values=0.0,
    )

    # --- Constants ---
    price_const = TensorConstant(
        name="Electricity spot price (EUR/kWh)",
        symbol="p",
        shape=[T],
        values=prices.tolist(),
    )
    load_const = TensorConstant(
        name="Electricity load (kWh/h)",
        symbol="l",
        shape=[T],
        values=loads.tolist(),
    )
    solar_const = TensorConstant(
        name="Solar production per panel (kWh/h per panel)",
        symbol="sol",
        shape=[T],
        values=solar.tolist(),
    )

    # --- Constraints ---
    constraints = [
        # Battery capacity links: E = 0 if y = 0; E in [14, 42] if y = 1
        Constraint(
            name="Capacity zero if not installed",
            symbol="cap_ub_con",
            func=["Add", "E", ["Negate", ["Multiply", 42.0, "y"]]],
            cons_type=ConstraintTypeEnum.LTE,
            is_linear=True,
            is_convex=True,
            is_twice_differentiable=True,
        ),
        Constraint(
            name="Minimum capacity if installed",
            symbol="cap_lb_con",
            func=["Add", ["Multiply", 14.0, "y"], ["Negate", "E"]],
            cons_type=ConstraintTypeEnum.LTE,
            is_linear=True,
            is_convex=True,
            is_twice_differentiable=True,
        ),
        # SOC dynamics: soc_1 = initial_soc + c_1 - d_1
        Constraint(
            name="SOC dynamics t=1",
            symbol="soc_con_1",
            func=["Add", ["At", "soc", 1], -initial_soc, ["Negate", ["At", "c", 1]], ["At", "d", 1]],
            cons_type=ConstraintTypeEnum.EQ,
            is_linear=True,
            is_convex=True,
            is_twice_differentiable=True,
        ),
        # SOC dynamics t=2..T as a single vector constraint
        Constraint(
            name="SOC dynamics t=2..T",
            symbol="soc_con",
            func=[
                "Add",
                ["Extract", "soc", ["Tuple", 2, T]],
                ["Negate", ["Extract", "soc", ["Tuple", 1, T - 1]]],
                ["Negate", ["Extract", "c", ["Tuple", 2, T]]],
                ["Extract", "d", ["Tuple", 2, T]],
            ],
            cons_type=ConstraintTypeEnum.EQ,
            is_linear=True,
            is_convex=True,
            is_twice_differentiable=True,
        ),
        # soc <= E (vector)
        Constraint(
            name="SOC capacity upper bound",
            symbol="soc_cap_con",
            func=["Subtract", "soc", "E"],
            cons_type=ConstraintTypeEnum.LTE,
            is_linear=True,
            is_convex=True,
            is_twice_differentiable=True,
        ),
        # Energy balance: buy - sell + d - c + n*sol = l (vector)
        Constraint(
            name="Energy balance",
            symbol="energy_bal",
            func=["Add", "buy", ["Negate", "sell"], "d", ["Negate", "c"], ["Multiply", "n", "sol"], ["Negate", "l"]],
            cons_type=ConstraintTypeEnum.EQ,
            is_linear=True,
            is_convex=True,
            is_twice_differentiable=True,
        ),
    ]

    return Problem(
        name="Summer cabin battery and solar investment optimization",
        description=(
            "Jointly optimize battery and solar panel investment with hourly scheduling over a summer season. "
            "Battery: 2000 EUR fixed + 310 EUR/kWh, 10 kW rate. Solar: 200 EUR per 160 W panel. "
            "Buying price includes 0.05 EUR/kWh transmission; selling uses spot price only."
        ),
        variables=[installed, capacity, n_panels, charge, discharge, soc, buy, sell],
        constants=[price_const, load_const, solar_const],
        objectives=[
            Objective(
                name="Electricity cost",
                description="Total electricity cost",
                symbol="f_1",
                func=[
                    "Add",
                    ["MatMul", "p", "buy"],
                    ["Multiply", 0.05, ["Sum", "buy"]],
                    ["Negate", ["MatMul", "p", "sell"]],
                ],
                unit="EUR",
                maximize=False,
                is_linear=True,
                is_convex=True,
                is_twice_differentiable=True,
            ),
            Objective(
                name="Investment cost",
                description="Total investment cost",
                symbol="f_2",
                func=["Add", ["Multiply", 2000.0, "y"], ["Multiply", 310.0, "E"], ["Multiply", 200.0, "n"]],
                unit="EUR",
                maximize=False,
                is_linear=True,
                is_convex=True,
                is_twice_differentiable=True,
            ),
        ],
        constraints=constraints,
    )

summer_cabin_battery_problem_split

summer_cabin_battery_problem_split(
    initial_soc: float = 0.0, n_panels_max: int = 50
) -> Problem

Same problem as summer_cabin_battery_problem but the time horizon is split into 3 equal segments.

Each segment has its own TensorVariables and TensorConstants. SOC continuity across segment boundaries is enforced by an equality constraint that reads the last element of the previous segment's SOC variable via the At operator. Objective values are identical to the monolithic form.

Parameters:

Name Type Description Default
initial_soc float

initial state of charge in kWh. Defaults to 0.0 (empty).

0.0
n_panels_max int

upper bound on number of solar panels. Defaults to 50.

50

Returns:

Type Description
Problem

A DESDEO Problem instance.

Source code in desdeo/problem/testproblems/summer_cabin_electricity.py
def summer_cabin_battery_problem_split(initial_soc: float = 0.0, n_panels_max: int = 50) -> "Problem":
    """Same problem as summer_cabin_battery_problem but the time horizon is split into 3 equal segments.

    Each segment has its own TensorVariables and TensorConstants.  SOC continuity across
    segment boundaries is enforced by an equality constraint that reads the last element of the
    previous segment's SOC variable via the At operator.  Objective values are identical to the
    monolithic form.

    Args:
        initial_soc: initial state of charge in kWh. Defaults to 0.0 (empty).
        n_panels_max: upper bound on number of solar panels. Defaults to 50.

    Returns:
        A DESDEO Problem instance.
    """
    df = generate_summer_cabin_electricity_data()
    prices = df["price_EUR_kWh"].to_numpy()
    loads = df["load_kWh"].to_numpy()
    solar = generate_solar_profile()
    T = len(prices)

    N_SEG = 3
    S = T // N_SEG  # 736 hours per segment

    # --- Shared scalar investment variables ---
    installed = Variable(
        name="Battery installed",
        symbol="y",
        variable_type=VariableTypeEnum.binary,
        lowerbound=0,
        upperbound=1,
        initial_value=0,
    )
    capacity = Variable(
        name="Battery capacity (kWh)",
        symbol="E",
        variable_type=VariableTypeEnum.real,
        lowerbound=0.0,
        upperbound=42.0,
        initial_value=0.0,
    )
    n_panels = Variable(
        name="Number of solar panels",
        symbol="n",
        variable_type=VariableTypeEnum.integer,
        lowerbound=0,
        upperbound=n_panels_max,
        initial_value=0,
    )

    # --- Per-segment variables and constants ---
    seg_vars = []  # seg_vars[k-1] = [charge_k, discharge_k, soc_k, buy_k, sell_k]
    seg_consts = []  # seg_consts[k-1] = [price_k, load_k, solar_k]

    for k in range(1, N_SEG + 1):
        sl = slice((k - 1) * S, k * S)
        seg_vars.append(
            [
                TensorVariable(
                    name=f"Charging power segment {k} (kW)",
                    symbol=f"c_s{k}",
                    shape=[S],
                    variable_type=VariableTypeEnum.real,
                    lowerbounds=0.0,
                    upperbounds=10.0,
                    initial_values=0.0,
                ),
                TensorVariable(
                    name=f"Discharging power segment {k} (kW)",
                    symbol=f"d_s{k}",
                    shape=[S],
                    variable_type=VariableTypeEnum.real,
                    lowerbounds=0.0,
                    upperbounds=10.0,
                    initial_values=0.0,
                ),
                TensorVariable(
                    name=f"State of charge segment {k} (kWh)",
                    symbol=f"soc_s{k}",
                    shape=[S],
                    variable_type=VariableTypeEnum.real,
                    lowerbounds=0.0,
                    upperbounds=42.0,
                    initial_values=initial_soc,
                ),
                TensorVariable(
                    name=f"Grid purchased segment {k} (kWh/h)",
                    symbol=f"buy_s{k}",
                    shape=[S],
                    variable_type=VariableTypeEnum.real,
                    lowerbounds=0.0,
                    upperbounds=None,
                    initial_values=0.0,
                ),
                TensorVariable(
                    name=f"Grid sold segment {k} (kWh/h)",
                    symbol=f"sell_s{k}",
                    shape=[S],
                    variable_type=VariableTypeEnum.real,
                    lowerbounds=0.0,
                    upperbounds=None,
                    initial_values=0.0,
                ),
            ]
        )
        seg_consts.append(
            [
                TensorConstant(
                    name=f"Spot price segment {k} (EUR/kWh)",
                    symbol=f"p_s{k}",
                    shape=[S],
                    values=prices[sl].tolist(),
                ),
                TensorConstant(
                    name=f"Load segment {k} (kWh/h)",
                    symbol=f"l_s{k}",
                    shape=[S],
                    values=loads[sl].tolist(),
                ),
                TensorConstant(
                    name=f"Solar per panel segment {k} (kWh/h)",
                    symbol=f"sol_s{k}",
                    shape=[S],
                    values=solar[sl].tolist(),
                ),
            ]
        )

    # --- Constraints ---
    constraints = [
        Constraint(
            name="Capacity zero if not installed",
            symbol="cap_ub_con",
            func=["Add", "E", ["Negate", ["Multiply", 42.0, "y"]]],
            cons_type=ConstraintTypeEnum.LTE,
            is_linear=True,
            is_convex=True,
            is_twice_differentiable=True,
        ),
        Constraint(
            name="Minimum capacity if installed",
            symbol="cap_lb_con",
            func=["Add", ["Multiply", 14.0, "y"], ["Negate", "E"]],
            cons_type=ConstraintTypeEnum.LTE,
            is_linear=True,
            is_convex=True,
            is_twice_differentiable=True,
        ),
    ]

    for k in range(1, N_SEG + 1):
        c_sym = f"c_s{k}"
        d_sym = f"d_s{k}"
        soc_sym = f"soc_s{k}"
        buy_sym = f"buy_s{k}"
        sell_sym = f"sell_s{k}"
        sol_sym = f"sol_s{k}"
        l_sym = f"l_s{k}"

        # SOC at t=1: reference initial_soc for segment 1, or last element of previous segment
        prev_soc_term = -initial_soc if k == 1 else ["Negate", ["At", f"soc_s{k - 1}", S]]

        constraints.append(
            Constraint(
                name=f"SOC dynamics segment {k} t=1",
                symbol=f"soc_con_s{k}_1",
                func=["Add", ["At", soc_sym, 1], prev_soc_term, ["Negate", ["At", c_sym, 1]], ["At", d_sym, 1]],
                cons_type=ConstraintTypeEnum.EQ,
                is_linear=True,
                is_convex=True,
                is_twice_differentiable=True,
            )
        )

        constraints.append(
            Constraint(
                name=f"SOC dynamics segment {k} t=2..{S}",
                symbol=f"soc_con_s{k}",
                func=[
                    "Add",
                    ["Extract", soc_sym, ["Tuple", 2, S]],
                    ["Negate", ["Extract", soc_sym, ["Tuple", 1, S - 1]]],
                    ["Negate", ["Extract", c_sym, ["Tuple", 2, S]]],
                    ["Extract", d_sym, ["Tuple", 2, S]],
                ],
                cons_type=ConstraintTypeEnum.EQ,
                is_linear=True,
                is_convex=True,
                is_twice_differentiable=True,
            )
        )

        constraints.append(
            Constraint(
                name=f"SOC capacity upper bound segment {k}",
                symbol=f"soc_cap_con_s{k}",
                func=["Subtract", soc_sym, "E"],
                cons_type=ConstraintTypeEnum.LTE,
                is_linear=True,
                is_convex=True,
                is_twice_differentiable=True,
            )
        )

        constraints.append(
            Constraint(
                name=f"Energy balance segment {k}",
                symbol=f"energy_bal_s{k}",
                func=[
                    "Add",
                    buy_sym,
                    ["Negate", sell_sym],
                    d_sym,
                    ["Negate", c_sym],
                    ["Multiply", "n", sol_sym],
                    ["Negate", l_sym],
                ],
                cons_type=ConstraintTypeEnum.EQ,
                is_linear=True,
                is_convex=True,
                is_twice_differentiable=True,
            )
        )

    # f_1 = sum over all segments of (p_sk · buy_sk + 0.05*sum(buy_sk) - p_sk · sell_sk)
    f1_terms = []
    for k in range(1, N_SEG + 1):
        f1_terms += [
            ["MatMul", f"p_s{k}", f"buy_s{k}"],
            ["Multiply", 0.05, ["Sum", f"buy_s{k}"]],
            ["Negate", ["MatMul", f"p_s{k}", f"sell_s{k}"]],
        ]

    all_variables = [installed, capacity, n_panels]
    all_constants = []
    for k in range(N_SEG):
        all_variables.extend(seg_vars[k])
        all_constants.extend(seg_consts[k])

    return Problem(
        name="Summer cabin battery and solar investment optimization (split)",
        description=(
            "Same as summer_cabin_battery_problem but the time horizon is split into 3 segments "
            "to reduce per-block tensor sizes while preserving solution equivalence."
        ),
        variables=all_variables,
        constants=all_constants,
        objectives=[
            Objective(
                name="Electricity cost",
                description="Total electricity cost",
                symbol="f_1",
                func=["Add", *f1_terms],
                unit="EUR",
                maximize=False,
                is_linear=True,
                is_convex=True,
                is_twice_differentiable=True,
            ),
            Objective(
                name="Investment cost",
                description="Total investment cost",
                symbol="f_2",
                func=["Add", ["Multiply", 2000.0, "y"], ["Multiply", 310.0, "E"], ["Multiply", 200.0, "n"]],
                unit="EUR",
                maximize=False,
                is_linear=True,
                is_convex=True,
                is_twice_differentiable=True,
            ),
        ],
        constraints=constraints,
    )

summer_cabin_battery_problem_split_scenario

summer_cabin_battery_problem_split_scenario(
    initial_soc: float = 0.0, n_panels_max: int = 50
) -> ScenarioModel

Build a ScenarioModel for the split summer cabin battery problem with two-level outage scenarios.

Scenario tree (4 leaf scenarios): ROOT → [S1, S2] — at the start of segment 2, S2 has a 4-hour grid outage S1 → [S1a, S1b] — at the start of segment 3, S1b has a 4-hour grid outage S2 → [S2a, S2b] — at the start of segment 3, S2b has a 4-hour grid outage

During an outage, grid buy and sell are forced to zero. An unmet-demand slack variable absorbs any energy balance gap; a binary indicator records whether the hour went unserved.

Unmet-demand variables and the f_3 objective are only added to scenarios where an outage can actually occur, keeping each scenario problem as small as possible: S1a — no outage, no extra variables, no f_3 S1b — s3 outage: 8 extra vars, f_3 = Sum(z_s3) S2a — s2 outage: 8 extra vars, f_3 = Sum(z_s2) S2b — both: 16 extra vars, f_3 = Sum(z_s2 + z_s3)

The base problem is the unmodified split problem. All additions live entirely in the ScenarioModel pool.

Non-anticipativity

ROOT: investment variables (y, E, n) and all segment-1 schedule variables. S1/S2: segment-2 schedule variables within each branch.

Parameters:

Name Type Description Default
initial_soc float

initial state of charge in kWh.

0.0
n_panels_max int

upper bound on number of solar panels.

50

Returns:

Type Description
ScenarioModel

A ScenarioModel instance.

Source code in desdeo/problem/testproblems/summer_cabin_electricity.py
def summer_cabin_battery_problem_split_scenario(
    initial_soc: float = 0.0,
    n_panels_max: int = 50,
) -> ScenarioModel:
    """Build a ScenarioModel for the split summer cabin battery problem with two-level outage scenarios.

    Scenario tree (4 leaf scenarios):
        ROOT → [S1, S2]   — at the start of segment 2, S2 has a 4-hour grid outage
        S1   → [S1a, S1b] — at the start of segment 3, S1b has a 4-hour grid outage
        S2   → [S2a, S2b] — at the start of segment 3, S2b has a 4-hour grid outage

    During an outage, grid buy and sell are forced to zero.  An unmet-demand slack
    variable absorbs any energy balance gap; a binary indicator records whether the
    hour went unserved.

    Unmet-demand variables and the f_3 objective are only added to scenarios where
    an outage can actually occur, keeping each scenario problem as small as possible:
        S1a — no outage, no extra variables, no f_3
        S1b — s3 outage: 8 extra vars, f_3 = Sum(z_s3)
        S2a — s2 outage: 8 extra vars, f_3 = Sum(z_s2)
        S2b — both:     16 extra vars, f_3 = Sum(z_s2 + z_s3)

    The base problem is the unmodified split problem.  All additions live entirely in
    the ScenarioModel pool.

    Non-anticipativity:
        ROOT: investment variables (y, E, n) and all segment-1 schedule variables.
        S1/S2: segment-2 schedule variables within each branch.

    Args:
        initial_soc: initial state of charge in kWh.
        n_panels_max: upper bound on number of solar panels.

    Returns:
        A ScenarioModel instance.
    """
    base = summer_cabin_battery_problem_split(initial_soc=initial_soc, n_panels_max=n_panels_max)
    H = _N_OUTAGE_HRS

    var_pool: list[Variable] = []
    var_idx: dict[str, int] = {}

    for k in (2, 3):
        v = TensorVariable(
            name=f"Unmet demand s{k} (kWh)",
            symbol=f"unmet_s{k}",
            shape=[H],
            variable_type=VariableTypeEnum.real,
            lowerbounds=0.0,
            upperbounds=None,
            initial_values=0.0,
        )
        var_idx[v.symbol] = len(var_pool)
        var_pool.append(v)
    for k in (2, 3):
        v = TensorVariable(
            name=f"Demand unserved indicator s{k}",
            symbol=f"z_s{k}",
            shape=[H],
            variable_type=VariableTypeEnum.binary,
            lowerbounds=0,
            upperbounds=1,
            initial_values=0,
        )
        var_idx[v.symbol] = len(var_pool)
        var_pool.append(v)

    def _f3(segments: tuple[int, ...]) -> Objective:
        z_terms = [["Sum", f"z_s{k}"] for k in segments]
        return Objective(
            name="Power outage",
            description="Hours with unserved electricity demand",
            symbol="f_3",
            func=z_terms[0] if len(z_terms) == 1 else ["Add", *z_terms],
            unit="h",
            maximize=False,
            is_linear=True,
            is_convex=True,
            is_twice_differentiable=True,
        )

    obj_pool: list[Objective] = []
    obj_idx: dict[str, int] = {}  # scenario name → pool index
    for scenario_name, segs in [("S2a", (2,)), ("S1b", (3,)), ("S2b", (2, 3))]:
        obj_idx[scenario_name] = len(obj_pool)
        obj_pool.append(_f3(segs))
    obj_idx["S1a"] = len(obj_pool)
    obj_pool.append(
        Objective(
            name="Power outage",
            description="Hours with unserved electricity demand",
            symbol="f_3",
            func=["Multiply", 0, "y"],  # always 0 — no outage possible in this scenario
            unit="h",
            maximize=False,
            is_linear=True,
            is_convex=True,
            is_twice_differentiable=True,
        )
    )

    con_pool: list[Constraint] = []
    con_idx: dict[str, int] = {}

    for k in (2, 3):
        c = Constraint(
            name=f"Energy balance s{k} t={H}+1.. (no unmet slack)",
            symbol=f"energy_bal_s{k}",
            func=[
                "Add",
                ["Exclude", f"buy_s{k}", ["Tuple", 1, H]],
                ["Negate", ["Exclude", f"sell_s{k}", ["Tuple", 1, H]]],
                ["Exclude", f"d_s{k}", ["Tuple", 1, H]],
                ["Negate", ["Exclude", f"c_s{k}", ["Tuple", 1, H]]],
                ["Multiply", "n", ["Exclude", f"sol_s{k}", ["Tuple", 1, H]]],
                ["Negate", ["Exclude", f"l_s{k}", ["Tuple", 1, H]]],
            ],
            cons_type=ConstraintTypeEnum.EQ,
            is_linear=True,
            is_convex=True,
            is_twice_differentiable=True,
        )
        con_idx[c.symbol] = len(con_pool)
        con_pool.append(c)

        c = Constraint(
            name=f"Energy balance s{k} t=1..{H} (with unmet slack)",
            symbol=f"energy_bal_out_s{k}",
            func=[
                "Add",
                ["Extract", f"d_s{k}", ["Tuple", 1, H]],
                ["Negate", ["Extract", f"c_s{k}", ["Tuple", 1, H]]],
                ["Multiply", "n", ["Extract", f"sol_s{k}", ["Tuple", 1, H]]],
                f"unmet_s{k}",
                ["Negate", ["Extract", f"l_s{k}", ["Tuple", 1, H]]],
            ],
            cons_type=ConstraintTypeEnum.EQ,
            is_linear=True,
            is_convex=True,
            is_twice_differentiable=True,
        )
        con_idx[c.symbol] = len(con_pool)
        con_pool.append(c)

    for k in (2, 3):
        c = Constraint(
            name=f"Big-M unmet indicator s{k} t=1..{H}",
            symbol=f"bigm_s{k}",
            func=["Subtract", f"unmet_s{k}", ["Multiply", _M_UNMET, f"z_s{k}"]],
            cons_type=ConstraintTypeEnum.LTE,
            is_linear=True,
            is_convex=True,
            is_twice_differentiable=True,
        )
        con_idx[c.symbol] = len(con_pool)
        con_pool.append(c)

    for k in (2, 3):
        c = Constraint(
            name=f"Outage no-trade s{k} t=1..{H}",
            symbol=f"outage_trade_s{k}",
            func=["Add", ["Extract", f"buy_s{k}", ["Tuple", 1, H]], ["Extract", f"sell_s{k}", ["Tuple", 1, H]]],
            cons_type=ConstraintTypeEnum.EQ,
            is_linear=True,
            is_convex=True,
            is_twice_differentiable=True,
        )
        con_idx[c.symbol] = len(con_pool)
        con_pool.append(c)

    _outage_segs: dict[str, tuple[int, ...]] = {"S1a": (), "S2a": (2,), "S1b": (3,), "S2b": (2, 3)}

    scenarios: dict[str, Scenario] = {}
    for name, segs in _outage_segs.items():
        variables: dict[str, int] = {}
        constraints: dict[str, int] = {}
        for k in segs:
            variables[f"unmet_s{k}"] = var_idx[f"unmet_s{k}"]
            variables[f"z_s{k}"] = var_idx[f"z_s{k}"]
            constraints[f"energy_bal_s{k}"] = con_idx[f"energy_bal_s{k}"]
            constraints[f"energy_bal_out_s{k}"] = con_idx[f"energy_bal_out_s{k}"]
            constraints[f"bigm_s{k}"] = con_idx[f"bigm_s{k}"]
            constraints[f"outage_trade_s{k}"] = con_idx[f"outage_trade_s{k}"]
        scenarios[name] = Scenario(
            variables=variables,
            objectives={"f_3": obj_idx[name]},
            constraints=constraints,
        )

    investments = ["y", "E", "n"]
    s1_sched = ["c_s1", "d_s1", "soc_s1", "buy_s1", "sell_s1"]
    s2_sched = ["c_s2", "d_s2", "soc_s2", "buy_s2", "sell_s2"]
    s2_unmet = ["unmet_s2", "z_s2"]

    return ScenarioModel(
        scenario_tree={
            "ROOT": ["S1", "S2"],
            "S1": ["S1a", "S1b"],
            "S2": ["S2a", "S2b"],
        },
        scenario_probabilities={
            "S1": 0.9,
            "S2": 0.1,
            "S1a": 0.81,
            "S1b": 0.09,
            "S2a": 0.09,
            "S2b": 0.01,
        },
        base_problem=base,
        variables=tuple(var_pool),
        objectives=tuple(obj_pool),
        constraints=tuple(con_pool),
        scenarios=scenarios,
        anticipation_stop={
            "ROOT": [*investments, *s1_sched],
            "S1": s2_sched,
            "S2": [*s2_sched, *s2_unmet],
        },
    )

summer_cabin_battery_robust_ev_problem

summer_cabin_battery_robust_ev_problem(
    initial_soc: float = 0.0, n_panels_max: int = 50
) -> Problem

Build a robust multi-objective problem from the split summer cabin battery scenario model.

Wraps summer_cabin_battery_problem_split_scenario (4-leaf outage scenario tree) and adds both worst-case robust and expected-value scalarization objectives for f_1 (electricity cost), f_2 (investment cost), and f_3 (unmet demand). Ideal and nadir bounds are computed via the payoff table method so that scalarization functions can be properly scaled.

Parameters:

Name Type Description Default
initial_soc float

initial state of charge in kWh.

0.0
n_panels_max int

upper bound on number of solar panels.

50

Returns:

Type Description
Problem

Combined scenario problem with ideal and nadir populated.

Source code in desdeo/problem/testproblems/summer_cabin_electricity.py
def summer_cabin_battery_robust_ev_problem(initial_soc: float = 0.0, n_panels_max: int = 50) -> Problem:
    """Build a robust multi-objective problem from the split summer cabin battery scenario model.

    Wraps `summer_cabin_battery_problem_split_scenario` (4-leaf outage scenario tree) and adds
    both worst-case robust and expected-value scalarization objectives for f_1 (electricity cost),
    f_2 (investment cost), and f_3 (unmet demand).  Ideal and nadir bounds are computed via the
    payoff table method so that scalarization functions can be properly scaled.

    Args:
        initial_soc: initial state of charge in kWh.
        n_panels_max: upper bound on number of solar panels.

    Returns:
        Combined scenario problem with ideal and nadir populated.
    """
    scenario_model = summer_cabin_battery_problem_split_scenario(initial_soc=initial_soc, n_panels_max=n_panels_max)

    combined_problem, symbol_maps = build_combined_scenario_problem(scenario_model)
    combined_problem, _ = add_worst_case_robust(
        scenario_model=scenario_model,
        symbols=["f_1", "f_2", "f_3"],
        combined=combined_problem,
        symbol_maps=symbol_maps,
    )
    combined_problem, _ = add_expected_value(
        scenario_model=scenario_model,
        symbols=["f_1", "f_2", "f_3"],
        combined=combined_problem,
        symbol_maps=symbol_maps,
    )

    ideal, nadir1 = payoff_table_method(combined_problem, CVXPYSolver)
    # payoff table does not find the actual nadir, so we need some trickery
    helper = combined_problem.add_constraints(
        [
            Constraint(
                name="helper",
                symbol="help",
                cons_type=ConstraintTypeEnum.LTE,
                func=["Subtract", "E_f_3", 0.75],
                is_linear=True,
                is_convex=True,
                is_twice_differentiable=True,
            )
        ]
    )
    _, nadir2 = payoff_table_method(helper, CVXPYSolver)
    nadir = {key: max(nadir1[key], nadir2[key]) for key in ideal}
    return combined_problem.update_ideal_and_nadir(ideal, nadir)

vehicle_crashworthiness

vehicle_crashworthiness() -> Problem

Defines the vehicle crash worthiness design problem.

Five decision variables describing the thickness of reinforced members (in mm) drive three minimized objectives: the frontal mass of the vehicle (kg), the collision acceleration experienced by passengers (m/s^2), and the toe board intrusion in the offset frontal crash (mm). Each \(x_i\) is bounded in \([1, 3]\). The objective functions are the polynomial surrogate models from Liao et al. 2008.

References

Liao, X., Li, Q., Yang, X., Zhang, W. & Li, W. (2007). Multiobjective optimization for crash safety design of vehicles using stepwise regression model. Structural and Multidisciplinary Optimization, 35(6), 561-569. https://doi.org/10.1007/s00158-007-0163-x

Returns:

Name Type Description
Problem Problem

an instance of the vehicle crash worthiness problem.

Source code in desdeo/problem/testproblems/vehicle_crashworthiness_problem.py
def vehicle_crashworthiness() -> Problem:
    r"""Defines the vehicle crash worthiness design problem.

    Five decision variables describing the thickness of reinforced members
    (in mm) drive three minimized objectives: the frontal mass of the
    vehicle (kg), the collision acceleration experienced by passengers
    (m/s^2), and the toe board intrusion in the offset frontal crash (mm).
    Each $x_i$ is bounded in $[1, 3]$. The objective functions are the
    polynomial surrogate models from Liao et al. 2008.

    References:
        Liao, X., Li, Q., Yang, X., Zhang, W. & Li, W. (2007).
            Multiobjective optimization for crash safety design of vehicles
            using stepwise regression model. Structural and Multidisciplinary
            Optimization, 35(6), 561-569.
            https://doi.org/10.1007/s00158-007-0163-x

    Returns:
        Problem: an instance of the vehicle crash worthiness problem.
    """
    variables = [
        Variable(
            name=f"x_{i}",
            symbol=f"x_{i}",
            variable_type=VariableTypeEnum.real,
            lowerbound=1.0,
            upperbound=3.0,
            initial_value=2.0,
        )
        for i in range(1, 6)
    ]

    f_1_expr = "1640.2823 + 2.3573285 * x_1 + 2.3220035 * x_2 + 4.5688768 * x_3 + 7.7213633 * x_4 + 4.4559504 * x_5"
    f_2_expr = (
        "6.5856"
        " + 1.15 * x_1"
        " - 1.0427 * x_2"
        " + 0.9738 * x_3"
        " + 0.8364 * x_4"
        " - 0.3695 * x_1 * x_4"
        " + 0.0861 * x_1 * x_5"
        " + 0.3628 * x_2 * x_4"
        " - 0.1106 * x_1**2"
        " - 0.3437 * x_3**2"
        " + 0.1764 * x_4**2"
    )
    f_3_expr = (
        "-0.0551"
        " + 0.0181 * x_1"
        " + 0.1024 * x_2"
        " + 0.0421 * x_3"
        " - 0.0073 * x_1 * x_2"
        " + 0.024 * x_2 * x_3"
        " - 0.0118 * x_2 * x_4"
        " - 0.0204 * x_3 * x_4"
        " - 0.008 * x_3 * x_5"
        " - 0.0241 * x_2**2"
        " + 0.0109 * x_4**2"
    )

    f_1 = Objective(
        name="f_1",
        symbol="f_1",
        func=f_1_expr,
        objective_type=ObjectiveTypeEnum.analytical,
        maximize=False,
        ideal=1600.0,
        nadir=1700.0,
        is_linear=True,
        is_convex=True,
        is_twice_differentiable=True,
    )
    f_2 = Objective(
        name="f_2",
        symbol="f_2",
        func=f_2_expr,
        objective_type=ObjectiveTypeEnum.analytical,
        maximize=False,
        ideal=6.0,
        nadir=12.0,
        is_linear=False,
        is_convex=False,
        is_twice_differentiable=True,
    )
    f_3 = Objective(
        name="f_3",
        symbol="f_3",
        func=f_3_expr,
        objective_type=ObjectiveTypeEnum.analytical,
        maximize=False,
        ideal=0.038,
        nadir=0.30,
        is_linear=False,
        is_convex=False,
        is_twice_differentiable=True,
    )

    return Problem(
        name="vehicle_crashworthiness",
        description="The vehicle crash worthiness design problem (Liao et al. 2008).",
        variables=variables,
        objectives=[f_1, f_2, f_3],
    )

water_management

water_management(six_obj=False) -> Problem

Water resource planning problem.

The constraints and objective functions for the water management problem are defined as follows:

\[\begin{align} \min\quad f_1(\mathbf{x}) &= 106780.37(x_2 + x_3) + 61704.67 \\ \min\quad f_2(\mathbf{x}) &= 3000x_1 \\ \min\quad f_3(\mathbf{x}) &= \frac{305700 \cdot 2289 x_2} {(0.06 \cdot 2289)^{0.65}} \\ \min\quad f_4(\mathbf{x}) &= 250 \cdot 2289 \cdot \exp(-39.75x_2 + 9.9x_3 + 2.74) \\ \min\quad f_5(\mathbf{x}) &= 25\left( \frac{1.39}{x_1x_2} + 4940x_3 - 80 \right) \\ g_1(\mathbf{x}) &= \frac{0.00139}{x_1 x_2} + 4.94x_3 - 0.08 \leq 1.00 \\ g_2(\mathbf{x}) &= \frac{0.000306}{x_1 x_2} + 1.082x_3 - 0.0986 \leq 1.00 \\ g_3(\mathbf{x}) &= \frac{12.307}{x_1 x_2} + 49408.24x_3 + 4051.02 \leq 50000.00 \\ g_4(\mathbf{x}) &= \frac{2.098}{x_1 x_2} + 8046.33x_3 - 696.71 \leq 16000.00 \\ g_5(\mathbf{x}) &= \frac{2.138}{x_1 x_2} + 7883.39x_3 - 705.04 \leq 10000.00 \\ g_6(\mathbf{x}) &= \frac{0.417}{x_1 x_2} + 1721.26x_3 - 136.54 \leq 2000.00 \\ g_7(\mathbf{x}) &= \frac{0.164}{x_1 x_2} + 631.13x_3 - 54.48 \leq 550.00 \\ \mathbf{x}^{L} &\leq \mathbf{x} \leq \mathbf{x}^{U}, \end{align}\]

where the lower and upper bounds on the variables are

\[ \mathbf{x}^{L} = (0.01,\,0.01,\,0.01), \qquad \mathbf{x}^{U} = (0.45,\,0.10,\,0.10). \]

Parameters:

Name Type Description Default
six_obj bool

If False, use the original 5 objectives. If true, use the sum of contraint violations as the 6th objective. Default is False.

False
References

Ray, T., Tai, K., & Seow, K. C. (2001). Multiobjective design optimization by an evolutionary algorithm. Engineering Optimization, 33(4), 399-424. (Section 4.4.)

Returns:

Name Type Description
Problem Problem

an instance of the water management problem.

Source code in desdeo/problem/testproblems/water_management_problem.py
def water_management(six_obj=False) -> Problem:
    r"""Water resource planning problem.

    The constraints and objective functions for the water management problem are defined as follows:

    \begin{align}
        \min\quad f_1(\mathbf{x})
        &= 106780.37(x_2 + x_3) + 61704.67 \\
        \min\quad f_2(\mathbf{x})
        &= 3000x_1 \\
        \min\quad f_3(\mathbf{x})
        &= \frac{305700 \cdot 2289 x_2} {(0.06 \cdot 2289)^{0.65}} \\
        \min\quad f_4(\mathbf{x})
        &= 250 \cdot 2289 \cdot \exp(-39.75x_2 + 9.9x_3 + 2.74) \\
        \min\quad f_5(\mathbf{x})
        &= 25\left( \frac{1.39}{x_1x_2} + 4940x_3 - 80 \right) \\
        g_1(\mathbf{x})
        &= \frac{0.00139}{x_1 x_2} + 4.94x_3 - 0.08 \leq 1.00 \\
        g_2(\mathbf{x})
        &= \frac{0.000306}{x_1 x_2} + 1.082x_3 - 0.0986 \leq 1.00 \\
        g_3(\mathbf{x})
        &= \frac{12.307}{x_1 x_2} + 49408.24x_3 + 4051.02 \leq 50000.00 \\
        g_4(\mathbf{x})
        &= \frac{2.098}{x_1 x_2} + 8046.33x_3 - 696.71 \leq 16000.00 \\
        g_5(\mathbf{x})
        &= \frac{2.138}{x_1 x_2} + 7883.39x_3 - 705.04 \leq 10000.00 \\
        g_6(\mathbf{x})
        &= \frac{0.417}{x_1 x_2} + 1721.26x_3 - 136.54 \leq 2000.00 \\
        g_7(\mathbf{x})
        &= \frac{0.164}{x_1 x_2} + 631.13x_3 - 54.48 \leq 550.00 \\
        \mathbf{x}^{L} &\leq \mathbf{x} \leq \mathbf{x}^{U},
    \end{align}

    where the lower and upper bounds on the variables are

    \[
    \mathbf{x}^{L} = (0.01,\,0.01,\,0.01),
    \qquad
    \mathbf{x}^{U} = (0.45,\,0.10,\,0.10).
    \]

    Arguments:
            six_obj (bool): If False, use the original 5 objectives. If true, use the sum of contraint violations as
                the 6th objective. Default is False.

    References:
        Ray, T., Tai, K., & Seow, K. C. (2001). Multiobjective design optimization by an evolutionary algorithm.
        Engineering Optimization, 33(4), 399-424. (Section 4.4.)

    Returns:
        Problem: an instance of the water management problem.
    """
    x_1 = Variable(
        name="x_1",
        symbol="x_1",
        variable_type=VariableTypeEnum.real,
        lowerbound=0.01,
        upperbound=0.45,
        initial_value=0.2,
    )

    x_2 = Variable(
        name="x_2",
        symbol="x_2",
        variable_type=VariableTypeEnum.real,
        lowerbound=0.01,
        upperbound=0.10,
        initial_value=0.05,
    )

    x_3 = Variable(
        name="x_3",
        symbol="x_3",
        variable_type=VariableTypeEnum.real,
        lowerbound=0.01,
        upperbound=0.10,
        initial_value=0.05,
    )

    # Constraints

    g_1 = Constraint(
        name="g_1",
        symbol="g_1",
        cons_type=ConstraintTypeEnum.LTE,
        func="0.00139 / (x_1 * x_2) + 4.94 * x_3 - 0.08 - 1.00",
        is_linear=False,
    )

    g_2 = Constraint(
        name="g_2",
        symbol="g_2",
        cons_type=ConstraintTypeEnum.LTE,
        func="0.000306 / (x_1 * x_2) + 1.082 * x_3 - 0.0986 - 1.00",
        is_linear=False,
    )

    g_3 = Constraint(
        name="g_3",
        symbol="g_3",
        cons_type=ConstraintTypeEnum.LTE,
        func="12.307 / (x_1 * x_2) + 49408.24 * x_3 + 4051.02 - 50000.00",
        is_linear=False,
    )

    g_4 = Constraint(
        name="g_4",
        symbol="g_4",
        cons_type=ConstraintTypeEnum.LTE,
        func="2.098 / (x_1 * x_2) + 8046.33 * x_3 - 696.71 - 16000.00",
        is_linear=False,
    )

    g_5 = Constraint(
        name="g_5",
        symbol="g_5",
        cons_type=ConstraintTypeEnum.LTE,
        func="2.138 / (x_1 * x_2) + 7883.39 * x_3 - 705.04 - 10000.00",
        is_linear=False,
    )

    g_6 = Constraint(
        name="g_6",
        symbol="g_6",
        cons_type=ConstraintTypeEnum.LTE,
        func="0.417 / (x_1 * x_2) + 1721.26 * x_3 - 136.54 - 2000.00",
        is_linear=False,
    )

    g_7 = Constraint(
        name="g_7",
        symbol="g_7",
        cons_type=ConstraintTypeEnum.LTE,
        func="0.164 / (x_1 * x_2) + 631.13 * x_3 - 54.48 - 550.00",
        is_linear=False,
    )

    # Objectives

    f_1 = Objective(
        name="f_1",
        symbol="f_1",
        func="106780.37 * (x_2 + x_3) + 61704.67",
        objective_type=ObjectiveTypeEnum.analytical,
        is_linear=False,
        is_convex=True,
        is_twice_differentiable=True,
    )

    f_2 = Objective(
        name="f_2",
        symbol="f_2",
        func="3000 * x_1",
        objective_type=ObjectiveTypeEnum.analytical,
        is_linear=True,
        is_convex=True,
        is_twice_differentiable=True,
    )

    f_3 = Objective(
        name="f_3",
        symbol="f_3",
        func="(305700 * 2289.0 * x_2)/((0.06*2289)**0.65)",
        objective_type=ObjectiveTypeEnum.analytical,
        is_linear=False,
        is_convex=True,
        is_twice_differentiable=True,
    )

    f_4 = Objective(
        name="f_4",
        symbol="f_4",
        func="250 * 2289 * Exp(-39.75 * x_2 + 9.9 * x_3 + 2.74)",
        objective_type=ObjectiveTypeEnum.analytical,
        is_linear=False,
        is_convex=True,
        is_twice_differentiable=True,
    )

    f_5 = Objective(
        name="f_5",
        symbol="f_5",
        func="25 * ((1.39 / (x_1 * x_2)) + 4940 * x_3 - 80)",
        objective_type=ObjectiveTypeEnum.analytical,
        is_linear=False,
        is_convex=True,
        is_twice_differentiable=True,
    )

    objectives = [f_1, f_2, f_3, f_4, f_5]
    if six_obj:
        big_func = (
            "Max(0.00139 / (x_1 * x_2) + 4.94 * x_3 - 0.08 - 1.00, 0)"
            " + Max(0.000306 / (x_1 * x_2) + 1.082 * x_3 - 0.0986 - 1.00, 0)"
            " + Max(12.307 / (x_1 * x_2) + 49408.24 * x_3 + 4051.02 - 50000.00, 0)"
            " + Max(2.098 / (x_1 * x_2) + 8046.33 * x_3 - 696.71 - 16000.00, 0)"
            " + Max(2.138 / (x_1 * x_2) + 7883.39 * x_3 - 705.04 - 10000.00, 0)"
            " + Max(0.417 / (x_1 * x_2) + 1721.26 * x_3 - 136.54 - 2000.00, 0)"
            " + Max(0.164 / (x_1 * x_2) + 631.13 * x_3 - 54.48 - 550.00, 0)"
        )
        f_6 = Objective(
            name="f_6",
            symbol="f_6",
            func=big_func,
            objective_type=ObjectiveTypeEnum.analytical,
            is_linear=False,
            is_convex=True,
            is_twice_differentiable=False,
        )
        objectives.append(f_6)

    return Problem(
        name="water_management_problem",
        description="Water resource planning problem",
        variables=[x_1, x_2, x_3],
        objectives=objectives,
        constraints=[
            g_1,
            g_2,
            g_3,
            g_4,
            g_5,
            g_6,
            g_7,
        ],
    )

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_1\) and \(f_2\) are objective functions, \(x_1,\dots,x_n\) are decision variables, \(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_1$ and $f_2$ are objective functions, $x_1,\dots,x_n$ are decision variables, $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,
    )

zdt4

zdt4(number_of_variables: int) -> Problem

Defines the ZDT4 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(\text{x}) &=g(\text{x})\cdot\left(h\right) \\ g(\text{x}) &=1+10\left(n-1\right)+\sum_{i=2}^n\left[x_i^2-10\cos\left(4\pi\cdot x_i\right)\right]\\ 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 variables, \(n\) is the number of decision variables, and \(g\) and \(h\) are auxiliary functions.

Source code in desdeo/problem/testproblems/zdt_problem.py
def zdt4(number_of_variables: int) -> Problem:
    r"""Defines the ZDT4 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(\text{x}) &=g(\text{x})\cdot\left(h\right) \\
        g(\text{x}) &=1+10\left(n-1\right)+\sum_{i=2}^n\left[x_i^2-10\cos\left(4\pi\cdot x_i\right)\right]\\
        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 variables, $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 + 10 * ({n} - 1)"
    g_expr_2 = "(" + " + ".join([f"x_{i}**2 - 10 * Cos(4 * {pi} * 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_{1}", symbol=f"x_{1}", variable_type="real", lowerbound=0, upperbound=1, initial_value=0.5),
    ] + [
        Variable(name=f"x_{i}", symbol=f"x_{i}", variable_type="real", lowerbound=-5, upperbound=5, initial_value=0)
        for i in range(2, 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="zdt4",
        description="The ZDT4 test problem.",
        variables=variables,
        objectives=objectives,
        extra_funcs=extras,
        is_convex=True,
        is_linear=False,
        is_twice_differentiable=True,
    )

zdt6

zdt6(number_of_variables: int) -> Problem

Defines the ZDT6 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}) &= 1 -\exp(-4x_1)\sin^{6}(6\pi x_1) \\ \min\quad f_2(\textbf{x}) &= g(\textbf{x})\left[1-\left(\frac{f_1(\textbf{x})} {g(\textbf{x})}\right)^{2}\right] \\ g(\textbf{x}) &= 1 + 9\left(\frac{\sum_{i=2}^{n} x_i}{n-1}\right)^{0.25} \end{align*}\]

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

Source code in desdeo/problem/testproblems/zdt_problem.py
def zdt6(number_of_variables: int) -> Problem:
    r"""Defines the ZDT6 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}) &= 1 -\exp(-4x_1)\sin^{6}(6\pi x_1) \\
        \min\quad f_2(\textbf{x}) &= g(\textbf{x})\left[1-\left(\frac{f_1(\textbf{x})}
        {g(\textbf{x})}\right)^{2}\right] \\
        g(\textbf{x}) &= 1 + 9\left(\frac{\sum_{i=2}^{n} x_i}{n-1}\right)^{0.25}
    \end{align*}

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

    f1_symbol = "f_1"
    f1_expr = f"1 - Exp(-4*x_1)*(Sin(6*{pi}*x_1))**6"

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

    f2_symbol = "f_2"
    f2_expr = f"{g_symbol} * (1 - ({f1_symbol} / {g_symbol})**2)"

    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=False,
            is_linear=False,
            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=False, is_linear=False, is_twice_differentiable=True
        ),
    ]

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

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`"""
    _parse_infix_to_func = field_validator("func", mode="before")(parse_infix_to_func)
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'

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: StrEnum

An enumerator for supported constraint expression types.

Source code in desdeo/problem/schema.py
class ConstraintTypeEnum(StrEnum):
    """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`"""
    _parse_infix_to_func = field_validator("func", mode="before")(parse_infix_to_func)
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'.

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`"""
    _parse_infix_to_func = field_validator("func", mode="before")(parse_infix_to_func)
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=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'.

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: StrEnum

An enumerator for supported objective function types.

Source code in desdeo/problem/schema.py
class ObjectiveTypeEnum(StrEnum):
    """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
 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
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
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,
            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 get_symbol_type_map(self) -> dict[str, str]:
        """Return a mapping of every named symbol to its element type.

        The type strings match the field names used in `Problem`:
        ``"variables"``, ``"constants"``, ``"objectives"``, ``"constraints"``,
        ``"extra_funcs"``, ``"scalarization_funcs"``.
        Scalarization functions with ``symbol=None`` are omitted.
        """
        mapping: dict[str, str] = {}
        for v in self.variables:
            mapping[v.symbol] = "variables"
        for c in self.constants or []:
            mapping[c.symbol] = "constants"
        for o in self.objectives:
            mapping[o.symbol] = "objectives"
        for c in self.constraints or []:
            mapping[c.symbol] = "constraints"
        for f in self.extra_funcs or []:
            mapping[f.symbol] = "extra_funcs"
        for s in self.scalarization_funcs or []:
            if s.symbol is not None:
                mapping[s.symbol] = "scalarization_funcs"
        return mapping

    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]: an objective dict with the ideal
                point values (which may be `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]: an objective dict with the nadir
                point values (which may be `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 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`."""
    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.

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,
        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]: an objective dict with the ideal point values (which may be 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]: an objective dict with the ideal
            point values (which may be `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]: an objective dict with the nadir point values (which may be 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]: an objective dict with the nadir
            point values (which may be `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_symbol_type_map
get_symbol_type_map() -> dict[str, str]

Return a mapping of every named symbol to its element type.

The type strings match the field names used in Problem: "variables", "constants", "objectives", "constraints", "extra_funcs", "scalarization_funcs". Scalarization functions with symbol=None are omitted.

Source code in desdeo/problem/schema.py
def get_symbol_type_map(self) -> dict[str, str]:
    """Return a mapping of every named symbol to its element type.

    The type strings match the field names used in `Problem`:
    ``"variables"``, ``"constants"``, ``"objectives"``, ``"constraints"``,
    ``"extra_funcs"``, ``"scalarization_funcs"``.
    Scalarization functions with ``symbol=None`` are omitted.
    """
    mapping: dict[str, str] = {}
    for v in self.variables:
        mapping[v.symbol] = "variables"
    for c in self.constants or []:
        mapping[c.symbol] = "constants"
    for o in self.objectives:
        mapping[o.symbol] = "objectives"
    for c in self.constraints or []:
        mapping[c.symbol] = "constraints"
    for f in self.extra_funcs or []:
        mapping[f.symbol] = "extra_funcs"
    for s in self.scalarization_funcs or []:
        if s.symbol is not None:
            mapping[s.symbol] = "scalarization_funcs"
    return mapping
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`"""
    _parse_infix_to_func = field_validator("func", mode="before")(parse_infix_to_func)
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.

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: StrEnum

An enumerator for the possible variable type domains of a problem.

Source code in desdeo/problem/schema.py
class VariableDomainTypeEnum(StrEnum):
    """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: StrEnum

An enumerator for possible variable types.

Source code in desdeo/problem/schema.py
class VariableTypeEnum(StrEnum):
    """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)

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

Scenario

Scenario model for representing and constructing scenario-based optimization problems.

Scenario

Bases: BaseModel

References elements from the ScenarioModel pools that apply to a specific scenario.

Each field maps a target symbol to a pool index. The symbol identifies which element in the base problem to replace (for existing symbols) or the label of a new element to add (for symbols not present in the base problem). The index is the position of the replacement or addition in the corresponding ScenarioModel pool list.

Using an index rather than a symbol allows multiple pool entries to share the same symbol (e.g. the same constant at different values across scenarios) without ambiguity.

Source code in desdeo/problem/scenario.py
class Scenario(BaseModel):
    """References elements from the ScenarioModel pools that apply to a specific scenario.

    Each field maps a target symbol to a pool index.  The symbol identifies which element in
    the base problem to replace (for existing symbols) or the label of a new element to add
    (for symbols not present in the base problem).  The index is the position of the replacement
    or addition in the corresponding ScenarioModel pool list.

    Using an index rather than a symbol allows multiple pool entries to share the same symbol
    (e.g. the same constant at different values across scenarios) without ambiguity.
    """

    model_config = ConfigDict(frozen=True)

    constants: dict[str, int] = Field(
        default={},
        description="Maps target symbol to pool index for constants (scalar or tensor).",
    )
    variables: dict[str, int] = Field(
        default={},
        description="Maps target symbol to pool index for variables (scalar or tensor).",
    )
    objectives: dict[str, int] = Field(
        default={},
        description="Maps target symbol to pool index for objectives.",
    )
    constraints: dict[str, int] = Field(
        default={},
        description="Maps target symbol to pool index for constraints.",
    )
    extra_funcs: dict[str, int] = Field(
        default={},
        description="Maps target symbol to pool index for extra functions.",
    )
    scalarization_funcs: dict[str, int] = Field(
        default={},
        description="Maps target symbol to pool index for scalarization functions.",
    )

ScenarioModel

Bases: BaseModel

Base class for scenario models using Pydantic.

Source code in desdeo/problem/scenario.py
class ScenarioModel(BaseModel):
    """Base class for scenario models using Pydantic."""

    model_config = ConfigDict(frozen=True)

    scenario_tree: dict[str, list[str]] = Field(
        description="A dictionary describing the scenario structure for the problem. "
        "Keys are scenario names, values are lists of child scenario names. "
        "Must contain root of the tree with key 'ROOT' and value being a list of top-level scenario names. "
        "A plain list of scenario names is also accepted and is automatically converted to {'ROOT': [...]}.",
        default={"ROOT": ["scenario_0"]},
    )

    @field_validator("scenario_tree", mode="before")
    @classmethod
    def coerce_list_to_tree(cls, v):
        """Normalise the scenario tree input.

        - A flat list of names is converted to ``{'ROOT': [...], name: [], ...}``.
        - A dict may omit leaf nodes (nodes that appear as children but have no
          own entry).  Missing entries are auto-inserted with an empty child list.
        """
        if isinstance(v, list):
            return {"ROOT": v, **{name: [] for name in v}}
        if isinstance(v, dict):
            all_children = {child for children in v.values() for child in children}
            missing_leaves = all_children - set(v)
            if missing_leaves:
                return {**v, **{name: [] for name in missing_leaves}}
        return v

    @field_validator("scenario_tree", mode="after")
    @classmethod
    def validate_tree_structure(cls, v):
        """Verify that scenario_tree is a valid rooted tree with ROOT as the universal ancestor."""
        if "ROOT" not in v:
            raise ValueError("scenario_tree must contain a 'ROOT' key.")

        child_counts: dict[str, int] = {}
        for children in v.values():
            for child in children:
                child_counts[child] = child_counts.get(child, 0) + 1

        if "ROOT" in child_counts:
            raise ValueError("'ROOT' must not appear as a child of any node.")
        multi_parent = [n for n, c in child_counts.items() if c > 1]
        if multi_parent:
            raise ValueError(f"Nodes have more than one parent (not a tree): {sorted(multi_parent)}.")

        visited: set[str] = set()
        stack = ["ROOT"]
        while stack:
            node = stack.pop()
            visited.add(node)
            stack.extend(v.get(node, []))
        unreachable = set(v) - visited
        if unreachable:
            raise ValueError(f"Nodes not reachable from ROOT: {sorted(unreachable)}.")

        return v

    scenario_probabilities: dict[str, float] = Field(
        default={},
        description="Maps each scenario tree node to its probability. ROOT is always included with probability 1.0. "
        "For every node, the probabilities of its children must sum to the node's own probability.",
    )

    @field_validator("scenario_probabilities", mode="before")
    @classmethod
    def coerce_none_probabilities(cls, v):
        """Coerce None to an empty dict so probability validation can be skipped."""
        return v if v is not None else {}

    @field_validator("scenario_probabilities", mode="after")
    @classmethod
    def validate_scenario_probabilities(cls, v, info):
        """Auto-inject ROOT=1.0 and verify child probabilities sum to their parent's probability."""
        if not v:
            return v

        tree: dict[str, list[str]] = info.data.get("scenario_tree", {})

        v.setdefault("ROOT", 1.0)

        for key in v:
            if key not in tree:
                raise ValueError(f"scenario_probabilities key '{key}' not found in scenario_tree.")

        for parent, children in tree.items():
            if not children:
                continue
            if not any(c in v for c in children):
                continue
            parent_prob = v.get(parent)
            if parent_prob is None:
                raise ValueError(f"Probability for node '{parent}' is missing.")
            missing = [c for c in children if c not in v]
            if missing:
                raise ValueError(f"Probabilities missing for children of '{parent}': {missing}.")
            child_sum = sum(v[c] for c in children)
            if not math.isclose(child_sum, parent_prob, rel_tol=1e-9):
                raise ValueError(f"Probabilities of children of '{parent}' sum to {child_sum}, expected {parent_prob}.")
        return v

    base_problem: "Problem" = Field(description="The base Problem instance that is modified for different scenarios.")

    constants: tuple["Constant | TensorConstant", ...] = Field(
        default=(),
        description="Pool of Constant and TensorConstant replacements available to scenarios.",
    )
    variables: tuple["Variable | TensorVariable", ...] = Field(
        default=(),
        description="Pool of Variable and TensorVariable replacements available to scenarios.",
    )
    objectives: tuple["Objective", ...] = Field(
        default=(),
        description="Pool of Objective replacements available to scenarios.",
    )
    constraints: tuple["Constraint", ...] = Field(
        default=(),
        description="Pool of Constraint replacements available to scenarios.",
    )
    extra_funcs: tuple["ExtraFunction", ...] = Field(
        default=(),
        description="Pool of ExtraFunction replacements available to scenarios.",
    )
    scalarization_funcs: tuple["ScalarizationFunction", ...] = Field(
        default=(),
        description="Pool of ScalarizationFunction replacements available to scenarios.",
    )

    scenarios: dict[str, "Scenario"] = Field(
        default={},
        description="A dictionary mapping scenario names to Scenario instances, "
        "which define which elements from the pools apply to each scenario.",
    )

    anticipation_stop: dict[str, list[str]] = Field(
        default={},
        description="Maps a scenario tree node name to a list of variable symbols that enforce "
        "non-anticipativity at that node: the listed variables must take the same value across "
        "all descendant scenarios of that node.",
    )

    @field_validator("anticipation_stop", mode="after")
    @classmethod
    def validate_anticipation_stop(cls, v, info):
        """Validate that anticipation_stop keys exist in scenario_tree and values are valid variable symbols."""
        data = info.data
        valid_nodes = set(data.get("scenario_tree", {}).keys())
        base_problem = data.get("base_problem")
        valid_variables = (
            {var.symbol for var in base_problem.variables} if base_problem and base_problem.variables else set()
        )
        # Pool variables (added to scenarios) are also valid anticipation targets.
        valid_variables |= {var.symbol for var in data.get("variables", ())}

        for node, symbols in v.items():
            if node not in valid_nodes:
                raise ValueError(f"anticipation_stop key '{node}' not found in scenario_tree.")
            for symbol in symbols:
                if symbol not in valid_variables:
                    raise ValueError(
                        f"Variable symbol '{symbol}' in anticipation_stop['{node}'] "
                        "not found in base_problem variables or the variable pool."
                    )
        return v

    def with_base_problem(
        self, problem: "Problem | None" = None, validate: bool = False, **updates: dict[str, any]
    ) -> "ScenarioModel":
        """Return a new ScenarioModel with the base_problem updated.

        By default uses model_copy, so validators are NOT re-run. Pass
        ``validate=True`` to reconstruct via model_validate, which re-runs all
        field validators (including anticipation_stop checks). Only safe when the
        update cannot remove variables or scenario-tree nodes that validators
        reference.

        Args:
            problem: if provided, replaces the base_problem entirely.
            validate: if True, reconstruct with model_validate so all validators
                run, which is somewhat slower. Defaults to False.
            **updates: keyword arguments forwarded to Problem.model_copy(update=...)
                to partially update the existing base_problem. Ignored when
                ``problem`` is given.

        Returns:
            A new ScenarioModel with the modified base_problem.
        """
        new_base = problem if problem is not None else self.base_problem.model_copy(update=updates)
        if validate:
            return type(self).model_validate(self.model_dump() | {"base_problem": new_base})
        return self.model_copy(update={"base_problem": new_base})

    @cached_property
    def leaf_scenarios(self) -> MappingProxyType[str, float]:
        """Return a read-only mapping from each leaf scenario name to its probability.

        Leaf scenarios are nodes in the scenario tree with no children that also
        appear in the scenarios dict.  If scenario_probabilities is empty, equal
        weights (1/n) are assigned to each leaf.
        """
        leaves = [
            n for n, children in self.scenario_tree.items() if n != "ROOT" and not children and n in self.scenarios
        ]
        if self.scenario_probabilities:
            return MappingProxyType({leaf: self.scenario_probabilities[leaf] for leaf in leaves})
        equal = 1.0 / len(leaves) if leaves else 0.0
        return MappingProxyType(dict.fromkeys(leaves, equal))

    def get_scenario_problem(self, scenario_name: str) -> "Problem":
        """Return a modified copy of base_problem with the elements defined in the named scenario applied."""
        if scenario_name not in self.scenarios:
            raise ValueError(f"Scenario '{scenario_name}' not found.")

        scenario = self.scenarios[scenario_name]

        def apply(base_list, pool_list, symbol_to_idx):
            if not symbol_to_idx:
                return base_list
            base_list = base_list or []
            base_symbols = {e.symbol for e in base_list}
            result = [pool_list[symbol_to_idx[e.symbol]] if e.symbol in symbol_to_idx else e for e in base_list]
            result += [pool_list[idx] for sym, idx in symbol_to_idx.items() if sym not in base_symbols]
            return result

        updates = {}
        if scenario.constants:
            updates["constants"] = apply(self.base_problem.constants, self.constants, scenario.constants)
        if scenario.variables:
            updates["variables"] = apply(self.base_problem.variables, self.variables, scenario.variables)
        if scenario.objectives:
            updates["objectives"] = apply(self.base_problem.objectives, self.objectives, scenario.objectives)
        if scenario.constraints:
            updates["constraints"] = apply(self.base_problem.constraints, self.constraints, scenario.constraints)
        if scenario.extra_funcs:
            updates["extra_funcs"] = apply(self.base_problem.extra_funcs, self.extra_funcs, scenario.extra_funcs)
        if scenario.scalarization_funcs:
            updates["scalarization_funcs"] = apply(
                self.base_problem.scalarization_funcs, self.scalarization_funcs, scenario.scalarization_funcs
            )

        return self.base_problem.model_copy(update=updates)

    @field_validator("scenarios", mode="after")
    @classmethod
    def validate_scenarios(cls, v, info):
        """Validate that scenario names exist in scenario_tree and that all indices are in bounds."""
        data = info.data
        valid_scenarios = set(data.get("scenario_tree", {}).keys())

        pool_lengths = {
            "constants": len(data.get("constants", [])),
            "variables": len(data.get("variables", [])),
            "objectives": len(data.get("objectives", [])),
            "constraints": len(data.get("constraints", [])),
            "extra_funcs": len(data.get("extra_funcs", [])),
            "scalarization_funcs": len(data.get("scalarization_funcs", [])),
        }

        for scenario_name, scenario in v.items():
            if scenario_name not in valid_scenarios:
                raise ValueError(f"Scenario '{scenario_name}' not found in scenario_tree.")
            for field, length in pool_lengths.items():
                for symbol, idx in getattr(scenario, field).items():
                    if idx < 0 or idx >= length:
                        raise ValueError(
                            f"Index {idx} for symbol '{symbol}' in scenario '{scenario_name}.{field}' "
                            f"is out of bounds (pool length {length})."
                        )
        return v

leaf_scenarios cached property

leaf_scenarios: MappingProxyType[str, float]

Return a read-only mapping from each leaf scenario name to its probability.

Leaf scenarios are nodes in the scenario tree with no children that also appear in the scenarios dict. If scenario_probabilities is empty, equal weights (1/n) are assigned to each leaf.

coerce_list_to_tree classmethod

coerce_list_to_tree(v)

Normalise the scenario tree input.

  • A flat list of names is converted to {'ROOT': [...], name: [], ...}.
  • A dict may omit leaf nodes (nodes that appear as children but have no own entry). Missing entries are auto-inserted with an empty child list.
Source code in desdeo/problem/scenario.py
@field_validator("scenario_tree", mode="before")
@classmethod
def coerce_list_to_tree(cls, v):
    """Normalise the scenario tree input.

    - A flat list of names is converted to ``{'ROOT': [...], name: [], ...}``.
    - A dict may omit leaf nodes (nodes that appear as children but have no
      own entry).  Missing entries are auto-inserted with an empty child list.
    """
    if isinstance(v, list):
        return {"ROOT": v, **{name: [] for name in v}}
    if isinstance(v, dict):
        all_children = {child for children in v.values() for child in children}
        missing_leaves = all_children - set(v)
        if missing_leaves:
            return {**v, **{name: [] for name in missing_leaves}}
    return v

coerce_none_probabilities classmethod

coerce_none_probabilities(v)

Coerce None to an empty dict so probability validation can be skipped.

Source code in desdeo/problem/scenario.py
@field_validator("scenario_probabilities", mode="before")
@classmethod
def coerce_none_probabilities(cls, v):
    """Coerce None to an empty dict so probability validation can be skipped."""
    return v if v is not None else {}

get_scenario_problem

get_scenario_problem(scenario_name: str) -> Problem

Return a modified copy of base_problem with the elements defined in the named scenario applied.

Source code in desdeo/problem/scenario.py
def get_scenario_problem(self, scenario_name: str) -> "Problem":
    """Return a modified copy of base_problem with the elements defined in the named scenario applied."""
    if scenario_name not in self.scenarios:
        raise ValueError(f"Scenario '{scenario_name}' not found.")

    scenario = self.scenarios[scenario_name]

    def apply(base_list, pool_list, symbol_to_idx):
        if not symbol_to_idx:
            return base_list
        base_list = base_list or []
        base_symbols = {e.symbol for e in base_list}
        result = [pool_list[symbol_to_idx[e.symbol]] if e.symbol in symbol_to_idx else e for e in base_list]
        result += [pool_list[idx] for sym, idx in symbol_to_idx.items() if sym not in base_symbols]
        return result

    updates = {}
    if scenario.constants:
        updates["constants"] = apply(self.base_problem.constants, self.constants, scenario.constants)
    if scenario.variables:
        updates["variables"] = apply(self.base_problem.variables, self.variables, scenario.variables)
    if scenario.objectives:
        updates["objectives"] = apply(self.base_problem.objectives, self.objectives, scenario.objectives)
    if scenario.constraints:
        updates["constraints"] = apply(self.base_problem.constraints, self.constraints, scenario.constraints)
    if scenario.extra_funcs:
        updates["extra_funcs"] = apply(self.base_problem.extra_funcs, self.extra_funcs, scenario.extra_funcs)
    if scenario.scalarization_funcs:
        updates["scalarization_funcs"] = apply(
            self.base_problem.scalarization_funcs, self.scalarization_funcs, scenario.scalarization_funcs
        )

    return self.base_problem.model_copy(update=updates)

validate_anticipation_stop classmethod

validate_anticipation_stop(v, info)

Validate that anticipation_stop keys exist in scenario_tree and values are valid variable symbols.

Source code in desdeo/problem/scenario.py
@field_validator("anticipation_stop", mode="after")
@classmethod
def validate_anticipation_stop(cls, v, info):
    """Validate that anticipation_stop keys exist in scenario_tree and values are valid variable symbols."""
    data = info.data
    valid_nodes = set(data.get("scenario_tree", {}).keys())
    base_problem = data.get("base_problem")
    valid_variables = (
        {var.symbol for var in base_problem.variables} if base_problem and base_problem.variables else set()
    )
    # Pool variables (added to scenarios) are also valid anticipation targets.
    valid_variables |= {var.symbol for var in data.get("variables", ())}

    for node, symbols in v.items():
        if node not in valid_nodes:
            raise ValueError(f"anticipation_stop key '{node}' not found in scenario_tree.")
        for symbol in symbols:
            if symbol not in valid_variables:
                raise ValueError(
                    f"Variable symbol '{symbol}' in anticipation_stop['{node}'] "
                    "not found in base_problem variables or the variable pool."
                )
    return v

validate_scenario_probabilities classmethod

validate_scenario_probabilities(v, info)

Auto-inject ROOT=1.0 and verify child probabilities sum to their parent's probability.

Source code in desdeo/problem/scenario.py
@field_validator("scenario_probabilities", mode="after")
@classmethod
def validate_scenario_probabilities(cls, v, info):
    """Auto-inject ROOT=1.0 and verify child probabilities sum to their parent's probability."""
    if not v:
        return v

    tree: dict[str, list[str]] = info.data.get("scenario_tree", {})

    v.setdefault("ROOT", 1.0)

    for key in v:
        if key not in tree:
            raise ValueError(f"scenario_probabilities key '{key}' not found in scenario_tree.")

    for parent, children in tree.items():
        if not children:
            continue
        if not any(c in v for c in children):
            continue
        parent_prob = v.get(parent)
        if parent_prob is None:
            raise ValueError(f"Probability for node '{parent}' is missing.")
        missing = [c for c in children if c not in v]
        if missing:
            raise ValueError(f"Probabilities missing for children of '{parent}': {missing}.")
        child_sum = sum(v[c] for c in children)
        if not math.isclose(child_sum, parent_prob, rel_tol=1e-9):
            raise ValueError(f"Probabilities of children of '{parent}' sum to {child_sum}, expected {parent_prob}.")
    return v

validate_scenarios classmethod

validate_scenarios(v, info)

Validate that scenario names exist in scenario_tree and that all indices are in bounds.

Source code in desdeo/problem/scenario.py
@field_validator("scenarios", mode="after")
@classmethod
def validate_scenarios(cls, v, info):
    """Validate that scenario names exist in scenario_tree and that all indices are in bounds."""
    data = info.data
    valid_scenarios = set(data.get("scenario_tree", {}).keys())

    pool_lengths = {
        "constants": len(data.get("constants", [])),
        "variables": len(data.get("variables", [])),
        "objectives": len(data.get("objectives", [])),
        "constraints": len(data.get("constraints", [])),
        "extra_funcs": len(data.get("extra_funcs", [])),
        "scalarization_funcs": len(data.get("scalarization_funcs", [])),
    }

    for scenario_name, scenario in v.items():
        if scenario_name not in valid_scenarios:
            raise ValueError(f"Scenario '{scenario_name}' not found in scenario_tree.")
        for field, length in pool_lengths.items():
            for symbol, idx in getattr(scenario, field).items():
                if idx < 0 or idx >= length:
                    raise ValueError(
                        f"Index {idx} for symbol '{symbol}' in scenario '{scenario_name}.{field}' "
                        f"is out of bounds (pool length {length})."
                    )
    return v

validate_tree_structure classmethod

validate_tree_structure(v)

Verify that scenario_tree is a valid rooted tree with ROOT as the universal ancestor.

Source code in desdeo/problem/scenario.py
@field_validator("scenario_tree", mode="after")
@classmethod
def validate_tree_structure(cls, v):
    """Verify that scenario_tree is a valid rooted tree with ROOT as the universal ancestor."""
    if "ROOT" not in v:
        raise ValueError("scenario_tree must contain a 'ROOT' key.")

    child_counts: dict[str, int] = {}
    for children in v.values():
        for child in children:
            child_counts[child] = child_counts.get(child, 0) + 1

    if "ROOT" in child_counts:
        raise ValueError("'ROOT' must not appear as a child of any node.")
    multi_parent = [n for n, c in child_counts.items() if c > 1]
    if multi_parent:
        raise ValueError(f"Nodes have more than one parent (not a tree): {sorted(multi_parent)}.")

    visited: set[str] = set()
    stack = ["ROOT"]
    while stack:
        node = stack.pop()
        visited.add(node)
        stack.extend(v.get(node, []))
    unreachable = set(v) - visited
    if unreachable:
        raise ValueError(f"Nodes not reachable from ROOT: {sorted(unreachable)}.")

    return v

with_base_problem

with_base_problem(
    problem: Problem | None = None,
    validate: bool = False,
    **updates: dict[str, any],
) -> ScenarioModel

Return a new ScenarioModel with the base_problem updated.

By default uses model_copy, so validators are NOT re-run. Pass validate=True to reconstruct via model_validate, which re-runs all field validators (including anticipation_stop checks). Only safe when the update cannot remove variables or scenario-tree nodes that validators reference.

Parameters:

Name Type Description Default
problem Problem | None

if provided, replaces the base_problem entirely.

None
validate bool

if True, reconstruct with model_validate so all validators run, which is somewhat slower. Defaults to False.

False
**updates dict[str, any]

keyword arguments forwarded to Problem.model_copy(update=...) to partially update the existing base_problem. Ignored when problem is given.

{}

Returns:

Type Description
ScenarioModel

A new ScenarioModel with the modified base_problem.

Source code in desdeo/problem/scenario.py
def with_base_problem(
    self, problem: "Problem | None" = None, validate: bool = False, **updates: dict[str, any]
) -> "ScenarioModel":
    """Return a new ScenarioModel with the base_problem updated.

    By default uses model_copy, so validators are NOT re-run. Pass
    ``validate=True`` to reconstruct via model_validate, which re-runs all
    field validators (including anticipation_stop checks). Only safe when the
    update cannot remove variables or scenario-tree nodes that validators
    reference.

    Args:
        problem: if provided, replaces the base_problem entirely.
        validate: if True, reconstruct with model_validate so all validators
            run, which is somewhat slower. Defaults to False.
        **updates: keyword arguments forwarded to Problem.model_copy(update=...)
            to partially update the existing base_problem. Ignored when
            ``problem`` is given.

    Returns:
        A new ScenarioModel with the modified base_problem.
    """
    new_base = problem if problem is not None else self.base_problem.model_copy(update=updates)
    if validate:
        return type(self).model_validate(self.model_dump() | {"base_problem": new_base})
    return self.model_copy(update={"base_problem": new_base})

JSON parser

desdeo.problem.json_parser

Defines a parser to parse multiobjective optimziation problems defined in a JSON format.

FormatEnum

Bases: StrEnum

Enumerates the supported formats the JSON format may be parsed to.

Source code in desdeo/problem/json_parser.py
class FormatEnum(StrEnum):
    """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.

Parses MathJSON expressions to polars, pyomo, sympy, gurobipy, or cvxpy expressions.

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
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
class MathParser:
    """A class to instantiate MathJSON parsers.

    Parses MathJSON expressions to polars, pyomo, sympy, gurobipy, or cvxpy expressions.
    """

    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"
        self.EXTRACT: str = "Extract"
        self.EXCLUDE: str = "Exclude"
        self.TUPLE: str = "Tuple"

        # 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 _collect_indices(raw_indices: tuple, n: int) -> list[int]:
            """Collect 0-based indices from a sequence of raw Extract/Exclude specs.

            Args:
                raw_indices: the index arguments passed to Extract or Exclude.
                    Each element is either a plain number (single 1-based index)
                    or a tuple ``(start, stop[, step])``.  Positive values count
                    from the front; negative values count from the back (e.g.
                    ``-1`` is the last element).
                n: total length of the array being indexed.  Required to
                    resolve negative indices and to clamp out-of-range values.
            """

            def _to_zero(i1: int) -> int:
                # Convert a 1-based (positive) or end-relative (negative) index to 0-based.
                return (i1 - 1) if i1 > 0 else (n + i1)

            indices = []
            for spec in raw_indices:
                if isinstance(spec, tuple):
                    start = _to_zero(int(spec[0]))
                    stop = _to_zero(int(spec[1]))
                    step = int(spec[2]) if len(spec) >= 3 else (1 if stop >= start else -1)  # noqa: PLR2004
                    rng = range(start, stop + 1, step) if step > 0 else range(start, stop - 1, step)
                    indices.extend(i for i in rng if 0 <= i < n)
                else:
                    zero = _to_zero(int(spec))
                    if 0 <= zero < n:
                        indices.append(zero)
            return indices

        def _extract_from_array(arr, *raw_indices):
            arr = np.asarray(arr)
            idx = _collect_indices(raw_indices, arr.shape[0])
            return arr[idx].tolist() if idx else []

        def _exclude_from_array(arr, *raw_indices):
            arr = np.asarray(arr)
            n = arr.shape[0]
            excluded = set(_collect_indices(raw_indices, n))
            keep = [i for i in range(n) if i not in excluded]
            return arr[keep].tolist() if keep else []

        def _polars_extract(expr, *raw_indices):
            def _fn(acc):
                arr = acc.to_numpy()
                if arr.ndim > 1:
                    idx = _collect_indices(raw_indices, arr.shape[-1])
                    result = arr[..., idx] if idx else np.empty((*arr.shape[:-1], 0), dtype=arr.dtype)
                else:
                    result = _extract_from_array(arr, *raw_indices)
                return pl.Series(values=result.tolist())

            return to_expr(expr).map_batches(_fn)

        def _polars_exclude(expr, *raw_indices):
            def _fn(acc):
                arr = acc.to_numpy()
                if arr.ndim > 1:
                    n = arr.shape[-1]
                    excluded = set(_collect_indices(raw_indices, n))
                    keep = [i for i in range(n) if i not in excluded]
                    result = arr[..., keep] if keep else np.empty((*arr.shape[:-1], 0), dtype=arr.dtype)
                else:
                    result = _exclude_from_array(arr, *raw_indices)
                return pl.Series(values=result.tolist())

            return to_expr(expr).map_batches(_fn)

        def _not_impl_extract(*_args):
            raise NotImplementedError("'Extract' is not implemented for this backend.")

        def _not_impl_exclude(*_args):
            raise NotImplementedError("'Exclude' is not implemented for this backend.")

        def to_expr(x: self.literals | pl.Expr):  # type: ignore
            """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_pow(base, exponent):
            base = to_expr(base)
            exponent = to_expr(exponent)
            # Detect a constant exponent (the operands have already been parsed to polars
            # expressions, so a literal arrives as pl.lit(...)). A constant exponent lets us
            # apply the power via a unary UDF so polars can infer the shape-preserving return
            # dtype: native `**` does not work on array columns, and an N-ary UDF nested in
            # other expressions cannot be inferred by polars >=1.31.
            try:
                exponent_value = pl.select(exponent).item()
            except Exception:
                exponent_value = None
            if exponent_value is not None:
                return base.map_batches(
                    lambda series, exp=exponent_value: pl.Series(values=np.power(series.to_numpy(), exp))
                )
            # Non-constant exponent (rare): native power, which works for scalar operands.
            return base**exponent

        def _polars_reduce_unary(expr, ufunc):
            def _map_function(acc, ufunc=ufunc):
                # Unary math functions (e.g. log, arctanh) can legitimately hit domain edges such as
                # log(0) or arctanh(+-1); the resulting inf/nan is the intended evaluation result, so
                # silence the benign numpy warnings.
                with np.errstate(divide="ignore", invalid="ignore"):
                    return pl.Series(values=ufunc(acc.to_numpy()))

            return to_expr(expr).map_batches(_map_function)

        def _polars_reduce_matmul(*exprs):
            # Numpy matmul (row-vector dot product OR matrix product) via an N-ary UDF.
            # No return dtype is declared: polars infers it from a sample, which works
            # because the surrounding +,-,*,/ are native expressions (not UDFs), so this
            # UDF is never nested inside another un-inferrable UDF.
            def _map_function(series_list):
                acc = series_list[0].to_numpy()
                for series in series_list[1:]:
                    x = series.to_numpy()
                    if acc.ndim == 2 and x.ndim == 2:  # noqa: PLR2004
                        # row vectors -> per-row dot product (polars has no "column" vectors)
                        acc = np.einsum("ij,ij->i", acc, x, optimize=True)
                    else:
                        acc = np.matmul(acc, x)
                return pl.Series(values=acc)

            return pl.map_batches(exprs=[to_expr(expr) for expr in exprs], function=_map_function)

        def _polars_summation(expr):
            """Polars matrix summation."""

            def _map_function(acc):
                acc_numpy = acc.to_numpy()
                return pl.Series(values=np.sum(acc_numpy, axis=tuple(range(1, acc_numpy.ndim))))

            return to_expr(expr).map_batches(_map_function)

        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: reduce(lambda a, b: to_expr(a) + to_expr(b), args),
            self.SUB: lambda *args: reduce(lambda a, b: to_expr(a) - to_expr(b), args),
            self.MUL: lambda *args: reduce(lambda a, b: to_expr(a) * to_expr(b), args),
            self.DIV: lambda *args: reduce(lambda a, b: to_expr(a) / to_expr(b), args),
            # Vector and matrix operations
            self.MATMUL: _polars_reduce_matmul,
            self.SUM: lambda x: _polars_summation(x),
            self.RANDOM_ACCESS: _polars_random_access,
            self.EXTRACT: _polars_extract,
            self.EXCLUDE: _polars_exclude,
            # 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: reduce(_polars_pow, 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
                    x_idx, y_idx = x.index_set(), y.index_set()
                    if x_idx.dimen != y_idx.dimen or len(x_idx) != len(y_idx):
                        msg = (
                            f"The shapes of x ({x_idx.dimen}D, len={len(x_idx)}) and "
                            f"y ({y_idx.dimen}D, len={len(y_idx)}) must match 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]

        def _pyomo_extract(indexed, *raw_indices):
            if not (hasattr(indexed, "index_set") and indexed.is_indexed()):
                msg = "'Extract' requires an indexed Pyomo expression."
                raise ParserError(msg)
            all_indices = sorted(indexed.index_set())
            n = len(all_indices)
            positions = _collect_indices(raw_indices, n)
            selected = [all_indices[p] for p in positions]
            new_set = pyomo.RangeSet(1, len(selected))
            idx_map = {i + 1: sel for i, sel in enumerate(selected)}
            expr = pyomo.Expression(new_set, rule=lambda _, i: indexed[idx_map[i]])
            expr.construct()
            return expr

        def _pyomo_exclude(indexed, *raw_indices):
            if not (hasattr(indexed, "index_set") and indexed.is_indexed()):
                msg = "'Exclude' requires an indexed Pyomo expression."
                raise ParserError(msg)
            all_indices = sorted(indexed.index_set())
            n = len(all_indices)
            excluded_pos = set(_collect_indices(raw_indices, n))
            keep = [all_indices[p] for p in range(n) if p not in excluded_pos]
            new_set = pyomo.RangeSet(1, len(keep))
            idx_map = {i + 1: k for i, k in enumerate(keep)}
            expr = pyomo.Expression(new_set, rule=lambda _, i: indexed[idx_map[i]])
            expr.construct()
            return expr

        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,
            self.EXTRACT: _pyomo_extract,
            self.EXCLUDE: _pyomo_exclude,
            # 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,
            self.EXTRACT: _not_impl_extract,
            self.EXCLUDE: _not_impl_exclude,
            # 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_multiply(*args):
            """Multiply, promoting scalar gp.Var * ndarray to MLinExpr via MVar.fromlist."""

            def _mul(a, b):
                if isinstance(a, gp.Var) and isinstance(b, np.ndarray):
                    return gp.MVar.fromlist([a]) * b
                if isinstance(b, gp.Var) and isinstance(a, np.ndarray):
                    return gp.MVar.fromlist([b]) * a
                return a * b

            return reduce(_mul, args)

        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)
                return a @ b

            return reduce(_matmul, args)

        def _gurobipy_summation(summand):
            """Gurobipy matrix summation."""

            def _sum(summand):
                if isinstance(summand, list):
                    summand = np.array(summand)
                return summand.sum()

            return _sum(summand)

        def _gurobipy_random_access(indexed, *indices):
            # 1-based indexing assumed in JSON format; convert to 0-based for Python/numpy/gp.MVar
            zero_based = tuple(int(i) - 1 for i in indices)
            if len(zero_based) == 1:
                return indexed[zero_based[0]]
            return indexed[zero_based]

        def _gurobipy_extract(arr, *raw_indices):
            idx = _collect_indices(raw_indices, arr.shape[0])
            return arr[idx]

        def _gurobipy_exclude(arr, *raw_indices):
            n = arr.shape[0]
            excluded = set(_collect_indices(raw_indices, n))
            keep = [i for i in range(n) if i not in excluded]
            return arr[keep]

        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: _gurobipy_multiply,
            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,
            self.EXTRACT: _gurobipy_extract,
            self.EXCLUDE: _gurobipy_exclude,
            # 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)
                return a @ b

            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(indexed, *indices):
            zero_based = tuple(int(i) - 1 for i in indices)
            if len(zero_based) == 1:
                return indexed[zero_based[0]]
            return indexed[zero_based]

        def _cvxpy_extract(expr, *raw_indices):
            n = expr.shape[0]
            idx = _collect_indices(raw_indices, n)
            return expr[idx]

        def _cvxpy_exclude(expr, *raw_indices):
            n = expr.shape[0]
            excluded = set(_collect_indices(raw_indices, n))
            keep = [i for i in range(n) if i not in excluded]
            return expr[keep]

        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,
            self.EXTRACT: _cvxpy_extract,
            self.EXCLUDE: _cvxpy_exclude,
            # 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/Exclude: index args must stay as raw Python values, not polars expressions.
            if expr[0] in (self.EXTRACT, self.EXCLUDE):
                collection = self.parse(expr[1])
                raw_indices = [self._parse_raw_index(e) for e in expr[2:]]
                return self.env[expr[0]](collection, *raw_indices)

            # 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])

            if expr[0] in (self.EXTRACT, self.EXCLUDE):
                collection = self._parse_to_pyomo(expr[1], model)
                raw_indices = [self._parse_raw_index(e) for e in expr[2:]]
                return self.env[expr[0]](collection, *raw_indices)

            # 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)

            if expr[0] in (self.EXTRACT, self.EXCLUDE):
                collection = self.parse(expr[1])
                raw_indices = [self._parse_raw_index(e) for e in expr[2:]]
                return self.env[expr[0]](collection, *raw_indices)

            # 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):
            if expr[0] in (self.EXTRACT, self.EXCLUDE):
                collection = self._parse_to_gurobipy(expr[1], callback)
                raw_indices = [self._parse_raw_index(e) for e in expr[2:]]
                return self.env[expr[0]](collection, *raw_indices)

            # 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)
            if len(expr) == 1 and isinstance(expr[0], str):
                # Terminal case, single string expression with unnecessary brackets, e.g., ["x1"] instead of "x1"
                return (
                    callback(expr[0]) + 0
                )  # adding 0 to ensure it's treated as an expression, not a variable reference

        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):
            if expr[0] in (self.EXTRACT, self.EXCLUDE):
                collection = self._parse_to_cvxpy(expr[1], callback)
                raw_indices = [self._parse_raw_index(e) for e in expr[2:]]
                return self.env[expr[0]](collection, *raw_indices)

            # 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
            parsed = [self._parse_to_cvxpy(e, callback) for e in expr]
            return parsed[0] if len(parsed) == 1 else parsed

        msg = f"Encountered unsupported type '{type(expr)}' during parsing."
        raise ParserError(msg)

    def _parse_raw_index(self, expr) -> int | tuple[int, ...]:
        """Convert a MathJSON index spec to a plain Python int or tuple (for Extract/Exclude)."""
        if isinstance(expr, (int, float)):
            return int(expr)
        if isinstance(expr, list) and expr[0] == self.TUPLE:
            return tuple(int(x) for x in expr[1:])
        msg = f"Extract/Exclude index must be an integer or Tuple range, got: {expr!r}"
        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
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
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"
    self.EXTRACT: str = "Extract"
    self.EXCLUDE: str = "Exclude"
    self.TUPLE: str = "Tuple"

    # 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 _collect_indices(raw_indices: tuple, n: int) -> list[int]:
        """Collect 0-based indices from a sequence of raw Extract/Exclude specs.

        Args:
            raw_indices: the index arguments passed to Extract or Exclude.
                Each element is either a plain number (single 1-based index)
                or a tuple ``(start, stop[, step])``.  Positive values count
                from the front; negative values count from the back (e.g.
                ``-1`` is the last element).
            n: total length of the array being indexed.  Required to
                resolve negative indices and to clamp out-of-range values.
        """

        def _to_zero(i1: int) -> int:
            # Convert a 1-based (positive) or end-relative (negative) index to 0-based.
            return (i1 - 1) if i1 > 0 else (n + i1)

        indices = []
        for spec in raw_indices:
            if isinstance(spec, tuple):
                start = _to_zero(int(spec[0]))
                stop = _to_zero(int(spec[1]))
                step = int(spec[2]) if len(spec) >= 3 else (1 if stop >= start else -1)  # noqa: PLR2004
                rng = range(start, stop + 1, step) if step > 0 else range(start, stop - 1, step)
                indices.extend(i for i in rng if 0 <= i < n)
            else:
                zero = _to_zero(int(spec))
                if 0 <= zero < n:
                    indices.append(zero)
        return indices

    def _extract_from_array(arr, *raw_indices):
        arr = np.asarray(arr)
        idx = _collect_indices(raw_indices, arr.shape[0])
        return arr[idx].tolist() if idx else []

    def _exclude_from_array(arr, *raw_indices):
        arr = np.asarray(arr)
        n = arr.shape[0]
        excluded = set(_collect_indices(raw_indices, n))
        keep = [i for i in range(n) if i not in excluded]
        return arr[keep].tolist() if keep else []

    def _polars_extract(expr, *raw_indices):
        def _fn(acc):
            arr = acc.to_numpy()
            if arr.ndim > 1:
                idx = _collect_indices(raw_indices, arr.shape[-1])
                result = arr[..., idx] if idx else np.empty((*arr.shape[:-1], 0), dtype=arr.dtype)
            else:
                result = _extract_from_array(arr, *raw_indices)
            return pl.Series(values=result.tolist())

        return to_expr(expr).map_batches(_fn)

    def _polars_exclude(expr, *raw_indices):
        def _fn(acc):
            arr = acc.to_numpy()
            if arr.ndim > 1:
                n = arr.shape[-1]
                excluded = set(_collect_indices(raw_indices, n))
                keep = [i for i in range(n) if i not in excluded]
                result = arr[..., keep] if keep else np.empty((*arr.shape[:-1], 0), dtype=arr.dtype)
            else:
                result = _exclude_from_array(arr, *raw_indices)
            return pl.Series(values=result.tolist())

        return to_expr(expr).map_batches(_fn)

    def _not_impl_extract(*_args):
        raise NotImplementedError("'Extract' is not implemented for this backend.")

    def _not_impl_exclude(*_args):
        raise NotImplementedError("'Exclude' is not implemented for this backend.")

    def to_expr(x: self.literals | pl.Expr):  # type: ignore
        """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_pow(base, exponent):
        base = to_expr(base)
        exponent = to_expr(exponent)
        # Detect a constant exponent (the operands have already been parsed to polars
        # expressions, so a literal arrives as pl.lit(...)). A constant exponent lets us
        # apply the power via a unary UDF so polars can infer the shape-preserving return
        # dtype: native `**` does not work on array columns, and an N-ary UDF nested in
        # other expressions cannot be inferred by polars >=1.31.
        try:
            exponent_value = pl.select(exponent).item()
        except Exception:
            exponent_value = None
        if exponent_value is not None:
            return base.map_batches(
                lambda series, exp=exponent_value: pl.Series(values=np.power(series.to_numpy(), exp))
            )
        # Non-constant exponent (rare): native power, which works for scalar operands.
        return base**exponent

    def _polars_reduce_unary(expr, ufunc):
        def _map_function(acc, ufunc=ufunc):
            # Unary math functions (e.g. log, arctanh) can legitimately hit domain edges such as
            # log(0) or arctanh(+-1); the resulting inf/nan is the intended evaluation result, so
            # silence the benign numpy warnings.
            with np.errstate(divide="ignore", invalid="ignore"):
                return pl.Series(values=ufunc(acc.to_numpy()))

        return to_expr(expr).map_batches(_map_function)

    def _polars_reduce_matmul(*exprs):
        # Numpy matmul (row-vector dot product OR matrix product) via an N-ary UDF.
        # No return dtype is declared: polars infers it from a sample, which works
        # because the surrounding +,-,*,/ are native expressions (not UDFs), so this
        # UDF is never nested inside another un-inferrable UDF.
        def _map_function(series_list):
            acc = series_list[0].to_numpy()
            for series in series_list[1:]:
                x = series.to_numpy()
                if acc.ndim == 2 and x.ndim == 2:  # noqa: PLR2004
                    # row vectors -> per-row dot product (polars has no "column" vectors)
                    acc = np.einsum("ij,ij->i", acc, x, optimize=True)
                else:
                    acc = np.matmul(acc, x)
            return pl.Series(values=acc)

        return pl.map_batches(exprs=[to_expr(expr) for expr in exprs], function=_map_function)

    def _polars_summation(expr):
        """Polars matrix summation."""

        def _map_function(acc):
            acc_numpy = acc.to_numpy()
            return pl.Series(values=np.sum(acc_numpy, axis=tuple(range(1, acc_numpy.ndim))))

        return to_expr(expr).map_batches(_map_function)

    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: reduce(lambda a, b: to_expr(a) + to_expr(b), args),
        self.SUB: lambda *args: reduce(lambda a, b: to_expr(a) - to_expr(b), args),
        self.MUL: lambda *args: reduce(lambda a, b: to_expr(a) * to_expr(b), args),
        self.DIV: lambda *args: reduce(lambda a, b: to_expr(a) / to_expr(b), args),
        # Vector and matrix operations
        self.MATMUL: _polars_reduce_matmul,
        self.SUM: lambda x: _polars_summation(x),
        self.RANDOM_ACCESS: _polars_random_access,
        self.EXTRACT: _polars_extract,
        self.EXCLUDE: _polars_exclude,
        # 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: reduce(_polars_pow, 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
                x_idx, y_idx = x.index_set(), y.index_set()
                if x_idx.dimen != y_idx.dimen or len(x_idx) != len(y_idx):
                    msg = (
                        f"The shapes of x ({x_idx.dimen}D, len={len(x_idx)}) and "
                        f"y ({y_idx.dimen}D, len={len(y_idx)}) must match 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]

    def _pyomo_extract(indexed, *raw_indices):
        if not (hasattr(indexed, "index_set") and indexed.is_indexed()):
            msg = "'Extract' requires an indexed Pyomo expression."
            raise ParserError(msg)
        all_indices = sorted(indexed.index_set())
        n = len(all_indices)
        positions = _collect_indices(raw_indices, n)
        selected = [all_indices[p] for p in positions]
        new_set = pyomo.RangeSet(1, len(selected))
        idx_map = {i + 1: sel for i, sel in enumerate(selected)}
        expr = pyomo.Expression(new_set, rule=lambda _, i: indexed[idx_map[i]])
        expr.construct()
        return expr

    def _pyomo_exclude(indexed, *raw_indices):
        if not (hasattr(indexed, "index_set") and indexed.is_indexed()):
            msg = "'Exclude' requires an indexed Pyomo expression."
            raise ParserError(msg)
        all_indices = sorted(indexed.index_set())
        n = len(all_indices)
        excluded_pos = set(_collect_indices(raw_indices, n))
        keep = [all_indices[p] for p in range(n) if p not in excluded_pos]
        new_set = pyomo.RangeSet(1, len(keep))
        idx_map = {i + 1: k for i, k in enumerate(keep)}
        expr = pyomo.Expression(new_set, rule=lambda _, i: indexed[idx_map[i]])
        expr.construct()
        return expr

    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,
        self.EXTRACT: _pyomo_extract,
        self.EXCLUDE: _pyomo_exclude,
        # 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,
        self.EXTRACT: _not_impl_extract,
        self.EXCLUDE: _not_impl_exclude,
        # 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_multiply(*args):
        """Multiply, promoting scalar gp.Var * ndarray to MLinExpr via MVar.fromlist."""

        def _mul(a, b):
            if isinstance(a, gp.Var) and isinstance(b, np.ndarray):
                return gp.MVar.fromlist([a]) * b
            if isinstance(b, gp.Var) and isinstance(a, np.ndarray):
                return gp.MVar.fromlist([b]) * a
            return a * b

        return reduce(_mul, args)

    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)
            return a @ b

        return reduce(_matmul, args)

    def _gurobipy_summation(summand):
        """Gurobipy matrix summation."""

        def _sum(summand):
            if isinstance(summand, list):
                summand = np.array(summand)
            return summand.sum()

        return _sum(summand)

    def _gurobipy_random_access(indexed, *indices):
        # 1-based indexing assumed in JSON format; convert to 0-based for Python/numpy/gp.MVar
        zero_based = tuple(int(i) - 1 for i in indices)
        if len(zero_based) == 1:
            return indexed[zero_based[0]]
        return indexed[zero_based]

    def _gurobipy_extract(arr, *raw_indices):
        idx = _collect_indices(raw_indices, arr.shape[0])
        return arr[idx]

    def _gurobipy_exclude(arr, *raw_indices):
        n = arr.shape[0]
        excluded = set(_collect_indices(raw_indices, n))
        keep = [i for i in range(n) if i not in excluded]
        return arr[keep]

    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: _gurobipy_multiply,
        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,
        self.EXTRACT: _gurobipy_extract,
        self.EXCLUDE: _gurobipy_exclude,
        # 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)
            return a @ b

        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(indexed, *indices):
        zero_based = tuple(int(i) - 1 for i in indices)
        if len(zero_based) == 1:
            return indexed[zero_based[0]]
        return indexed[zero_based]

    def _cvxpy_extract(expr, *raw_indices):
        n = expr.shape[0]
        idx = _collect_indices(raw_indices, n)
        return expr[idx]

    def _cvxpy_exclude(expr, *raw_indices):
        n = expr.shape[0]
        excluded = set(_collect_indices(raw_indices, n))
        keep = [i for i in range(n) if i not in excluded]
        return expr[keep]

    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,
        self.EXTRACT: _cvxpy_extract,
        self.EXCLUDE: _cvxpy_exclude,
        # 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_raw_index
_parse_raw_index(expr) -> int | tuple[int, ...]

Convert a MathJSON index spec to a plain Python int or tuple (for Extract/Exclude).

Source code in desdeo/problem/json_parser.py
def _parse_raw_index(self, expr) -> int | tuple[int, ...]:
    """Convert a MathJSON index spec to a plain Python int or tuple (for Extract/Exclude)."""
    if isinstance(expr, (int, float)):
        return int(expr)
    if isinstance(expr, list) and expr[0] == self.TUPLE:
        return tuple(int(x) for x in expr[1:])
    msg = f"Extract/Exclude index must be an integer or Tuple range, got: {expr!r}"
    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):
        if expr[0] in (self.EXTRACT, self.EXCLUDE):
            collection = self._parse_to_cvxpy(expr[1], callback)
            raw_indices = [self._parse_raw_index(e) for e in expr[2:]]
            return self.env[expr[0]](collection, *raw_indices)

        # 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
        parsed = [self._parse_to_cvxpy(e, callback) for e in expr]
        return parsed[0] if len(parsed) == 1 else parsed

    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):
        if expr[0] in (self.EXTRACT, self.EXCLUDE):
            collection = self._parse_to_gurobipy(expr[1], callback)
            raw_indices = [self._parse_raw_index(e) for e in expr[2:]]
            return self.env[expr[0]](collection, *raw_indices)

        # 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)
        if len(expr) == 1 and isinstance(expr[0], str):
            # Terminal case, single string expression with unnecessary brackets, e.g., ["x1"] instead of "x1"
            return (
                callback(expr[0]) + 0
            )  # adding 0 to ensure it's treated as an expression, not a variable reference

    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/Exclude: index args must stay as raw Python values, not polars expressions.
        if expr[0] in (self.EXTRACT, self.EXCLUDE):
            collection = self.parse(expr[1])
            raw_indices = [self._parse_raw_index(e) for e in expr[2:]]
            return self.env[expr[0]](collection, *raw_indices)

        # 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])

        if expr[0] in (self.EXTRACT, self.EXCLUDE):
            collection = self._parse_to_pyomo(expr[1], model)
            raw_indices = [self._parse_raw_index(e) for e in expr[2:]]
            return self.env[expr[0]](collection, *raw_indices)

        # 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)

        if expr[0] in (self.EXTRACT, self.EXCLUDE):
            collection = self.parse(expr[1])
            raw_indices = [self._parse_raw_index(e) for e in expr[2:]]
            return self.env[expr[0]](collection, *raw_indices)

        # 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 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

        # 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

        # 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 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)

        # 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)

        # 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 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)

        # 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)

        # 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 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)

    # 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)

    # 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 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)

    # 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)

    # 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 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

    # 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

    # 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: StrEnum

Defines the supported modes for the PolarsEvaluator.

Source code in desdeo/problem/evaluator.py
class PolarsEvaluatorModesEnum(StrEnum):
    """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: StrEnum

An enumerator for the possible dimensions of the variables of a problem.

Source code in desdeo/problem/evaluator.py
class VariableDimensionEnum(StrEnum):
    """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
 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
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 scalarization functions, if any
        if problem.scalarization_funcs is not None:
            model = self.init_scalarizations(problem, model)

        # Add constraints, if any
        if problem.constraints is not None:
            model = self.init_constraints(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:  # noqa: C901
        """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)
                    case (_, _, VariableTypeEnum.binary):
                        domain = pyomo.Binary
                    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 hasattr(pyomo_expr, "is_indexed") and pyomo_expr.is_indexed():
                        _e = pyomo_expr
                        data = {k: _e[k] == 0 for k in _e.index_set()}
                        pyomo_expr = pyomo.Constraint(_e.index_set(), rule=data)
                    else:
                        pyomo_expr = pyomo.Constraint(expr=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 scalarization functions, if any
    if problem.scalarization_funcs is not None:
        model = self.init_scalarizations(problem, model)

    # Add constraints, if any
    if problem.constraints is not None:
        model = self.init_constraints(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 hasattr(pyomo_expr, "is_indexed") and pyomo_expr.is_indexed():
                    _e = pyomo_expr
                    data = {k: _e[k] == 0 for k in _e.index_set()}
                    pyomo_expr = pyomo.Constraint(_e.index_set(), rule=data)
                else:
                    pyomo_expr = pyomo.Constraint(expr=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:  # noqa: C901
    """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)
                case (_, _, VariableTypeEnum.binary):
                    domain = pyomo.Binary
                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."""

Sympy evaluator

desdeo.problem.sympy_evaluator

Implements and evaluator based on sympy expressions.

SympyEvaluator

Defines an evaluator that can be used to evaluate instances of Problem utilizing sympy.

Source code in desdeo/problem/sympy_evaluator.py
class SympyEvaluator:
    """Defines an evaluator that can be used to evaluate instances of Problem utilizing sympy."""

    def __init__(self, problem: Problem):
        """Initializes the evaluator.

        Args:
            problem (Problem): the problem to be evaluated.
        """
        if variable_dimension_enumerate(problem) not in SUPPORTED_VAR_DIMENSIONS:
            msg = "SymPy evaluator does not yet support tensors."
            raise SympyEvaluatorError(msg)

        # Collect all the symbols and expressions in the problem
        parser = MathParser(to_format=FormatEnum.sympy)

        self.variable_symbols = [var.symbol for var in problem.variables]
        self.constant_expressions = (
            {const.symbol: parser.parse(const.value) for const in problem.constants}
            if problem.constants is not None
            else None
        )

        self.extra_expressions = (
            {extra.symbol: parser.parse(extra.func) for extra in problem.extra_funcs}
            if problem.extra_funcs is not None
            else None
        )

        self.objective_expressions = {obj.symbol: parser.parse(obj.func) for obj in problem.objectives}

        self.constraint_expressions = (
            {con.symbol: parser.parse(con.func) for con in problem.constraints}
            if problem.constraints is not None
            else None
        )

        self.scalarization_expressions = (
            {scal.symbol: parser.parse(scal.func) for scal in problem.scalarization_funcs}
            if problem.scalarization_funcs is not None
            else None
        )

        # replace symbols and create lambda functions ready to be called
        # replace constants in extra functions, if they exist
        if self.extra_expressions is not None:
            _extra_expressions = (
                {
                    k: self.extra_expressions[k].subs(self.constant_expressions, evaluate=False)
                    for k in self.extra_expressions
                }
                if self.constant_expressions is not None
                else deepcopy(self.extra_expressions)
            )
        else:
            _extra_expressions = None

        # replace constants in objective functions, if constants have been defined
        _objective_expressions = (
            {
                k: self.objective_expressions[k].subs(self.constant_expressions, evaluate=False)
                for k in self.objective_expressions
            }
            if self.constant_expressions is not None
            else deepcopy(self.objective_expressions)
        )

        # replace extra functions in objective functions, if extra functions have been defined
        _objective_expressions = (
            (
                {
                    k: _objective_expressions[k].subs(self.extra_expressions, evaluate=False)
                    for k in _objective_expressions
                }
            )
            if self.extra_expressions is not None
            else _objective_expressions
        )

        # always minimized objective expressions
        _objective_expressions_min = {
            f"{obj.symbol}_min": -_objective_expressions[obj.symbol]
            if obj.maximize
            else _objective_expressions[obj.symbol]
            for obj in problem.objectives
        }

        # replace stuff in the constraint expressions if any are defined
        if self.constraint_expressions is not None:
            # replace constants
            _constraint_expressions = (
                {
                    k: self.constraint_expressions[k].subs(self.constant_expressions, evaluate=False)
                    for k in self.constraint_expressions
                }
                if self.constant_expressions is not None
                else deepcopy(self.constraint_expressions)
            )

            # replace extra functions
            _constraint_expressions = (
                {
                    k: _constraint_expressions[k].subs(_extra_expressions, evaluate=False)
                    for k in _constraint_expressions
                }
                if _extra_expressions is not None
                else _constraint_expressions
            )

            # replace objective functions
            _constraint_expressions = {
                k: _constraint_expressions[k].subs(_objective_expressions, evaluate=False)
                for k in _constraint_expressions
            }
            _constraint_expressions = {
                k: _constraint_expressions[k].subs(_objective_expressions_min, evaluate=False)
                for k in _constraint_expressions
            }

        else:
            _constraint_expressions = None

        # replace stuff in scalarization expressions if any are defined
        if self.scalarization_expressions is not None:
            # replace constants
            _scalarization_expressions = (
                {
                    k: self.scalarization_expressions[k].subs(self.constant_expressions, evaluate=False)
                    for k in self.scalarization_expressions
                }
                if self.constant_expressions is not None
                else deepcopy(self.scalarization_expressions)
            )

            # replace extra functions
            _scalarization_expressions = (
                {
                    k: _scalarization_expressions[k].subs(_extra_expressions, evaluate=False)
                    for k in _scalarization_expressions
                }
                if _extra_expressions is not None
                else _scalarization_expressions
            )

            # replace constraints
            _scalarization_expressions = (
                {
                    k: _scalarization_expressions[k].subs(_constraint_expressions, evaluate=False)
                    for k in _scalarization_expressions
                }
                if _constraint_expressions is not None
                else _scalarization_expressions
            )

            # replace objectives
            _scalarization_expressions = {
                k: _scalarization_expressions[k].subs(_objective_expressions, evaluate=False)
                for k in _scalarization_expressions
            }

            _scalarization_expressions = {
                k: _scalarization_expressions[k].subs(_objective_expressions_min, evaluate=False)
                for k in _scalarization_expressions
            }

        else:
            _scalarization_expressions = None

        # initialize callable lambdas
        self.lambda_exprs = {
            _k: _v
            for _d in [
                {k: sp.lambdify(self.variable_symbols, d[k]) for k in d}
                for d in [
                    _extra_expressions,
                    _objective_expressions,
                    _objective_expressions_min,
                    _constraint_expressions,
                    _scalarization_expressions,
                ]
                if d is not None
            ]
            for _k, _v in _d.items()
        }

        self.problem = problem
        self.parser = parser

    def evaluate(self, xs: dict[str, float | int | bool]) -> dict[str, float | int | bool]:
        """Evaluate the the whole problem with a given decision variable dict.

        Args:
            xs (dict[str, float  |  int  |  bool]): a dict with keys representing decision variable
                symbols and values with the decision variable value.

        Returns:
            dict[str, float | int | bool]: a dict with keys corresponding to each symbol
                defined for the problem being evaluated and the corresponding expression's
                value.
        """
        return {k: self.lambda_exprs[k](**xs) for k in self.lambda_exprs} | xs

    def evaluate_target(self, xs: dict[str, float | int | bool], target: str) -> float:
        """Evaluates only the specified target with given decision variables.

        Args:
            xs (dict[str, float  |  int  |  bool]): a dict with keys representing decision variable
                symbols and values with the decision variable value.
            target (str): the symbol of the function expressions to be evaluated.

        Returns:
            float: the value of the target once evaluated.
        """
        return self.lambda_exprs[target](**xs)

    def evaluate_constraints(self, xs: dict[str, float | int | bool]) -> dict[str, float | int | bool]:
        """Evaluates the constraints of the problem with given decision variables.

        Args:
            xs (dict[str, float  |  int  |  bool]): a dict with keys representing decision variable
                symbols and values with the decision variable value.

        Returns:
            dict[str, float | int | bool]: a dict with keys being the constraints symbols
                and values being the value of the corresponding constraint.
        """
        return {k: self.lambda_exprs[k](**xs) for k in [constr.symbol for constr in self.problem.constraints]}
__init__
__init__(problem: Problem)

Initializes the evaluator.

Parameters:

Name Type Description Default
problem Problem

the problem to be evaluated.

required
Source code in desdeo/problem/sympy_evaluator.py
def __init__(self, problem: Problem):
    """Initializes the evaluator.

    Args:
        problem (Problem): the problem to be evaluated.
    """
    if variable_dimension_enumerate(problem) not in SUPPORTED_VAR_DIMENSIONS:
        msg = "SymPy evaluator does not yet support tensors."
        raise SympyEvaluatorError(msg)

    # Collect all the symbols and expressions in the problem
    parser = MathParser(to_format=FormatEnum.sympy)

    self.variable_symbols = [var.symbol for var in problem.variables]
    self.constant_expressions = (
        {const.symbol: parser.parse(const.value) for const in problem.constants}
        if problem.constants is not None
        else None
    )

    self.extra_expressions = (
        {extra.symbol: parser.parse(extra.func) for extra in problem.extra_funcs}
        if problem.extra_funcs is not None
        else None
    )

    self.objective_expressions = {obj.symbol: parser.parse(obj.func) for obj in problem.objectives}

    self.constraint_expressions = (
        {con.symbol: parser.parse(con.func) for con in problem.constraints}
        if problem.constraints is not None
        else None
    )

    self.scalarization_expressions = (
        {scal.symbol: parser.parse(scal.func) for scal in problem.scalarization_funcs}
        if problem.scalarization_funcs is not None
        else None
    )

    # replace symbols and create lambda functions ready to be called
    # replace constants in extra functions, if they exist
    if self.extra_expressions is not None:
        _extra_expressions = (
            {
                k: self.extra_expressions[k].subs(self.constant_expressions, evaluate=False)
                for k in self.extra_expressions
            }
            if self.constant_expressions is not None
            else deepcopy(self.extra_expressions)
        )
    else:
        _extra_expressions = None

    # replace constants in objective functions, if constants have been defined
    _objective_expressions = (
        {
            k: self.objective_expressions[k].subs(self.constant_expressions, evaluate=False)
            for k in self.objective_expressions
        }
        if self.constant_expressions is not None
        else deepcopy(self.objective_expressions)
    )

    # replace extra functions in objective functions, if extra functions have been defined
    _objective_expressions = (
        (
            {
                k: _objective_expressions[k].subs(self.extra_expressions, evaluate=False)
                for k in _objective_expressions
            }
        )
        if self.extra_expressions is not None
        else _objective_expressions
    )

    # always minimized objective expressions
    _objective_expressions_min = {
        f"{obj.symbol}_min": -_objective_expressions[obj.symbol]
        if obj.maximize
        else _objective_expressions[obj.symbol]
        for obj in problem.objectives
    }

    # replace stuff in the constraint expressions if any are defined
    if self.constraint_expressions is not None:
        # replace constants
        _constraint_expressions = (
            {
                k: self.constraint_expressions[k].subs(self.constant_expressions, evaluate=False)
                for k in self.constraint_expressions
            }
            if self.constant_expressions is not None
            else deepcopy(self.constraint_expressions)
        )

        # replace extra functions
        _constraint_expressions = (
            {
                k: _constraint_expressions[k].subs(_extra_expressions, evaluate=False)
                for k in _constraint_expressions
            }
            if _extra_expressions is not None
            else _constraint_expressions
        )

        # replace objective functions
        _constraint_expressions = {
            k: _constraint_expressions[k].subs(_objective_expressions, evaluate=False)
            for k in _constraint_expressions
        }
        _constraint_expressions = {
            k: _constraint_expressions[k].subs(_objective_expressions_min, evaluate=False)
            for k in _constraint_expressions
        }

    else:
        _constraint_expressions = None

    # replace stuff in scalarization expressions if any are defined
    if self.scalarization_expressions is not None:
        # replace constants
        _scalarization_expressions = (
            {
                k: self.scalarization_expressions[k].subs(self.constant_expressions, evaluate=False)
                for k in self.scalarization_expressions
            }
            if self.constant_expressions is not None
            else deepcopy(self.scalarization_expressions)
        )

        # replace extra functions
        _scalarization_expressions = (
            {
                k: _scalarization_expressions[k].subs(_extra_expressions, evaluate=False)
                for k in _scalarization_expressions
            }
            if _extra_expressions is not None
            else _scalarization_expressions
        )

        # replace constraints
        _scalarization_expressions = (
            {
                k: _scalarization_expressions[k].subs(_constraint_expressions, evaluate=False)
                for k in _scalarization_expressions
            }
            if _constraint_expressions is not None
            else _scalarization_expressions
        )

        # replace objectives
        _scalarization_expressions = {
            k: _scalarization_expressions[k].subs(_objective_expressions, evaluate=False)
            for k in _scalarization_expressions
        }

        _scalarization_expressions = {
            k: _scalarization_expressions[k].subs(_objective_expressions_min, evaluate=False)
            for k in _scalarization_expressions
        }

    else:
        _scalarization_expressions = None

    # initialize callable lambdas
    self.lambda_exprs = {
        _k: _v
        for _d in [
            {k: sp.lambdify(self.variable_symbols, d[k]) for k in d}
            for d in [
                _extra_expressions,
                _objective_expressions,
                _objective_expressions_min,
                _constraint_expressions,
                _scalarization_expressions,
            ]
            if d is not None
        ]
        for _k, _v in _d.items()
    }

    self.problem = problem
    self.parser = parser
evaluate
evaluate(
    xs: dict[str, float | int | bool],
) -> dict[str, float | int | bool]

Evaluate the the whole problem with a given decision variable dict.

Parameters:

Name Type Description Default
xs dict[str, float | int | bool]

a dict with keys representing decision variable symbols and values with the decision variable value.

required

Returns:

Type Description
dict[str, float | int | bool]

dict[str, float | int | bool]: a dict with keys corresponding to each symbol defined for the problem being evaluated and the corresponding expression's value.

Source code in desdeo/problem/sympy_evaluator.py
def evaluate(self, xs: dict[str, float | int | bool]) -> dict[str, float | int | bool]:
    """Evaluate the the whole problem with a given decision variable dict.

    Args:
        xs (dict[str, float  |  int  |  bool]): a dict with keys representing decision variable
            symbols and values with the decision variable value.

    Returns:
        dict[str, float | int | bool]: a dict with keys corresponding to each symbol
            defined for the problem being evaluated and the corresponding expression's
            value.
    """
    return {k: self.lambda_exprs[k](**xs) for k in self.lambda_exprs} | xs
evaluate_constraints
evaluate_constraints(
    xs: dict[str, float | int | bool],
) -> dict[str, float | int | bool]

Evaluates the constraints of the problem with given decision variables.

Parameters:

Name Type Description Default
xs dict[str, float | int | bool]

a dict with keys representing decision variable symbols and values with the decision variable value.

required

Returns:

Type Description
dict[str, float | int | bool]

dict[str, float | int | bool]: a dict with keys being the constraints symbols and values being the value of the corresponding constraint.

Source code in desdeo/problem/sympy_evaluator.py
def evaluate_constraints(self, xs: dict[str, float | int | bool]) -> dict[str, float | int | bool]:
    """Evaluates the constraints of the problem with given decision variables.

    Args:
        xs (dict[str, float  |  int  |  bool]): a dict with keys representing decision variable
            symbols and values with the decision variable value.

    Returns:
        dict[str, float | int | bool]: a dict with keys being the constraints symbols
            and values being the value of the corresponding constraint.
    """
    return {k: self.lambda_exprs[k](**xs) for k in [constr.symbol for constr in self.problem.constraints]}
evaluate_target
evaluate_target(
    xs: dict[str, float | int | bool], target: str
) -> float

Evaluates only the specified target with given decision variables.

Parameters:

Name Type Description Default
xs dict[str, float | int | bool]

a dict with keys representing decision variable symbols and values with the decision variable value.

required
target str

the symbol of the function expressions to be evaluated.

required

Returns:

Name Type Description
float float

the value of the target once evaluated.

Source code in desdeo/problem/sympy_evaluator.py
def evaluate_target(self, xs: dict[str, float | int | bool], target: str) -> float:
    """Evaluates only the specified target with given decision variables.

    Args:
        xs (dict[str, float  |  int  |  bool]): a dict with keys representing decision variable
            symbols and values with the decision variable value.
        target (str): the symbol of the function expressions to be evaluated.

    Returns:
        float: the value of the target once evaluated.
    """
    return self.lambda_exprs[target](**xs)

SympyEvaluatorError

Bases: Exception

Raised when an exception with a Sympy evaluator is encountered.

Source code in desdeo/problem/sympy_evaluator.py
class SympyEvaluatorError(Exception):
    """Raised when an exception with a Sympy evaluator is encountered."""

Gurobipy evaluator

desdeo.problem.gurobipy_evaluator

Defines an evaluator compatible with the Problem JSON format and transforms it into a GurobipyModel.

GurobipyEvaluator

Defines as evaluator that transforms an instance of Problem into a GurobipyModel.

Source code in desdeo/problem/gurobipy_evaluator.py
 33
 34
 35
 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
class GurobipyEvaluator:
    """Defines as evaluator that transforms an instance of Problem into a GurobipyModel."""

    variables: dict[str, gp.Var | gp.MVar]
    constraints: dict[str, gp.Constr]
    objective_functions: dict[str, gp.Var | gp.MVar | gp.LinExpr | gp.QuadExpr | gp.MLinExpr | gp.MQuadExpr]
    scalarizations: dict[str, gp.Var | gp.MVar | gp.LinExpr | gp.QuadExpr | gp.MLinExpr | gp.MQuadExpr]
    extra_functions: dict[
        str, gp.Var | gp.MVar | gp.LinExpr | gp.QuadExpr | gp.MLinExpr | gp.MQuadExpr | gp.GenExpr | int | float
    ]
    constants: dict[str, int | float | list[int] | list[float]]

    model: gp.Model

    def __init__(self, problem: Problem):
        """Initialized the evaluator.

        Args:
            problem (Problem): the problem to be transformed in a GurobipyModel.
        """
        self.model = gp.Model(problem.name)
        self.objective_functions = {}
        self.scalarizations = {}
        self.extra_functions = {}
        self.constants = {}
        self.variables = {}
        self.constraints = {}

        # set the parser
        self.parse = MathParser(to_format=FormatEnum.gurobipy).parse

        # Add variables
        self.variables = self.init_variables(problem)

        # Add constants, if any
        if problem.constants is not None:
            self.constants = self.init_constants(problem)

        # Add extra expressions, if any
        if problem.extra_funcs is not None:
            self.extra_functions = self.init_extras(problem)

        # Add objective function expressions
        self.objective_functions = self.init_objectives(problem)

        # Add scalarization functions, if any
        if problem.scalarization_funcs is not None:
            self.scalarizations = self.init_scalarizations(problem)

        # Add constraints, if any
        if problem.constraints is not None:
            self.constraints = self.init_constraints(problem)

        self.problem = problem

    def init_variables(self, problem: Problem) -> dict[str, gp.Var | gp.MVar]:
        """Add variables to the GurobipyModel.

        Args:
            problem (Problem): problem from which to extract the variables.

        Raises:
            GurobipyEvaluatorError: when a problem in extracting the variables is encountered.
                I.e., the variables are of a non supported type.

        Returns:
            dict[str, gp.Var | gp.MVar]: the variables added to the model.
        """
        variables = {}
        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 var.variable_type:
                    case VariableTypeEnum.integer:
                        # variable is integer
                        domain = gp.GRB.INTEGER
                    case VariableTypeEnum.real:
                        # variable is real
                        domain = gp.GRB.CONTINUOUS
                    case VariableTypeEnum.binary:
                        domain = gp.GRB.BINARY
                    case _:
                        msg = f"Could not figure out the type for variable {var}."
                        raise GurobipyEvaluatorError(msg)

                # add the variable to the model
                gvar = self.model.addVar(lb=lowerbound, ub=upperbound, vtype=domain, name=var.symbol)
                # set the initial value, if one has been defined
                if var.initial_value is not None:
                    gvar.setAttr("Start", var.initial_value)
                variables[var.symbol] = gvar

            elif isinstance(var, TensorVariable):
                # handle tensor variables, i.e., vectors etc..
                lowerbounds = (
                    var.get_lowerbound_values()
                    if var.lowerbounds is not None
                    else np.full(var.shape, float("-inf")).tolist()
                )
                upperbounds = (
                    var.get_upperbound_values()
                    if var.upperbounds is not None
                    else np.full(var.shape, float("inf")).tolist()
                )

                # figure out the variable type
                match var.variable_type:
                    case VariableTypeEnum.integer:
                        # variable is integer
                        domain = gp.GRB.INTEGER
                    case VariableTypeEnum.real:
                        # variable is real
                        domain = gp.GRB.CONTINUOUS
                    case VariableTypeEnum.binary:
                        domain = gp.GRB.BINARY
                    case _:
                        msg = f"Could not figure out the type for variable {var}."
                        raise GurobipyEvaluatorError(msg)

                # add the variable to the model
                gvar = self.model.addMVar(
                    shape=tuple(var.shape),
                    lb=np.array(lowerbounds),
                    ub=np.array(upperbounds),
                    vtype=domain,
                    name=var.symbol,
                )
                # set the initial value, if one has been defined
                if var.initial_values is not None:
                    gvar.setAttr("Start", np.array(var.get_initial_values()))
                variables[var.symbol] = gvar

        # update the model before returning, so that other expressions can reference the variables
        self.model.update()

        return variables

    def init_constants(self, problem: Problem) -> dict[str, int | float | list[int] | list[float]]:
        """Add constants to a GurobipyEvaluator.

        Gurobi does not really have constants, so this function instead
        stores them in a dict, that is then stored in the evaluator.
        This is necessary to get the MathParser to understand the constants
        used in the problem, but updating them at a later point will not update
        any expression referencing them. The expressions that have been defined
        using these constants will keep using the numeric value of the constant
        at the time when the expression was created.
        Thus, it might be best to avoid using constants if you are intending
        to use the gurobipy solver.

        Args:
            problem (Problem): problem from which to extract the constants.

        Raises:
            GurobipyEvaluatorError: when the domain of a constant cannot be figured out.

        Returns:
            dict[str, int | float]: a dict containing the constants.
        """
        constants: dict[str, int | float | list[int] | list[float]] = {}
        for con in problem.constants:
            if isinstance(con, Constant):
                constants[con.symbol] = con.value
            elif isinstance(con, TensorConstant):
                constants[con.symbol] = con.get_values()

        return constants

    def init_extras(
        self, problem: Problem
    ) -> dict[str, gp.Var | gp.MVar | gp.LinExpr | gp.QuadExpr | gp.MLinExpr | gp.MQuadExpr | gp.GenExpr | int | float]:
        """Add extra function expressions to a Gurobipy Model.

        Gurobi does not support extra expressions natively, so this function instead
        stores them in a dict to be used by the evaluator.
        This is necessary to get the MathParser to understand the extra expressions
        used in the problem, but updating them at a later point will not update
        any expression referencing them. The expressions that have been defined
        using these extra expressions will keep using the value of the extra expression
        at the time when the expression was created.
        Thus, it might be best to avoid using extra expressions if you are intending
        to use the gurobipy solver.

        Args:
            problem (Problem): problem from which the extract the extra function expressions.

        Returns:
            GurobipyModel: the GurobipyModel with the expressions added as attributes.
        """
        extra_functions: dict[
            str, gp.Var | gp.MVar | gp.LinExpr | gp.QuadExpr | gp.MLinExpr | gp.MQuadExpr | gp.GenExpr | int | float
        ] = {}

        for extra in problem.extra_funcs:
            extra_functions[extra.symbol] = self.parse(extra.func, callback=self.get_expression_by_name)

        return extra_functions

    def init_objectives(
        self, problem: Problem
    ) -> dict[str, gp.Var | gp.MVar | gp.LinExpr | gp.QuadExpr | gp.MLinExpr | gp.MQuadExpr]:
        """Add objective function expressions to a Gurobipy Model.

        Does not yet add any actual gurobipy optimization objectives, only creates a dict containing the
        expressions of the objectives. The objective expressions are stored in the
        evaluator and appropiate gurobipy objective must be added to the model before solving.

        Args:
            problem (Problem): problem from which to extract the objective function expresions.

        Returns:
            dict: dict containing the objective functions.
        """
        objective_functions: dict[str, gp.Var | gp.MVar | gp.LinExpr | gp.QuadExpr | gp.MLinExpr | gp.MQuadExpr] = {}
        for obj in problem.objectives:
            gp_expr = self.parse(obj.func, callback=self.get_expression_by_name)
            if isinstance(gp_expr, int | float):
                warnings.warn(
                    "One or more of the problem objectives seems to be a constant.",
                    GurobipyEvaluatorWarning,
                    stacklevel=2,
                )
            if isinstance(gp_expr, gp.GenExpr):
                msg = f"Gurobi does not support objective functions that are not linear or quadratic {gp_expr}"
                raise GurobipyEvaluatorError(msg)

            objective_functions[obj.symbol] = gp_expr

            # the obj.symbol_min objectives are used when optimizing and building scalarizations etc...
            objective_functions[f"{obj.symbol}_min"] = -(gp_expr) if obj.maximize else gp_expr

        return objective_functions

    def init_constraints(self, problem: Problem, model: gp.Model | None = None) -> gp.Model:
        """Add constraint expressions to a Gurobipy Model.

        Args:
            problem (Problem): the problem from which to extract the constraint function expressions.
            model (GurobipyModel | None): the GurobipyModel to add the expressions to.

        Raises:
            GurobipyEvaluatorError: when an unsupported constraint type is encountered.

        Returns:
            GurobipyModel: the GurobipyModel with the constraint expressions added.
        """
        constraints = {}
        for cons in problem.constraints:
            gp_expr = self.parse(cons.func, callback=self.get_expression_by_name)

            match con_type := cons.cons_type:
                case ConstraintTypeEnum.LTE:
                    # constraints in DESDEO are defined such that they must be less than zero
                    gp_expr = _le(gp_expr, 0)
                case ConstraintTypeEnum.EQ:
                    gp_expr = _eq(gp_expr, 0)
                case _:
                    msg = f"Constraint type of {con_type} not supported. Must be one of {ConstraintTypeEnum}."
                    raise GurobipyEvaluatorError(msg)

            constraints[cons.symbol] = self.model.addConstr(gp_expr, name=cons.symbol)

        self.model.update()
        return constraints

    def init_scalarizations(
        self, problem: Problem
    ) -> dict[str, gp.Var | gp.MVar | gp.LinExpr | gp.QuadExpr | gp.MLinExpr | gp.MQuadExpr]:
        """Add scalrization expressions to a gurobipy model.

        Scalarizations work identically to objectives, except they are stored in a different
        dict in the GurobipyModel. If you want to solve the problem using a scalarization, the
        evaluator needs to set it as an optimization target first.

        Args:
            problem (Problem): the problem from which to extract the scalarization function expressions.

        Returns:
            dict: the dict with the scalarization expressions. Scalarization functions are always minimized.
        """
        scalarizations: dict[str, gp.Var | gp.MVar | gp.LinExpr | gp.QuadExpr | gp.MLinExpr | gp.MQuadExpr] = {}

        for scal in problem.scalarization_funcs:
            scalarizations[scal.symbol] = self.parse(scal.func, self.get_expression_by_name)

        return scalarizations

    def add_constraint(self, constraint: Constraint) -> gp.Constr:
        """Add a constraint expression to a GurobipyModel.

        If adding a lot of constraints, this function may end up being very slow compared
        to adding the constraints to the stored model directly, because of the model.update() calls.

        Args:
            constraint (Constraint): the constraint function expression.

        Raises:
            GurobipyEvaluatorError: when an unsupported constraint type is encountered.

        Returns:
            gurobipy.Constr: The gurobipy constraint that was added.
        """
        gp_expr = self.parse(constraint.func, self.get_expression_by_name)

        match con_type := constraint.cons_type:
            case ConstraintTypeEnum.LTE:
                # constraints in DESDEO are defined such that they must be less than zero
                gp_expr = _le(gp_expr, 0)
            case ConstraintTypeEnum.EQ:
                gp_expr = _eq(gp_expr, 0)
            case _:
                msg = f"Constraint type of {con_type} not supported. Must be one of {ConstraintTypeEnum}."
                raise GurobipyEvaluatorError(msg)

        return_cons = self.model.addConstr(gp_expr, name=constraint.symbol)
        self.model.update()
        self.constraints[constraint.symbol] = return_cons
        return return_cons

    def add_objective(self, obj: Objective):
        """Adds an objective function expression to a GurobipyModel.

        Does not yet add any actual gurobipy optimization objectives, only adds them to the dict
        containing the expressions of the objectives. The objective expressions are stored in the
        GurobipyModel and the evaluator must add the appropiate gurobipy objective before solving.

        Args:
            obj (Objective): the objective function expression to be added.
        """
        gp_expr = self.parse(obj.func, self.get_expression_by_name)
        if isinstance(gp_expr, int | float):
            warnings.warn(
                "One or more of the problem objectives seems to be a constant.", GurobipyEvaluatorWarning, stacklevel=2
            )
        if isinstance(gp_expr, gp.GenExpr):
            msg = f"Gurobi does not support objective functions that are not linear or quadratic {gp_expr}"
            raise GurobipyEvaluatorError(msg)

        self.objective_functions[obj.symbol] = gp_expr

        # the obj.symbol_min objectives are used when optimizing and building scalarizations etc...
        self.objective_functions[f"{obj.symbol}_min"] = -gp_expr if obj.maximize else gp_expr

    def add_scalarization_function(self, scal: ScalarizationFunction):
        """Adds a scalrization expression to a gurobipy model.

        Scalarizations work identically to objectives, except they are stored in a different
        dict in the GurobipyModel. If you want to solve the problem using a scalarization, the
        evaluator needs to set it as an optimization target first.

        Args:
            scal (ScalarizationFunction): The scalarization function to be added.
        """
        self.scalarizations[scal.symbol] = self.parse(scal.func, self.get_expression_by_name)

    def add_variable(self, var: Variable | TensorVariable) -> gp.Var | gp.MVar:
        """Add variables to the GurobipyModel.

        If adding a lot of variables, this function may end up being very slow compared
        to adding the variables to the stored model directly, because of the model.update() calls.

        Args:
            var (Variable): The definition of the variable to be added.

        Raises:
            GurobipyEvaluatorError: when a problem in extracting the variables is encountered.
                I.e., the variables are of a non supported type.

        Returns:
            gp.Var: the variable that was added to the model.
        """
        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 var.variable_type:
                case VariableTypeEnum.integer:
                    # variable is integer
                    domain = gp.GRB.INTEGER
                case VariableTypeEnum.real:
                    # variable is real
                    domain = gp.GRB.CONTINUOUS
                case VariableTypeEnum.binary:
                    domain = gp.GRB.BINARY
                case _:
                    msg = f"Could not figure out the type for variable {var}."
                    raise GurobipyEvaluatorError(msg)

            # add the variable to the model
            gvar = self.model.addVar(lb=lowerbound, ub=upperbound, vtype=domain, name=var.symbol)
            # set the initial value, if one has been defined
            if var.initial_value is not None:
                gvar.setAttr("Start", var.initial_value)
        elif isinstance(var, TensorVariable):
            # handle tensor variables, i.e., vectors etc..
            lowerbounds = (
                var.get_lowerbound_values()
                if var.lowerbounds is not None
                else np.full(var.shape, float("-inf")).tolist()
            )
            upperbounds = (
                var.get_upperbound_values()
                if var.upperbounds is not None
                else np.full(var.shape, float("inf")).tolist()
            )

            # figure out the variable type
            match var.variable_type:
                case VariableTypeEnum.integer:
                    # variable is integer
                    domain = gp.GRB.INTEGER
                case VariableTypeEnum.real:
                    # variable is real
                    domain = gp.GRB.CONTINUOUS
                case VariableTypeEnum.binary:
                    domain = gp.GRB.BINARY
                case _:
                    msg = f"Could not figure out the type for variable {var}."
                    raise GurobipyEvaluatorError(msg)

            # add the variable to the model
            gvar = self.model.addMVar(
                shape=tuple(var.shape),
                lb=np.array(lowerbounds),
                ub=np.array(upperbounds),
                vtype=domain,
                name=var.symbol,
            )
            # set the initial value, if one has been defined
            if var.initial_values is not None:
                gvar.setAttr("Start", np.array(var.get_initial_values()))

        self.model.update()
        self.variables[var.symbol] = gvar
        return gvar

    def get_expression_by_name(
        self, name: str
    ) -> (
        gp.Var | gp.MVar | gp.LinExpr | gp.QuadExpr | gp.MLinExpr | gp.MQuadExpr | gp.GenExpr | int | float | np.ndarray
    ):
        """Returns a gurobipy expression corresponding to the name.

        Only looks for variables, objective functions, scalarizations, extra functions, and constants.
        This will not find constraints.

        Args:
            name (str): The symbol of the expression.

        Returns:
            gurobipy expression: A mathematical expression that gp.Model can use either as a constraint or an objective
        """
        if name in self.variables:
            expression = self.variables[name]
        elif name in self.objective_functions:
            expression = self.objective_functions[name]
        elif name in self.scalarizations:
            expression = self.scalarizations[name]
        elif name in self.extra_functions:
            expression = self.extra_functions[name]
        elif name in self.constants:
            if isinstance(self.constants[name], list):
                expression = np.array(self.constants[name])
            else:
                expression = self.constants[name]
        else:
            msg = f"No expression with name {name} found in the gurobipy model."
            raise GurobipyEvaluatorError(msg)
        return expression

    def get_values(self) -> dict[str, float | int | bool | list[float] | list[int]]:
        """Get the values from the Gurobipy Model in a 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:
            result_dict[var.symbol] = self.variables[var.symbol].getAttr(gp.GRB.Attr.X)
        for obj in self.problem.objectives:
            result_dict[obj.symbol] = self.objective_functions[obj.symbol].getValue()

        if self.problem.constants is not None:
            for con in self.problem.constants:
                result_dict[con.symbol] = self.constants[con.symbol]

        if self.problem.extra_funcs is not None:
            for extra in self.problem.extra_funcs:
                result_dict[extra.symbol] = self.extra_functions[extra.symbol].getValue()

        if self.problem.constraints is not None:
            for const in self.problem.constraints:
                con = self.constraints[const.symbol]
                if isinstance(con, gp.MQConstr):
                    slack = con.QCSlack
                    result_dict[const.symbol] = (-np.array(slack)).tolist() if hasattr(slack, "__len__") else -slack
                elif isinstance(con, gp.MConstr):
                    result_dict[const.symbol] = (-con.Slack).tolist()
                elif isinstance(con, gp.QConstr):
                    result_dict[const.symbol] = -con.getAttr("QCSlack")
                else:
                    result_dict[const.symbol] = -con.getAttr("Slack")

        if self.problem.scalarization_funcs is not None:
            for scal in self.problem.scalarization_funcs:
                result_dict[scal.symbol] = self.scalarizations[scal.symbol].getValue()

        return result_dict

    def remove_constraint(self, symbol: str):
        """Removes a constraint from the model.

        If removing a lot of constraints or dealing with a very large model this function
        may be slow because of the model.update() calls. Accessing the stored model directly
        may be faster.

        Args:
            symbol (str): a str representing the symbol of the constraint to be removed.
        """
        self.model.remove(self.constraints[symbol])
        self.constraints.pop(symbol)
        self.model.update()

    def remove_variable(self, symbol: str):
        """Removes a variable from the model.

        If removing a lot of variables or dealing with a very large model this function
        may be slow because of the model.update() calls. Accessing the stored model directly
        may be faster.

        Args:
            symbol (str): a str representing the symbol of the variable to be removed.
        """
        self.model.remove(self.variables[symbol])
        self.variables.pop(symbol)
        self.model.update()

    def set_optimization_target(self, target: str, maximize: bool = False):
        """Sets a minimization objective to match the target objective or scalarization of the gurobipy model.

        Args:
            target (str): an str representing a symbol. Needs to match an objective function or scalarization
                function already found in the model.
            maximize (bool): If true, the target function is maximized instead of minimized

        Raises:
            GurobipyEvaluatorError: the given target was not an attribute of the gurobipy model.
        """
        if target in self.objective_functions:
            objective = self.problem.get_objective(symbol=target, copy=False)
            maximize = False if objective is None else bool(objective.maximize)
        elif target in self.scalarizations:
            maximize = False
        else:
            msg = f"The gurobipy model has no objective or scalarization named {target}."
            raise GurobipyEvaluatorError(msg)

        obj_expr = self.get_expression_by_name(target)

        if maximize:
            self.model.setObjective(obj_expr, sense=gp.GRB.MAXIMIZE)
        else:
            self.model.setObjective(obj_expr)
__init__
__init__(problem: Problem)

Initialized the evaluator.

Parameters:

Name Type Description Default
problem Problem

the problem to be transformed in a GurobipyModel.

required
Source code in desdeo/problem/gurobipy_evaluator.py
def __init__(self, problem: Problem):
    """Initialized the evaluator.

    Args:
        problem (Problem): the problem to be transformed in a GurobipyModel.
    """
    self.model = gp.Model(problem.name)
    self.objective_functions = {}
    self.scalarizations = {}
    self.extra_functions = {}
    self.constants = {}
    self.variables = {}
    self.constraints = {}

    # set the parser
    self.parse = MathParser(to_format=FormatEnum.gurobipy).parse

    # Add variables
    self.variables = self.init_variables(problem)

    # Add constants, if any
    if problem.constants is not None:
        self.constants = self.init_constants(problem)

    # Add extra expressions, if any
    if problem.extra_funcs is not None:
        self.extra_functions = self.init_extras(problem)

    # Add objective function expressions
    self.objective_functions = self.init_objectives(problem)

    # Add scalarization functions, if any
    if problem.scalarization_funcs is not None:
        self.scalarizations = self.init_scalarizations(problem)

    # Add constraints, if any
    if problem.constraints is not None:
        self.constraints = self.init_constraints(problem)

    self.problem = problem
add_constraint
add_constraint(constraint: Constraint) -> gp.Constr

Add a constraint expression to a GurobipyModel.

If adding a lot of constraints, this function may end up being very slow compared to adding the constraints to the stored model directly, because of the model.update() calls.

Parameters:

Name Type Description Default
constraint Constraint

the constraint function expression.

required

Raises:

Type Description
GurobipyEvaluatorError

when an unsupported constraint type is encountered.

Returns:

Type Description
Constr

gurobipy.Constr: The gurobipy constraint that was added.

Source code in desdeo/problem/gurobipy_evaluator.py
def add_constraint(self, constraint: Constraint) -> gp.Constr:
    """Add a constraint expression to a GurobipyModel.

    If adding a lot of constraints, this function may end up being very slow compared
    to adding the constraints to the stored model directly, because of the model.update() calls.

    Args:
        constraint (Constraint): the constraint function expression.

    Raises:
        GurobipyEvaluatorError: when an unsupported constraint type is encountered.

    Returns:
        gurobipy.Constr: The gurobipy constraint that was added.
    """
    gp_expr = self.parse(constraint.func, self.get_expression_by_name)

    match con_type := constraint.cons_type:
        case ConstraintTypeEnum.LTE:
            # constraints in DESDEO are defined such that they must be less than zero
            gp_expr = _le(gp_expr, 0)
        case ConstraintTypeEnum.EQ:
            gp_expr = _eq(gp_expr, 0)
        case _:
            msg = f"Constraint type of {con_type} not supported. Must be one of {ConstraintTypeEnum}."
            raise GurobipyEvaluatorError(msg)

    return_cons = self.model.addConstr(gp_expr, name=constraint.symbol)
    self.model.update()
    self.constraints[constraint.symbol] = return_cons
    return return_cons
add_objective
add_objective(obj: Objective)

Adds an objective function expression to a GurobipyModel.

Does not yet add any actual gurobipy optimization objectives, only adds them to the dict containing the expressions of the objectives. The objective expressions are stored in the GurobipyModel and the evaluator must add the appropiate gurobipy objective before solving.

Parameters:

Name Type Description Default
obj Objective

the objective function expression to be added.

required
Source code in desdeo/problem/gurobipy_evaluator.py
def add_objective(self, obj: Objective):
    """Adds an objective function expression to a GurobipyModel.

    Does not yet add any actual gurobipy optimization objectives, only adds them to the dict
    containing the expressions of the objectives. The objective expressions are stored in the
    GurobipyModel and the evaluator must add the appropiate gurobipy objective before solving.

    Args:
        obj (Objective): the objective function expression to be added.
    """
    gp_expr = self.parse(obj.func, self.get_expression_by_name)
    if isinstance(gp_expr, int | float):
        warnings.warn(
            "One or more of the problem objectives seems to be a constant.", GurobipyEvaluatorWarning, stacklevel=2
        )
    if isinstance(gp_expr, gp.GenExpr):
        msg = f"Gurobi does not support objective functions that are not linear or quadratic {gp_expr}"
        raise GurobipyEvaluatorError(msg)

    self.objective_functions[obj.symbol] = gp_expr

    # the obj.symbol_min objectives are used when optimizing and building scalarizations etc...
    self.objective_functions[f"{obj.symbol}_min"] = -gp_expr if obj.maximize else gp_expr
add_scalarization_function
add_scalarization_function(scal: ScalarizationFunction)

Adds a scalrization expression to a gurobipy model.

Scalarizations work identically to objectives, except they are stored in a different dict in the GurobipyModel. If you want to solve the problem using a scalarization, the evaluator needs to set it as an optimization target first.

Parameters:

Name Type Description Default
scal ScalarizationFunction

The scalarization function to be added.

required
Source code in desdeo/problem/gurobipy_evaluator.py
def add_scalarization_function(self, scal: ScalarizationFunction):
    """Adds a scalrization expression to a gurobipy model.

    Scalarizations work identically to objectives, except they are stored in a different
    dict in the GurobipyModel. If you want to solve the problem using a scalarization, the
    evaluator needs to set it as an optimization target first.

    Args:
        scal (ScalarizationFunction): The scalarization function to be added.
    """
    self.scalarizations[scal.symbol] = self.parse(scal.func, self.get_expression_by_name)
add_variable
add_variable(
    var: Variable | TensorVariable,
) -> gp.Var | gp.MVar

Add variables to the GurobipyModel.

If adding a lot of variables, this function may end up being very slow compared to adding the variables to the stored model directly, because of the model.update() calls.

Parameters:

Name Type Description Default
var Variable

The definition of the variable to be added.

required

Raises:

Type Description
GurobipyEvaluatorError

when a problem in extracting the variables is encountered. I.e., the variables are of a non supported type.

Returns:

Type Description
Var | MVar

gp.Var: the variable that was added to the model.

Source code in desdeo/problem/gurobipy_evaluator.py
def add_variable(self, var: Variable | TensorVariable) -> gp.Var | gp.MVar:
    """Add variables to the GurobipyModel.

    If adding a lot of variables, this function may end up being very slow compared
    to adding the variables to the stored model directly, because of the model.update() calls.

    Args:
        var (Variable): The definition of the variable to be added.

    Raises:
        GurobipyEvaluatorError: when a problem in extracting the variables is encountered.
            I.e., the variables are of a non supported type.

    Returns:
        gp.Var: the variable that was added to the model.
    """
    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 var.variable_type:
            case VariableTypeEnum.integer:
                # variable is integer
                domain = gp.GRB.INTEGER
            case VariableTypeEnum.real:
                # variable is real
                domain = gp.GRB.CONTINUOUS
            case VariableTypeEnum.binary:
                domain = gp.GRB.BINARY
            case _:
                msg = f"Could not figure out the type for variable {var}."
                raise GurobipyEvaluatorError(msg)

        # add the variable to the model
        gvar = self.model.addVar(lb=lowerbound, ub=upperbound, vtype=domain, name=var.symbol)
        # set the initial value, if one has been defined
        if var.initial_value is not None:
            gvar.setAttr("Start", var.initial_value)
    elif isinstance(var, TensorVariable):
        # handle tensor variables, i.e., vectors etc..
        lowerbounds = (
            var.get_lowerbound_values()
            if var.lowerbounds is not None
            else np.full(var.shape, float("-inf")).tolist()
        )
        upperbounds = (
            var.get_upperbound_values()
            if var.upperbounds is not None
            else np.full(var.shape, float("inf")).tolist()
        )

        # figure out the variable type
        match var.variable_type:
            case VariableTypeEnum.integer:
                # variable is integer
                domain = gp.GRB.INTEGER
            case VariableTypeEnum.real:
                # variable is real
                domain = gp.GRB.CONTINUOUS
            case VariableTypeEnum.binary:
                domain = gp.GRB.BINARY
            case _:
                msg = f"Could not figure out the type for variable {var}."
                raise GurobipyEvaluatorError(msg)

        # add the variable to the model
        gvar = self.model.addMVar(
            shape=tuple(var.shape),
            lb=np.array(lowerbounds),
            ub=np.array(upperbounds),
            vtype=domain,
            name=var.symbol,
        )
        # set the initial value, if one has been defined
        if var.initial_values is not None:
            gvar.setAttr("Start", np.array(var.get_initial_values()))

    self.model.update()
    self.variables[var.symbol] = gvar
    return gvar
get_expression_by_name
get_expression_by_name(
    name: str,
) -> (
    gp.Var
    | gp.MVar
    | gp.LinExpr
    | gp.QuadExpr
    | gp.MLinExpr
    | gp.MQuadExpr
    | gp.GenExpr
    | int
    | float
    | np.ndarray
)

Returns a gurobipy expression corresponding to the name.

Only looks for variables, objective functions, scalarizations, extra functions, and constants. This will not find constraints.

Parameters:

Name Type Description Default
name str

The symbol of the expression.

required

Returns:

Type Description
Var | MVar | LinExpr | QuadExpr | MLinExpr | MQuadExpr | GenExpr | int | float | ndarray

gurobipy expression: A mathematical expression that gp.Model can use either as a constraint or an objective

Source code in desdeo/problem/gurobipy_evaluator.py
def get_expression_by_name(
    self, name: str
) -> (
    gp.Var | gp.MVar | gp.LinExpr | gp.QuadExpr | gp.MLinExpr | gp.MQuadExpr | gp.GenExpr | int | float | np.ndarray
):
    """Returns a gurobipy expression corresponding to the name.

    Only looks for variables, objective functions, scalarizations, extra functions, and constants.
    This will not find constraints.

    Args:
        name (str): The symbol of the expression.

    Returns:
        gurobipy expression: A mathematical expression that gp.Model can use either as a constraint or an objective
    """
    if name in self.variables:
        expression = self.variables[name]
    elif name in self.objective_functions:
        expression = self.objective_functions[name]
    elif name in self.scalarizations:
        expression = self.scalarizations[name]
    elif name in self.extra_functions:
        expression = self.extra_functions[name]
    elif name in self.constants:
        if isinstance(self.constants[name], list):
            expression = np.array(self.constants[name])
        else:
            expression = self.constants[name]
    else:
        msg = f"No expression with name {name} found in the gurobipy model."
        raise GurobipyEvaluatorError(msg)
    return expression
get_values
get_values() -> dict[
    str, float | int | bool | list[float] | list[int]
]

Get the values from the Gurobipy Model in a 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 | list[float] | list[int]]

dict[str, float | int | bool]: a dict with keys equivalent to the symbols defined in self.problem.

Source code in desdeo/problem/gurobipy_evaluator.py
def get_values(self) -> dict[str, float | int | bool | list[float] | list[int]]:
    """Get the values from the Gurobipy Model in a 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:
        result_dict[var.symbol] = self.variables[var.symbol].getAttr(gp.GRB.Attr.X)
    for obj in self.problem.objectives:
        result_dict[obj.symbol] = self.objective_functions[obj.symbol].getValue()

    if self.problem.constants is not None:
        for con in self.problem.constants:
            result_dict[con.symbol] = self.constants[con.symbol]

    if self.problem.extra_funcs is not None:
        for extra in self.problem.extra_funcs:
            result_dict[extra.symbol] = self.extra_functions[extra.symbol].getValue()

    if self.problem.constraints is not None:
        for const in self.problem.constraints:
            con = self.constraints[const.symbol]
            if isinstance(con, gp.MQConstr):
                slack = con.QCSlack
                result_dict[const.symbol] = (-np.array(slack)).tolist() if hasattr(slack, "__len__") else -slack
            elif isinstance(con, gp.MConstr):
                result_dict[const.symbol] = (-con.Slack).tolist()
            elif isinstance(con, gp.QConstr):
                result_dict[const.symbol] = -con.getAttr("QCSlack")
            else:
                result_dict[const.symbol] = -con.getAttr("Slack")

    if self.problem.scalarization_funcs is not None:
        for scal in self.problem.scalarization_funcs:
            result_dict[scal.symbol] = self.scalarizations[scal.symbol].getValue()

    return result_dict
init_constants
init_constants(
    problem: Problem,
) -> dict[str, int | float | list[int] | list[float]]

Add constants to a GurobipyEvaluator.

Gurobi does not really have constants, so this function instead stores them in a dict, that is then stored in the evaluator. This is necessary to get the MathParser to understand the constants used in the problem, but updating them at a later point will not update any expression referencing them. The expressions that have been defined using these constants will keep using the numeric value of the constant at the time when the expression was created. Thus, it might be best to avoid using constants if you are intending to use the gurobipy solver.

Parameters:

Name Type Description Default
problem Problem

problem from which to extract the constants.

required

Raises:

Type Description
GurobipyEvaluatorError

when the domain of a constant cannot be figured out.

Returns:

Type Description
dict[str, int | float | list[int] | list[float]]

dict[str, int | float]: a dict containing the constants.

Source code in desdeo/problem/gurobipy_evaluator.py
def init_constants(self, problem: Problem) -> dict[str, int | float | list[int] | list[float]]:
    """Add constants to a GurobipyEvaluator.

    Gurobi does not really have constants, so this function instead
    stores them in a dict, that is then stored in the evaluator.
    This is necessary to get the MathParser to understand the constants
    used in the problem, but updating them at a later point will not update
    any expression referencing them. The expressions that have been defined
    using these constants will keep using the numeric value of the constant
    at the time when the expression was created.
    Thus, it might be best to avoid using constants if you are intending
    to use the gurobipy solver.

    Args:
        problem (Problem): problem from which to extract the constants.

    Raises:
        GurobipyEvaluatorError: when the domain of a constant cannot be figured out.

    Returns:
        dict[str, int | float]: a dict containing the constants.
    """
    constants: dict[str, int | float | list[int] | list[float]] = {}
    for con in problem.constants:
        if isinstance(con, Constant):
            constants[con.symbol] = con.value
        elif isinstance(con, TensorConstant):
            constants[con.symbol] = con.get_values()

    return constants
init_constraints
init_constraints(
    problem: Problem, model: Model | None = None
) -> gp.Model

Add constraint expressions to a Gurobipy Model.

Parameters:

Name Type Description Default
problem Problem

the problem from which to extract the constraint function expressions.

required
model GurobipyModel | None

the GurobipyModel to add the expressions to.

None

Raises:

Type Description
GurobipyEvaluatorError

when an unsupported constraint type is encountered.

Returns:

Name Type Description
GurobipyModel Model

the GurobipyModel with the constraint expressions added.

Source code in desdeo/problem/gurobipy_evaluator.py
def init_constraints(self, problem: Problem, model: gp.Model | None = None) -> gp.Model:
    """Add constraint expressions to a Gurobipy Model.

    Args:
        problem (Problem): the problem from which to extract the constraint function expressions.
        model (GurobipyModel | None): the GurobipyModel to add the expressions to.

    Raises:
        GurobipyEvaluatorError: when an unsupported constraint type is encountered.

    Returns:
        GurobipyModel: the GurobipyModel with the constraint expressions added.
    """
    constraints = {}
    for cons in problem.constraints:
        gp_expr = self.parse(cons.func, callback=self.get_expression_by_name)

        match con_type := cons.cons_type:
            case ConstraintTypeEnum.LTE:
                # constraints in DESDEO are defined such that they must be less than zero
                gp_expr = _le(gp_expr, 0)
            case ConstraintTypeEnum.EQ:
                gp_expr = _eq(gp_expr, 0)
            case _:
                msg = f"Constraint type of {con_type} not supported. Must be one of {ConstraintTypeEnum}."
                raise GurobipyEvaluatorError(msg)

        constraints[cons.symbol] = self.model.addConstr(gp_expr, name=cons.symbol)

    self.model.update()
    return constraints
init_extras
init_extras(
    problem: Problem,
) -> dict[
    str,
    gp.Var
    | gp.MVar
    | gp.LinExpr
    | gp.QuadExpr
    | gp.MLinExpr
    | gp.MQuadExpr
    | gp.GenExpr
    | int
    | float,
]

Add extra function expressions to a Gurobipy Model.

Gurobi does not support extra expressions natively, so this function instead stores them in a dict to be used by the evaluator. This is necessary to get the MathParser to understand the extra expressions used in the problem, but updating them at a later point will not update any expression referencing them. The expressions that have been defined using these extra expressions will keep using the value of the extra expression at the time when the expression was created. Thus, it might be best to avoid using extra expressions if you are intending to use the gurobipy solver.

Parameters:

Name Type Description Default
problem Problem

problem from which the extract the extra function expressions.

required

Returns:

Name Type Description
GurobipyModel dict[str, Var | MVar | LinExpr | QuadExpr | MLinExpr | MQuadExpr | GenExpr | int | float]

the GurobipyModel with the expressions added as attributes.

Source code in desdeo/problem/gurobipy_evaluator.py
def init_extras(
    self, problem: Problem
) -> dict[str, gp.Var | gp.MVar | gp.LinExpr | gp.QuadExpr | gp.MLinExpr | gp.MQuadExpr | gp.GenExpr | int | float]:
    """Add extra function expressions to a Gurobipy Model.

    Gurobi does not support extra expressions natively, so this function instead
    stores them in a dict to be used by the evaluator.
    This is necessary to get the MathParser to understand the extra expressions
    used in the problem, but updating them at a later point will not update
    any expression referencing them. The expressions that have been defined
    using these extra expressions will keep using the value of the extra expression
    at the time when the expression was created.
    Thus, it might be best to avoid using extra expressions if you are intending
    to use the gurobipy solver.

    Args:
        problem (Problem): problem from which the extract the extra function expressions.

    Returns:
        GurobipyModel: the GurobipyModel with the expressions added as attributes.
    """
    extra_functions: dict[
        str, gp.Var | gp.MVar | gp.LinExpr | gp.QuadExpr | gp.MLinExpr | gp.MQuadExpr | gp.GenExpr | int | float
    ] = {}

    for extra in problem.extra_funcs:
        extra_functions[extra.symbol] = self.parse(extra.func, callback=self.get_expression_by_name)

    return extra_functions
init_objectives
init_objectives(
    problem: Problem,
) -> dict[
    str,
    gp.Var
    | gp.MVar
    | gp.LinExpr
    | gp.QuadExpr
    | gp.MLinExpr
    | gp.MQuadExpr,
]

Add objective function expressions to a Gurobipy Model.

Does not yet add any actual gurobipy optimization objectives, only creates a dict containing the expressions of the objectives. The objective expressions are stored in the evaluator and appropiate gurobipy objective must be added to the model before solving.

Parameters:

Name Type Description Default
problem Problem

problem from which to extract the objective function expresions.

required

Returns:

Name Type Description
dict dict[str, Var | MVar | LinExpr | QuadExpr | MLinExpr | MQuadExpr]

dict containing the objective functions.

Source code in desdeo/problem/gurobipy_evaluator.py
def init_objectives(
    self, problem: Problem
) -> dict[str, gp.Var | gp.MVar | gp.LinExpr | gp.QuadExpr | gp.MLinExpr | gp.MQuadExpr]:
    """Add objective function expressions to a Gurobipy Model.

    Does not yet add any actual gurobipy optimization objectives, only creates a dict containing the
    expressions of the objectives. The objective expressions are stored in the
    evaluator and appropiate gurobipy objective must be added to the model before solving.

    Args:
        problem (Problem): problem from which to extract the objective function expresions.

    Returns:
        dict: dict containing the objective functions.
    """
    objective_functions: dict[str, gp.Var | gp.MVar | gp.LinExpr | gp.QuadExpr | gp.MLinExpr | gp.MQuadExpr] = {}
    for obj in problem.objectives:
        gp_expr = self.parse(obj.func, callback=self.get_expression_by_name)
        if isinstance(gp_expr, int | float):
            warnings.warn(
                "One or more of the problem objectives seems to be a constant.",
                GurobipyEvaluatorWarning,
                stacklevel=2,
            )
        if isinstance(gp_expr, gp.GenExpr):
            msg = f"Gurobi does not support objective functions that are not linear or quadratic {gp_expr}"
            raise GurobipyEvaluatorError(msg)

        objective_functions[obj.symbol] = gp_expr

        # the obj.symbol_min objectives are used when optimizing and building scalarizations etc...
        objective_functions[f"{obj.symbol}_min"] = -(gp_expr) if obj.maximize else gp_expr

    return objective_functions
init_scalarizations
init_scalarizations(
    problem: Problem,
) -> dict[
    str,
    gp.Var
    | gp.MVar
    | gp.LinExpr
    | gp.QuadExpr
    | gp.MLinExpr
    | gp.MQuadExpr,
]

Add scalrization expressions to a gurobipy model.

Scalarizations work identically to objectives, except they are stored in a different dict in the GurobipyModel. If you want to solve the problem using a scalarization, the evaluator needs to set it as an optimization target first.

Parameters:

Name Type Description Default
problem Problem

the problem from which to extract the scalarization function expressions.

required

Returns:

Name Type Description
dict dict[str, Var | MVar | LinExpr | QuadExpr | MLinExpr | MQuadExpr]

the dict with the scalarization expressions. Scalarization functions are always minimized.

Source code in desdeo/problem/gurobipy_evaluator.py
def init_scalarizations(
    self, problem: Problem
) -> dict[str, gp.Var | gp.MVar | gp.LinExpr | gp.QuadExpr | gp.MLinExpr | gp.MQuadExpr]:
    """Add scalrization expressions to a gurobipy model.

    Scalarizations work identically to objectives, except they are stored in a different
    dict in the GurobipyModel. If you want to solve the problem using a scalarization, the
    evaluator needs to set it as an optimization target first.

    Args:
        problem (Problem): the problem from which to extract the scalarization function expressions.

    Returns:
        dict: the dict with the scalarization expressions. Scalarization functions are always minimized.
    """
    scalarizations: dict[str, gp.Var | gp.MVar | gp.LinExpr | gp.QuadExpr | gp.MLinExpr | gp.MQuadExpr] = {}

    for scal in problem.scalarization_funcs:
        scalarizations[scal.symbol] = self.parse(scal.func, self.get_expression_by_name)

    return scalarizations
init_variables
init_variables(
    problem: Problem,
) -> dict[str, gp.Var | gp.MVar]

Add variables to the GurobipyModel.

Parameters:

Name Type Description Default
problem Problem

problem from which to extract the variables.

required

Raises:

Type Description
GurobipyEvaluatorError

when a problem in extracting the variables is encountered. I.e., the variables are of a non supported type.

Returns:

Type Description
dict[str, Var | MVar]

dict[str, gp.Var | gp.MVar]: the variables added to the model.

Source code in desdeo/problem/gurobipy_evaluator.py
def init_variables(self, problem: Problem) -> dict[str, gp.Var | gp.MVar]:
    """Add variables to the GurobipyModel.

    Args:
        problem (Problem): problem from which to extract the variables.

    Raises:
        GurobipyEvaluatorError: when a problem in extracting the variables is encountered.
            I.e., the variables are of a non supported type.

    Returns:
        dict[str, gp.Var | gp.MVar]: the variables added to the model.
    """
    variables = {}
    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 var.variable_type:
                case VariableTypeEnum.integer:
                    # variable is integer
                    domain = gp.GRB.INTEGER
                case VariableTypeEnum.real:
                    # variable is real
                    domain = gp.GRB.CONTINUOUS
                case VariableTypeEnum.binary:
                    domain = gp.GRB.BINARY
                case _:
                    msg = f"Could not figure out the type for variable {var}."
                    raise GurobipyEvaluatorError(msg)

            # add the variable to the model
            gvar = self.model.addVar(lb=lowerbound, ub=upperbound, vtype=domain, name=var.symbol)
            # set the initial value, if one has been defined
            if var.initial_value is not None:
                gvar.setAttr("Start", var.initial_value)
            variables[var.symbol] = gvar

        elif isinstance(var, TensorVariable):
            # handle tensor variables, i.e., vectors etc..
            lowerbounds = (
                var.get_lowerbound_values()
                if var.lowerbounds is not None
                else np.full(var.shape, float("-inf")).tolist()
            )
            upperbounds = (
                var.get_upperbound_values()
                if var.upperbounds is not None
                else np.full(var.shape, float("inf")).tolist()
            )

            # figure out the variable type
            match var.variable_type:
                case VariableTypeEnum.integer:
                    # variable is integer
                    domain = gp.GRB.INTEGER
                case VariableTypeEnum.real:
                    # variable is real
                    domain = gp.GRB.CONTINUOUS
                case VariableTypeEnum.binary:
                    domain = gp.GRB.BINARY
                case _:
                    msg = f"Could not figure out the type for variable {var}."
                    raise GurobipyEvaluatorError(msg)

            # add the variable to the model
            gvar = self.model.addMVar(
                shape=tuple(var.shape),
                lb=np.array(lowerbounds),
                ub=np.array(upperbounds),
                vtype=domain,
                name=var.symbol,
            )
            # set the initial value, if one has been defined
            if var.initial_values is not None:
                gvar.setAttr("Start", np.array(var.get_initial_values()))
            variables[var.symbol] = gvar

    # update the model before returning, so that other expressions can reference the variables
    self.model.update()

    return variables
remove_constraint
remove_constraint(symbol: str)

Removes a constraint from the model.

If removing a lot of constraints or dealing with a very large model this function may be slow because of the model.update() calls. Accessing the stored model directly may be faster.

Parameters:

Name Type Description Default
symbol str

a str representing the symbol of the constraint to be removed.

required
Source code in desdeo/problem/gurobipy_evaluator.py
def remove_constraint(self, symbol: str):
    """Removes a constraint from the model.

    If removing a lot of constraints or dealing with a very large model this function
    may be slow because of the model.update() calls. Accessing the stored model directly
    may be faster.

    Args:
        symbol (str): a str representing the symbol of the constraint to be removed.
    """
    self.model.remove(self.constraints[symbol])
    self.constraints.pop(symbol)
    self.model.update()
remove_variable
remove_variable(symbol: str)

Removes a variable from the model.

If removing a lot of variables or dealing with a very large model this function may be slow because of the model.update() calls. Accessing the stored model directly may be faster.

Parameters:

Name Type Description Default
symbol str

a str representing the symbol of the variable to be removed.

required
Source code in desdeo/problem/gurobipy_evaluator.py
def remove_variable(self, symbol: str):
    """Removes a variable from the model.

    If removing a lot of variables or dealing with a very large model this function
    may be slow because of the model.update() calls. Accessing the stored model directly
    may be faster.

    Args:
        symbol (str): a str representing the symbol of the variable to be removed.
    """
    self.model.remove(self.variables[symbol])
    self.variables.pop(symbol)
    self.model.update()
set_optimization_target
set_optimization_target(
    target: str, maximize: bool = False
)

Sets a minimization objective to match the target objective or scalarization of the gurobipy model.

Parameters:

Name Type Description Default
target str

an str representing a symbol. Needs to match an objective function or scalarization function already found in the model.

required
maximize bool

If true, the target function is maximized instead of minimized

False

Raises:

Type Description
GurobipyEvaluatorError

the given target was not an attribute of the gurobipy model.

Source code in desdeo/problem/gurobipy_evaluator.py
def set_optimization_target(self, target: str, maximize: bool = False):
    """Sets a minimization objective to match the target objective or scalarization of the gurobipy model.

    Args:
        target (str): an str representing a symbol. Needs to match an objective function or scalarization
            function already found in the model.
        maximize (bool): If true, the target function is maximized instead of minimized

    Raises:
        GurobipyEvaluatorError: the given target was not an attribute of the gurobipy model.
    """
    if target in self.objective_functions:
        objective = self.problem.get_objective(symbol=target, copy=False)
        maximize = False if objective is None else bool(objective.maximize)
    elif target in self.scalarizations:
        maximize = False
    else:
        msg = f"The gurobipy model has no objective or scalarization named {target}."
        raise GurobipyEvaluatorError(msg)

    obj_expr = self.get_expression_by_name(target)

    if maximize:
        self.model.setObjective(obj_expr, sense=gp.GRB.MAXIMIZE)
    else:
        self.model.setObjective(obj_expr)

GurobipyEvaluatorError

Bases: Exception

Raised when an error within the GurobipyEvaluator class is encountered.

Source code in desdeo/problem/gurobipy_evaluator.py
class GurobipyEvaluatorError(Exception):
    """Raised when an error within the GurobipyEvaluator class is encountered."""

GurobipyEvaluatorWarning

Bases: UserWarning

Raised when the problem contains features that are poorly supported in gurobipy.

Source code in desdeo/problem/gurobipy_evaluator.py
class GurobipyEvaluatorWarning(UserWarning):
    """Raised when the problem contains features that are poorly supported in gurobipy."""

CVXPY evaluator

desdeo.problem.cvxpy_evaluator

Defines an evaluator compatible with the Problem JSON format and transforms it into a CVXPY problem.

CVXPYEvaluator

Defines an evaluator that transforms an instance of Problem into a CVXPY problem.

Source code in desdeo/problem/cvxpy_evaluator.py
 31
 32
 33
 34
 35
 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
class CVXPYEvaluator:
    """Defines an evaluator that transforms an instance of Problem into a CVXPY problem."""

    variables: dict[str, cp.Variable]
    parameters: dict[str, cp.Parameter]
    constraints: dict[str, cp.Constraint]
    constraint_expressions: dict[str, cp.Expression]
    objective_functions: dict[str, cp.Expression]
    scalarizations: dict[str, cp.Expression]
    extra_functions: dict[str, cp.Expression]
    constants: dict[str, int | float | list[int] | list[float]]

    problem_model: cp.Problem
    objective_expr: cp.Expression | None

    def __init__(self, problem: Problem):
        """Initialized the evaluator.

        Args:
            problem (Problem): the problem to be transformed into a CVXPY problem.
        """
        self.objective_functions = {}
        self.scalarizations = {}
        self.extra_functions = {}
        self.constants = {}
        self.variables = {}
        self.parameters = {}
        self.constraints = {}
        self.constraint_expressions = {}
        self.objective_expr = None
        self.problem_model = None

        # set the parser
        self.parse = MathParser(to_format=FormatEnum.cvxpy).parse

        # Add variables
        self.variables = self.init_variables(problem)

        # Add constants as parameters, if any
        if problem.constants is not None:
            self.parameters, self.constants = self.init_parameters(problem)

        # Add extra expressions, if any
        if problem.extra_funcs is not None:
            self.extra_functions = self.init_extras(problem)

        # Add objective function expressions
        self.objective_functions = self.init_objectives(problem)

        # Add scalarization functions, if any
        if problem.scalarization_funcs is not None:
            self.scalarizations = self.init_scalarizations(problem)

        # Add constraints, if any
        if problem.constraints is not None:
            self.constraints = self.init_constraints(problem)

        self.problem = problem

    def init_variables(self, problem: Problem) -> dict[str, cp.Variable]:
        """Add variables to the CVXPY problem.

        Args:
            problem (Problem): problem from which to extract the variables.

        Raises:
            CVXPYEvaluatorError: when a problem in extracting the variables is encountered.
                I.e., the variables are of a non supported type.

        Returns:
            dict[str, cp.Variable]: the variables for the problem.
        """
        for var in problem.variables:
            self.add_variable(var)
        return self.variables

    def init_parameters(
        self, problem: Problem
    ) -> tuple[dict[str, cp.Parameter], dict[str, int | float | list[int] | list[float]]]:
        """Add constants as CVXPY parameters.

        CVXPY uses parameters for values that can be changed without redefining the problem.
        This allows constants to be updated between solves.

        Args:
            problem (Problem): problem from which to extract the constants.

        Raises:
            CVXPYEvaluatorError: when the domain of a constant cannot be figured out.

        Returns:
            tuple: a tuple containing (parameters dict, constants dict).
        """
        parameters: dict[str, cp.Parameter] = {}
        constants: dict[str, int | float | list[int] | list[float]] = {}

        for con in problem.constants:
            if isinstance(con, Constant):
                param = cp.Parameter(name=con.symbol, nonneg=con.value >= 0)
                param.value = con.value
                parameters[con.symbol] = param
                constants[con.symbol] = con.value
            elif isinstance(con, TensorConstant):
                values = con.get_values()
                param = cp.Parameter(shape=np.array(values).shape, name=con.symbol)
                param.value = np.array(values)
                parameters[con.symbol] = param
                constants[con.symbol] = values

        return parameters, constants

    def init_extras(self, problem: Problem) -> dict[str, cp.Expression]:
        """Add extra function expressions to a CVXPY evaluator.

        Args:
            problem (Problem): problem from which the extract the extra function expressions.

        Returns:
            dict[str, cp.Expression]: a dict containing the extra function expressions.
        """
        extra_functions: dict[str, cp.Expression] = {}

        for extra in problem.extra_funcs:
            extra_functions[extra.symbol] = self.parse(extra.func, callback=self.get_expression_by_name)

        return extra_functions

    def init_objectives(self, problem: Problem) -> dict[str, cp.Expression]:
        """Add objective function expressions to a CVXPY evaluator.

        Does not yet add any actual CVXPY optimization objectives, only creates a dict containing the
        expressions of the objectives.

        Args:
            problem (Problem): problem from which to extract the objective function expresions.

        Returns:
            dict[str, cp.Expression]: dict containing the objective functions.
        """
        for obj in problem.objectives:
            self.add_objective(obj)
        return self.objective_functions

    def init_constraints(self, problem: Problem) -> dict[str, cp.Constraint]:
        """Add constraint expressions to a CVXPY problem.

        Args:
            problem (Problem): the problem from which to extract the constraint function expressions.

        Raises:
            CVXPYEvaluatorError: when an unsupported constraint type is encountered.

        Returns:
            dict[str, cp.Constraint]: dict of constraints keyed by symbol.
        """
        for cons in problem.constraints:
            self.add_constraint(cons)
        return self.constraints

    def init_scalarizations(self, problem: Problem) -> dict[str, cp.Expression]:
        """Add scalarization expressions to a CVXPY evaluator.

        Scalarizations work identically to objectives, except they are stored in a different
        dict in the CVXPYEvaluator.

        Args:
            problem (Problem): the problem from which to extract the scalarization function expressions.

        Returns:
            dict[str, cp.Expression]: the dict with the scalarization expressions.
        """
        scalarizations: dict[str, cp.Expression] = {}

        for scal in problem.scalarization_funcs:
            scalarizations[scal.symbol] = self.parse(scal.func, self.get_expression_by_name)

        return scalarizations

    def add_constraint(self, constraint: Constraint) -> cp.Constraint:
        """Add a constraint expression to the CVXPY problem.

        Args:
            constraint (Constraint): the constraint function expression.

        Raises:
            CVXPYEvaluatorError: when an unsupported constraint type is encountered.

        Returns:
            cp.Constraint: The CVXPY constraint that was added.
        """
        expr = self.parse(constraint.func, self.get_expression_by_name)
        self.constraint_expressions[constraint.symbol] = expr

        match constraint.cons_type:
            case ConstraintTypeEnum.LTE:
                cvxpy_constraint = expr <= 0
            case ConstraintTypeEnum.EQ:
                cvxpy_constraint = expr == 0
            case _:
                msg = f"Constraint type of {constraint.cons_type} not supported. Must be one of {ConstraintTypeEnum}."
                raise CVXPYEvaluatorError(msg)

        self.constraints[constraint.symbol] = cvxpy_constraint
        return cvxpy_constraint

    def add_objective(self, obj: Objective):
        """Adds an objective function expression to the CVXPY evaluator.

        Does not yet add any actual CVXPY optimization objectives, only adds them to the dict
        containing the expressions of the objectives.

        Args:
            obj (Objective): the objective function expression to be added.
        """
        expr = self.parse(obj.func, self.get_expression_by_name)
        if isinstance(expr, (int, float)):
            warnings.warn(
                "One or more of the problem objectives seems to be a constant.",
                CVXPYEvaluatorWarning,
                stacklevel=2,
            )

        self.objective_functions[obj.symbol] = expr

        # the obj.symbol_min objectives are used when optimizing and building scalarizations etc...
        self.objective_functions[f"{obj.symbol}_min"] = -(expr) if obj.maximize else expr

    def add_scalarization_function(self, scal: ScalarizationFunction):
        """Adds a scalarization expression to the CVXPY evaluator.

        Scalarizations work identically to objectives, except they are stored in a different
        dict in the CVXPYEvaluator.

        Args:
            scal (ScalarizationFunction): The scalarization function to be added.
        """
        self.scalarizations[scal.symbol] = self.parse(scal.func, self.get_expression_by_name)

    def add_variable(self, var: Variable | TensorVariable) -> cp.Variable:
        """Add a variable to the CVXPY evaluator.

        Args:
            var (Variable | TensorVariable): The definition of the variable to be added.

        Raises:
            CVXPYEvaluatorError: when a problem in extracting the variables is encountered.

        Returns:
            cp.Variable: the variable that was added.
        """
        if isinstance(var, Variable):
            # handle regular variables
            lowerbound = var.lowerbound
            upperbound = var.upperbound

            # Set bounds
            bounds = None
            if lowerbound is not None or upperbound is not None:
                lb = lowerbound if lowerbound is not None else -cp.inf
                ub = upperbound if upperbound is not None else cp.inf
                bounds = [lb, ub]

            # figure out the variable type
            match var.variable_type:
                case VariableTypeEnum.integer:
                    # variable is integer
                    cv_var = cp.Variable(integer=True, bounds=bounds, name=var.symbol)
                case VariableTypeEnum.real:
                    # variable is real
                    cv_var = cp.Variable(bounds=bounds, name=var.symbol)
                case VariableTypeEnum.binary:
                    cv_var = cp.Variable(boolean=True, bounds=bounds, name=var.symbol)
                case _:
                    msg = f"Could not figure out the type for variable {var}."
                    raise CVXPYEvaluatorError(msg)

        elif isinstance(var, TensorVariable):
            # handle tensor variables, i.e., vectors etc..
            lowerbounds = var.get_lowerbound_values() if var.lowerbounds is not None else None
            upperbounds = var.get_upperbound_values() if var.upperbounds is not None else None

            # Set bounds
            bounds = None
            if lowerbounds is not None or upperbounds is not None:
                lb = np.array(lowerbounds) if lowerbounds is not None else -np.inf
                ub = np.array(upperbounds) if upperbounds is not None else np.inf
                bounds = [lb, ub]

            # figure out the variable type
            match var.variable_type:
                case VariableTypeEnum.integer:
                    # variable is integer
                    cv_var = cp.Variable(shape=tuple(var.shape), integer=True, bounds=bounds, name=var.symbol)
                case VariableTypeEnum.real:
                    # variable is real
                    cv_var = cp.Variable(shape=tuple(var.shape), bounds=bounds, name=var.symbol)
                case VariableTypeEnum.binary:
                    cv_var = cp.Variable(shape=tuple(var.shape), boolean=True, bounds=bounds, name=var.symbol)
                case _:
                    msg = f"Could not figure out the type for variable {var}."
                    raise CVXPYEvaluatorError(msg)

        self.variables[var.symbol] = cv_var
        return cv_var

    def get_expression_by_name(self, name: str) -> cp.Expression | np.ndarray | int | float:
        """Returns a CVXPY expression corresponding to the name.

        Looks for variables, parameters, objective functions, scalarizations, and extra functions.

        Args:
            name (str): The symbol of the expression.

        Returns:
            cp.Expression | np.ndarray | int | float: A mathematical expression that CVXPY can use.
        """
        if name in self.variables:
            expression = self.variables[name]
        elif name in self.parameters:
            expression = self.parameters[name]
        elif name in self.objective_functions:
            expression = self.objective_functions[name]
        elif name in self.scalarizations:
            expression = self.scalarizations[name]
        elif name in self.extra_functions:
            expression = self.extra_functions[name]
        elif name in self.constants:
            if isinstance(self.constants[name], list):
                expression = np.array(self.constants[name])
            else:
                expression = self.constants[name]
        else:
            msg = f"No expression with name {name} found in the CVXPY model."
            raise CVXPYEvaluatorError(msg)
        return expression

    def get_values(self) -> dict[str, float | int | bool | list[float] | list[int] | np.ndarray]:
        """Get the values from the CVXPY solution in a dict.

        The keys of the dict will be the symbols defined in the problem utilized to initialize the evaluator.
        Can only be called after the problem has been solved.

        Returns:
            dict: a dict with keys equivalent to the symbols defined in self.problem.

        Raises:
            CVXPYEvaluatorError: if the problem has not been solved yet.
        """
        if self.problem_model is None or self.problem_model.status not in [cp.OPTIMAL, cp.OPTIMAL_INACCURATE]:
            msg = "Problem has not been solved yet or did not achieve optimal status."
            raise CVXPYEvaluatorError(msg)

        result_dict = {}

        for var in self.problem.variables:
            value = self.variables[var.symbol].value
            if value is None:
                msg = f"Variable {var.symbol} has not been solved yet."
                raise CVXPYEvaluatorError(msg)
            result_dict[var.symbol] = value

        for obj in self.problem.objectives:
            result_dict[obj.symbol] = self.objective_functions[obj.symbol].value

        if self.problem.constants is not None:
            for con in self.problem.constants:
                result_dict[con.symbol] = self.constants[con.symbol]

        if self.problem.extra_funcs is not None:
            for extra in self.problem.extra_funcs:
                result_dict[extra.symbol] = self.extra_functions[extra.symbol].value

        if self.problem.scalarization_funcs is not None:
            for scal in self.problem.scalarization_funcs:
                result_dict[scal.symbol] = self.scalarizations[scal.symbol].value

        if self.problem.constraints is not None:
            for con in self.problem.constraints:
                result_dict[con.symbol] = self.constraint_expressions[con.symbol].value

        return result_dict

    def set_optimization_target(self, target: str):
        """Sets an optimization objective for the CVXPY problem.

        Args:
            target (str): a str representing a symbol. Needs to match an objective function or scalarization

        Raises:
            CVXPYEvaluatorError: the given target was not found in the evaluator.
        """
        if target in self.objective_functions:
            objective = self.problem.get_objective(symbol=target, copy=False)
            maximize = False if objective is None else bool(objective.maximize)
        elif target in self.scalarizations:
            maximize = False
        else:
            msg = f"The CVXPY model has no objective or scalarization named {target}."
            raise CVXPYEvaluatorError(msg)

        obj_expr = self.get_expression_by_name(target)

        objective = cp.Maximize(obj_expr) if maximize else cp.Minimize(obj_expr)

        self.objective_expr = objective
        self.problem_model = cp.Problem(objective, list(self.constraints.values()))

    def solve(self, **kwargs: dict[str, any]):
        """Solve the CVXPY problem.

        Args:
            **kwargs (dict[str, any]): additional arguments to pass to cp.Problem.solve().
        """
        if self.problem_model is None:
            msg = "No optimization target has been set. Call set_optimization_target() first."
            raise CVXPYEvaluatorError(msg)

        if self.problem_model.is_dcp():
            self.problem_model.solve(**kwargs)
        elif self.problem_model.is_dgp():
            kwargs["gp"] = True
            self.problem_model.solve(**kwargs)
        else:
            warnings.warn(
                "The problem does not appear to be DCP or DGP. CVXPY may not be able to solve it.",
                CVXPYEvaluatorWarning,
                stacklevel=2,
            )
            self.problem_model.solve(**kwargs)

    def update_parameter(self, symbol: str, value: int | float | list[int] | list[float] | np.ndarray):
        """Update the value of a parameter (constant).

        Args:
            symbol (str): the symbol of the parameter to update.
            value: the new value for the parameter.

        Raises:
            CVXPYEvaluatorError: if the parameter does not exist.
        """
        if symbol not in self.parameters:
            msg = f"Parameter {symbol} not found in the evaluator."
            raise CVXPYEvaluatorError(msg)

        self.parameters[symbol].value = value
        if isinstance(value, (list, np.ndarray)):
            self.constants[symbol] = list(value) if isinstance(value, np.ndarray) else value
        else:
            self.constants[symbol] = value
__init__
__init__(problem: Problem)

Initialized the evaluator.

Parameters:

Name Type Description Default
problem Problem

the problem to be transformed into a CVXPY problem.

required
Source code in desdeo/problem/cvxpy_evaluator.py
def __init__(self, problem: Problem):
    """Initialized the evaluator.

    Args:
        problem (Problem): the problem to be transformed into a CVXPY problem.
    """
    self.objective_functions = {}
    self.scalarizations = {}
    self.extra_functions = {}
    self.constants = {}
    self.variables = {}
    self.parameters = {}
    self.constraints = {}
    self.constraint_expressions = {}
    self.objective_expr = None
    self.problem_model = None

    # set the parser
    self.parse = MathParser(to_format=FormatEnum.cvxpy).parse

    # Add variables
    self.variables = self.init_variables(problem)

    # Add constants as parameters, if any
    if problem.constants is not None:
        self.parameters, self.constants = self.init_parameters(problem)

    # Add extra expressions, if any
    if problem.extra_funcs is not None:
        self.extra_functions = self.init_extras(problem)

    # Add objective function expressions
    self.objective_functions = self.init_objectives(problem)

    # Add scalarization functions, if any
    if problem.scalarization_funcs is not None:
        self.scalarizations = self.init_scalarizations(problem)

    # Add constraints, if any
    if problem.constraints is not None:
        self.constraints = self.init_constraints(problem)

    self.problem = problem
add_constraint
add_constraint(constraint: Constraint) -> cp.Constraint

Add a constraint expression to the CVXPY problem.

Parameters:

Name Type Description Default
constraint Constraint

the constraint function expression.

required

Raises:

Type Description
CVXPYEvaluatorError

when an unsupported constraint type is encountered.

Returns:

Type Description
Constraint

cp.Constraint: The CVXPY constraint that was added.

Source code in desdeo/problem/cvxpy_evaluator.py
def add_constraint(self, constraint: Constraint) -> cp.Constraint:
    """Add a constraint expression to the CVXPY problem.

    Args:
        constraint (Constraint): the constraint function expression.

    Raises:
        CVXPYEvaluatorError: when an unsupported constraint type is encountered.

    Returns:
        cp.Constraint: The CVXPY constraint that was added.
    """
    expr = self.parse(constraint.func, self.get_expression_by_name)
    self.constraint_expressions[constraint.symbol] = expr

    match constraint.cons_type:
        case ConstraintTypeEnum.LTE:
            cvxpy_constraint = expr <= 0
        case ConstraintTypeEnum.EQ:
            cvxpy_constraint = expr == 0
        case _:
            msg = f"Constraint type of {constraint.cons_type} not supported. Must be one of {ConstraintTypeEnum}."
            raise CVXPYEvaluatorError(msg)

    self.constraints[constraint.symbol] = cvxpy_constraint
    return cvxpy_constraint
add_objective
add_objective(obj: Objective)

Adds an objective function expression to the CVXPY evaluator.

Does not yet add any actual CVXPY optimization objectives, only adds them to the dict containing the expressions of the objectives.

Parameters:

Name Type Description Default
obj Objective

the objective function expression to be added.

required
Source code in desdeo/problem/cvxpy_evaluator.py
def add_objective(self, obj: Objective):
    """Adds an objective function expression to the CVXPY evaluator.

    Does not yet add any actual CVXPY optimization objectives, only adds them to the dict
    containing the expressions of the objectives.

    Args:
        obj (Objective): the objective function expression to be added.
    """
    expr = self.parse(obj.func, self.get_expression_by_name)
    if isinstance(expr, (int, float)):
        warnings.warn(
            "One or more of the problem objectives seems to be a constant.",
            CVXPYEvaluatorWarning,
            stacklevel=2,
        )

    self.objective_functions[obj.symbol] = expr

    # the obj.symbol_min objectives are used when optimizing and building scalarizations etc...
    self.objective_functions[f"{obj.symbol}_min"] = -(expr) if obj.maximize else expr
add_scalarization_function
add_scalarization_function(scal: ScalarizationFunction)

Adds a scalarization expression to the CVXPY evaluator.

Scalarizations work identically to objectives, except they are stored in a different dict in the CVXPYEvaluator.

Parameters:

Name Type Description Default
scal ScalarizationFunction

The scalarization function to be added.

required
Source code in desdeo/problem/cvxpy_evaluator.py
def add_scalarization_function(self, scal: ScalarizationFunction):
    """Adds a scalarization expression to the CVXPY evaluator.

    Scalarizations work identically to objectives, except they are stored in a different
    dict in the CVXPYEvaluator.

    Args:
        scal (ScalarizationFunction): The scalarization function to be added.
    """
    self.scalarizations[scal.symbol] = self.parse(scal.func, self.get_expression_by_name)
add_variable
add_variable(var: Variable | TensorVariable) -> cp.Variable

Add a variable to the CVXPY evaluator.

Parameters:

Name Type Description Default
var Variable | TensorVariable

The definition of the variable to be added.

required

Raises:

Type Description
CVXPYEvaluatorError

when a problem in extracting the variables is encountered.

Returns:

Type Description
Variable

cp.Variable: the variable that was added.

Source code in desdeo/problem/cvxpy_evaluator.py
def add_variable(self, var: Variable | TensorVariable) -> cp.Variable:
    """Add a variable to the CVXPY evaluator.

    Args:
        var (Variable | TensorVariable): The definition of the variable to be added.

    Raises:
        CVXPYEvaluatorError: when a problem in extracting the variables is encountered.

    Returns:
        cp.Variable: the variable that was added.
    """
    if isinstance(var, Variable):
        # handle regular variables
        lowerbound = var.lowerbound
        upperbound = var.upperbound

        # Set bounds
        bounds = None
        if lowerbound is not None or upperbound is not None:
            lb = lowerbound if lowerbound is not None else -cp.inf
            ub = upperbound if upperbound is not None else cp.inf
            bounds = [lb, ub]

        # figure out the variable type
        match var.variable_type:
            case VariableTypeEnum.integer:
                # variable is integer
                cv_var = cp.Variable(integer=True, bounds=bounds, name=var.symbol)
            case VariableTypeEnum.real:
                # variable is real
                cv_var = cp.Variable(bounds=bounds, name=var.symbol)
            case VariableTypeEnum.binary:
                cv_var = cp.Variable(boolean=True, bounds=bounds, name=var.symbol)
            case _:
                msg = f"Could not figure out the type for variable {var}."
                raise CVXPYEvaluatorError(msg)

    elif isinstance(var, TensorVariable):
        # handle tensor variables, i.e., vectors etc..
        lowerbounds = var.get_lowerbound_values() if var.lowerbounds is not None else None
        upperbounds = var.get_upperbound_values() if var.upperbounds is not None else None

        # Set bounds
        bounds = None
        if lowerbounds is not None or upperbounds is not None:
            lb = np.array(lowerbounds) if lowerbounds is not None else -np.inf
            ub = np.array(upperbounds) if upperbounds is not None else np.inf
            bounds = [lb, ub]

        # figure out the variable type
        match var.variable_type:
            case VariableTypeEnum.integer:
                # variable is integer
                cv_var = cp.Variable(shape=tuple(var.shape), integer=True, bounds=bounds, name=var.symbol)
            case VariableTypeEnum.real:
                # variable is real
                cv_var = cp.Variable(shape=tuple(var.shape), bounds=bounds, name=var.symbol)
            case VariableTypeEnum.binary:
                cv_var = cp.Variable(shape=tuple(var.shape), boolean=True, bounds=bounds, name=var.symbol)
            case _:
                msg = f"Could not figure out the type for variable {var}."
                raise CVXPYEvaluatorError(msg)

    self.variables[var.symbol] = cv_var
    return cv_var
get_expression_by_name
get_expression_by_name(
    name: str,
) -> cp.Expression | np.ndarray | int | float

Returns a CVXPY expression corresponding to the name.

Looks for variables, parameters, objective functions, scalarizations, and extra functions.

Parameters:

Name Type Description Default
name str

The symbol of the expression.

required

Returns:

Type Description
Expression | ndarray | int | float

cp.Expression | np.ndarray | int | float: A mathematical expression that CVXPY can use.

Source code in desdeo/problem/cvxpy_evaluator.py
def get_expression_by_name(self, name: str) -> cp.Expression | np.ndarray | int | float:
    """Returns a CVXPY expression corresponding to the name.

    Looks for variables, parameters, objective functions, scalarizations, and extra functions.

    Args:
        name (str): The symbol of the expression.

    Returns:
        cp.Expression | np.ndarray | int | float: A mathematical expression that CVXPY can use.
    """
    if name in self.variables:
        expression = self.variables[name]
    elif name in self.parameters:
        expression = self.parameters[name]
    elif name in self.objective_functions:
        expression = self.objective_functions[name]
    elif name in self.scalarizations:
        expression = self.scalarizations[name]
    elif name in self.extra_functions:
        expression = self.extra_functions[name]
    elif name in self.constants:
        if isinstance(self.constants[name], list):
            expression = np.array(self.constants[name])
        else:
            expression = self.constants[name]
    else:
        msg = f"No expression with name {name} found in the CVXPY model."
        raise CVXPYEvaluatorError(msg)
    return expression
get_values
get_values() -> dict[
    str,
    float
    | int
    | bool
    | list[float]
    | list[int]
    | np.ndarray,
]

Get the values from the CVXPY solution in a dict.

The keys of the dict will be the symbols defined in the problem utilized to initialize the evaluator. Can only be called after the problem has been solved.

Returns:

Name Type Description
dict dict[str, float | int | bool | list[float] | list[int] | ndarray]

a dict with keys equivalent to the symbols defined in self.problem.

Raises:

Type Description
CVXPYEvaluatorError

if the problem has not been solved yet.

Source code in desdeo/problem/cvxpy_evaluator.py
def get_values(self) -> dict[str, float | int | bool | list[float] | list[int] | np.ndarray]:
    """Get the values from the CVXPY solution in a dict.

    The keys of the dict will be the symbols defined in the problem utilized to initialize the evaluator.
    Can only be called after the problem has been solved.

    Returns:
        dict: a dict with keys equivalent to the symbols defined in self.problem.

    Raises: