Skip to content

Variable

Bases: BaseOperableBlock

A decision variable for an optimization model.

Parameters:

Name Type Description Default
*indexing_sets SetTypes | Iterable[SetTypes]

If no indexing_sets are provided, a single variable with no dimensions is created. Otherwise, a variable is created for each element in the Cartesian product of the indexing_sets (see Set for details on behaviour).

()
lb Operable | None

The lower bound for all variables.

None
ub Operable | None

The upper bound for all variables.

None
vtype VType | VTypeValue

The type of the variable. Can be either a VType enum or a string. Default is VType.CONTINUOUS.

CONTINUOUS
equals Operable | None

When specified, a variable is created and a constraint is added to make the variable equal to the provided expression.

None

Examples:

>>> import pandas as pd
>>> m = pf.Model()
>>> df = pd.DataFrame(
...     {"dim1": [1, 1, 2, 2, 3, 3], "dim2": ["a", "b", "a", "b", "a", "b"]}
... )
>>> Variable(df)
<Variable 'unnamed' height=6>
┌──────┬──────┐
│ dim1 ┆ dim2 │
│ (3)  ┆ (2)  │
╞══════╪══════╡
│ 1    ┆ a    │
│ 1    ┆ b    │
│ 2    ┆ a    │
│ 2    ┆ b    │
│ 3    ┆ a    │
│ 3    ┆ b    │
└──────┴──────┘

Variables cannot be used until they're added to the model.

>>> m.constraint = Variable(df) <= 3
Traceback (most recent call last):
...
ValueError: Cannot use 'Variable' before it has been added to a model.

Instead, assign the variable to the model first:

>>> m.v = Variable(df)
>>> m.constraint = m.v <= 3
>>> m.v
<Variable 'v' height=6>
┌──────┬──────┬──────────┐
│ dim1 ┆ dim2 ┆ variable │
│ (3)  ┆ (2)  ┆          │
╞══════╪══════╪══════════╡
│ 1    ┆ a    ┆ v[1,a]   │
│ 1    ┆ b    ┆ v[1,b]   │
│ 2    ┆ a    ┆ v[2,a]   │
│ 2    ┆ b    ┆ v[2,b]   │
│ 3    ┆ a    ┆ v[3,a]   │
│ 3    ┆ b    ┆ v[3,b]   │
└──────┴──────┴──────────┘
>>> m.v2 = Variable(df[["dim1"]])
Traceback (most recent call last):
...
ValueError: Duplicate rows found in input data.
>>> m.v3 = Variable(df[["dim1"]].drop_duplicates())
>>> m.v3
<Variable 'v3' height=3>
┌──────┬──────────┐
│ dim1 ┆ variable │
│ (3)  ┆          │
╞══════╪══════════╡
│ 1    ┆ v3[1]    │
│ 2    ┆ v3[2]    │
│ 3    ┆ v3[3]    │
└──────┴──────────┘

Methods:

Name Description
next

Creates an expression where the variable at each label is the next variable in the specified dimension.

to_expr

Converts the Variable to an Expression.

Attributes:

Name Type Description
attr Container

Allows reading and writing variable attributes similarly to Model.attr.

solution

Retrieves a variable's optimal value after the model has been solved.

vtype VType
Source code in pyoframe/_core.py
def __init__(
    self,
    *indexing_sets: SetTypes | Iterable[SetTypes],
    lb: Operable | None = None,
    ub: Operable | None = None,
    vtype: VType | VTypeValue = VType.CONTINUOUS,
    equals: Operable | None = None,
):
    if equals is not None:
        if isinstance(equals, (float, int)):
            if lb is not None:
                raise ValueError("Cannot specify 'lb' when 'equals' is a constant.")
            if ub is not None:
                raise ValueError("Cannot specify 'ub' when 'equals' is a constant.")
            lb = ub = equals
            equals = None
        else:
            assert len(indexing_sets) == 0, (
                "Cannot specify both 'equals' and 'indexing_sets'"
            )
            equals = equals.to_expr()
            indexing_sets = (equals,)

    data = Set(*indexing_sets).data if len(indexing_sets) > 0 else pl.DataFrame()
    super().__init__(data)

    self.vtype: VType = VType(vtype)
    self._attr = Container(self._set_attribute, self._get_attribute)
    self._equals: Expression | None = equals

    if lb is not None and not isinstance(lb, (float, int)):
        self._lb_expr, self.lb = lb, None
    else:
        self._lb_expr, self.lb = None, lb
    if ub is not None and not isinstance(ub, (float, int)):
        self._ub_expr, self.ub = ub, None
    else:
        self._ub_expr, self.ub = None, ub

attr: Container

Allows reading and writing variable attributes similarly to Model.attr.

solution

Retrieves a variable's optimal value after the model has been solved.

