Skip to content

Commit

Permalink
Merge pull request #137 from jezsadler/main
Browse files Browse the repository at this point in the history
Replacing assert statements with Exceptions
  • Loading branch information
rmisener authored Dec 4, 2023
2 parents f50290b + 7d660c4 commit b43ac5e
Show file tree
Hide file tree
Showing 12 changed files with 953 additions and 104 deletions.
14 changes: 12 additions & 2 deletions src/omlt/gbt/gbt_formulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,13 +197,23 @@ def _branching_y(tree_id, branch_node_id):
node_mask = (nodes_tree_ids == tree_id) & (nodes_node_ids == branch_node_id)
feature_id = nodes_feature_ids[node_mask]
branch_value = nodes_values[node_mask]
assert len(feature_id) == 1 and len(branch_value) == 1
if len(branch_value) != 1:
raise ValueError(
f"The given tree_id and branch_node_id do not uniquely identify a branch value."
)
if len(feature_id) != 1:
raise ValueError(
f"The given tree_id and branch_node_id do not uniquely identify a feature."
)
feature_id = feature_id[0]
branch_value = branch_value[0]
(branch_y_idx,) = np.where(
branch_value_by_feature_id[feature_id] == branch_value
)
assert len(branch_y_idx) == 1
if len(branch_y_idx) != 1:
raise ValueError(
f"The given tree_id and branch_node_id do not uniquely identify a branch index."
)
return block.y[feature_id, branch_y_idx[0]]

def _sum_of_z_l(tree_id, start_node_id):
Expand Down
26 changes: 19 additions & 7 deletions src/omlt/gbt/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,24 +56,36 @@ def scaling_object(self, scaling_object):
def _model_num_inputs(model):
"""Returns the number of input variables"""
graph = model.graph
assert len(graph.input) == 1
if len(graph.input) != 1:
raise ValueError(
f"Model graph input field is multi-valued {graph.input}. A single value is required."
)
return _tensor_size(graph.input[0])


def _model_num_outputs(model):
"""Returns the number of output variables"""
graph = model.graph
assert len(graph.output) == 1
if len(graph.output) != 1:
raise ValueError(
f"Model graph output field is multi-valued {graph.output}. A single value is required."
)
return _tensor_size(graph.output[0])


def _tensor_size(tensor):
"""Returns the size of an input tensor"""
tensor_type = tensor.type.tensor_type
size = None
for dim in tensor_type.shape.dim:
if dim.dim_value is not None and dim.dim_value > 0:
assert size is None
size = dim.dim_value
assert size is not None
dim_values = [
dim.dim_value
for dim in tensor_type.shape.dim
if dim.dim_value is not None and dim.dim_value > 0
]
if len(dim_values) == 1:
size = dim_values[0]
elif dim_values == []:
raise ValueError(f"Tensor {tensor} has no positive dimensions.")
else:
raise ValueError(f"Tensor {tensor} has multiple positive dimensions.")
return size
186 changes: 143 additions & 43 deletions src/omlt/io/onnx_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,17 @@ def parse_network(self, graph, scaling_object, input_bounds):
dim_value = 1
size.append(dim.dim_value)
dim_value *= dim.dim_value
assert dim_value is not None
if dim_value is None:
raise ValueError(
f'All dimensions in graph "{graph.name}" input tensor have 0 value.'
)
assert network_input is None
network_input = InputLayer(size)
self._node_map[input.name] = network_input
network.add_layer(network_input)

assert network_input is not None
if network_input is None:
raise ValueError(f'No valid input layer found in graph "{graph.name}".')

self._nodes = nodes
self._nodes_by_output = nodes_by_output
Expand Down Expand Up @@ -109,11 +113,14 @@ def parse_network(self, graph, scaling_object, input_bounds):
# Now connect inputs to the current node
for input in node_inputs:
self._nodes[input][2].append(node.name)
else:
assert node.op_type == "Constant"
elif node.op_type == "Constant":
for output in node.output:
value = _parse_constant_value(node)
self._constants[output] = value
else:
raise ValueError(
f'Nodes must have inputs or have op_type "Constant". Node "{node.name}" has no inputs and op_type "{node.op_type}".'
)

# traverse graph
self._node_stack = list(inputs)
Expand Down Expand Up @@ -169,34 +176,54 @@ def _visit_node(self, node, next_nodes):

def _consume_dense_nodes(self, node, next_nodes):
"""Starting from a MatMul node, consume nodes to form a dense Ax + b node."""
assert node.op_type == "MatMul"
assert len(node.input) == 2
if node.op_type != "MatMul":
raise ValueError(
f"{node.name} is a {node.op_type} node, only MatMul nodes can be used as starting points for consumption."
)
if len(node.input) != 2:
raise ValueError(
f"{node.name} input has {len(node.input)} dimensions, only nodes with 2 input dimensions can be used as starting points for consumption."
)

