pub enum NlBody {
Tree(Expr),
Quad(Box<QuadBody>),
}Expand description
The nonlinear body of the objective, or of one constraint row, as the parser left it.
Before gh #588 Q5 this was always an Expr, and for most bodies it
still is. The second variant exists because the Expr DAG is what sets
peak RSS on a quadratic model — 2.32 M nodes for a ten-row
qcqp500-3c — and every consumer of a recognized body wants the
degree-≤2 coefficients rather than the tree they would have to walk to
recover them. So the parser recognizes those bodies from the token
stream and never builds the tree at all.
The distinction is deliberately impossible to ignore. Nine consumers
read these bodies (§5.3 of dev-notes/quadratic-structure-exploitation.md
enumerates them) and several would read a missing tree as “this row is
linear” — a silent wrong answer. An enum makes every one of them a
compile error until it says which reading it wants.
Variants§
Tree(Expr)
The expression tree.
Quad(Box<QuadBody>)
A degree-2 form recognized while the token stream was consumed. The
tree was never built; NlProblem::con_expr rebuilds it on demand,
byte for byte, by re-parsing the same bytes with the same parser.
Implementations§
Source§impl NlBody
impl NlBody
Sourcepub fn is_trivially_zero(&self) -> bool
pub fn is_trivially_zero(&self) -> bool
The identity zero — “this row has no nonlinear part”. A recognized body is degree 2 by construction, so it is never trivially zero; the question still has to be asked through here rather than by matching on a tree that may not exist.
Sourcepub fn quad(&self) -> Option<&Quad2>
pub fn quad(&self) -> Option<&Quad2>
The recognized degree-2 form, when the parser produced one.
Sourcepub fn tree(&self) -> Option<&Expr>
pub fn tree(&self) -> Option<&Expr>
The tree, when there is one resident. None for a recognized body —
use NlProblem::con_expr / NlProblem::obj_expr to rebuild it.
Sourcepub fn analyze_quadratic(&self) -> Option<BTreeMap<(usize, usize), f64>>
pub fn analyze_quadratic(&self) -> Option<BTreeMap<(usize, usize), f64>>
This body as a degree-≤2 Hessian, or None if it is not provably
quadratic — crate::nl_quadratic::analyze_quadratic for a tree,
and the form the parser already proved for a recognized body. The
two answers are the same by construction and asserted to be so bit
for bit; this is the accessor that keeps the corpus from being
re-recognized once per consumer.
None also when a term was dropped getting to the form — see
Self::analyze_quadratic_full.
Sourcepub fn analyze_quadratic_full(
&self,
) -> Option<(BTreeMap<(usize, usize), f64>, Vec<(usize, f64)>, f64)>
pub fn analyze_quadratic_full( &self, ) -> Option<(BTreeMap<(usize, usize), f64>, Vec<(usize, f64)>, f64)>
Self::analyze_quadratic with the linear and constant parts.
§A form that dropped a term is not this body
Everything downstream of here reads coefficients out: the
classifier decides a problem class from the Hessian, and
qp_extract builds P, c, A and G from all three parts. So
this accessor owes its callers a form that is the whole body, and
a recognized form is only that when nothing was dropped reaching
it. 2⁵³·x₀² + x₀² − 2⁵³·x₀² folds to x₀² and stores nothing;
(10⁻²⁰⁰·x₀)·(10⁻²⁰⁰·x₀) underflows the same way (gh #683).
Handing that form out is what routed the reproduction in gh #685
to the LP fast path: with the row’s only quadratic term gone
the classifier saw a linear row, qp_extract folded an empty
linear part into G, and the constraint left the model
altogether — min −x₀ subject to a vanished row walks x₀ to its
10⁶ bound and reports Optimal. A wrong answer, on the default
route, with no option set (gh #685 part 2).
The gate is Quad2::lost_terms and not an emptiness test, for
the reason spelled out on Self::admitted_quad_form: partial
cancellation leaves a non-empty map that is still short a term.
Refusing costs reach only — the row falls back to the AD tape,
and the model to the NLP path, which solves it soundly.
lost_terms is the inexact fold and not the drop it leads to
(gh #687), so the reach given up here is only the reach that has
to be. x − x cancels exactly — nothing was lost, the form is
the body, and it is still handed out; 2⁵³·x + x − 2⁵³·x loses
the x at fl(2⁵³ + 1), and that is what this refuses.
Use Self::quad_terms_dropped to tell the two Nones apart.
Sourcepub fn quad_terms_dropped(&self) -> bool
pub fn quad_terms_dropped(&self) -> bool
Whether the recognizer reached a degree-≤2 form for this body but
lost at least one term getting there — the case
Self::analyze_quadratic_full refuses.
false both for a body that recognized cleanly and for one that
did not recognize at all, so this separates the two reasons that
accessor answers None; it is not a nonlinearity test. Meant for
the refusal path (the classifier naming its reason), not the hot
one: on a tree it re-runs the recognizer.
Sourcepub fn admitted_quad_form(
&self,
) -> Option<(BTreeMap<(usize, usize), f64>, Vec<(usize, f64)>, f64)>
pub fn admitted_quad_form( &self, ) -> Option<(BTreeMap<(usize, usize), f64>, Vec<(usize, f64)>, f64)>
The form the constant-structure evaluator is allowed to use — i.e.
Self::analyze_quadratic_full behind the exactness gate.
A recognized body has already passed that gate: the parser admits
only a flat sum of monomials, which is the same rule
crate::nl_quadratic::is_expanded_quadratic applies to a tree.
Reading a factored form out of stored coefficients cancels — five
digits on (x − 500000)² — so the gate is on both arms or on
neither (gh #588, Q4).
A body this refuses for that reason is not out of reach, only out of
this representation: Self::admitted_factored_form serves it by
keeping the squares factored (gh #673), and only a body neither can
express keeps its tape.
§The second gate: a term that was lost is a term that is missing
is_expanded_quadratic is a gate on the shape the coefficients
were derived from. It says nothing about whether the derivation kept
them. A flat sum of monomials passes it and still folds to a form
with an entry missing, because the fold is floating-point addition:
2⁵³·x₀² + x₀² − 2⁵³·x₀² is x₀² and stores nothing, and
(10⁻²⁰⁰·x₀)·(10⁻²⁰⁰·x₀) underflows the same way (gh #683).
Evaluating that form is not a five-digit cancellation, it is a
missing term. At x₀ = 3 the row reads 0 where its own tape reads
16, and ∂g/∂x reads [0, 0] where the tape reads [8, 0] — so
the ≤ the row sits under stops constraining anything at all. In the
reproduction (issue_685_cancelled_quadratic_evaluation) the solve
then walks the objective variable to its -10⁶ floor and reports
Optimal, where the same bytes down the tape stop at -0.281. On
the default route, with no option set. So the form is admitted only
when Quad2::lost_terms is clear (gh #685 part 1).
It has to be that flag and not an emptiness test. Partial
cancellation is the same defect wearing a different face:
2⁵³·x₀² + x₀² − 2⁵³·x₀² + x₁² keeps x₁², so the map is not empty
and Self::provably_affine answers Some(false) quite correctly —
while the read-out is still short an entire x₀². A gate that looked
at emptiness would pass this and stay wrong.
And it is the loss, not the drop. x₀² − x₀² folds through
fl(1) + fl(−1) = 0 with nothing rounded away, so its read-out is
the whole body and it keeps this fast path; gating on the drop
refused it alongside the absorbing row above, for arithmetic that
lost nothing (gh #687).
The cost is reach, not correctness: a row that lost a term goes back to the AD tape, which is where it was before Q4.
Sourcepub fn admitted_factored_form(&self) -> Option<FactoredQuadratic>
pub fn admitted_factored_form(&self) -> Option<FactoredQuadratic>
The factored form the constant-structure evaluator may use when
Self::admitted_quad_form refuses (gh #673).
That accessor’s gate is is_expanded_quadratic, and what it refuses
is a body whose read-out would be an algebraic expansion of what
the writer wrote — (x − 500000)² read back as
x² − 10⁶x + 2.5·10¹¹, five digits gone. The refusal was never
about the body being unsuitable for constant-structure evaluation;
it was about the representation. So a body written as a sum of
squared residuals — which is every least-squares model, and 41 of
airport.nl’s 42 rows — is served here instead, by keeping the
writer’s own grouping and squaring it at evaluation time exactly as
the tape does. See
recognize_factored_quadratic
for what is admitted and why it costs no accuracy.
Answers None for a body the parser recognized: a
NlBody::Quad is an already-flat sum of monomials by
construction, so it has no factoring left to keep and
Self::admitted_quad_form has already served it.
§This arm answers for the bodies refused on shape, and only those
Self::admitted_quad_form says None for two different reasons,
and only one of them is this arm’s. A body refused because its shape
is factored is what this serves. A body refused because a term went
missing in the fold (Quad2::lost_terms, gh #685) keeps its
tape, and the explicit is_expanded_quadratic test here is what
keeps it there: those bodies are flat sums of monomials, so the
square-shaped ones among them — 2⁵³x₀² + x₀² − 2⁵³x₀² is three —
would otherwise be admitted here by the back door.
What breaks when they are is worth stating, because it is not that
the fast path becomes less accurate. Measured with this test
removed: the row whose tape answers 16.0 at x₀ = 3 is answered
9.0 by the factored arm — and 9.0 is the mathematically right
value of x₀², which the compensated outer sum (gh #702) recovers
and the tape’s naive fold does not. End to end the reproduction
moves from −1.812 to −2.236, which is −√5, the true optimum of
the model those bytes describe.
It is still a defect, for the reason this file’s own doc comment
gives: the tape is the reference because it is what the row means
to this solver, not because it is exact. Two routes over the same
bytes that answer 9 and 16 are a POUNCE_DBG_NO_QUAD-shaped
divergence whichever one is closer to the algebra. (The Hessian
pattern diverges too — Σ 2wₖbₖbₖᵀ folds that row’s (0, 0) to
exactly 0.0, 2⁵⁴ + 2 tying back to 2⁵⁴, and a zero entry is
not stored where the tape declares one: nnz_h 2 → 1.)
Pinned by
a_row_that_dropped_a_term_is_not_admitted_as_a_factored_form_either.
Callers must still try Self::admitted_quad_form first: both can
answer for the same body (a bare x² is a monomial and a square),
and the expanded arm is the cheaper evaluation — a matvec over a
merged row rather than one squaring per term.
Sourcepub fn provably_affine(&self) -> Option<bool>
pub fn provably_affine(&self) -> Option<bool>
Whether this body is provably affine — degree ≤ 1 — as a
three-valued answer: Some(true) proved affine, Some(false)
proved to have a nonzero second derivative, None no proof
either way.
This is the degree question without the exactness gate
Self::admitted_quad_form applies, and the difference is the
point (gh #588, Q6). That gate exists because reading a value out
of stored coefficients cancels for a factored form; nothing is
read out here. The answer is used to decide whether a derivative
may be reused across iterates — a question about the
degree of the body, which a factored (x − a)² answers just as
well as an expanded one.
None is not evidence of nonlinearity: the recognizer refuses
2·(x + 1), which is affine. Consumers must treat it as “not
established”.
§What the exactness argument above still does not buy
It is true that nothing is evaluated from the coefficients here.
What the argument missed is that the degree answer is itself
computed by the coefficient arithmetic: the recognizer sums a
row’s quadratic coefficients in floating point and drops the ones
that reach exactly zero, so 2⁵³·x² + x² − 2⁵³·x² — and
(10⁻²⁰⁰·x)·(10⁻²⁰⁰·x), by underflow — folded to an empty
quadratic map and were reported proved affine. Q6’s consumer then
froze those rows’ Jacobians for the whole solve (gh #683).
So an empty quadratic map is a proof of degree ≤ 1 only when no term
went missing getting there, which is what
Quad2::lost_terms
records. When one did, this answers None — the state the contract
already reserved for “not established”, which is why the fix needs
nothing of its consumer.
A term that cancelled exactly did not go missing, and gh #687 is
where that stopped costing a proof: x₀² − x₀² is degree 0 by an
add that rounded nothing, its tape holds ∂g/∂x at zero for every
x, and answering None for it gave up a whole solve of frozen
Jacobian to be safe from arithmetic that never happened.
Deliberately answers from the term maps rather than from
Self::analyze_quadratic_full’s triplets: on qssp180 that is
65 341 recognized rows, and materializing a QuadHessian per row
to ask whether it is empty is the allocation-per-object cost Q3
removed from the recognizer in the first place.
Sourcepub fn collect_vars(&self, out: &mut BTreeSet<usize>)
pub fn collect_vars(&self, out: &mut BTreeSet<usize>)
Add this body’s structural variable support to out.
Trait Implementations§
Auto Trait Implementations§
impl Freeze for NlBody
impl RefUnwindSafe for NlBody
impl Send for NlBody
impl Sync for NlBody
impl Unpin for NlBody
impl UnsafeUnpin for NlBody
impl UnwindSafe for NlBody
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
impl<T, U> Imply<T> for U
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more