Return type is a DataFrame if the variable has dimensions, otherwise it is a single value. Binary and integer variables are returned as integers.

Examples:

>>> m = pf.Model()
>>> m.var_continuous = pf.Variable({"dim1": [1, 2, 3]}, lb=5, ub=5)
>>> m.var_integer = pf.Variable(
...     {"dim1": [1, 2, 3]}, lb=4.5, ub=5.5, vtype=pf.VType.INTEGER
... )
>>> m.var_dimensionless = pf.Variable(
...     lb=4.5, ub=5.5, vtype=pf.VType.INTEGER
... )
>>> m.var_continuous.solution
Traceback (most recent call last):
...
RuntimeError: Failed to retrieve solution for variable. Are you sure the model has been solved?
>>> m.optimize()
>>> m.var_continuous.solution
shape: (3, 2)
┌──────┬──────────┐
│ dim1 ┆ solution │
│ ---  ┆ ---      │
│ i64  ┆ f64      │
╞══════╪══════════╡
│ 1    ┆ 5.0      │
│ 2    ┆ 5.0      │
│ 3    ┆ 5.0      │
└──────┴──────────┘
>>> m.var_integer.solution
shape: (3, 2)
┌──────┬──────────┐
│ dim1 ┆ solution │
│ ---  ┆ ---      │
│ i64  ┆ i64      │
╞══════╪══════════╡
│ 1    ┆ 5        │
│ 2    ┆ 5        │
│ 3    ┆ 5        │
└──────┴──────────┘
>>> m.var_dimensionless.solution
5

vtype: VType = VType(vtype)

next(dim: str, wrap_around: bool = False) -> Expression

Creates an expression where the variable at each label is the next variable in the specified dimension.

Parameters:

Name Type Description Default
dim str

The dimension over which to shift the variable.

required
wrap_around bool

If True, the last label in the dimension is connected to the first label.

False

Examples:

>>> import pandas as pd
>>> time_dim = pd.DataFrame({"time": ["00:00", "06:00", "12:00", "18:00"]})
>>> space_dim = pd.DataFrame({"city": ["Toronto", "Berlin"]})
>>> m = pf.Model()
>>> m.bat_charge = pf.Variable(time_dim, space_dim)
>>> m.bat_flow = pf.Variable(time_dim, space_dim)
>>> # Fails because the dimensions are not the same
>>> m.bat_charge + m.bat_flow == m.bat_charge.next("time")
Traceback (most recent call last):
...
pyoframe._constants.PyoframeError: Cannot subtract the two expressions below because expression 1 has extra labels.
Expression 1:       (bat_charge + bat_flow)
Expression 2:       bat_charge.next(…)
Extra labels in expression 1:
┌───────┬─────────┐
│ time  ┆ city    │
╞═══════╪═════════╡
│ 18:00 ┆ Toronto │
│ 18:00 ┆ Berlin  │
└───────┴─────────┘
Use .drop_extras() or .keep_extras() to indicate how the extra labels should be handled. Learn more at
    https://bravos-power.github.io/pyoframe/latest/learn/concepts/addition
