Warning

Models built with `pyomo.kernel`

components are not yet compatible with pyomo extension modules (e.g., `PySP`

, `pyomo.dae`

, `pyomo.gdp`

).

# The Kernel Library

The `pyomo.kernel`

library is an experimental modeling interface designed to provide a better experience for users doing concrete modeling and advanced application development with Pyomo. It includes the basic set of modeling components necessary to build algebraic models, which have been redesigned from the ground up to make it easier for users to customize and extend. For a side-by-side comparison of `pyomo.kernel`

and `pyomo.environ`

syntax, visit the link below.

Models built from `pyomo.kernel`

components are fully compatible with the standard solver interfaces included with Pyomo. A minimal example script that defines and solves a model is shown below.

```
# ___________________________________________________________________________
#
# Pyomo: Python Optimization Modeling Objects
# Copyright (c) 2008-2024
# National Technology and Engineering Solutions of Sandia, LLC
# Under the terms of Contract DE-NA0003525 with National Technology and
# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
# rights in this software.
# This software is distributed under the 3-clause BSD License.
# ___________________________________________________________________________
import pyomo.kernel as pmo
model = pmo.block()
model.x = pmo.variable()
model.c = pmo.constraint(model.x >= 1)
model.o = pmo.objective(model.x)
opt = pmo.SolverFactory("ipopt")
result = opt.solve(model)
assert str(result.solver.termination_condition) == "optimal"
```

## Notable Improvements

### More Control of Model Structure

Containers in `pyomo.kernel`

are analogous to indexed components in `pyomo.environ`

. However, `pyomo.kernel`

containers allow for additional layers of structure as they can be nested within each other as long as they have compatible categories. The following example shows this using `pyomo.kernel.variable`

containers.

```
vlist = pyomo.kernel.variable_list()
vlist.append(pyomo.kernel.variable_dict())
vlist[0]['x'] = pyomo.kernel.variable()
```

As the next section will show, the standard modeling component containers are also compatible with user-defined classes that derive from the existing modeling components.

### Sub-Classing

The existing components and containers in `pyomo.kernel`

are designed to make sub-classing easy. User-defined classes that derive from the standard modeling components and containers in `pyomo.kernel`

are compatible with existing containers of the same component category. As an example, in the following code we see that the `pyomo.kernel.block_list`

container can store both `pyomo.kernel.block`

objects as well as a user-defined `Widget`

object that derives from `pyomo.kernel.block`

. The `Widget`

object can also be placed on another block object as an attribute and treated itself as a block.

```
class Widget(pyomo.kernel.block):
...
model = pyomo.kernel.block()
model.blist = pyomo.kernel.block_list()
model.blist.append(Widget())
model.blist.append(pyomo.kernel.block())
model.w = Widget()
model.w.x = pyomo.kernel.variable()
```

The next series of examples goes into more detail on how to implement derived components or containers.

The following code block shows a class definition for a non-negative variable, starting from `pyomo.kernel.variable`

as a base class.

```
class NonNegativeVariable(pyomo.kernel.variable):
"""A non-negative variable."""
__slots__ = ()
def __init__(self, **kwds):
if 'lb' not in kwds:
kwds['lb'] = 0
if kwds['lb'] < 0:
raise ValueError("lower bound must be non-negative")
super(NonNegativeVariable, self).__init__(**kwds)
#
# restrict assignments to x.lb to non-negative numbers
#
@property
def lb(self):
# calls the base class property getter
return pyomo.kernel.variable.lb.fget(self)
@lb.setter
def lb(self, lb):
if lb < 0:
raise ValueError("lower bound must be non-negative")
# calls the base class property setter
pyomo.kernel.variable.lb.fset(self, lb)
```

The `NonNegativeVariable`

class prevents negative values from being stored into its lower bound during initialization or later on through assignment statements (e.g, `x.lb = -1`

fails). Note that the `__slots__ == ()`

line at the beginning of the class definition is optional, but it is recommended if no additional data members are necessary as it reduces the memory requirement of the new variable type.

The next code block defines a custom variable container called `Point`

that represents a 3-dimensional point in Cartesian space. The new type derives from the `pyomo.kernel.variable_tuple`

container and uses the `NonNegativeVariable`

type we defined previously in the z coordinate.

```
class Point(pyomo.kernel.variable_tuple):
"""A 3-dimensional point in Cartesian space with the
z coordinate restricted to non-negative values."""
__slots__ = ()
def __init__(self):
super(Point, self).__init__(
(pyomo.kernel.variable(), pyomo.kernel.variable(), NonNegativeVariable())
)
@property
def x(self):
return self[0]
@property
def y(self):
return self[1]
@property
def z(self):
return self[2]
```

The `Point`

class can be treated like a tuple storing three variables, and it can be placed inside of other variable containers or added as attributes to blocks. The property methods included in the class definition provide an additional syntax for accessing the three variables it stores, as the next code example will show.

The following code defines a class for building a convex second-order cone constraint from a `Point`

object. It derives from the `pyomo.kernel.constraint`

class, overriding the constructor to build the constraint expression and utilizing the property methods on the point class to increase readability.

