Skip to main content

Coefficient arithmetic

A coefficient is often derived from several parameters: fuel price divided by efficiency, a cost scaled by a factor. In nimopt a coefficient is a parameter read at its sets or an arithmetic combination of such readings. +, -, *, / and a power by a number combine them. The combination is symbolic: it contains references, derives its dimensions from its operands, and is evaluated once, when the term it multiplies is materialised. A derived coefficient can therefore appear in a definition before any data exists.

import numpy as np
from nimopt import Model, Param, Set, Sum

G = Set("G", np.array(["base", "peak"]))
T = Set("T", np.arange(3))
price = Param.from_dense("fuel_price", (G, T), np.full((2, 3), 30.0))
eta = Param.from_dense("efficiency", (G, T), np.array([[0.5] * 3, [0.4] * 3]))
load = Param.from_dense("load", (T,), np.full(3, 100.0))
cap = Param.from_dense("capacity", (G, T), np.full((2, 3), 80.0))

unit_cost = price[G, T] / eta[G, T]

m = Model("dispatch", sense="min")
gen = m.var("gen", (G, T), lower=0.0, upper=cap)
m.constraint("balance", Sum(G, gen[G, T]) == load[T])
m.set_objective(Sum(G, T, unit_cost[G, T] * gen[G, T]))

print(unit_cost.name, unit_cost.dims)
print(m.solve().objective)
Output
(fuel_price / efficiency) ('G', 'T')
18900.0

Reading a derived coefficient​

unit_cost[G, T] reads a combination the same way price[G, T] reads a parameter, and the sets given are checked against the combination's dimensions. A transposed or incomplete index raises ValueError.

import numpy as np
from nimopt import Param, Set

G = Set("G", np.array(["base", "peak"]))
T = Set("T", np.arange(3))
price = Param.from_dense("fuel_price", (G, T), np.full((2, 3), 30.0))
eta = Param.from_dense("efficiency", (G, T), np.array([[0.5] * 3, [0.4] * 3]))

(price[G, T] / eta[G, T])[T, G]
Raises ValueError
ValueError: coefficient (fuel_price / efficiency) is declared over ('G', 'T'); got ('T', 'G')

A bare parameter has no arithmetic: price * 2.0 raises TypeError. Read the parameter at its sets first and combine the references.

Alignment​

Two operands with the same dimensions align entry by entry. Operands whose dimensions nest or overlap align on the shared dimensions and broadcast over the rest, with the left operand's order first. Operands sharing no dimension raise ValueError. Their product would be an outer product, and a linear model does not require one. The same rule applies when a coefficient multiplies a variable. A number has no dimensions and scales.

import numpy as np
from nimopt import Param, Set

G = Set("G", np.array(["base", "peak"]))
T = Set("T", np.arange(3))
over_g = Param.from_dense("over_g", (G,), np.ones(2))
over_t = Param.from_dense("over_t", (T,), np.ones(3))

over_g[G] * over_t[T]
Raises ValueError
ValueError: frames ('G',) and ('T',) share no dimension; pass operands that share a dimension

A coefficient never adds a column the variable does not have. The variable's members define the columns; a coefficient can only reduce which of them receive a nonzero. A coefficient with dimensions the variable lacks defines rows over those dimensions: this is how a term maps rows to columns.

Division by zero​

A divisor that is zero raises ZeroDivisionError, and the message gives the coordinate. A quotient of infinity is not passed to a solver. The check covers a Python number, a NumPy scalar and a coefficient with a zero at any coordinate. A NumPy scalar divides to infinity where a Python number raises.

import numpy as np
from nimopt import Param, Set

G = Set("G", np.array(["base", "peak"]))
T = Set("T", np.arange(3))
price = Param.from_dense("fuel_price", (G, T), np.full((2, 3), 30.0))
holed = Param.from_dense("holed", (G, T), np.array([[0.5, 0.0, 0.5], [0.4] * 3]))

price[G, T] / holed[G, T]
Raises ZeroDivisionError
ZeroDivisionError: divisor holed is zero at 1 coordinate(s), first at {'G': 'base', 'T': 1}; remove the zeros or divide by another parameter

Constants​

An expression consists of terms and a constant. x + 1 <= 5 produces the row x <= 4: the constant moves to the right-hand side when the constraint is built, and into the reported objective value as a fixed cost. A constant adds no column. A constant alone is neither an objective nor a constraint, and raises.

import numpy as np
from nimopt import Model, Param, Set, Sum

P = Set("P", np.array(["a"]))
one = Param.from_dense("one", (P,), np.ones(1))

m = Model("m", sense="max")
x = m.var("x", (P,), upper=100.0)
m.constraint("cap", one[P] * x[P] + 1.0 <= 5.0)
m.set_objective(Sum(P, one[P] * x[P]) + 7.0)

print(m.assemble().n_cols, m.assemble().row_upper)
print(m.solve().objective)
Output
1 [4.]
11.0

Forms that raise​

A line of modeling arithmetic produces a linear term, or raises with a message that gives the form to write instead. There is no third outcome.

WrittenForm to write instead
x[P] * y[P]a linear term has one variable; a coefficient multiplies it
x[P] ** 2a coefficient takes the power, and a variable multiplies it
x[P] / y[P]a variable in a denominator is not linear
2.0 / x[P]the same: write the reciprocal as a coefficient
x[P] / 0.0a divisor of zero is handled before it is passed to an expression
x[P] < 1.0<= and >=; an LP has no row for a strict inequality
x[P] > 1.0the same
x[P] != 1.0one bound per constraint
0.0 <= x[P] <= 1.0each bound as its own constraint
x[P] + c[P]a coefficient has no row until a variable multiplies it
c[P] * (x[P] + 1.0)a coefficient times a constant is one value per row; the constant goes to the right-hand side
abs(x[P]), min and maxreduce with Sum, or bound the expression with two rows
np.sum(x[P])Sum and its sets
np.array([...]) * x[P]Param.from_dense, read at its sets
Sum(P, Sum(P, x[P]))a dimension is reduced once
Sum(P - 1, x[P])Sum(P, x[P - 1]): the lag belongs on the reference
T - 1.7a lag is a whole number of members

Each raises with a nimopt message, not with a bare Python error. The test suite executes the table and the replacement forms the messages give.

import numpy as np
from nimopt import Model, Set

P = Set("P", np.array(["a", "b"]))
m = Model("m")
x = m.var("x", (P,))

np.array([1.0, 2.0]) * x[P]
Raises TypeError
TypeError: a coefficient is a parameter; build one with `Param.from_dense` or `Param.from_long` and read it at its sets. A product of two expressions is not linear.

A NumPy array multiplying a term would let NumPy apply the operator and return an array of expressions. The expression types reject the ufunc, and the message describes how a coefficient is built.