Problem
python/discopt/nn/ is named for one of the four things it does. It is the ML predictor embedding + trainable surrogate layer, and its own code says so — formulations/base.py:1 reads "Formulation wrappers for embedding trained ML models into discopt", not "networks".
What actually lives there today:
| Component |
Files |
Not a neural network |
| Decision trees / ensembles |
tree.py, formulations/tree_ensemble.py, TreeFormulation (formulations/base.py:12) |
✔ |
| sklearn tree/forest/GBM readers |
readers/sklearn_reader.py |
✔ |
| Trainable surrogate protocol |
surrogate.py (Surrogate, runtime_checkable) |
✔ — explicitly admits GP/kernel means, polynomials, splines, soft trees, symbolic formulas |
| Kernel expansion surrogate |
trainable.py:430 (TrainableKernelExpansion) |
✔ |
The dispatcher is literally named add_predictor() (predictor.py:24), not add_network. The name misdirects readers into thinking trees and non-NN surrogates are unsupported, when they are first-class.
A second, smaller misconception the name encourages: that this module is "the MIP-representable predictors module". Only two of four encodings are MIP — relu_bigm.py and formulations/tree_ensemble.py emit binaries + big-M; full_space.py emits dm.sigmoid/tanh/softplus (full_space.py:17-22) and reduced_space.py emits nested expressions with continuous intermediates and zero binaries (reduced_space.py:115-133). MIP-representability explains why two of the files must exist; it does not describe the module.
Proposal
Rename the package to discopt.ml, keeping discopt.nn as a deprecation shim.
ml matches the existing short-subsystem naming pattern (dae, gp, llm, mo, ro) and covers both regimes the module spans — frozen predictors you optimize over, and trainable surrogates you optimize with — without claiming either. (discopt.surrogates and discopt.predictors were considered; the first breaks the short-name pattern, the second misdescribes the trainable half, since a TrainableNetwork is not a predictor you embed.)
Note discopt.gp is geometric programming, so there is no collision with a Gaussian-process reading of these names.
Blast radius (measured 2026-09-11 on main)
139 reference sites outside the package itself
| Area |
Files |
python/tests/ |
13 |
python/discopt/ (outside nn/) |
4 |
docs/notebooks/ |
3 |
docs/dev/ |
3 |
docs/design/ |
1 |
Only one real code import exists outside the package — everything else is docstring prose:
python/discopt/modeling/examples.py:498-499 — from discopt.nn import NetworkDefinition, NNFormulation / from discopt.nn.network import Activation, DenseLayer
python/discopt/dae/fit.py:7,118,136 — docstring references only
python/discopt/ro/counterpart.py:25 — docstring reference only
python/discopt/skills/agents/nn-embedding-expert.md:3,8 — skill doc prose
discopt.nn is not re-exported from python/discopt/__init__.py and is not named in pyproject.toml — it is reachable only by explicit submodule import. That bounds the external break: a shim at discopt/nn/__init__.py re-exporting the public surface with a DeprecationWarning covers every downstream user.
Scope
In scope:
git mv python/discopt/nn python/discopt/ml, update intra-package imports.
- Add
python/discopt/nn/__init__.py shim: re-export the full public surface from discopt.ml, emit DeprecationWarning on import. Keep submodule paths working (discopt.nn.network, discopt.nn.formulations.base, discopt.nn.readers.*) — several tests and docs import those directly.
- Update the 4 in-package files, 13 test files, 3 notebooks, 4 docs, and the CLAUDE.md architecture paragraph.
- Add a shim regression test:
import discopt.nn still works, warns, and yields objects identical to the discopt.ml ones.
- Rename
python/discopt/skills/agents/nn-embedding-expert.md and refresh its prose (it currently describes the module as NN-only, which is the same defect at the doc layer).
Explicitly out of scope — no file reorganization. readers/ → definitions → formulations/ is a clean pipeline and the frozen/trainable split is already documented at nn/__init__.py:3-21. The one genuine structural oddity — trainable.py and surrogate.py serve dae/fit.py rather than the embedding path — has no correctness or performance defect behind it, so moving them would be a public-API break bought with an aesthetic argument. Per the repo's evidence-first rule that needs a measurement first; there isn't one. If that changes, it belongs in a separate issue, and this one should stay a pure rename.
Verification
Pure rename with a shim — no solver-core code is touched, so no bound or node-count behavior is implicated. Gates:
ruff check python/ + ruff format --check python/
mypy python/discopt/
pytest python/tests/test_nn_formulations.py python/tests/test_nn_equivalence.py (the equivalence harness from T-N0.1 is the real guard — it asserts the embedded formulation reproduces net.forward() / ensemble.predict())
pytest -m smoke
pytest -m slow python/tests/test_adversarial_recent_fixes.py
- Re-execute
docs/notebooks/nn_embedding.ipynb and docs/notebooks/neural_dae.ipynb, verify exit 0
jupyter-book build docs/ with zero warnings
Context
Came out of a design discussion about why trees and other surrogate families appear to lack module-level support. They do not lack it — it is all in nn/, under a name that hides it. Related history: #1 (the original add_predictor feature) and docs/dev/nn-module-plan.md T-N4.2, which refreshed the CLAUDE.md paragraph to list the full module surface but left the package name as-is.
Problem
python/discopt/nn/is named for one of the four things it does. It is the ML predictor embedding + trainable surrogate layer, and its own code says so —formulations/base.py:1reads "Formulation wrappers for embedding trained ML models into discopt", not "networks".What actually lives there today:
tree.py,formulations/tree_ensemble.py,TreeFormulation(formulations/base.py:12)readers/sklearn_reader.pysurrogate.py(Surrogate,runtime_checkable)trainable.py:430(TrainableKernelExpansion)The dispatcher is literally named
add_predictor()(predictor.py:24), notadd_network. The name misdirects readers into thinking trees and non-NN surrogates are unsupported, when they are first-class.A second, smaller misconception the name encourages: that this module is "the MIP-representable predictors module". Only two of four encodings are MIP —
relu_bigm.pyandformulations/tree_ensemble.pyemit binaries + big-M;full_space.pyemitsdm.sigmoid/tanh/softplus(full_space.py:17-22) andreduced_space.pyemits nested expressions with continuous intermediates and zero binaries (reduced_space.py:115-133). MIP-representability explains why two of the files must exist; it does not describe the module.Proposal
Rename the package to
discopt.ml, keepingdiscopt.nnas a deprecation shim.mlmatches the existing short-subsystem naming pattern (dae,gp,llm,mo,ro) and covers both regimes the module spans — frozen predictors you optimize over, and trainable surrogates you optimize with — without claiming either. (discopt.surrogatesanddiscopt.predictorswere considered; the first breaks the short-name pattern, the second misdescribes the trainable half, since aTrainableNetworkis not a predictor you embed.)Note
discopt.gpis geometric programming, so there is no collision with a Gaussian-process reading of these names.Blast radius (measured 2026-09-11 on
main)python/tests/python/discopt/(outsidenn/)docs/notebooks/docs/dev/docs/design/Only one real code import exists outside the package — everything else is docstring prose:
python/discopt/modeling/examples.py:498-499—from discopt.nn import NetworkDefinition, NNFormulation/from discopt.nn.network import Activation, DenseLayerpython/discopt/dae/fit.py:7,118,136— docstring references onlypython/discopt/ro/counterpart.py:25— docstring reference onlypython/discopt/skills/agents/nn-embedding-expert.md:3,8— skill doc prosediscopt.nnis not re-exported frompython/discopt/__init__.pyand is not named inpyproject.toml— it is reachable only by explicit submodule import. That bounds the external break: a shim atdiscopt/nn/__init__.pyre-exporting the public surface with aDeprecationWarningcovers every downstream user.Scope
In scope:
git mv python/discopt/nn python/discopt/ml, update intra-package imports.python/discopt/nn/__init__.pyshim: re-export the full public surface fromdiscopt.ml, emitDeprecationWarningon import. Keep submodule paths working (discopt.nn.network,discopt.nn.formulations.base,discopt.nn.readers.*) — several tests and docs import those directly.import discopt.nnstill works, warns, and yields objects identical to thediscopt.mlones.python/discopt/skills/agents/nn-embedding-expert.mdand refresh its prose (it currently describes the module as NN-only, which is the same defect at the doc layer).Explicitly out of scope — no file reorganization.
readers/ → definitions → formulations/is a clean pipeline and the frozen/trainable split is already documented atnn/__init__.py:3-21. The one genuine structural oddity —trainable.pyandsurrogate.pyservedae/fit.pyrather than the embedding path — has no correctness or performance defect behind it, so moving them would be a public-API break bought with an aesthetic argument. Per the repo's evidence-first rule that needs a measurement first; there isn't one. If that changes, it belongs in a separate issue, and this one should stay a pure rename.Verification
Pure rename with a shim — no solver-core code is touched, so no bound or node-count behavior is implicated. Gates:
ruff check python/+ruff format --check python/mypy python/discopt/pytest python/tests/test_nn_formulations.py python/tests/test_nn_equivalence.py(the equivalence harness from T-N0.1 is the real guard — it asserts the embedded formulation reproducesnet.forward()/ensemble.predict())pytest -m smokepytest -m slow python/tests/test_adversarial_recent_fixes.pydocs/notebooks/nn_embedding.ipynbanddocs/notebooks/neural_dae.ipynb, verify exit 0jupyter-book build docs/with zero warningsContext
Came out of a design discussion about why trees and other surrogate families appear to lack module-level support. They do not lack it — it is all in
nn/, under a name that hides it. Related history: #1 (the originaladd_predictorfeature) anddocs/dev/nn-module-plan.mdT-N4.2, which refreshed the CLAUDE.md paragraph to list the full module surface but left the package name as-is.