import networkx as nx
from omlt.neuralnet.layer import Layer
[docs]class NetworkDefinition:
def __init__(
self, scaling_object=None, scaled_input_bounds=None, unscaled_input_bounds=None
):
"""
Create a network definition object used to create the neural network
formulation in Pyomo
Args:
scaling_object : ScalingInterface or None
A scaling object to specify the scaling parameters for the
neural network inputs and outputs. If None, then no
scaling is performed.
scaled_input_bounds : dict or None
A dict that contains the bounds on the scaled variables (the
direct inputs to the neural network). If None, then no bounds
are specified or they are generated using unscaled bounds.
unscaled_input_bounds: dict or None
A dict that contains the bounds on the scaled variables (the
direct inputs to the neural network). If supplied the scaled_input_bounds
parameter will be generated using the scaling object.
If None, then no bounds are specified.
"""
self.__layers_by_id = dict()
self.__graph = nx.DiGraph()
self.__scaling_object = scaling_object
# Process input bounds to insure scaled input bounds exist for formulations
if scaled_input_bounds is None:
if unscaled_input_bounds is not None and scaling_object is not None:
lbs = scaling_object.get_scaled_input_expressions(
{k: t[0] for k, t in unscaled_input_bounds.items()}
)
ubs = scaling_object.get_scaled_input_expressions(
{k: t[1] for k, t in unscaled_input_bounds.items()}
)
scaled_input_bounds = {
k: (lbs[k], ubs[k]) for k in unscaled_input_bounds.keys()
}
# If unscaled input bounds provided and no scaler provided, scaled input bounds = unscaled input bounds
elif unscaled_input_bounds is not None and scaling_object is None:
scaled_input_bounds = unscaled_input_bounds
self.__unscaled_input_bounds = unscaled_input_bounds
self.__scaled_input_bounds = scaled_input_bounds
[docs] def add_layer(self, layer):
"""
Add a layer to the network.
Parameters
----------
layer : Layer
the layer to add to the network
"""
layer_id = id(layer)
self.__layers_by_id[layer_id] = layer
self.__graph.add_node(layer_id)
[docs] def add_edge(self, from_layer, to_layer):
"""
Add an edge between two layers.
Parameters
----------
from_layer : Layer
the layer with the outbound connection
to_layer : Layer
the layer with the inbound connection
"""
id_to = id(to_layer)
id_from = id(from_layer)
if id_to not in self.__layers_by_id:
raise ValueError(f"Inbound layer {to_layer} not found in network.")
if id_from not in self.__layers_by_id:
raise ValueError(f"Outbound layer {from_layer} not found in network.")
self.__graph.add_edge(id_from, id_to)
@property
def scaling_object(self):
"""Return an instance of the scaling object that supports the ScalingInterface"""
return self.__scaling_object
@property
def scaled_input_bounds(self):
"""Return a dict of tuples containing lower and upper bounds of neural network inputs"""
return self.__scaled_input_bounds
@property
def unscaled_input_bounds(self):
"""Return a dict of tuples containing lower and upper bounds of unscaled neural network inputs"""
return self.__unscaled_input_bounds
@property
def input_layers(self):
"""Return an iterator over the input layers"""
for layer_id, in_degree in self.__graph.in_degree():
if in_degree == 0:
yield self.__layers_by_id[layer_id]
@property
def input_nodes(self):
"""An alias for input_layers"""
return self.input_layers
@property
def output_layers(self):
"""Return an iterator over the output layer"""
for layer_id, out_degree in self.__graph.out_degree():
if out_degree == 0:
yield self.__layers_by_id[layer_id]
@property
def output_nodes(self):
"""An alias for output_layers"""
return self.output_layers
[docs] def layer(self, layer_id):
"""Return the layer with the given id"""
return self.__layers_by_id[layer_id]
@property
def layers(self):
"""Return an iterator over all the layers"""
for layer_id in nx.topological_sort(self.__graph):
yield self.__layers_by_id[layer_id]
[docs] def predecessors(self, layer):
"""Return an iterator over the layers with outbound connections into the layer"""
if isinstance(layer, Layer):
layer = id(layer)
for node_id in self.__graph.predecessors(layer):
yield self.__layers_by_id[node_id]
[docs] def successors(self, layer):
"""Return an iterator over the layers with an inbound connection from the layer"""
if isinstance(layer, Layer):
layer = id(layer)
for node_id in self.__graph.successors(layer):
yield self.__layers_by_id[node_id]
def __str__(self):
return f"NetworkDefinition(num_layers={len(self.__layers_by_id)})"