enforce-nn 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. enforce_nn-1.0.0/.gitignore +108 -0
  2. enforce_nn-1.0.0/LICENSE +21 -0
  3. enforce_nn-1.0.0/PKG-INFO +349 -0
  4. enforce_nn-1.0.0/README.md +308 -0
  5. enforce_nn-1.0.0/notebooks/.gitkeep +0 -0
  6. enforce_nn-1.0.0/notebooks/analysis/function_fitting_equality_results_analysis.ipynb +1040 -0
  7. enforce_nn-1.0.0/notebooks/analysis/linearization_visual.ipynb +265 -0
  8. enforce_nn-1.0.0/notebooks/analysis/memory_footprint.ipynb +232 -0
  9. enforce_nn-1.0.0/notebooks/tutorials/01_equality_constraints.ipynb +544 -0
  10. enforce_nn-1.0.0/notebooks/tutorials/02_inequality_constraints.ipynb +535 -0
  11. enforce_nn-1.0.0/notebooks/tutorials/03_parametric_optimization.ipynb +4647 -0
  12. enforce_nn-1.0.0/pyproject.toml +78 -0
  13. enforce_nn-1.0.0/scripts/plot_sin_ineq_all_benchmarking.py +126 -0
  14. enforce_nn-1.0.0/scripts/register_baselines.py +107 -0
  15. enforce_nn-1.0.0/scripts/run_benchmark.py +702 -0
  16. enforce_nn-1.0.0/scripts/run_hyperparameter_study.sh +36 -0
  17. enforce_nn-1.0.0/src/__init__.py +0 -0
  18. enforce_nn-1.0.0/src/benchmark_problems/config_benchmarking.py +175 -0
  19. enforce_nn-1.0.0/src/benchmark_problems/engineering_problems/extraction_column.py +26 -0
  20. enforce_nn-1.0.0/src/benchmark_problems/engineering_problems/pooling.py +58 -0
  21. enforce_nn-1.0.0/src/benchmark_problems/function_fitting/equality/constraints.py +35 -0
  22. enforce_nn-1.0.0/src/benchmark_problems/function_fitting/equality/functions.py +30 -0
  23. enforce_nn-1.0.0/src/benchmark_problems/function_fitting/inequality/sin_ineq.py +49 -0
  24. enforce_nn-1.0.0/src/benchmark_problems/parametric_optimization/opt_problem.py +132 -0
  25. enforce_nn-1.0.0/src/benchmark_problems/parametric_optimization/ssl_loss.py +51 -0
  26. enforce_nn-1.0.0/src/enforce/__init__.py +0 -0
  27. enforce_nn-1.0.0/src/enforce/config.py +72 -0
  28. enforce_nn-1.0.0/src/enforce/fb_inequality_constraints.py +199 -0
  29. enforce_nn-1.0.0/src/enforce/model.py +730 -0
  30. enforce_nn-1.0.0/src/engines/__init__.py +0 -0
  31. enforce_nn-1.0.0/src/engines/evaluate.py +203 -0
  32. enforce_nn-1.0.0/src/engines/train.py +229 -0
  33. enforce_nn-1.0.0/src/visualization/__init__.py +0 -0
  34. enforce_nn-1.0.0/src/visualization/plot_benchmarking.py +626 -0
  35. enforce_nn-1.0.0/static/ENFORCE_graphical_abstract.png +0 -0
  36. enforce_nn-1.0.0/static/profile_GL.png +0 -0
  37. enforce_nn-1.0.0/tests/__init__.py +0 -0
  38. enforce_nn-1.0.0/tests/fixtures/benchmark_baselines/.gitkeep +0 -0
  39. enforce_nn-1.0.0/tests/fixtures/benchmark_baselines/extraction_column.json +77 -0
  40. enforce_nn-1.0.0/tests/fixtures/benchmark_baselines/function_fitting.json +35 -0
  41. enforce_nn-1.0.0/tests/fixtures/benchmark_baselines/nonconvex_linear.json +19 -0
  42. enforce_nn-1.0.0/tests/fixtures/benchmark_baselines/nonconvex_nonlinear.json +19 -0
  43. enforce_nn-1.0.0/tests/fixtures/benchmark_baselines/pooling.json +75 -0
  44. enforce_nn-1.0.0/tests/fixtures/benchmark_baselines/sin_ineq.json +43 -0
  45. enforce_nn-1.0.0/tests/test_benchmark_reproducibility.py +355 -0
  46. enforce_nn-1.0.0/tests/test_benchmark_smoke.py +169 -0
  47. enforce_nn-1.0.0/tests/test_benchmarking_config.py +383 -0
  48. enforce_nn-1.0.0/tests/test_enforce_config.py +131 -0
  49. enforce_nn-1.0.0/tests/test_enforce_model.py +274 -0
  50. enforce_nn-1.0.0/tests/test_evaluator.py +385 -0
  51. enforce_nn-1.0.0/tests/test_trainer.py +348 -0
  52. enforce_nn-1.0.0/tests/utils.py +38 -0
  53. enforce_nn-1.0.0/uv.lock +1944 -0
