Skip to content

quicopt.pyomo

pyomo

quicopt.pyomo — a Pyomo model → a Quicopt Program.

Reads the model at Pyomo's expression-tree level: the single active objective and each constraint are walked into Quicopt's expression graph, variable bounds and domain into VarDecl, and each constraint into a Zero/Nonneg row. The result is a flat Program (one scalar VarDecl per Pyomo VarData; Pyomo has already expanded indexed components).

An operator the service does not support raises, rather than being approximated or dropped — so a model that imports is a model that means what it says.

Example

Maximize 3x + 5y subject to x + 2y ≤ 5, with 0 ≤ x ≤ 4 and y binary:

import pyomo.environ as pyo
from quicopt import Client

m = pyo.ConcreteModel()
m.x = pyo.Var(bounds=(0, 4))
m.y = pyo.Var(domain=pyo.Binary)
m.cap = pyo.Constraint(expr=m.x + 2 * m.y <= 5)
m.obj = pyo.Objective(expr=3 * m.x + 5 * m.y, sense=pyo.maximize)

result = Client().solve(m)                # the import below happens inside
print(result.status, result.objective)    # optimal 14.0
print(result.solution)                    # {'x1': 3.0, 'x2': 1.0}

Client.solve takes the Pyomo model as it stands and calls this module on the way out, so the import is not a step to perform — reach for import_model only to hold the Program itself.

Note what the solution is keyed by: x1/x2, not m.x/m.y. Variables are named positionally, because Pyomo has already expanded indexed components and declaration order is what is left to identify them by.

import_model

import_model(m)

Convert a Pyomo ConcreteModel into a Quicopt Program.

Each VarData becomes a scalar VarDecl (x{i} in declaration order) carrying its bounds and domain (BinaryBINARY, integer domains→ INTEGER, else CONTINUOUS); the active objective and every constraint become Quicopt expressions. Requires exactly one active objective; an absent Pyomo variable bound means unbounded in that direction (±Inf). A variable that is fixed but carries no value raises: its pin has no value to pin to, and the ±Inf reading of an absent bound would turn it into a free variable instead.

A variable given a distribution (see quicopt.stochastic) is not a decision at all: it leaves the variable list, becomes a named declaration in Program.sources, and its every use becomes a reference to that name. The scenario count and seed ride along as model data. Names are assigned across all variables before the random ones are set aside, so which variables are random never renumbers the rest. A model that declares no uncertainty imports to exactly the Program it always did.

Source code in src/quicopt/pyomo.py
def import_model(m):
    """Convert a Pyomo ``ConcreteModel`` into a Quicopt ``Program``.

    Each ``VarData`` becomes a scalar ``VarDecl`` (``x{i}`` in declaration order)
    carrying its bounds and domain (``Binary``→``BINARY``, integer domains→
    ``INTEGER``, else ``CONTINUOUS``); the active objective and every constraint
    become Quicopt expressions. Requires exactly one active objective; an absent
    Pyomo variable bound means unbounded in that direction (±Inf). A variable that
    is fixed but carries no value **raises**: its pin has no value to pin *to*, and the ±Inf
    reading of an absent bound would turn it into a free variable instead.

    A variable given a distribution (see ``quicopt.stochastic``) is not a decision
    at all: it leaves the variable list, becomes a named declaration in
    ``Program.sources``, and its every use becomes a reference to that name. The
    scenario count and seed ride along as model data. Names are assigned across
    *all* variables before the random ones are set aside, so which variables are
    random never renumbers the rest. A model that declares no uncertainty imports
    to exactly the ``Program`` it always did.
    """
    declared = _declarations(m)
    sources = {} if declared is None else declared.sources
    scenarios = 1 if declared is None else declared.scenarios
    seed = 1 if declared is None else declared.seed

    vis = list(m.component_data_objects(pyo.Var))
    name = {id(v): f"x{i + 1}" for i, v in enumerate(vis)}
    var = lambda v: (SourceRef(sources[id(v)][1]) if id(v) in sources
                     else Var(name[id(v)], ()))

    vars = []
    for v in vis:
        if id(v) in sources:                   # a random variable is declared, never decided
            _check_source(v, sources[id(v)][1])
            continue
        domain = BINARY if v.is_binary() else INTEGER if v.is_integer() else CONTINUOUS
        if v.fixed and v.value is None:            # else the pin below is (None, None) ⇒ ±Inf ⇒ silently free
            raise ValueError(f"variable '{v.name}' is fixed but has no value")
        lb, ub = (v.value, v.value) if v.fixed else (v.lb, v.ub)   # a fixed var ⇒ a [val, val] pin
        lb = -inf if lb is None else lb                            # an absent Pyomo bound ⇒ ±Inf (free)
        ub =  inf if ub is None else ub
        start = v.value if v.value is not None else min(max(0.0, lb), ub)   # clamp 0 into [lb, ub]
        vars.append(VarDecl(name[id(v)], [], domain, float(lb), float(ub), float(start)))

    objs = list(m.component_data_objects(pyo.Objective, active=True))
    if len(objs) != 1:
        raise ValueError(f"expected exactly one active objective, found {len(objs)}")
    sense = "min" if objs[0].sense == pyo.minimize else "max"
    objective = _expr(objs[0].expr, var)

    cons = []
    for c in m.component_data_objects(pyo.Constraint, active=True):
        lb = pyo.value(c.lower) if c.has_lb() else -inf   # an absent side ⇒ free, emits no row
        ub = pyo.value(c.upper) if c.has_ub() else inf
        _emit(cons, _expr(c.body, var), lb, ub)

    return Program(vars=vars, objective=objective, sense=sense, constraints=cons,
                   scenarios=scenarios, scenario_seed=seed,
                   sources=_source_decls(sources, scenarios, var))