Skip to content

Commit 75be2db

Browse files
bammariemsunshine
andauthored
Resolve Failing Tests And Patch For Bounds in Linear Tree (#179)
This PR addresses several issues that have resulted in failing tests as well as a patch for unscaled bounds in linear-tree. Here is everything that is addressed in this PR - Adding a free disk space step since runners are running out of space for Github CI - Update pyproject.toml to point to https://github.com/All-Things-Optimal/linear-tree - Update pyproject.toml to ignore ruff "PLW1641" and "TRY003" - Limit multiple big-M tranfsformation to one thread - Skip MBM transformation tests until newest release of pyomo (>6.9.5) due to bug that prevents mbm from transforming blocks - Update ONNX parser to work with most recent version of torch for convolutional neural networks - Add tests to ONNX parser to check errors for strides and kernel_shape attributes in onnx graph for convolution nodes and reshape nodes - Skip import_networks.ipynb until tf2onnx addresses dependency issue (last tf2onnx release was january 2024) - Fix bug with unscaled input bounds being called before it is defined in lt_formulations.py - Address several ruff and ruff format issues **Legal Acknowledgement**\ By contributing to this software project, I agree my contributions are submitted under the BSD license. I represent I am authorized to make the contributions and grant the license. If my employer has rights to intellectual property that includes these contributions, I represent that I have received permission to make contributions and grant the required license on behalf of that employer. --------- Co-authored-by: Ethan Sunshine <93541000+emsunshine@users.noreply.github.com>
1 parent 7e698a7 commit 75be2db

18 files changed

Lines changed: 690 additions & 539 deletions

.github/workflows/tests.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ jobs:
1010
runs-on: ubuntu-22.04
1111
steps:
1212
- uses: actions/checkout@v4
13+
- name: Free disk space
14+
run: |
15+
sudo rm -rf /usr/share/dotnet
16+
sudo rm -rf /usr/local/lib/android
17+
sudo rm -rf /opt/ghc
18+
sudo rm -rf /usr/local/share/boost
19+
df -h
1320
- uses: actions/setup-python@v5
1421
with:
1522
python-version: "3.12"
@@ -23,6 +30,13 @@ jobs:
2330
runs-on: ubuntu-22.04
2431
steps:
2532
- uses: actions/checkout@v4
33+
- name: Free disk space
34+
run: |
35+
sudo rm -rf /usr/share/dotnet
36+
sudo rm -rf /usr/local/lib/android
37+
sudo rm -rf /opt/ghc
38+
sudo rm -rf /usr/local/share/boost
39+
df -h
2640
- uses: actions/setup-python@v5
2741
with:
2842
python-version: ${{ matrix.python-version }}
@@ -33,6 +47,13 @@ jobs:
3347
runs-on: ubuntu-22.04
3448
steps:
3549
- uses: actions/checkout@v4
50+
- name: Free disk space
51+
run: |
52+
sudo rm -rf /usr/share/dotnet
53+
sudo rm -rf /usr/local/lib/android
54+
sudo rm -rf /opt/ghc
55+
sudo rm -rf /usr/local/share/boost
56+
df -h
3657
- uses: actions/setup-python@v5
3758
with:
3859
python-version: "3.12"
@@ -46,6 +67,13 @@ jobs:
4667
runs-on: ubuntu-22.04
4768
steps:
4869
- uses: actions/checkout@v4
70+
- name: Free disk space
71+
run: |
72+
sudo rm -rf /usr/share/dotnet
73+
sudo rm -rf /usr/local/lib/android
74+
sudo rm -rf /opt/ghc
75+
sudo rm -rf /usr/local/share/boost
76+
df -h
4977
- uses: actions/setup-python@v5
5078
with:
5179
python-version: ${{ matrix.python-version }}
@@ -74,6 +102,13 @@ jobs:
74102
runs-on: ubuntu-22.04
75103
steps:
76104
- uses: actions/checkout@v4
105+
- name: Free disk space
106+
run: |
107+
sudo rm -rf /usr/share/dotnet
108+
sudo rm -rf /usr/local/lib/android
109+
sudo rm -rf /opt/ghc
110+
sudo rm -rf /usr/local/share/boost
111+
df -h
77112
- uses: actions/setup-python@v5
78113
with:
79114
python-version: ${{ matrix.python-version }}

docs/notebooks/neuralnet/import_network.ipynb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -864,7 +864,7 @@
864864
},
865865
{
866866
"cell_type": "code",
867-
"execution_count": 12,
867+
"execution_count": null,
868868
"metadata": {},
869869
"outputs": [
870870
{
@@ -885,7 +885,7 @@
885885
" torch.onnx.export(\n",
886886
" model,\n",
887887
" x,\n",
888-
" f,\n",
888+
" f.name,\n",
889889
" input_names=[\"input\"],\n",
890890
" output_names=[\"output\"],\n",
891891
" dynamic_axes={\"input\": {0: \"batch_size\"}, \"output\": {0: \"batch_size\"}},\n",

docs/notebooks/neuralnet/mnist_example_convolutional.ipynb

Lines changed: 386 additions & 379 deletions
Large diffs are not rendered by default.

docs/notebooks/neuralnet/mnist_example_dense.ipynb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,7 @@
362362
},
363363
{
364364
"cell_type": "code",
365-
"execution_count": 8,
365+
"execution_count": null,
366366
"metadata": {},
367367
"outputs": [],
368368
"source": [
@@ -371,7 +371,7 @@
371371
" torch.onnx.export(\n",
372372
" model2,\n",
373373
" x_temp,\n",
374-
" f,\n",
374+
" f.name,\n",
375375
" input_names=[\"input\"],\n",
376376
" output_names=[\"output\"],\n",
377377
" dynamic_axes={\"input\": {0: \"batch_size\"}, \"output\": {0: \"batch_size\"}},\n",

docs/notebooks/trees/bo_with_trees.ipynb

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -154,14 +154,15 @@
154154
},
155155
{
156156
"cell_type": "code",
157-
"execution_count": 52,
157+
"execution_count": null,
158158
"id": "c54d0dad",
159159
"metadata": {},
160160
"outputs": [],
161161
"source": [
162-
"def write_onnx_to_file(onnx_model, path, file_name=\"output.onnx\"):\n",
163-
" from pathlib import Path\n",
162+
"from pathlib import Path\n",
163+
"\n",
164164
"\n",
165+
"def write_onnx_to_file(onnx_model, path, file_name=\"output.onnx\"):\n",
165166
" with open(Path(path) / file_name, \"wb\") as onnx_file:\n",
166167
" onnx_file.write(onnx_model.SerializeToString())\n",
167168
" print(f\"Onnx model written to {onnx_file.name}\")"
@@ -248,7 +249,7 @@
248249
},
249250
{
250251
"cell_type": "code",
251-
"execution_count": 55,
252+
"execution_count": null,
252253
"id": "376f0893",
253254
"metadata": {
254255
"scrolled": false
@@ -268,14 +269,15 @@
268269
}
269270
],
270271
"source": [
272+
"import matplotlib.pyplot as plt\n",
273+
"\n",
271274
"# defining initial data\n",
272275
"random.seed(10)\n",
273276
"data = generate_samples(5, f_bnds)\n",
274277
"\n",
275278
"\n",
276279
"def plot_progress(data, input_bounds):\n",
277280
" # plot contour line and data points\n",
278-
" import matplotlib.pyplot as plt\n",
279281
"\n",
280282
" fig = plt.figure()\n",
281283
" ax = fig.add_axes([0, 0, 2, 2])\n",
@@ -363,7 +365,7 @@
363365
},
364366
{
365367
"cell_type": "code",
366-
"execution_count": 57,
368+
"execution_count": null,
367369
"id": "04352480",
368370
"metadata": {},
369371
"outputs": [
@@ -402,6 +404,7 @@
402404
}
403405
],
404406
"source": [
407+
"import matplotlib.pyplot as plt\n",
405408
"from tqdm.notebook import tqdm\n",
406409
"\n",
407410
"for _ in tqdm(range(80)):\n",
@@ -420,7 +423,6 @@
420423
"\n",
421424
"def plot_progress(data):\n",
422425
" # set up plot\n",
423-
" import matplotlib.pyplot as plt\n",
424426
"\n",
425427
" fig = plt.figure()\n",
426428
" ax = fig.add_axes([0, 0, 2, 2])\n",

pyproject.toml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ dependencies = [
1515
"pyomo>=6.7.3",
1616
"onnx",
1717
"onnxruntime",
18+
"onnxscript",
1819
]
1920
requires-python = ">=3.9"
2021
dynamic = ["version"]
@@ -23,7 +24,7 @@ license = { file = "LICENSE.rst" }
2324
description = "OMLT is a Python package for representing machine learning models (such as neural networks) within the Pyomo optimization environment."
2425

2526
[project.optional-dependencies]
26-
linear-tree = ["linear-tree"]
27+
linear-tree = ["linear-tree @ git+https://github.com/All-Things-Optimal/linear-tree.git@main"]
2728
keras = ["tensorflow", "keras"]
2829
keras-gpu = ["tensorflow[and-cuda]", "keras"]
2930
torch = ["torch", "torch-geometric"]
@@ -98,7 +99,9 @@ ignore = [
9899
"ANN002",
99100
"ANN201",
100101
"ANN202",
101-
"ANN204"
102+
"ANN204",
103+
"PLW1641",
104+
"TRY003"
102105
]
103106

104107
[tool.ruff.lint.pydocstyle]
@@ -171,6 +174,7 @@ module = [
171174
"tensorflow.*",
172175
"torch_geometric.*",
173176
"juliacall.*",
177+
"onnxscript.*",
174178
]
175179
ignore_missing_imports = true
176180

src/omlt/io/onnx_parser.py

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
ATTR_INT = 2
3131
ATTR_TENSOR = 4
3232
ATTR_INTS = 7
33+
ATTR_STRING = 3
3334

3435

3536
class NetworkParser:
@@ -262,7 +263,7 @@ def _consume_dense_nodes( # noqa: C901, PLR0912
262263

263264
input_output_size = _get_input_output_size(input_layer, transformer)
264265

265-
output_size = input_output_size[:-1] + [node_weights.shape[1]]
266+
output_size = [*input_output_size[:-1], node_weights.shape[1]]
266267

267268
activation = "linear"
268269
if len(next_nodes) == 1:
@@ -316,12 +317,12 @@ def _consume_gemm_dense_nodes(self, node, next_nodes):
316317
input_output_size = _get_input_output_size(input_layer, transformer)
317318

318319
# output is the same size as input except for the last dimension
319-
output_size = input_output_size[:-1] + [weights.shape[1]]
320+
output_size = [*input_output_size[:-1], weights.shape[1]]
320321

321322
activation = "linear"
322323
if len(next_nodes) == 1:
323324
# check if Relu
324-
type_, maybe_node, maybe_next_nodes = self._nodes[next_nodes[0]]
325+
_, maybe_node, maybe_next_nodes = self._nodes[next_nodes[0]]
325326
if maybe_node.op_type in _ACTIVATION_OP_TYPES:
326327
node = maybe_node
327328
activation = node.op_type.lower()
@@ -376,15 +377,25 @@ def _consume_conv_nodes(self, node, next_nodes): # noqa: PLR0912, C901, PLR0915
376377
biases = np.zeros(out_channels) if in_2 is None else self._initializers[in_2]
377378

378379
attr = _collect_attributes(node)
379-
380+
if "strides" not in attr:
381+
node_name = node.name
382+
msg = f"{node_name} is missing required 'strides' attribute."
383+
raise ValueError(msg)
380384
strides = attr["strides"]
385+
381386
# check only kernel shape and stride are set
382-
if attr["kernel_shape"] != kernel_shape:
383-
msg = (
384-
f"Kernel shape attribute {attr['kernel_shape']} does not match"
385-
f" initialized kernel shape {kernel_shape}."
386-
)
387-
raise ValueError(msg)
387+
if "kernel_shape" in attr:
388+
if attr["kernel_shape"] != kernel_shape:
389+
msg = (
390+
f"Kernel shape attribute {attr['kernel_shape']} does not match"
391+
f" initialized kernel shape {kernel_shape}."
392+
)
393+
raise ValueError(msg)
394+
else:
395+
# infer kernel shape from weights (ONNX default behavior)
396+
attr["kernel_shape"] = list(kernel_shape)
397+
# Assign to _kernel_shape attribute for testing purposes
398+
self._kernel_shape = list(kernel_shape)
388399
if len(kernel_shape) != len(strides):
389400
msg = (
390401
f"Initialized kernel shape {kernel_shape} has {len(kernel_shape)} "
@@ -437,7 +448,7 @@ def _consume_conv_nodes(self, node, next_nodes): # noqa: PLR0912, C901, PLR0915
437448
activation = "linear"
438449
if len(next_nodes) == 1:
439450
# check if Relu
440-
type_, maybe_node, maybe_next_nodes = self._nodes[next_nodes[0]]
451+
_, maybe_node, maybe_next_nodes = self._nodes[next_nodes[0]]
441452
if maybe_node.op_type in _ACTIVATION_OP_TYPES:
442453
node = maybe_node
443454
activation = maybe_node.op_type.lower()
@@ -479,7 +490,16 @@ def _consume_reshape_nodes(self, node, next_nodes):
479490
raise ValueError(msg)
480491
[in_0, in_1] = list(node.input)
481492
input_layer = self._node_map[in_0]
482-
new_shape = self._constants[in_1]
493+
if in_1 in self._constants:
494+
new_shape = self._constants[in_1]
495+
elif in_1 in self._initializers:
496+
new_shape = self._initializers[in_1]
497+
else:
498+
msg = (
499+
f"Reshape node {node.name} has shape input {in_1} "
500+
"that is neither a Constant nor an initializer."
501+
)
502+
raise KeyError(msg)
483503
output_size = np.empty(input_layer.output_size).reshape(new_shape).shape
484504
transformer = IndexMapper(input_layer.output_size, list(output_size))
485505
self._node_map[node.output[0]] = (transformer, input_layer)
@@ -584,7 +604,7 @@ def _consume_pool_nodes(self, node, next_nodes): # noqa: PLR0912, C901, PLR0915
584604
activation = "linear"
585605
if len(next_nodes) == 1:
586606
# check if Relu
587-
type_, maybe_node, maybe_next_nodes = self._nodes[next_nodes[0]]
607+
_, maybe_node, maybe_next_nodes = self._nodes[next_nodes[0]]
588608
if maybe_node.op_type in _ACTIVATION_OP_TYPES:
589609
node = maybe_node
590610
activation = maybe_node.op_type.lower()
@@ -624,6 +644,8 @@ def _collect_attributes(node):
624644
r[attr.name] = numpy_helper.to_array(attr.t)
625645
elif attr.type == ATTR_INTS: # INTS
626646
r[attr.name] = list(attr.ints)
647+
elif attr.type == ATTR_STRING: # STRING
648+
r[attr.name] = attr.s.decode("utf-8")
627649
else:
628650
msg = f"unhandled attribute type {attr.type}"
629651
raise RuntimeError(msg)

src/omlt/linear_tree/lt_formulation.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,9 @@ def _add_gdp_formulation_to_block( # noqa: PLR0913 C901
360360
# Use the input_bounds and the linear models in the leaves to calculate
361361
# the lower and upper bounds on the output variable. Required for Pyomo.GDP
362362
scaled_output_bounds = _build_output_bounds(model_definition, scaled_input_bounds)
363+
unscaled_output_bounds = _build_output_bounds(
364+
model_definition, unscaled_input_bounds
365+
)
363366

364367
# Outputs are automatically scaled based on whether inputs are scaled
365368
for output_idx in output_indices:
@@ -374,13 +377,6 @@ def _add_gdp_formulation_to_block( # noqa: PLR0913 C901
374377
max_scaled_output = np.max(scaled_output_bounds[:, 1])
375378
min_scaled_output = np.min(scaled_output_bounds[:, 0])
376379

377-
if unscaled_input_bounds is not None:
378-
unscaled_output_bounds = _build_output_bounds(
379-
model_definition, unscaled_input_bounds
380-
)
381-
block.outputs.setub(unscaled_output_bounds[1])
382-
block.outputs.setlb(unscaled_output_bounds[0])
383-
384380
if model_definition.is_scaled is True:
385381
block.intermediate_output = pe.Var(
386382
set_index, bounds=(min_scaled_output, max_scaled_output)
@@ -439,5 +435,7 @@ def disjunction_rule(b, tree):
439435

440436
transformation_string = "gdp." + transformation
441437

442-
if transformation != "custom":
438+
if transformation == "mbigm":
439+
pe.TransformationFactory(transformation_string).apply_to(block, threads=1)
440+
elif transformation != "custom":
443441
pe.TransformationFactory(transformation_string).apply_to(block)

src/omlt/neuralnet/layers/full_space.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def full_space_dense_layer(net_block, net, layer_block, layer): # noqa: PLR0912
1616
\end{align*}
1717
1818
"""
19-
input_layer, input_layer_block = _input_layer_and_block(net_block, net, layer)
19+
_, input_layer_block = _input_layer_and_block(net_block, net, layer)
2020

2121
constraint_factory = OmltConstraintFactory()
2222
layer_block.dense_layer = constraint_factory.new_constraint(
@@ -105,7 +105,7 @@ def full_space_gnn_layer(net_block, net, layer_block, layer):
105105
\end{cases}
106106
\end{align*}
107107
"""
108-
input_layer, input_layer_block = _input_layer_and_block(net_block, net, layer)
108+
_, input_layer_block = _input_layer_and_block(net_block, net, layer)
109109

110110
var_factory = OmltVarFactory()
111111
constraint_factory = OmltConstraintFactory()
@@ -237,7 +237,7 @@ def full_space_conv2d_layer(net_block, net, layer_block, layer):
237237
succ_layer.activation = layer.activation
238238
layer.activation = "linear"
239239

240-
input_layer, input_layer_block = _input_layer_and_block(net_block, net, layer)
240+
_, input_layer_block = _input_layer_and_block(net_block, net, layer)
241241
constraint_factory = OmltConstraintFactory()
242242

243243
layer_block.convolutional_layer = constraint_factory.new_constraint(

0 commit comments

Comments
 (0)