```
class SOC(pyomo.kernel.constraint):
"""A convex second-order cone constraint"""
__slots__ = ()
def __init__(self, point):
assert isinstance(point.z, NonNegativeVariable)
super(SOC, self).__init__(point.x**2 + point.y**2 <= point.z**2)
```

### Reduced Memory Usage

The `pyomo.kernel`

library offers significant opportunities to reduce memory requirements for highly structured models. The situation where this is most apparent is when expressing a model in terms of many small blocks consisting of singleton components. As an example, consider expressing a model consisting of a large number of voltage transformers. One option for doing so might be to define a Transformer component as a subclass of `pyomo.kernel.block`

. The example below defines such a component, including some helper methods for connecting input and output voltage variables and updating the transformer ratio.

```
class Transformer(pyomo.kernel.block):
def __init__(self):
super(Transformer, self).__init__()
self._a = pyomo.kernel.parameter()
self._v_in = pyomo.kernel.expression()
self._v_out = pyomo.kernel.expression()
self._c = pyomo.kernel.constraint(self._a * self._v_out == self._v_in)
def set_ratio(self, a):
assert a > 0
self._a.value = a
def connect_v_in(self, v_in):
self._v_in.expr = v_in
def connect_v_out(self, v_out):
self._v_out.expr = v_out
```

A simplified version of this using `pyomo.environ`

components might look like what is below.

```
def Transformer():
b = pyomo.environ.Block(concrete=True)
b._a = pyomo.environ.Param(mutable=True)
b._v_in = pyomo.environ.Expression()
b._v_out = pyomo.environ.Expression()
b._c = pyomo.environ.Constraint(expr=b._a * b._v_out == b._v_in)
return b
```

The transformer expressed using `pyomo.kernel`

components requires roughly 2 KB of memory, whereas the `pyomo.environ`

version requires roughly 8.4 KB of memory (an increase of more than 4x). Additionally, the `pyomo.kernel`

transformer is fully compatible with all existing `pyomo.kernel`

block containers.

### Direct Support For Conic Constraints with Mosek

Pyomo 5.6.3 introduced support into `pyomo.kernel`

for six conic constraint forms that are directly recognized
by the new Mosek solver interface. These are

`conic.quadratic`

:\(\;\;\sum_{i}x_i^2 \leq r^2,\;\;r\geq 0\)

`conic.rotated_quadratic`

:\(\;\;\sum_{i}x_i^2 \leq 2 r_1 r_2,\;\;r_1,r_2\geq 0\)

`conic.primal_exponential`

:\(\;\;x_1\exp(x_2/x_1) \leq r,\;\;x_1,r\geq 0\)

`conic.primal_power`

(\(\alpha\) is a constant):\(\;\;||x||_2 \leq r_1^{\alpha} r_2^{1-\alpha},\;\;r_1,r_2\geq 0,\;0 < \alpha < 1\)

`conic.dual_exponential`

:\(\;\;-x_2\exp((x_1/x_2)-1) \leq r,\;\;x_2\leq0,\;r\geq 0\)

`conic.dual_power`

(\(\alpha\) is a constant):\(\;\;||x||_2 \leq (r_1/\alpha)^{\alpha} (r_2/(1-\alpha))^{1-\alpha},\;\;r_1,r_2\geq 0,\;0 < \alpha < 1\)

Other solver interfaces will treat these objects as general
nonlinear or quadratic constraints, and may or may not have
the ability to identify their convexity. For instance,
Gurobi will recognize the expressions produced by the
`quadratic`

and `rotated_quadratic`

objects
as representing convex domains as long as the variables
involved satisfy the convexity conditions. However, other
solvers may not include this functionality.

Each of these conic constraint classes are of the same
category type as standard `pyomo.kernel.constraint`

object, and, thus, are directly supported by the standard
constraint containers (`constraint_tuple`

,
`constraint_list`

, `constraint_dict`

).

Each conic constraint class supports two methods of instantiation. The first method is to directly instantiate a conic constraint object, providing all necessary input variables:

```
import pyomo.kernel as pmo
m = pmo.block()
m.x1 = pmo.variable(lb=0)
m.x2 = pmo.variable()
m.r = pmo.variable(lb=0)
m.q = pmo.conic.primal_exponential(x1=m.x1, x2=m.x2, r=m.r)
```

This method may be limiting if utilizing the Mosek solver as the user must ensure that additional conic constraints do not use variables that are directly involved in any existing conic constraints (this is a limitation the Mosek solver itself).

To overcome this limitation, and to provide a more general
way of defining conic domains, each conic constraint class
provides the `as_domain`

class method. This
alternate constructor has the same argument signature as the
class, but in place of each variable, one can optionally
provide a constant, a linear expression, or
`None`

. The `as_domain`

class method returns
a `block`

object that includes the core conic
constraint, auxiliary variables used to express the conic
constraint, as well as auxiliary constraints that link the
inputs (that are not `None`

) to the auxiliary
variables. Example:

```
import pyomo.kernel as pmo
import math
m = pmo.block()
m.x = pmo.variable(lb=0)
m.y = pmo.variable(lb=0)
m.b = pmo.conic.primal_exponential.as_domain(
x1=math.sqrt(2) * m.x, x2=2.0, r=2 * (m.x + m.y)
)
```