Getting started
Opti problems consist of a definition of
- the input parameters \(x \in \mathbb{X}\),
- the output parameters \(y \in \mathbb{Y}\),
- the objectives \(s(y)\) (optional),
- the input constraints \(g(x) \leq 0\) (optional),
- the output constraints \(h(y)\) (optional),
- a data set of previous function evaluations \(\{x, y\}\) (optional)
- and the function \(f(x)\) to be optimized (optional).
Parameters
Input and output spaces are defined using Parameters
objects.
For example a mixed input space of three continuous, one discrete and one categorical parameter(s), along with an output space of continuous parameters can be defined as:
import opti
inputs = opti.Parameters([
opti.Continuous("x1", domain=[0, 1]),
opti.Continuous("x2", domain=[0, 1]),
opti.Continuous("x3", domain=[0, 1]),
opti.Discrete("x4", domain=[1, 2, 5, 7.5]),
opti.Categorical("x5", domain=["A", "B", "C"])
])
outputs = opti.Parameters([
opti.Continuous("y1", domain=[0, None]),
opti.Continuous("y2", domain=[None, None]),
opti.Continuous("y3", domain=[0, 100])
])
Individual parameters can be indexed by name.
inputs["x5"]
>>> Categorical("x5", domain=["A", "B", "C"])
inputs.names
>>> ["x1", "x2", "x3", "x4", "x5"]
We can sample from individual parameters, parameter spaces or parameter spaces including constraints (more on that later)
x5 = inputs["x1"].sample(3)
print(x5.values)
>>> array([0.72405216, 0.14914942, 0.46051132])
X = inputs.sample(5)
print(X)
>>> x1 x2 x3 x4 x5
0 0.760116 0.063584 0.518885 7.5 A
1 0.807928 0.496213 0.885545 1.0 C
2 0.351253 0.993993 0.340414 5.0 B
3 0.385825 0.857306 0.355267 1.0 C
4 0.191907 0.993494 0.384322 2.0 A
We can also check for each point in a dataframe, whether it is contained in the space.
inputs.contains(X)
>>> array([ True, True, True, True, True])
In general all opti functions operate on dataframes and thus use the parameter name to identify corresponding column. Hence, a dataframe may contain additional columns and columns may be in arbitrary order. The index of a dataframe is preserved, meaning that the returned dataframe will have the same indices as the original dataframe.
Constraints
Input constraints are defined separately from the input space. The following constraints are supported.
Linear constraints (LinearEquality
and LinearInequality
) are expressions of the form \(\sum_i a_i x_i = b\) or \(\leq b\) for equality and inequality constraints respectively.
They take a list of names of the input parameters they are operating on, a list of left-hand-side coefficients \(a_i\) and a right-hand-side constant \(b\).
# A mixture: x1 + x2 + x3 = 1
constr1 = opti.LinearEquality(["x1", "x2", "x3"], lhs=1, rhs=1)
# x1 + 2 * x3 < 0.8
constr2 = opti.LinearInequality(["x1", "x3"], lhs=[1, 2], rhs=0.8)
Nonlinear constraints (NonlinearEquality
and NonlinearInequality
) take any expression that can be evaluated by pandas.eval, including mathematical operators such as sin
, exp
, log10
or exponentiation.
# The unit circle: x1**2 + x2**2 = 1
constr3 = opti.NonlinearEquality("x1**2 + x2**2 - 1")
# Require x1 < 0.5 if x5 == "A"
constr4 = opti.NonlinearInequality("(x1 - 0.5) * (x5 =='A')")
Finally, there is a combinatorical constraint (NChooseK
) to express that we only want to have \(k\) out of the \(n\) parameters to take positive values.
Think of a mixture, where we have long list of possible ingredients, but want to limit number of ingredients in any given recipe.
# Only 2 out of 3 parameters can be greater than zero
constr5 = opti.NChooseK(["x1", "x2", "x3"], max_active=2)
Constraints can be grouped in a container which acts as the union constraints.
constraints = opti.Constraints([constr1, constr2, constr3, constr4, constr5])
We can check whether a point satisfies individual constraints or the list of constraints.
constr2.satisfied(X).values
>>> array([False, False, True, True, True])
The distance to the constraint boundary can also be evaluated for use in numerical optimization methods, where values \(\leq 0\) correspond to a satisified constraint.
constr2(X).values
>>> array([ 0.479001 , 0.89347371, -0.10833372, -0.05890873, -0.22377122])
Opti contains a number of methods to draw random samples from constrained spaces, see the sampling reference.
Objectives
In an optimization problem we need to define the target direction or target value individually for each output. This is done using objectives \(s_m(y_m)\) so that a mixed objective optimization becomes a minimization problem.
objectives = opti.Objectives([
opti.Minimize("y1"),
opti.Maximize("y2"),
opti.CloseToTarget("y3", target=7)
])
We can compute objective values from output values to see the objective transformation applied.
Y = pd.DataFrame({
"y1": [1, 2, 3],
"y2": [7, 4, 5],
"y3": [5, 6.9, 12]
})
objectives(Y)
>>> minimize_y1 maximize_y2 closetotarget_y3
0 1 -7 4.00
1 2 -4 0.01
2 3 -5 25.00
Objectives can also be used as output constraints. This is different from an objective in that we want the constraint to be satisfied and not explore possible tradeoffs.
Problem
Finally, a problem is the combination of inputs, outputs, objectives, constraints, output_constraints, (true) function and data.
problem = opti.Problem(
inputs=inputs,
outputs=outputs,
constraints=constraints,
objectives=objectives
)
config = problem.to_config()
problem = opti.Problem(**config)
problem.to_json("problem.json")
problem = opti.read_json("problem.json")