[in_0, in_1] = list(node.input)
input_layer, transformer = self._node_input_and_transformer(in_0)
node_weights = self._initializers[in_1]

assert len(next_nodes) == 1
if len(next_nodes) != 1:
raise ValueError(
f"Next nodes must have length 1, {next_nodes} has length {len(next_nodes)}"
)

# expect 'Add' node ahead
type_, node, maybe_next_nodes = self._nodes[next_nodes[0]]
assert type_ == "node"
assert node.op_type == "Add"
if type_ != "node":
raise TypeError(f"Expected a node next, got a {type_} instead.")
if node.op_type != "Add":
raise ValueError(
f"The first node to be consumed, {node.name}, is a {node.op_type} node. Only Add nodes are supported."
)

# extract biases
next_nodes = maybe_next_nodes
assert len(node.input) == 2
[in_0, in_1] = list(node.input)

if in_0 in self._initializers:
node_biases = self._initializers[in_0]
else:
assert in_1 in self._initializers
elif in_1 in self._initializers:
node_biases = self._initializers[in_1]
else:
raise ValueError(f"Node inputs were not found in graph initializers.")

assert len(node_weights.shape) == 2
assert node_weights.shape[1] == node_biases.shape[0]
assert len(node.output) == 1
if len(node_weights.shape) != 2:
raise ValueError(f"Node weights must be a 2-dimensional matrix.")
if node_weights.shape[1] != node_biases.shape[0]:
raise ValueError(
f"Node weights has {node_weights.shape[1]} columns; node biases has {node_biases.shape[0]} rows. These must be equal."
)
if len(node.output) != 1:
raise ValueError(
f"Node output is {node.output} but should be a single value."
)

input_output_size = _get_input_output_size(input_layer, transformer)

Expand Down Expand Up @@ -226,8 +253,14 @@ def _consume_dense_nodes(self, node, next_nodes):

def _consume_gemm_dense_nodes(self, node, next_nodes):
"""Starting from a Gemm node, consume nodes to form a dense aAB + bC node."""
assert node.op_type == "Gemm"
assert len(node.input) == 3
if node.op_type != "Gemm":
raise ValueError(
f"{node.name} is a {node.op_type} node, only Gemm nodes can be used as starting points for consumption."
)
if len(node.input) != 3:
raise ValueError(
f"{node.name} input has {len(node.input)} dimensions, only nodes with 3 input dimensions can be used as starting points for consumption."
)

attr = _collect_attributes(node)
alpha = attr["alpha"]
Expand Down Expand Up @@ -275,8 +308,15 @@ def _consume_conv_nodes(self, node, next_nodes):
Starting from a Conv node, consume nodes to form a convolution node with
(optional) activation function.
"""
assert node.op_type == "Conv"
assert len(node.input) in [2, 3]
if node.op_type != "Conv":
raise ValueError(
f"{node.name} is a {node.op_type} node, only Conv nodes can be used as starting points for consumption."
)
if len(node.input) not in [2, 3]:
raise ValueError(
f"{node.name} input has {len(node.input)} dimensions, only nodes with 2 or 3 input dimensions can be used as starting points for consumption."
)

if len(node.input) == 2:
[in_0, in_1] = list(node.input)
in_2 = None
Expand All @@ -295,18 +335,43 @@ def _consume_conv_nodes(self, node, next_nodes):
attr = _collect_attributes(node)

strides = attr["strides"]

# check only kernel shape and stride are set
# everything else is not supported
assert biases.shape == (out_channels,)
assert in_channels == input_output_size[0]
assert attr["kernel_shape"] == kernel_shape
assert attr["dilations"] == [1, 1]
assert attr["group"] == 1
if "pads" in attr:
assert not np.any(attr["pads"]) # pads all zero
assert len(kernel_shape) == len(strides)
assert len(input_output_size) == len(kernel_shape) + 1
if attr["kernel_shape"] != kernel_shape:
raise ValueError(
f"Kernel shape attribute {attr['kernel_shape']} does not match initialized kernel shape {kernel_shape}."
)
if len(kernel_shape) != len(strides):
raise ValueError(
f"Initialized kernel shape {kernel_shape} has {len(kernel_shape)} dimensions. Strides attribute has {len(strides)} dimensions. These must be equal."
)
if len(input_output_size) != len(kernel_shape) + 1:
raise ValueError(
f"Input/output size ({input_output_size}) must have one more dimension than initialized kernel shape ({kernel_shape})."
)

# Check input, output have correct dimensions
if biases.shape != (out_channels,):
raise ValueError(
f"Biases shape {biases.shape} must match output weights channels {(out_channels,)}."
)
if in_channels != input_output_size[0]:
raise ValueError(
f"Input/output size ({input_output_size}) first dimension must match input weights channels ({in_channels})."
)

# Other attributes are not supported
if "dilations" in attr and attr["dilations"] != [1, 1]:
raise ValueError(
f"{node} has non-identity dilations ({attr['dilations']}). This is not supported."
)
if attr["group"] != 1:
raise ValueError(
f"{node} has multiple groups ({attr['group']}). This is not supported."
)
if "pads" in attr and np.any(attr["pads"]):
raise ValueError(
f"{node} has non-zero pads ({attr['pads']}). This is not supported."
)

# generate new nodes for the node output
padding = 0
Expand All @@ -326,7 +391,10 @@ def _consume_conv_nodes(self, node, next_nodes):

# convolute image one channel at the time
# expect 2d image with channels
assert len(input_output_size) == 3
if len(input_output_size) != 3:
raise ValueError(
f"Expected a 2D image with channels, got {input_output_size}."
)

conv_layer = ConvLayer2D(
input_output_size,
Expand All @@ -343,8 +411,14 @@ def _consume_conv_nodes(self, node, next_nodes):

def _consume_reshape_nodes(self, node, next_nodes):
"""Parse a Reshape node."""
assert node.op_type == "Reshape"
assert len(node.input) == 2
if node.op_type != "Reshape":
raise ValueError(
f"{node.name} is a {node.op_type} node, only Reshape nodes can be used as starting points for consumption."
)
if len(node.input) != 2:
raise ValueError(
f"{node.name} input has {len(node.input)} dimensions, only nodes with 2 input dimensions can be used as starting points for consumption."
)
[in_0, in_1] = list(node.input)
input_layer = self._node_map[in_0]
new_shape = self._constants[in_1]
Expand All @@ -358,23 +432,34 @@ def _consume_pool_nodes(self, node, next_nodes):
Starting from a MaxPool node, consume nodes to form a pooling node with
(optional) activation function.
"""
assert node.op_type in _POOLING_OP_TYPES
if node.op_type not in _POOLING_OP_TYPES:
raise ValueError(
f"{node.name} is a {node.op_type} node, only MaxPool nodes can be used as starting points for consumption."
)
pool_func_name = "max"

