Skip to main content

NlBody

Enum NlBody 

Source
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

Source

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.

Source

pub fn quad(&self) -> Option<&Quad2>

The recognized degree-2 form, when the parser produced one.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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 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.

Source

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.

Source

pub fn collect_vars(&self, out: &mut BTreeSet<usize>)

Add this body’s structural variable support to out.

Trait Implementations§

Source§

impl Clone for NlBody

Source§

fn clone(&self) -> NlBody

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for NlBody

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error>

Formats the value using the given formatter. Read more
Source§

impl From<Expr> for NlBody

Source§

fn from(e: Expr) -> NlBody

Converts to this type from the input type.

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> ByRef<T> for T

Source§

fn by_ref(&self) -> &T

Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Imply<T> for U
where T: ?Sized, U: ?Sized,

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self>

Converts 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 more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
where F: FnOnce(&Self) -> bool,

Converts 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
Source§

impl<T> Pointable for T

Source§

const ALIGN: usize

The alignment of pointer.
Source§

type Init = T

The type for initializers.
Source§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
Source§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
Source§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
Source§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more