Skip to content

Commit

Permalink
Unit tests for NN formulations
Browse files Browse the repository at this point in the history
  • Loading branch information
jezsadler committed Oct 20, 2023
1 parent 7d39a62 commit d65b928
Show file tree
Hide file tree
Showing 2 changed files with 201 additions and 2 deletions.
19 changes: 17 additions & 2 deletions src/omlt/neuralnet/nn_formulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ def layer(b, layer_id):
# currently only support a single input layer
input_layers = list(net.input_layers)
if len(input_layers) != 1:
raise ValueError("Multiple input layers are not currently supported.")
raise ValueError("Multiple input layers are not currently supported.")
input_layer = input_layers[0]

@block.Constraint(input_layer.output_indexes)
Expand All @@ -206,7 +206,7 @@ def input_assignment(b, *output_index):
# currently only support a single output layer
output_layers = list(net.output_layers)
if len(output_layers) != 1:
raise ValueError("Multiple output layers are not currently supported.")
raise ValueError("Multiple output layers are not currently supported.")
output_layer = output_layers[0]

@block.Constraint(output_layer.output_indexes)
Expand Down Expand Up @@ -308,6 +308,16 @@ def __init__(self, network_structure, activation_functions=None):
)
if activation_functions is not None:
self._activation_functions.update(activation_functions)

# If we want to do network input/output validation at initialize time instead
# of build time, as it is for FullSpaceNNFormulation:
#
# network_inputs = list(self.__network_definition.input_nodes)
# if len(network_inputs) != 1:
# raise ValueError("Multiple input layers are not currently supported.")
# network_outputs = list(self.__network_definition.output_nodes)
# if len(network_outputs) != 1:
# raise ValueError("Multiple output layers are not currently supported.")

def _supported_default_activation_functions(self):
return dict(_DEFAULT_ACTIVATION_FUNCTIONS)
Expand Down Expand Up @@ -503,6 +513,11 @@ def layer(b, layer_id):
else:
raise ValueError("ReluPartitionFormulation supports only Dense layers")

# This check is never hit. The formulation._build_formulation() function is
# only ever called by an OmltBlock.build_formulation(), and that runs the
# input_indexes and output_indexes first, which will catch any formulations
# with multiple input or output layers.

# setup input variables constraints
# currently only support a single input layer
input_layers = list(net.input_layers)
Expand Down
184 changes: 184 additions & 0 deletions tests/neuralnet/test_nn_formulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
NetworkDefinition,
ReducedSpaceNNFormulation,
ReducedSpaceSmoothNNFormulation,
ReluPartitionFormulation,
)
from omlt.neuralnet.layer import (
ConvLayer2D,
Expand All @@ -17,6 +18,10 @@
InputLayer,
PoolingLayer2D,
)
from omlt.neuralnet.layers.full_space import full_space_dense_layer
from omlt.neuralnet.layers.partition_based import partition_based_dense_relu_layer
from omlt.neuralnet.layers.reduced_space import reduced_space_dense_layer



def two_node_network(activation, input_value):
Expand Down Expand Up @@ -325,3 +330,182 @@ def test_maxpool_FullSpaceNNFormulation():
m.obj1 = pyo.Objective(expr=0)
status = pyo.SolverFactory("cbc").solve(m, tee=False)
assert abs(pyo.value(m.neural_net_block.outputs[0, 0, 0]) - y[0, 0, 0]) < 1e-6

def _test_formulation_initialize_extra_input(network_formulation):
"""
network_formulation can be:
'FullSpace',
'ReducedSpace'
"""
net, y = two_node_network("linear", -2.0)
extra_input = InputLayer([1])
net.add_layer(extra_input)
with pytest.raises(ValueError) as excinfo:
if network_formulation == 'FullSpace':
formulation = FullSpaceNNFormulation(net)
elif network_formulation == 'ReducedSpace':
formulation = ReducedSpaceNNFormulation(net)
expected_msg = "Multiple input layers are not currently supported."
assert str(excinfo.value) == expected_msg

def _test_formulation_added_extra_input(network_formulation):
"""
network_formulation can be:
'FullSpace',
'ReducedSpace'
'relu'
"""
net, y = two_node_network("linear", -2.0)
extra_input = InputLayer([1])
if network_formulation == 'FullSpace':
formulation = FullSpaceNNFormulation(net)
elif network_formulation == 'ReducedSpace':
formulation = ReducedSpaceNNFormulation(net)
elif network_formulation == 'relu':
formulation = ReluPartitionFormulation(net)
net.add_layer(extra_input)
with pytest.raises(ValueError) as excinfo:
formulation.input_indexes
expected_msg = "Multiple input layers are not currently supported."
assert str(excinfo.value) == expected_msg