# ONNX network should not contain indices output from MaxPool - not supported by OMLT
assert len(node.output) == 1
if len(node.output) != 1:
raise ValueError(
f"The ONNX contains indices output from MaxPool. This is not supported by OMLT."
)
if len(node.input) != 1:
raise ValueError(
f"{node.name} input has {len(node.input)} dimensions, only nodes with 1 input dimension can be used as starting points for consumption."
)

assert len(node.input) == 1
input_layer, transformer = self._node_input_and_transformer(node.input[0])
input_output_size = _get_input_output_size(input_layer, transformer)

# currently only support 2D image with channels.
if len(input_output_size) == 4:
# this means there is an extra dimension for number of batches
# batches not supported, so only accept if they're not there or there is only 1 batch
assert input_output_size[0] == 1
if input_output_size[0] != 1:
raise ValueError(
f"{node.name} has {input_output_size[0]} batches, only a single batch is supported."
)
input_output_size = input_output_size[1:]
assert len(input_output_size) == 3

in_channels = input_output_size[0]

Expand All @@ -385,11 +470,26 @@ def _consume_pool_nodes(self, node, next_nodes):

# check only kernel shape, stride, storage order are set
# everything else is not supported
assert ("dilations" not in attr) or (attr["dilations"] == [1, 1])
assert ("pads" not in attr) or (not np.any(attr["pads"]))
assert ("auto_pad" not in attr) or (attr["auto_pad"] == "NOTSET")
assert len(kernel_shape) == len(strides)
assert len(input_output_size) == len(kernel_shape) + 1
if "dilations" in attr and attr["dilations"] != [1, 1]:
raise ValueError(
f"{node.name} has non-identity dilations ({attr['dilations']}). This is not supported."
)
if "pads" in attr and np.any(attr["pads"]):
raise ValueError(
f"{node.name} has non-zero pads ({attr['pads']}). This is not supported."
)
if ("auto_pad" in attr) and (attr["auto_pad"] != "NOTSET"):
raise ValueError(
f"{node.name} has autopad set ({attr['auto_pad']}). This is not supported."
)
if len(kernel_shape) != len(strides):
raise ValueError(
f"Kernel shape {kernel_shape} has {len(kernel_shape)} dimensions. Strides attribute has {len(strides)} dimensions. These must be equal."
)
if len(input_output_size) != len(kernel_shape) + 1:
raise ValueError(
f"Input/output size ({input_output_size}) must have one more dimension than kernel shape ({kernel_shape})."
)

output_shape_wrapper = math.floor
if "ceil_mode" in attr and attr["ceil_mode"] == 1:
Expand Down
Loading

0 comments on commit b43ac5e

Please sign in to comment.