Summary
floor and ceil are absent from the expression IR entirely. They are not in
the Rust MathFunc enum (crates/discopt-core/src/expr.rs:50), not in
modeling/core.py, and not exported anywhere:
discopt.floor -> AttributeError: module 'discopt' has no attribute 'floor'
dm.floor -> AttributeError: module 'discopt.modeling' has no attribute 'floor'
dm.ceil -> AttributeError
The only occurrences in the tree are constant folding in the GAMS parser
(python/discopt/modeling/gams_parser.py:1527-1530), which evaluates
ceil(2.3) when the argument is already a literal. There is no path that
produces a symbolic node over a variable.
Spun out of #1233 (CHECKS_EXECUTED 10; the three probes above plus the
min/max set that became the sibling issue).
Why this is worth its own issue
#1233's comparison paragraph names them:
For comparison, Pyomo exports its full intrinsic set (sqrt, log, log10,
exp, the trig and hyperbolic families, ceil, floor) from
pyomo.environ.
but its Suggested fix listed only the 13 names that already existed in
core.py, so the PR closing it (branch claude/awesome-goodall-kjoq0d) did not
add these — every other name in that comparison is now exported, these two are
not, because there is nothing to export.
The concrete consequence is an import gap, not just an ergonomics gap. GAMS
is the only route to a full-license BARON (the bundled /Applications/AMPL/baron
is demo-limited to 10 variables — see CLAUDE.md's three-way head-to-head
section), and minlplib.org/gms/ instances that use floor/ceil over a
variable have no import path today. Worth measuring how many corpus instances
that is before deciding priority — that is the entry experiment below.
Why it is not a small change
floor/ceil are discontinuous, which puts them in a different class from
every intrinsic currently in the IR. Each of these needs a decision, and getting
any of them wrong is a false-certificate risk rather than a slowdown
(CLAUDE.md §1):
- Relaxation.
floor(x) over a box [l, u] has a valid convex
envelope, but it is not the interval-arithmetic result the existing
elementwise machinery would produce. The standard treatment is a MILP
reformulation (floor(x) = z, z integer, z <= x <= z + 1 - eps), which
changes the variable set, not just the DAG.
- Derivatives. The AD tape has no meaningful gradient here. The NLP
relaxation would need the reformulated integer variable rather than the
function node.
- FBBT.
crates/discopt-core/src/presolve/fbbt.rs would need forward and
backward propagation rules for a step function.
.nl / GAMS / LP writers. .nl has opcodes for both; the writers would
need to emit them, and the round-trip test corpus would need instances that
exercise them.
Entry experiment (run before implementing, per CLAUDE.md §4)
Hypothesis: floor/ceil over a variable appear in enough of the MINLPLib
corpus to justify the reformulation work.
Measurement: grep ~/Dropbox/projects/discopt-minlp-benchmark/minlplib/ for
.gms instances containing floor(/ceil( with a non-literal argument, and
count how many currently fail to import. Report the count and the instance names.
Kill criterion: if the count is ~0, close this as "not reachable from the
corpus" and instead make the refusal explicit — today dm.floor is an
AttributeError from a missing name, which reads as an oversight rather than a
deliberate exclusion. A named NotImplementedError explaining that
discontinuous intrinsics need a MILP reformulation would be the whole fix, and
is worth doing either way.
Scope note
If implemented, sign is worth reviewing at the same time: it is in the IR
(MathFunc::Sign) and is equally discontinuous, so whatever treatment
floor/ceil get should either match it or explain why it differs.
Summary
floorandceilare absent from the expression IR entirely. They are not inthe Rust
MathFuncenum (crates/discopt-core/src/expr.rs:50), not inmodeling/core.py, and not exported anywhere:The only occurrences in the tree are constant folding in the GAMS parser
(
python/discopt/modeling/gams_parser.py:1527-1530), which evaluatesceil(2.3)when the argument is already a literal. There is no path thatproduces a symbolic node over a variable.
Spun out of #1233 (
CHECKS_EXECUTED 10; the three probes above plus themin/max set that became the sibling issue).
Why this is worth its own issue
#1233's comparison paragraph names them:
but its Suggested fix listed only the 13 names that already existed in
core.py, so the PR closing it (branchclaude/awesome-goodall-kjoq0d) did notadd these — every other name in that comparison is now exported, these two are
not, because there is nothing to export.
The concrete consequence is an import gap, not just an ergonomics gap. GAMS
is the only route to a full-license BARON (the bundled
/Applications/AMPL/baronis demo-limited to 10 variables — see CLAUDE.md's three-way head-to-head
section), and
minlplib.org/gms/instances that usefloor/ceilover avariable have no import path today. Worth measuring how many corpus instances
that is before deciding priority — that is the entry experiment below.
Why it is not a small change
floor/ceilare discontinuous, which puts them in a different class fromevery intrinsic currently in the IR. Each of these needs a decision, and getting
any of them wrong is a false-certificate risk rather than a slowdown
(CLAUDE.md §1):
floor(x)over a box[l, u]has a valid convexenvelope, but it is not the interval-arithmetic result the existing
elementwise machinery would produce. The standard treatment is a MILP
reformulation (
floor(x) = z,zinteger,z <= x <= z + 1 - eps), whichchanges the variable set, not just the DAG.
relaxation would need the reformulated integer variable rather than the
function node.
crates/discopt-core/src/presolve/fbbt.rswould need forward andbackward propagation rules for a step function.
.nl/ GAMS / LP writers..nlhas opcodes for both; the writers wouldneed to emit them, and the round-trip test corpus would need instances that
exercise them.
Entry experiment (run before implementing, per CLAUDE.md §4)
Hypothesis:
floor/ceilover a variable appear in enough of the MINLPLibcorpus to justify the reformulation work.
Measurement: grep
~/Dropbox/projects/discopt-minlp-benchmark/minlplib/for.gmsinstances containingfloor(/ceil(with a non-literal argument, andcount how many currently fail to import. Report the count and the instance names.
Kill criterion: if the count is ~0, close this as "not reachable from the
corpus" and instead make the refusal explicit — today
dm.flooris anAttributeErrorfrom a missing name, which reads as an oversight rather than adeliberate exclusion. A named
NotImplementedErrorexplaining thatdiscontinuous intrinsics need a MILP reformulation would be the whole fix, and
is worth doing either way.
Scope note
If implemented,
signis worth reviewing at the same time: it is in the IR(
MathFunc::Sign) and is equally discontinuous, so whatever treatmentfloor/ceilget should either match it or explain why it differs.