@@ -0,0 +1,108 @@
1
+ training_output/
2
+ data/
3
+ .ruff_cache/
4
+ # Byte-compiled / optimized / DLL files
5
+ __pycache__/
6
+ *.py[cod]
7
+ *$py.class
8
+ .claude
9
+
10
+ # C extensions
11
+ *.so
12
+
13
+ # Distribution / packaging
14
+ .Python
15
+ build/
16
+ develop-eggs/
17
+ dist/
18
+ downloads/
19
+ eggs/
20
+ .eggs/
21
+ lib/
22
+ lib64/
23
+ parts/
24
+ sdist/
25
+ var/
26
+ wheels/
27
+ *.egg-info/
28
+ .installed.cfg
29
+ *.egg
30
+
31
+ # PyInstaller
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py,cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+
53
+ # Jupyter Notebook
54
+ .ipynb_checkpoints
55
+
56
+ # pyenv
57
+ .python-version
58
+
59
+ # Environments
60
+ .env
61
+ .venv
62
+ env/
63
+ venv/
64
+ ENV/
65
+ env.bak/
66
+ venv.bak/
67
+
68
+ # Spyder project settings
69
+ .spyderproject
70
+ .spyproject
71
+
72
+ # Rope project settings
73
+ .ropeproject
74
+
75
+ # mkdocs documentation
76
+ /site
77
+
78
+ # mypy
79
+ .mypy_cache/
80
+ .dmypy.json
81
+ dmypy.json
82
+
83
+ # Pyre type checker
84
+ .pyre/
85
+
86
+ # pytype static type analyzer
87
+ .pytype/
88
+
89
+ # profiling data
90
+ .profdata
91
+
92
+ # vscode settings
93
+ .vscode/
94
+
95
+ # Editors and IDEs
96
+ .idea/
97
+ *.swp
98
+ *.swo
99
+ *.swn
100
+
101
+ # OS generated files
102
+ .DS_Store
103
+ .DS_Store?
104
+ ._*
105
+ .Spotlight-V100
106
+ .Trashes
107
+ ehthumbs.db
108
+ Thumbs.db
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Giacomo Lastrucci (Delft University of Technology)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,349 @@
1
+ Metadata-Version: 2.4
2
+ Name: enforce-nn
3
+ Version: 1.0.0
4
+ Summary: A hard-constrained neural network framework that enforces nonlinear equality and inequality constraints at inference time via adaptive-depth neural projection.
5
+ Project-URL: Homepage, https://www.pi-research.org/
6
+ Project-URL: Source, https://github.com/process-intelligence-research/ENFORCE
7
+ Author-email: Giacomo Lastrucci <G.Lastrucci@tudelft.nl>, Artur Schweidtmann <A.Schweidtmann@tudelft.nl>
8
+ License: The MIT License (MIT)
9
+
10
+ Copyright (c) 2026 Giacomo Lastrucci (Delft University of Technology)
11
+
12
+ Permission is hereby granted, free of charge, to any person obtaining a copy
13
+ of this software and associated documentation files (the "Software"), to deal
14
+ in the Software without restriction, including without limitation the rights
15
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
+ copies of the Software, and to permit persons to whom the Software is
17
+ furnished to do so, subject to the following conditions:
18
+
19
+ The above copyright notice and this permission notice shall be included in
20
+ all copies or substantial portions of the Software.
21
+
22
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
28
+ THE SOFTWARE.
29
+ License-File: LICENSE
30
+ Keywords: constrained learning,hard-constrained neural networks,parametric optimization,physics-informed machine learning,trustworthy AI
31
+ Classifier: License :: OSI Approved :: MIT License
32
+ Classifier: Operating System :: OS Independent
33
+ Classifier: Programming Language :: Python :: 3
34
+ Requires-Python: >=3.10
35
+ Requires-Dist: matplotlib>=3.7
36
+ Requires-Dist: numpy>=1.24
37
+ Requires-Dist: pandas>=2.0
38
+ Requires-Dist: scikit-learn>=1.3
39
+ Requires-Dist: torch>=2.0
40
+ Description-Content-Type: text/markdown
41
+
42
+ <p align="center">
43
+ <!-- You can add your logo in the _src_ below -->
44
+ <img src="https://www.pi-research.org/media/logo_hu8494cc98fadf15586318dd8eaf906d76_68826_0x70_resize_lanczos_3.png" />
45
+ </p>
46
+
47
+ # ENFORCE - Nonlinear Constrained Learning with Adaptive-depth Neural Projection
48
+
49
+ ![ENFORCE graphical abstract](static/ENFORCE_graphical_abstract.png)
50
+
51
+ [![arXiv](https://img.shields.io/badge/arXiv-2502.06774-b31b1b.svg)](https://arxiv.org/abs/2502.06774)
52
+
53
+ **Nonlinear Constrained Learning with Adaptive-depth Neural Projection.**
54
+
55
+ ENFORCE combines a neural network backbone with an **AdaNP** (Adaptive-depth Neural Projection) module to drive predictions toward feasibility with respect to nonlinear equality and inequality constraints. At each forward pass, AdaNP iteratively applies a linearize-and-project correction - an SQP-inspired Gauss-Newton step - until the constraint residual falls below a prescribed tolerance ε.
56
+
57
+ For constraints that are **affine in the output** `y`, a single NP step achieves exact feasibility. For general nonlinear constraints, ε-feasibility is obtained locally: under standard regularity conditions (LICQ, C² smoothness) and when the backbone prediction is sufficiently close to the constraint manifold, AdaNP reduces the residual `‖c(x,ỹ)‖` below ε with a linear convergence rate. The model is trained with standard unconstrained optimization (Adam), not constrained solvers.
58
+
59
+ Inequality constraints `g(x,y) ≤ 0` are handled via a Fischer-Burmeister (FB) reformulation that converts them into equalities in an extended output space `[y, λ]`, so the same AdaNP projection applies without modification.
60
+
61
+ ## Reference
62
+
63
+ If you use ENFORCE in your work, please cite the [paper](https://arxiv.org/abs/2502.06774):
64
+
65
+ ```bibtex
66
+ @Article{Lastrucci2025_ENFORCENonlinearConstrained,
67
+ author = {Lastrucci, Giacomo and Schweidtmann, Artur M.},
68
+ journal = {arXiv preprint arXiv:2502.06774},
69
+ title = {ENFORCE: Nonlinear Constrained Learning with Adaptive-depth Neural Projection},
70
+ year = {2025},
71
+ copyright = {arXiv.org perpetual, non-exclusive license},
72
+ doi = {https://doi.org/10.48550/arXiv.2502.06774},
73
+ keywords = {Machine Learning (cs.LG), FOS: Computer and information sciences},
74
+ publisher = {arXiv},
75
+ }
76
+ ```
77
+
78
+ ## Installation
79
+
80
+ ### pip
81
+
82
+ ```
83
+ pip install enforce-nn
84
+ ```
85
+
86
+ PyTorch must be installed separately for your hardware (CPU or CUDA):
87
+
88
+ ```
89
+ # CPU
90
+ pip install torch --index-url https://download.pytorch.org/whl/cpu
91
+
92
+ # CUDA - see https://pytorch.org/get-started/locally/ for the right command
93
+ ```
94
+
95
+ ### uv
96
+
97
+ ```
98
+ uv add enforce-nn
99
+ ```
100
+
101
+ By default this resolves PyTorch from the CPU index (configured in `pyproject.toml`). For CUDA, install PyTorch manually following the [PyTorch installation guide](https://pytorch.org/get-started/locally/) and override the source entry.
102
+
103
+ ### From source
104
+
105
+ ```
106
+ git clone https://github.com/giacomolastrucci/ENFORCE
107
+ cd ENFORCE
108
+ pip install -e .
109
+ # or
110
+ uv sync
111
+ ```
112
+
113
+ ## Quick start
114
+
115
+ ### Supervised - nonlinear equality constraint
116
+
117
+ Fit `x → (y₁, y₂)` subject to the nonlinear constraint `(0.5 y₁)² + x² + y₂ = 0`:
118
+
119
+ ```python
120
+ import torch
121
+ import numpy as np
122
+ from enforce import ENFORCEConfig, ENFORCE
123
+ from enforce.engines.train import Trainer, TrainingConfig
124
+ from enforce.engines.evaluate import Evaluator, EvaluationConfig
125
+ from enforce.data.data_utils import generate_data, scale_data
126
+
127
+ # 1. Data
128
+ x_train, y_train = generate_data([...], n=500)
129
+ x_test, y_test = generate_data([...], n=200)
130
+ x_tr_s, y_tr_s, x_te_s, y_te_s, sp = scale_data(x_train, y_train, x_test, y_test)
131
+
132
+ # 2. Constraint c(x, y) -> [BS, NC] (operates on unscaled x and y)
133
+ def my_constraint(x, y):
134
+ return ((0.5 * y[:, 0])**2 + x[:, 0]**2 + y[:, 1]).unsqueeze(1)
135
+
136
+ # 3. Build
137
+ scaling_input = (torch.tensor(sp["input_mean"]), torch.tensor(sp["input_std"]))
138
+ scaling_output = (torch.tensor(sp["output_mean"]), torch.tensor(sp["output_std"]))
139
+ cfg = ENFORCEConfig(input_neurons=1, output_neurons=2, hidden_neurons=64,
140
+ hidden_layers=1, training_tolerance=1e-4,
141
+ inference_tolerance=1e-6, max_it=100,
142
+ supervised=True, weight_loss_displacement=0.5)
143
+ model = ENFORCE(scaling_input=scaling_input, scaling_output=scaling_output,
144
+ c=my_constraint, config=cfg, constrained=True, weighting_option=1)
145
+
146
+ # 4. Train / evaluate (do NOT wrap in torch.no_grad() - AdaNP needs autograd)
147
+ x_tr_t = torch.tensor(x_tr_s, dtype=torch.float32)
148
+ y_tr_t = torch.tensor(y_tr_s, dtype=torch.float32)
149
+ x_te_t = torch.tensor(x_te_s, dtype=torch.float32)
150
+ y_te_t = torch.tensor(y_te_s, dtype=torch.float32)
151
+
152
+ model = Trainer(model, TrainingConfig(epochs=2000)).fit(x_tr_t, y_tr_t)
153
+ result = Evaluator(model, EvaluationConfig()).evaluate(x_te_t, y_te_t, sp)
154
+
155
+ preds = result.predictions # shape [N, 2], already unscaled
156
+ ```
157
+
158
+ ### Self-supervised - parametric optimization with inequality
159
+
160
+ For each `x ∈ [2, 4]`, minimize `‖y‖²` subject to `y₁² + y₂ = x` (equality) and `y₁ ≥ 0` (inequality via FB):
161
+
162
+ ```python
163
+ import torch, torch.nn as nn, numpy as np
164
+ from enforce import ENFORCEConfig, ENFORCE
165
+ from enforce.fb_inequality_constraints import FischerBurmeisterReformulation
166
+ from enforce.engines.train import Trainer, TrainingConfig
167
+ from enforce.engines.evaluate import Evaluator, EvaluationConfig
168
+ from enforce.data.data_utils import scale_data
169
+
170
+ # 1. Constraints
171
+ def parabola(x, y): return (y[:, 0]**2 + y[:, 1] - x[:, 0]).unsqueeze(1)
172
+ def g_nonneg(x, y): return -y[:, 0] # y1 >= 0 => g = -y1 <= 0
173
+
174
+ fb = FischerBurmeisterReformulation(n_original_outputs=2, inequalities=[g_nonneg])
175
+
176
+ def c_full(x, y_ext): # NC=2 <= NO=3 ✓
177
+ y = y_ext[:, :2]
178
+ return torch.cat([parabola(x, y), fb(x, y_ext)], dim=1)
179
+
180
+ # 2. SSL objective - minimize ||y||²
181
+ class MinNorm(nn.Module):
182
+ def forward(self, x, y_ext):
183
+ return torch.mean(torch.sum(y_ext[:, :2]**2, dim=1))
184
+
185
+ # 3. Dummy labels (no targets needed in self-supervised mode)
186
+ N = 2000
187
+ x_train = np.random.uniform(2.0, 4.0, (N, 1)).astype(np.float32)
188
+ y_dummy = fb.extend_outputs(np.zeros((N, 2), dtype=np.float32))
189
+ x_tr_s, y_tr_s, _, _, sp = scale_data(x_train, y_dummy, x_train, y_dummy)
190
+
191
+ scaling_input = (torch.tensor(sp["input_mean"]), torch.tensor(sp["input_std"]))
192
+ scaling_output = (torch.tensor(sp["output_mean"]), torch.tensor(sp["output_std"]))
193
+
194
+ # 4. Build - output_neurons=fb.no (network predicts y only; λ appended in forward())
195
+ cfg = ENFORCEConfig(input_neurons=1, output_neurons=fb.no, hidden_neurons=64,
196
+ hidden_layers=2, training_tolerance=1e-4,
197
+ inference_tolerance=1e-6, max_it=100,
198
+ supervised=False, weight_loss_displacement=0.5)
199
+ model = ENFORCE(scaling_input=scaling_input, scaling_output=scaling_output,
200
+ c=c_full, fb=fb, ssl_loss=MinNorm(), config=cfg,
201
+ constrained=True, weighting_option=1)
202
+
203
+ # 5. Train / evaluate
204
+ x_tr_t = torch.tensor(x_tr_s, dtype=torch.float32)
205
+ y_tr_t = torch.tensor(y_tr_s, dtype=torch.float32)
206
+ model = Trainer(model, TrainingConfig(epochs=2000, n_original_outputs=fb.no)).fit(x_tr_t, y_tr_t)
207
+
208
+ result = Evaluator(model, EvaluationConfig(
209
+ n_original_outputs=fb.no, inequalities=fb.inequalities
210
+ )).evaluate(x_tr_t, y_tr_t, sp)
211
+ preds = fb.extract_outputs(result.predictions) # [N, 2] - y1, y2 only
212
+ ```
213
+
214
+ ## Tutorials
215
+
216
+ Step-by-step notebooks in the `notebooks/tutorials/` folder:
217
+
218
+ | Notebook | Topic |
219
+ |---|---|
220
+ | `01_equality_constraints.ipynb` | Supervised fitting with a nonlinear equality constraint (unit circle) |
221
+ | `02_inequality_constraints.ipynb` | Supervised fitting with inequality bounds via Fischer-Burmeister |
222
+ | `03_parametric_optimization.ipynb` | Self-supervised parametric optimization - mixed equality + inequality, MLP comparison |
223
+
224
+ ## How it works
225
+
226
+ ENFORCE appends an **AdaNP** module to any backbone network. Each NP layer solves a linearized QP: given the backbone output `ŷ` and the constraint Jacobian `B = ∂c/∂y|_{x,ŷ}`, the closed-form correction step is
227
+
228
+ ```
229
+ ỹ = (I − Bᵀ(BBᵀ)⁻¹B) ŷ + Bᵀ(BBᵀ)⁻¹v, with v = Bŷ − c(x, ŷ)
230
+ ```
231
+
232
+ This is the solution to the locally linearized projection problem and corresponds to a Gauss-Newton SQP step (without Hessian of constraints). AdaNP stacks NP layers adaptively until `‖c(x, ỹ)‖ < ε` or `max_it` is reached, relinearizing at each iterate. The Jacobian `B` is computed via automatic differentiation through the constraint function `c` only (not the backbone), so the per-step cost is `O(N_C³)`.
233
+
234
+ **Scope of the ε-feasibility claim.** For constraints affine in `y`, a single NP step achieves exact feasibility. For nonlinear constraints, convergence is local: it requires the backbone prediction to lie in a neighborhood of the constraint manifold where LICQ holds and the linearization is accurate. If the backbone is far from feasibility or the Jacobian is ill-conditioned, residuals may remain above ε within the depth cap.
235
+
236
+ **Inequality constraints** `g(x,y) ≤ 0` are reformulated using the Fischer-Burmeister function
237
+
238
+ ```
239
+ φ_FB(λᵢ, −gᵢ) = √(λᵢ² + gᵢ² + ε_FB) − λᵢ + gᵢ = 0
240
+ ```
241
+
242
+ which encodes the KKT complementarity conditions as an equality. AdaNP then operates in the extended space `[y, λ]` without any modification to the core algorithm.
243
+
244
+ **Training.** ENFORCE is trained with standard Adam on a loss `ℓ = ℓ_task + λ_D ‖ŷ − ỹ‖² + λ_C ‖c(x,ỹ)‖`, where the displacement penalty `λ_D ‖ŷ − ỹ‖²` encourages the backbone to produce predictions already close to the constraints manifold, reducing the depth needed at inference. An adaptive activation heuristic (inspired by trust-region methods) enables AdaNP only when the projection improves a task-specific loss measure, providing an automatic warm-up phase before constrained learning begins.
245
+
246
+ ## Reproducing paper results
247
+
248
+ The original benchmark datasets can be downloaded from [here](https://surfdrive.surf.nl/s/wxH67jTWfAbqTH5) and placed in `data/raw`. The original benchmark training results can be downloaded from [here](https://surfdrive.surf.nl/s/KN5W6fBrWL8Ftry) (7.6 GB).
249
+
250
+ All benchmarks are run through `scripts/run_benchmark.py`. Select the problem by setting `PROBLEM` in `src/benchmark_problems/config_benchmarking.py`, then run from the repo root:
251
+
252
+ ```
253
+ python scripts/run_benchmark.py
254
+ ```
255
+
256
+ ### Function fitting (equality constraint)
257
+
258
+ ```python
259
+ # src/benchmark_problems/config_benchmarking.py
260
+ PROBLEM = "function_fitting"
261
+ MODEL = "BOTH" # trains ENFORCE and MLP baseline
262
+ ```
263
+
264
+ To reproduce the hyperparameter study (sweep over `λ_D` and `ε_T`):
265
+
266
+ ```
267
+ bash scripts/run_hyperparameter_study.sh
268
+ ```
269
+
270
+ ### Function fitting with inequality constraints
271
+
272
+ ```python
273
+ PROBLEM = "sin_ineq"
274
+ MODEL = "BOTH"
275
+ ```
276
+
277
+ ### Engineering case studies
278
+
279
+ ```python
280
+ PROBLEM = "extraction_column" # extractive distillation surrogate
281
+ # or
282
+ PROBLEM = "pooling" # pooling problem (equality + inequality)
283
+ MODEL = "BOTH"
284
+ ```
285
+
286
+ Both require the datasets from [Iftakher et al.](https://github.com/SOULS-TAMU/kkt-hardnet). Download it from [here](https://surfdrive.surf.nl/s/wxH67jTWfAbqTH5). Place the CSV files under `data/raw/<problem>/` as expected by the data paths in `config_benchmarking.py`.
287
+
288
+ ### Parametric optimization benchmarks
289
+
290
+ ```python
291
+ PROBLEM = "nonconvex_linear" # nonconvex objective, linear equality constraints
292
+ # or
293
+ PROBLEM = "nonconvex_nonlinear" # nonconvex objective, nonlinear equality constraints
294
+ MODEL = "ENFORCE"
295
+ ```
296
+
297
+ These problems require pre-generated data files. [Download](https://surfdrive.surf.nl/s/wxH67jTWfAbqTH5) them and place them under `data/raw/<problem>/` following the filenames in `config_benchmarking.py`. Data can be generated from the DC3 [repository](https://github.com/locuslab/DC3).
298
+
299
+ ### General settings
300
+
301
+ Key flags in `src/benchmark_problems/config_benchmarking.py`:
302
+
303
+ | Flag | Default | Effect |
304
+ |---|---|---|
305
+ | `MODEL` | `"BOTH"` | `"ENFORCE"`, `"MLP"`, or `"BOTH"` |
306
+ | `N` | `5` | number of independent runs |
307
+ | `PLOT` | `True` | save result figures |
308
+ | `SAVE` | `True` | save model weights and metrics |
309
+ | `FIX_SEED` | `False` | fix random seed across runs |
310
+
311
+ ## Contributors
312
+
313
+ | | | |
314
+ | --- | --- | --- |
315
+ | <img src="static/profile_GL.png" width="50"> | [Giacomo Lastrucci](https://www.pi-research.org/author/giacomo-lastrucci/) | <a href="https://www.linkedin.com/in/giacomo-lastrucci/" rel="nofollow noreferrer"> <img src="https://i.sstatic.net/gVE0j.png" > </a> <a href="https://scholar.google.com/citations?user=P0_vdtQAAAAJ&hl=en" rel="nofollow noreferrer"> <img src="https://raw.githubusercontent.com/process-intelligence-research/pyDEXPI/master/docs/logos/google-scholar-square.svg" width="14"> </a> |
316
+ | <img src="https://raw.githubusercontent.com/process-intelligence-research/pyDEXPI/master/docs/photos/Artur.jpg" width="50"> | [Artur M. Schweidtmann](https://www.pi-research.org/author/artur-schweidtmann/) | <a href="https://www.linkedin.com/in/schweidtmann/" rel="nofollow noreferrer"> <img src="https://i.sstatic.net/gVE0j.png" > </a> <a href="https://scholar.google.com/citations?user=g-GwouoAAAAJ&hl=en" rel="nofollow noreferrer"> <img src="https://raw.githubusercontent.com/process-intelligence-research/pyDEXPI/master/docs/logos/google-scholar-square.svg" width="14"> </a> |
317
+
318
+
319
+ ## Acknowledgements
320
+
321
+ This research is supported by Shell Global Solutions International B.V., for which we express sincere gratitude.
322
+
323
+ ## License
324
+
325
+ MIT - see `LICENSE`.
326
+
327
+ **Copyright (C) 2025 Artur Schweidtmann, TU Delft**
328
+
329
+ ## 👨‍💼 Contact & Support
330
+
331
+ **Dr. Artur Schweidtmann**
332
+ *Process Intelligence Research*
333
+ *TU Delft*
334
+
335
+ ### Connect with us:
336
+ <p align="left">
337
+ <a href="https://twitter.com/ASchweidtmann" target="_blank">
338
+ <img align="center" src="https://img.shields.io/badge/X-000000?style=for-the-badge&logo=x&logoColor=white" alt="X (Twitter)" />
339
+ </a>
340
+ </p>
341
+ <p align="left">
342
+ <a href="https://www.linkedin.com/in/schweidtmann/" target="_blank">
343
+ <img src="https://img.shields.io/badge/LinkedIn-0077B5?style=for-the-badge&logo=linkedin&logoColor=white" alt="LinkedIn" />
344
+ </a>
345
+ </p>
346
+
347
+ ---
348
+
349
+ *Built with ❤️ by the Process Intelligence Research team at TU Delft*