>>> (m.bat_charge + m.bat_flow).drop_extras() == m.bat_charge.next("time")
<Constraint 'unnamed' height=6 terms=18 type=linear>
┌───────┬─────────┬────────────────────────────────────────────────────────────────────────────────┐
│ time  ┆ city    ┆ constraint                                                                     │
│ (3)   ┆ (2)     ┆                                                                                │
╞═══════╪═════════╪════════════════════════════════════════════════════════════════════════════════╡
│ 00:00 ┆ Toronto ┆ bat_charge[00:00,Toronto] + bat_flow[00:00,Toronto]                            │
│       ┆         ┆ - bat_charge[06:00,Toronto] = 0                                                │
│ 00:00 ┆ Berlin  ┆ bat_charge[00:00,Berlin] + bat_flow[00:00,Berlin] - bat_charge[06:00,Berlin]   │
│       ┆         ┆ = 0                                                                            │
│ 06:00 ┆ Toronto ┆ bat_charge[06:00,Toronto] + bat_flow[06:00,Toronto]                            │
│       ┆         ┆ - bat_charge[12:00,Toronto] = 0                                                │
│ 06:00 ┆ Berlin  ┆ bat_charge[06:00,Berlin] + bat_flow[06:00,Berlin] - bat_charge[12:00,Berlin]   │
│       ┆         ┆ = 0                                                                            │
│ 12:00 ┆ Toronto ┆ bat_charge[12:00,Toronto] + bat_flow[12:00,Toronto]                            │
│       ┆         ┆ - bat_charge[18:00,Toronto] = 0                                                │
│ 12:00 ┆ Berlin  ┆ bat_charge[12:00,Berlin] + bat_flow[12:00,Berlin] - bat_charge[18:00,Berlin]   │
│       ┆         ┆ = 0                                                                            │
└───────┴─────────┴────────────────────────────────────────────────────────────────────────────────┘
>>> (m.bat_charge + m.bat_flow) == m.bat_charge.next(
...     "time", wrap_around=True
... )
<Constraint 'unnamed' height=8 terms=24 type=linear>
┌───────┬─────────┬────────────────────────────────────────────────────────────────────────────────┐
│ time  ┆ city    ┆ constraint                                                                     │
│ (4)   ┆ (2)     ┆                                                                                │
╞═══════╪═════════╪════════════════════════════════════════════════════════════════════════════════╡
│ 00:00 ┆ Toronto ┆ bat_charge[00:00,Toronto] + bat_flow[00:00,Toronto]                            │
│       ┆         ┆ - bat_charge[06:00,Toronto] = 0                                                │
│ 00:00 ┆ Berlin  ┆ bat_charge[00:00,Berlin] + bat_flow[00:00,Berlin] - bat_charge[06:00,Berlin]   │
│       ┆         ┆ = 0                                                                            │
│ 06:00 ┆ Toronto ┆ bat_charge[06:00,Toronto] + bat_flow[06:00,Toronto]                            │
│       ┆         ┆ - bat_charge[12:00,Toronto] = 0                                                │
│ 06:00 ┆ Berlin  ┆ bat_charge[06:00,Berlin] + bat_flow[06:00,Berlin] - bat_charge[12:00,Berlin]   │
│       ┆         ┆ = 0                                                                            │
│ 12:00 ┆ Toronto ┆ bat_charge[12:00,Toronto] + bat_flow[12:00,Toronto]                            │
│       ┆         ┆ - bat_charge[18:00,Toronto] = 0                                                │
│ 12:00 ┆ Berlin  ┆ bat_charge[12:00,Berlin] + bat_flow[12:00,Berlin] - bat_charge[18:00,Berlin]   │
│       ┆         ┆ = 0                                                                            │
│ 18:00 ┆ Toronto ┆ bat_charge[18:00,Toronto] + bat_flow[18:00,Toronto]                            │
│       ┆         ┆ - bat_charge[00:00,Toronto] = 0                                                │
│ 18:00 ┆ Berlin  ┆ bat_charge[18:00,Berlin] + bat_flow[18:00,Berlin] - bat_charge[00:00,Berlin]   │
│       ┆         ┆ = 0                                                                            │
└───────┴─────────┴────────────────────────────────────────────────────────────────────────────────┘
Source code in pyoframe/_core.py
@return_new
def next(self, dim: str, wrap_around: bool = False):
    """Creates an expression where the variable at each label is the next variable in the specified dimension.

    Parameters:
        dim:
            The dimension over which to shift the variable.
        wrap_around:
            If `True`, the last label in the dimension is connected to the first label.

    Examples:
        >>> import pandas as pd
        >>> time_dim = pd.DataFrame({"time": ["00:00", "06:00", "12:00", "18:00"]})
        >>> space_dim = pd.DataFrame({"city": ["Toronto", "Berlin"]})
        >>> m = pf.Model()
        >>> m.bat_charge = pf.Variable(time_dim, space_dim)
        >>> m.bat_flow = pf.Variable(time_dim, space_dim)
        >>> # Fails because the dimensions are not the same
        >>> m.bat_charge + m.bat_flow == m.bat_charge.next("time")
        Traceback (most recent call last):
        ...
        pyoframe._constants.PyoframeError: Cannot subtract the two expressions below because expression 1 has extra labels.
        Expression 1:	(bat_charge + bat_flow)
        Expression 2:	bat_charge.next(…)
        Extra labels in expression 1:
        ┌───────┬─────────┐
        │ time  ┆ city    │
        ╞═══════╪═════════╡
        │ 18:00 ┆ Toronto │
        │ 18:00 ┆ Berlin  │
        └───────┴─────────┘
        Use .drop_extras() or .keep_extras() to indicate how the extra labels should be handled. Learn more at
            https://bravos-power.github.io/pyoframe/latest/learn/concepts/addition

        >>> (m.bat_charge + m.bat_flow).drop_extras() == m.bat_charge.next("time")
        <Constraint 'unnamed' height=6 terms=18 type=linear>
        ┌───────┬─────────┬────────────────────────────────────────────────────────────────────────────────┐
        │ time  ┆ city    ┆ constraint                                                                     │
        │ (3)   ┆ (2)     ┆                                                                                │
        ╞═══════╪═════════╪════════════════════════════════════════════════════════════════════════════════╡
        │ 00:00 ┆ Toronto ┆ bat_charge[00:00,Toronto] + bat_flow[00:00,Toronto]                            │
        │       ┆         ┆ - bat_charge[06:00,Toronto] = 0                                                │
        │ 00:00 ┆ Berlin  ┆ bat_charge[00:00,Berlin] + bat_flow[00:00,Berlin] - bat_charge[06:00,Berlin]   │
        │       ┆         ┆ = 0                                                                            │
        │ 06:00 ┆ Toronto ┆ bat_charge[06:00,Toronto] + bat_flow[06:00,Toronto]                            │
        │       ┆         ┆ - bat_charge[12:00,Toronto] = 0                                                │
        │ 06:00 ┆ Berlin  ┆ bat_charge[06:00,Berlin] + bat_flow[06:00,Berlin] - bat_charge[12:00,Berlin]   │
        │       ┆         ┆ = 0                                                                            │
        │ 12:00 ┆ Toronto ┆ bat_charge[12:00,Toronto] + bat_flow[12:00,Toronto]                            │
        │       ┆         ┆ - bat_charge[18:00,Toronto] = 0                                                │
        │ 12:00 ┆ Berlin  ┆ bat_charge[12:00,Berlin] + bat_flow[12:00,Berlin] - bat_charge[18:00,Berlin]   │
        │       ┆         ┆ = 0                                                                            │
        └───────┴─────────┴────────────────────────────────────────────────────────────────────────────────┘

        >>> (m.bat_charge + m.bat_flow) == m.bat_charge.next(
        ...     "time", wrap_around=True
        ... )
        <Constraint 'unnamed' height=8 terms=24 type=linear>
        ┌───────┬─────────┬────────────────────────────────────────────────────────────────────────────────┐
        │ time  ┆ city    ┆ constraint                                                                     │
        │ (4)   ┆ (2)     ┆                                                                                │
        ╞═══════╪═════════╪════════════════════════════════════════════════════════════════════════════════╡
        │ 00:00 ┆ Toronto ┆ bat_charge[00:00,Toronto] + bat_flow[00:00,Toronto]                            │
        │       ┆         ┆ - bat_charge[06:00,Toronto] = 0                                                │
        │ 00:00 ┆ Berlin  ┆ bat_charge[00:00,Berlin] + bat_flow[00:00,Berlin] - bat_charge[06:00,Berlin]   │
        │       ┆         ┆ = 0                                                                            │
        │ 06:00 ┆ Toronto ┆ bat_charge[06:00,Toronto] + bat_flow[06:00,Toronto]                            │
        │       ┆         ┆ - bat_charge[12:00,Toronto] = 0                                                │
        │ 06:00 ┆ Berlin  ┆ bat_charge[06:00,Berlin] + bat_flow[06:00,Berlin] - bat_charge[12:00,Berlin]   │
        │       ┆         ┆ = 0                                                                            │
        │ 12:00 ┆ Toronto ┆ bat_charge[12:00,Toronto] + bat_flow[12:00,Toronto]                            │
        │       ┆         ┆ - bat_charge[18:00,Toronto] = 0                                                │
        │ 12:00 ┆ Berlin  ┆ bat_charge[12:00,Berlin] + bat_flow[12:00,Berlin] - bat_charge[18:00,Berlin]   │
        │       ┆         ┆ = 0                                                                            │
        │ 18:00 ┆ Toronto ┆ bat_charge[18:00,Toronto] + bat_flow[18:00,Toronto]                            │
        │       ┆         ┆ - bat_charge[00:00,Toronto] = 0                                                │
        │ 18:00 ┆ Berlin  ┆ bat_charge[18:00,Berlin] + bat_flow[18:00,Berlin] - bat_charge[00:00,Berlin]   │
        │       ┆         ┆ = 0                                                                            │
        └───────┴─────────┴────────────────────────────────────────────────────────────────────────────────┘

    """
    wrapped = (
        self.data.select(dim)
        .unique(maintain_order=Config.maintain_order)
        .sort(by=dim)
    )
    wrapped = wrapped.with_columns(pl.col(dim).shift(-1).alias("__next"))
    if wrap_around:
        wrapped = wrapped.with_columns(pl.col("__next").fill_null(pl.first(dim)))
    else:
        wrapped = wrapped.drop_nulls(dim)

    expr = self.to_expr()
    data = expr.data.rename({dim: "__prev"})

    data = data.join(
        wrapped,
        left_on="__prev",
        right_on="__next",
        # We use "right" instead of "left" to maintain consistency with the behavior without maintain_order
        maintain_order="right" if Config.maintain_order else None,
    ).drop(["__prev", "__next"], strict=False)

    return data

to_expr() -> Expression

Converts the Variable to an Expression.

Source code in pyoframe/_core.py
def to_expr(self) -> Expression:
    """Converts the Variable to an Expression."""
    self._assert_has_ids()
    return self._new(self.data.drop(SOLUTION_KEY, strict=False), self.name)  # pyright: ignore[reportArgumentType], we know it's safe after _assert_has_ids()