def _test_formulation_build_extra_input(network_formulation):
"""
network_formulation can be:
'FullSpace',
'ReducedSpace'
'relu'
"""
net, y = two_node_network("linear", -2.0)
extra_input = InputLayer([1])
if network_formulation == 'FullSpace':
formulation = FullSpaceNNFormulation(net)
elif network_formulation == 'ReducedSpace':
formulation = ReducedSpaceNNFormulation(net)
elif network_formulation == 'relu':
formulation = ReluPartitionFormulation(net)
net.add_layer(extra_input)
m = pyo.ConcreteModel()
m.neural_net_block = OmltBlock()
with pytest.raises(ValueError) as excinfo:
m.neural_net_block.build_formulation(formulation)
expected_msg = "Multiple input layers are not currently supported."
assert str(excinfo.value) == expected_msg

def _test_formulation_added_extra_output(network_formulation):
"""
network_formulation can be:
'FullSpace',
'ReducedSpace'
'relu'
"""
net, y = two_node_network("linear", -2.0)
extra_output = DenseLayer(
[1, 2],
[1, 2],
activation="linear",
weights=np.array([[1.0, 0.0], [5.0, 1.0]]),
biases=np.array([3.0, 4.0]),
)
if network_formulation == 'FullSpace':
formulation = FullSpaceNNFormulation(net)
elif network_formulation == 'ReducedSpace':
formulation = ReducedSpaceNNFormulation(net)
elif network_formulation == 'relu':
formulation = ReluPartitionFormulation(net)
net.add_layer(extra_output)
net.add_edge(list(net.layers)[-2],extra_output)
with pytest.raises(ValueError) as excinfo:
formulation.output_indexes
expected_msg = "Multiple output layers are not currently supported."
assert str(excinfo.value) == expected_msg

def _test_formulation_initialize_extra_output(network_formulation):
"""
network_formulation can be:
'FullSpace',
'ReducedSpace'
"""
net, y = two_node_network("linear", -2.0)
extra_output = DenseLayer(
[1, 2],
[1, 2],
activation="linear",
weights=np.array([[1.0, 0.0], [5.0, 1.0]]),
biases=np.array([3.0, 4.0]),
)
net.add_layer(extra_output)
net.add_edge(list(net.layers)[-2],extra_output)
with pytest.raises(ValueError) as excinfo:
if network_formulation == 'FullSpace':
formulation = FullSpaceNNFormulation(net)
elif network_formulation == 'ReducedSpace':
formulation = ReducedSpaceNNFormulation(net)
expected_msg = "Multiple output layers are not currently supported."
assert str(excinfo.value) == expected_msg

def test_FullSpaceNNFormulation_invalid_network():
_test_formulation_initialize_extra_input("FullSpace")
_test_formulation_added_extra_input("FullSpace")
_test_formulation_build_extra_input("FullSpace")
_test_formulation_initialize_extra_output("FullSpace")
_test_formulation_added_extra_output("FullSpace")

def test_ReducedSpaceNNFormulation_invalid_network():
# _test_formulation_initialize_extra_input("ReducedSpace")
_test_formulation_added_extra_input("ReducedSpace")
_test_formulation_build_extra_input("ReducedSpace")
# _test_formulation_initialize_extra_output("ReducedSpace")
_test_formulation_added_extra_output("ReducedSpace")

def test_ReluPartitionFormulation_invalid_network():
_test_formulation_added_extra_input("relu")
_test_formulation_build_extra_input("relu")
_test_formulation_added_extra_output("relu")

def _test_dense_layer_multiple_predecessors(layer_type):
m = pyo.ConcreteModel()
m.neural_net_block = OmltBlock()
net, y = two_node_network(None, -2.0)
extra_input = InputLayer([1])
test_layer = list(net.layers)[2]
net.add_layer(extra_input)
net.add_edge(extra_input,test_layer)
with pytest.raises(ValueError) as excinfo:
if layer_type == 'PartitionBased':
partition_based_dense_relu_layer(m,net,m,test_layer,None)
elif layer_type == 'ReducedSpace':
reduced_space_dense_layer(m,net,m,test_layer,None)
expected_msg = f"Layer {test_layer} has multiple predecessors."
assert str(excinfo.value) == expected_msg

def _test_dense_layer_no_predecessors(layer_type):
"""
Layer type can be "ReducedSpace", or "PartitionBased".
"""
m = pyo.ConcreteModel()
net = NetworkDefinition(scaled_input_bounds=[(-10.0, 10.0)])

test_layer = DenseLayer(
[1],
[1, 2],
activation=None,
weights=np.array([[1.0, -1.0]]),
biases=np.array([1.0, 2.0]),
)
net.add_layer(test_layer)
with pytest.raises(ValueError) as excinfo:
if layer_type == 'PartitionBased':
partition_based_dense_relu_layer(m,net,m,test_layer,None)
elif layer_type == 'ReducedSpace':
reduced_space_dense_layer(m,net,m,test_layer,None)
expected_msg = f"Layer {test_layer} is not an input layer, but has no predecessors."
assert str(excinfo.value) == expected_msg

def test_partition_based_dense_layer_predecessors():
_test_dense_layer_multiple_predecessors("PartitionBased")
_test_dense_layer_no_predecessors("PartitionBased")

def test_reduced_space_dense_layer_predecessors():
_test_dense_layer_multiple_predecessors("ReducedSpace")
_test_dense_layer_no_predecessors("ReducedSpace")

0 comments on commit d65b928

Please sign in to comment.