Skip to content

Releases: gcol33/tulpa

tulpa 0.0.198

Choose a tag to compare

@gcol33 gcol33 released this 15 Aug 13:04
  • tulpa_re_aghq() returns the mode/theta cross-Hessian block (#398).
    Adds blup_cross (Bf) to the return value: the per-group -d^2 ell_g / d theta db block at the mode, needed to draw a group's BLUP jointly with
    theta instead of independently (Cinv %*% t(Bf) is the first-order
    db_hat/dtheta correction) -- the same joint-draw contract
    .tobs_community_em()-based tulpaObs families already expose via their own
    Cinv/Bf. Computed via a cheap central finite difference of
    theta_score over b (O(d) oracle calls, independent of n_theta).
    Declines to NA (never a silent 0) when the oracle's theta_score has no
    analytic implementation, as on the R-closure bridge (make_site/
    make_group) -- REGroupOracle gains has_theta_score() to signal this.
    Verified against the closed-form binomial-GLMM cross term and an
    independent finite-difference re-solve of the group's mode under a
    perturbed theta. Unblocks gcol33/tulpaObs#220 (ms_abun()'s posterior-SBC
    registration).

  • tulpa_re_aghq() also returns the FULL per-group joint covariance
    across RE terms
    (blup_cov_g, blup_cross_g). blup_var only ever
    exposed the per-term diagonal of a group's posterior covariance; when a
    group carries more than one RE term sharing the same grouping factor (e.g.
    an abundance-arm term and a detection-arm term on the same species), the
    group's mode is found jointly across every term's coefficients, so real
    posterior covariance can exist BETWEEN terms -- cpp_aghq_blups() already
    inverts the full joint Hessian to get it, it just discarded everything off
    the diagonal before this. Drawing a group's terms independently would
    repeat gcol33/tulpaObs#226 one level deeper (inside a group instead of
    between theta and a group). Validated against a closed-form joint-Hessian
    construction on a toy model with deliberately collinear RE terms; the new
    diagonal is byte-identical to the pre-existing blup_var.

tulpa 0.0.197

Choose a tag to compare

@gcol33 gcol33 released this 11 Aug 22:05
  • Fix: the batched joint nested-Laplace driver's DENSE path could converge a
    species to a different mode than its own independent fit
    (#397).
    run_multi_block_nested_laplace_joint_batch's dense branch (small/medium
    fields, n_x < SPARSE_THRESHOLD) solved every Newton step and the final
    log-determinant through the fixed-ridge-only dispatch_factor_solve /
    dispatch_factor_log_det, instead of the PD-escalating
    joint_pd_step_solve_dense (the #344 fix) the single-species
    dense joint driver and the batched driver's own SPARSE branch already use.
    A coupled likelihood whose observed Hessian is indefinite away from the mode
    (the occupancy mixture's dark-cell term is not concave everywhere) could hit
    a negative pivot at one outer-grid cell; with no ridge escalation the
    Cholesky's sqrt produced NaN, silently dropping that cell's weight from
    the fit and shifting every reported summary. Confirmed data-dependent (not
    batch-slot-dependent): the same species diverged identically whether alone,
    paired, or duplicated against itself, and grid cells before the affected one
    matched the independent fit to machine precision. Fixed by routing both the
    per-iteration dense solve and the final-pass log-determinant through
    joint_pd_step_solve_dense with JointPDMode::LM, mirroring the
    single-species driver and the batch driver's own sparse branch exactly.

tulpa 0.0.196

Choose a tag to compare

@gcol33 gcol33 released this 11 Aug 11:30

0.0.196

  • The simulation-based-calibration and goodness-of-fit entry points are S3
    generics
    , so a downstream package registers a method on its own fit object
    instead of exporting a second name for the same concept. sbc(),
    pit_residuals(), test_dispersion(), test_outliers() and
    test_zero_inflation() now dispatch; every existing body became the
    default method, and sbc("prior_predictive", ...) / sbc("posterior", ...)
    keep working unchanged through sbc.character(). A call passing the
    experiment BY NAME (sbc(experiment = "posterior")) has to drop the name or
    use object =, since the first argument is what dispatch reads.

  • dic() and cpo() are new generics over the same criteria layer
    [tulpa_criteria()] computes, for the two criteria the loo package owns no
    generic for. The default methods take a pointwise log-likelihood matrix. WAIC
    and PSIS-LOO deliberately get none: loo::waic() and loo::loo() already
    exist, and a same-named generic here would mask them.

  • test_uniformity() muffles only the ties warning a discrete response
    produces, rather than every warning the Kolmogorov-Smirnov test can raise.

tulpa 0.0.195

Choose a tag to compare

@gcol33 gcol33 released this 10 Aug 16:31

0.0.195

  • A reported hyperparameter bound that left the node range now says so
    (#390). An endpoint past the outermost NODE is produced by the
    outside rule -- extend mirrors a half-cell beyond the outer coordinate --
    so it is an extrapolation rather than a bound the design supports.
    theta_ci_outside_nodes records it per axis ("lower" / "upper" /
    "both" / NA), the same rule that makes a declined placement say so
    (#293). Recorded rather than corrected: the two used to be
    indistinguishable on the fit.

  • The recentred axis's mode-SD ceiling is settled: max_sd_u = 3 is kept, on
    evidence.
    #387 could not score its VALUE because every ladder
    came back flat or non-monotone. The reason was the LEVEL, not the fixture.
    Over 48 (cap, span, node-count, clamp-policy) rungs on two ceiling-reaching
    fixtures at 200 seeds, the reported bound leaves the node range on 56-90% of
    fits at nominal 0.95 at EVERY setting and on 0% at nominal 0.50 -- so 0.50 is
    the level whose bound the design supports, and 0.95 is where the earlier
    ladders were measuring the extend rule instead.

    At nominal 0.50: the cap is EXACTLY inert at nine nodes (1.5, 3 and 6 give
    identical coverage to three decimals in 7 of 8 cells); at the shipped five
    nodes, 3 is nearer nominal than 1.5 at span = 4 (0.450 / 0.415 against
    0.370) and ties at span = 2.5; and 6 reaches 0.520 only on a doubled width.
    Under the shipped sd_clamp_policy = "decline" a lower ceiling also is not
    free -- dropping to 1.5 abandons the placement on 32-39% of fits. span and
    n_pts are kept with it: nine nodes moves 50% coverage FURTHER from nominal
    while costing proportionally more inner solves.

tulpa 0.0.194

Choose a tag to compare

@gcol33 gcol33 released this 10 Aug 16:32

0.0.194

  • CUDA is used when a device is available, and there is now exactly one
    definition of the batched-CUDA backend
    (#396).
    cuda_batched_cholesky and its siblings were defined TWICE, differently:
    gpu_backend.h compiled stubs returning FALSE in the #else branch of
    #ifdef TULPA_ENABLE_CUDA -- which neither Makevars ever defined -- while
    gpu_nngp_laplace.h included gpu_cuda.h directly and compiled the real
    ones. Two inline definitions of the same entity across translation units is
    an ODR violation: the linker keeps one COMDAT and discards the rest, so
    whether CUDA ran at all was decided by link order rather than by any switch,
    and nothing in the package could report which had been built.

    gpu_cuda.h is now included from exactly one place, and the NNGP kernels
    reach it through gpu_backend.h like everything else. Compiling it in needs
    no CUDA SDK at build time and no GPU at run time: the driver, cuBLAS and
    cuSOLVER entry points resolve dynamically and every entry returns FALSE
    when absent, which is what makes "use CUDA if available" expressible as a
    default. TULPA_DISABLE_CUDA builds the stubs instead, and that is now a
    whole-program choice.

    cpp_gpu_backend_kind() reports "cuda" or "stub", so which
    implementation was compiled is observable rather than inferred -- a silent
    either/or is what let this sit. It is deliberately separate from
    cpp_gpu_available(), which asks whether a usable device is present at run
    time.

    Two closed investigations should be re-read in this light: #283
    fixed a column-major/row-major bug in this path, and #389 ruled
    the GPU dispatch out as a cause -- a conclusion consistent with the stub
    having won the link. The per-matrix batch verification added in 0.0.192
    (#392) is what now guards the path on machines where it does run,
    and the #389 determinism arbiter passes with the CUDA backend
    live on a device-equipped machine.

tulpa 0.0.152

Choose a tag to compare

@gcol33 gcol33 released this 08 Aug 22:30

0.0.152

  • Neither free outer cell rule is promoted, decided by coverage rather than by
    grid accuracy (#331). 200 seeds at each of three resolutions, all
    four arms post-processed off one solve per seed so the comparison is paired.
    The placement rule contracts the atom set instead of correcting it -- its
    sigma_1 width ratio is 0.4776 / 0.5840 / 0.4465, flat in resolution rather
    than decaying, because each atom moves a share of its own box and that share
    does not shrink as the boxes do -- and covers 128 / 129 / 118 of 200 against
    the shipped 200 / 200 / 200. The mass rule never moves a sigma_1 trial and
    widens that interval by 4.1% to 9.8%.
  • The decisive number is that the placement rule is CLOSER to the fine grid and
    still loses: per-seed distance to the same seed's 1296-cell width is 0.3576
    against the shipped 0.4194, and it gives up 72 seeds of coverage. The two
    error directions do not cost the same, so a rule selected on distance to a
    finer grid can be selected against on calibration. That is what the issue was
    opened to establish. On the fixed effects coverage cannot separate the arms at
    any affordable N (1 discordant seed of 200 on the intercept, 0 on the slope),
    so only the width is a measurement there; the seed count was chosen from an
    exact sign-test power calculation rather than from the deficit the
    pre-gcol33/tulpa#332 fixture appeared to carry.
  • A two-dimensional pre-refinement descriptor does not select a per-cell
    correction either (#333), measured by one-cell intervention on the
    global read over 2520 cells from 216 fits. The mass and location descriptors
    do separate once the box truncation is applied (Spearman 0.8496 against 0.9495
    in the unbounded metric), but the best label per quadrant buys +0.0000 over
    the single best label overall in 23 of 30 scored combinations, and where the
    direction is strongest it runs backwards. The issue's central hypothesis is
    refuted: the four- and five-level grids occupy statistically indistinguishable
    regions of the plane.
  • Those three negative results have one mechanism and the header now says so
    once rather than leaving it to be rediscovered: posterior mass dominates every
    level of this grid. A second, structural reason is recorded alongside it --
    the outer read is a weighted quantile, which couples cells through ordering
    and cumulative mass, so summed per-cell improvements exceed the whole-grid
    improvement by 8.48x to 68.58x and at five levels carry opposite signs. A
    perfect per-cell classifier would still not compose into a grid rule.
  • A declined outer cell-mass or barycentre axis says which gate it fell at
    (#334): boundary, no_factor, cancellation and, for the
    barycentre, out_of_box, tallied per cell on both sides of the refinement
    gate. n_axes_declined is unchanged and rides alongside. The reasons are
    taken at the same constants that already gate the refusals rather than
    re-derived afterwards, and each one is triggered by a real fixture rather than
    shipped as an unobserved string -- out_of_box turning out to be unreachable
    through the closed form by construction, since its error bound sits under the
    in-box slack.

tulpa 0.0.150

Choose a tag to compare

@gcol33 gcol33 released this 08 Aug 17:16

0.0.150

  • The cross-cell estimator asymmetry in local-CCD refinement is documented as a
    known and instrumented property rather than left reading as an expected
    consequence (#319). Weight conservation is the statement on a flat
    integrand; the integrand refinement is selected for is peaked, so a refined
    cell's mass is re-estimated by a 25-node rule while its unrefined siblings keep
    the base grid's single atom, and the refined cell's share rises. The header now
    says so, points at log_mass_ratio as the per-cell reading, and records what
    the asymmetry costs in coverage rather than in grid-internal metrics.

0.0.149

  • A CI-coverage gate now reaches local-CCD refinement (#320). The
    joint recovery fixture builds one iid block, so it has one latent axis and
    .joint_local_ccd_engage() -- which requires four -- declined silently: every
    number measured on this path over four rounds came from grid dumps rather than
    from a coverage sweep. recov_fit_joint_local_ccd() is that sweep at four
    crossed groupings on a deliberately coarse four-level base grid, run twice on
    the same seeds with the refinement on and off, and a companion test asserts the
    refinement actually engaged rather than trusting the settings.
  • Measured, 150 seeds x 2 coefficients per level: pooled coverage 0.8800 with
    refinement on against 0.8800 off at nominal 0.95, and 0.7300 against 0.7267 at
    0.80, against a standard error of 0.0126. What refinement moves is the width,
    in the direction #319 predicts -- the intercept's mean 95% interval
    is 2.9% narrower -- but that buys 0 of 300 trials at 0.95 and 1 at 0.80. Where
    it pays is the hyperparameter axis: the sigma_1 interval is more than
    fourfold sharper (0.2330 against 1.0590) with half the median bias (0.0245
    against 0.0592) while still covering 149 of 150 against a nominal 0.95.
  • recov_sweep() takes the nominal level to judge at (z / level, defaulting
    to the 95% every existing gate runs at) and reports mean interval width, so a
    second level and the width effect are read through the shared harness rather
    than a parallel one.

0.0.148

  • A locally refined cell now reports the ratio of the two estimates of its own
    mass it already carries (#323): the coarse atom
    Delta_c exp(ell_c) the base grid gave it against the refined cloud
    Delta_c sum_j delta_j exp(ell_j) its own nodes give it, as
    log_mass_ratio = logSumExp_j(log delta_j + ell_j - ell_c) over the full node
    set including the centre. That comparison is the embedded-rule local error
    indicator classical adaptive cubature uses to decide whether a subdivided
    region's estimate is comparable to its unrefined siblings', and this grid
    evaluates both rules already, so it costs no inner solve. Recorded per cell on
    local_ccd_info with the two masses it is formed from and max_node_weight,
    the share the single largest node takes of its own cell's refined mass, for
    refined and declined cells alike.
  • The centring score gains its curvature-scaled form (#324). A
    central composite design identifies a full quadratic exactly, so the whitened
    Hessian sits in the same coefficient vector the gradient does, and
    mode_gain = 0.5 g' (-H)^-1 g is the nats the quadratic model predicts the
    log-density gains by moving the expansion centre to the cell's own fitted
    peak. The plain gradient norm offset says nothing about how sharply the
    log-marginal falls away in the direction it points, so two cells with the same
    norm and curvature an order of magnitude apart are displaced by very different
    amounts; the scaled form is comparable across cells and the unscaled one is
    kept for the cases the scaled one cannot be formed in. mode_gain declines to
    NA where -H is not positive definite: a cell whose fitted quadratic is not
    concave has no interior peak to be displaced from.
  • The three scores are orthogonal and none of them gates: misfit is
    non-quadraticity, offset / mode_gain are off-centring, log_mass_ratio is
    mass correction, and skew_max reads misfit and nothing else.

0.0.147

  • The outer-grid dump / rebuild harness is in the test suite
    (tests/testthat/helper-outer-grid-dump.R, #322). A candidate
    construction for the outer integration weights is pure post-processing of a
    fit that already ran, so outer_grid_dump() writes the grid state
    (joint_grid, log_marginal, dnode, weight_kind, the axis tags and
    domains, the support the read was taken off, and the summary the fit shipped)
    and outer_grid_rebuild() re-reads the per-axis summary under any weight
    vector. The read goes through the engine's own .nl_axis_quantiles() ->
    .nl_summary_quantile(), never a second copy of it, and the round-trip
    assertion in test-outer-grid-dump.R -- rebuild-with-own-weights equals the
    shipped read -- is what makes an offline difference attributable to the
    weights alone. It holds exactly (0.000e+00) on a tensor grid, a global CCD and
    a locally refined grid.
  • outer_grid_noise_floor() estimates the scale below which a difference
    between two reads is not resolved by the grid, as the spread of the read under
    a weight-preserving coarsening of each axis's own atom set (consecutive atoms
    merged at their weighted mean carrying their summed weight). Total mass and
    each group's first moment are exactly preserved, so only resolution is
    removed. On a one-axis dump with a Gaussian outer log-marginal the floor
    bounds the read's true error against the closed-form quantiles at every
    resolution from 9 to 81 levels.
  • The joint multi-block driver records dnode on the fit beside the integration
    weights it was folded into. Recovering it afterwards is a division by
    exp(log_marginal), which loses the scale and is undefined on a cell whose
    inner solve returned no finite marginal.

0.0.146

  • The local-CCD cubic misfit score now reports the whitened gradient its own
    least-squares fit already estimated, and the refinement carries it per cell as
    offset / offset_declined on local_ccd_info (#321). The score
    puts the linear term in its own design columns, so a cell whose outer
    log-marginal is a perfectly good quadratic that simply is not centred on the
    cell fits exactly and scores near zero however steep the gradient across it:
    passing it certifies that the design can represent the cell, not that the
    cell's coordinate is a representative point of it. offset is the
    standardized displacement of the cell's own peak from the cell's coordinate,
    in units of the marginal spread the whitening used, and nothing gates on it --
    a gradient across the cell is a cross-cell estimator question, orthogonal to
    the local shape skew_max reads.

0.0.145

  • Local CCD refinement of the joint outer grid now keeps a refined cell's node
    cloud only where the cell's own outer log-marginal is close to the quadratic
    the cloud was placed from, and puts the cell back as its own mass atom where it
    is not (#318). The refinement was a large win on an outer target
    that is quadratic in the transformed coordinate (summed absolute endpoint error
    against closed-form axis quantiles 7.3118 against 24.1142 for not refining, 48
    configurations of an equicorrelated Gaussian) and a net loss on a skewed one
    (26.2467 against 23.1874 over 27 configurations of a Gaussian copula with
    Gamma(2) marginals, 42.8578 against 38.1609 over 48).

    The mechanism is the cell's own non-quadraticity, and it is measurable from the
    design rather than inferred. A central composite design identifies a full
    quadratic exactly, so the least-squares residual of the nodes' measured
    log-marginals against intercept + gradient + Hessian in the whitened offset is
    the part of the cell the design cannot represent; the nodes are evaluated
    whatever the residual says, so the score costs no inner solve.
    .joint_local_ccd_misfit() reports it as a standardized cubic magnitude on the
    same convention the inner-Laplace gamma_3 uses, and on the Gaussian target it
    is identically zero in all 48 configurations while on every skewed family it
    exceeds 0.08.

    The threshold is .NL_DIAG$gamma3_ok (0.5), one number for the inner band and
    this gate because both are a standardized third-order departure from the
    Gaussian the approximation was placed from. Where it belongs was measured: on
    an eight-family ladder (the Gaussian target plus Gaussian copulas with
    Gamma(1), (2), (4), (8), (16), (32) and (64) marginals, 48 configurations each
    bar 32 for Gamma(1)), 0.5 is the only threshold on the ladder 0.01 to 2 that
    improves or ties every family. Per family, gated against refining
    unconditionally: 7.3118 / 7.3118 on the Gaussian, then 36.3139 / 37.2185,
    40.8376 / 42.8578, 31.9596 / 34.3650, 34.1830 / 36.8361, 44.4462 / 47.2740,
    61.8421 / 63.1718 and 83.2492 / 83.2492 down the ladder; pooled 340.1435
    against 352.2843, with 365.0831 for never refining. Lower thresholds score
    better pooled (0.175 gives 337.4142) by regressing on the two least skewed
    families. 399 of 1196 candidate cells are declined across the ladder, none of
    them on the Gaussian target.

    On the four-axis two-block fixture the gate is measurably neutral, which is
    what it has to be: over the 20 distinct refined configurations the summed
    absolute endpoint error against the converged m = 13 reference is 2.11091
    gated against 2.12847 unconditional and 3.12915 unrefined, and the largest
    single-configuration movement is 0.00696 against that reference's own 0.01716
    endpoint noise floor. Its per-cell scores there run 0.053 to 3.822, and the
    design-dominated m = 3 configuration reads 0.126, so it keeps its cloud.
    control$local_ccd$skew_max overrides the threshold; $local_ccd_info gains
    misfit, skew_max, cells_declined, misfit_declined and
    `n_c...

Read more

tulpa 0.0.136

Choose a tag to compare

@gcol33 gcol33 released this 07 Aug 15:21

0.0.136

  • The joint nested-Laplace grid no longer returns numbers that depend on what
    else the machine was doing.
    Two identical fits could disagree in their last
    bits, because two inputs to the coupled-cell scatter's partition were read
    from live machine state rather than from the problem:

    • The scatter splits its per-cell loop into C chunks and reduces them in a
      fixed chunk order, which makes the reduce independent of which thread ran
      each chunk. C itself, though, came from team / act, where act was a
      count of the solves in flight at that instant (an atomic fetch_add). The
      chunk count sets the chunk boundaries, the boundaries set the summation
      order, and floating-point addition is not associative -- so C moving with
      the machine's load moved the answer. It is now read from the cell index:
      n_grid - k_grid bounds how many peers a cell can have and estimates the
      same tail width from grid geometry alone. n_outer also replaces
      omp_get_num_threads(), so an OMP dynamic team adjustment cannot move it
      either.

    • The outer width n_outer was clamped against a live available_ram_bytes()
      reading, so the same model fitted twice in one session could resolve
      different widths (and therefore different partitions) depending on what the
      box had allocated in between. Both memory readings are now taken once per
      session. The model-dependent term is still computed per call, so a larger
      model is still clamped harder; only the machine-state term is frozen.

    No parallelism is given up for this. The chunks are dispatched as OpenMP
    tasks, so however many threads are genuinely idle still drain them -- only the
    partition is pinned, never the number of workers executing it. In the bulk of
    the grid the budget is 1 exactly as before, so those cells stay serial and
    allocate no partial buffers.

  • tulpa_nested_laplace_joint() reports n_outer, the outer width the
    solve actually ran at after the memory clamp. When two fits of one model
    report different widths, that is the explanation for a shift in their last
    bits.

tulpa 0.0.135

Choose a tag to compare

@gcol33 gcol33 released this 07 Aug 13:05
  • A prior block missing its required fields now errors instead of segfaulting
    the session
    (#299). Each .NL_REGISTRY entry declares, per
    dispatch path, the fields its converter indexes; the shared
    .nl_check_block_fields() checks them at the four boundaries that feed the
    kernels (.nl_dispatch(), .nl_block_axis_grid(),
    .nl_block_spec_for_cpp(), .joint_block_spec_for_cpp(), plus the
    single-block joint packer). A block naming a field wrongly -- a typo, a stale
    name after a rename, a block copied from a different family -- used to reach
    the C++ side as a zero-length vector, which the kernels index with no bounds
    check; it now raises
    prior block 'icar' is missing required field(s): spatial_idx, adj_row_ptr, adj_col_idx, n_neighbors. A field present but empty counts as missing, since
    that is the same out-of-bounds read. The per-branch presence checks that had
    accumulated in the joint converter are replaced by the shared one, so the
    declaration is the single source of truth; test-nl-required-fields.R walks
    every registry entry on every declared path dropping one field at a time, and
    lints both converters' sources so a field read unconditionally by a branch but
    left undeclared fails the suite.

tulpa 0.0.134

Choose a tag to compare

@gcol33 gcol33 released this 07 Aug 13:07
  • fit_st_nested()'s auto-recenter no longer switches itself off when a grid
    knob is set to the engine's own default value
    (#294). The
    spatiotemporal rescue guarded on the PRESENCE of any of tau_lower,
    tau_upper, n_grid_spatial, n_grid_temporal, n_grid_rho, rho_lower,
    rho_upper in control, so control = list(n_grid_spatial = 4L) -- 4L being
    the default -- returned at the first guard and left a railed grid railed. That
    is #293 one level down: a wrapper package exposing its own
    n_grid argument, defaulted to the engine's value, threads it through on
    every fit and disabled the rescue for all of them. A knob is now a PIN only
    when its value differs from .nl_st_default() and it carries no
    [auto_grid()] mark, the same provenance question the three grid-vector
    rescues ask.

    Pinning is also PER AXIS rather than all-or-nothing: tau_lower / tau_upper
    hold the two precision axes (they build both), n_grid_spatial /
    n_grid_temporal one each, and n_grid_rho / rho_lower / rho_upper the
    ar1 autocorrelation axis. A pinned axis keeps exactly the nodes its knobs
    built and is named in the new outer_grid_pinned_axes; the rest are recentred
    as usual. Only when EVERY axis is pinned does the rescue decline outright.

  • auto_grid() now marks a scalar knob or a prior specification, not just a
    grid vector.
    One front door for "this value is my default, not the user's
    choice", across the three shapes that question arises in.

  • The auto-recenter's second-attempt PC prior is no longer suppressed by a
    prior_sigma the caller merely supplied
    (#297). The escalation
    that exists for a runaway, near-separation mode engaged only when
    prior_sigma was NULL, so a wrapper stamping a prior_sigma of its own
    turned attempt 2 into a second geometry recenter while the fit still reported
    outer_grid_recenter_attempts = 2 as if the full escalation had run. The
    suppression is now decided by provenance -- an auto_grid()-marked spec, or
    one equal by value to the engine's own PC(U = 3, alpha = 0.01), is a default
    and does not hold the prior back -- and when a genuine pin does suppress it
    the fit carries outer_grid_prior_declined = "prior_pinned".

  • The outer Pareto-k now says WHY it declined (#295). Roughly
    two dozen distinct decline paths all arrived as the single value
    pareto_k = NA, and the print method admitted as much ("outer diagnostic not
    run or proposal degenerate"). "You turned it off", "this family's support can
    never be scored", "the outer Hessian came back non-finite" and "the weights
    carry no mass" are not interchangeable, and a batch reading pareto_k across
    many fits could not tell a permanent structural limitation from a live signal
    about the fit. Every decline now carries a reason from a closed vocabulary in
    fit$pareto_k_declined: "not_requested", "not_applicable",
    "unguessable_axis" (naming the axis, e.g. car_proper's rho_car -- read the
    quadrature ESS instead, permanently), "draws_too_few", "grid_too_small",
    "no_varying_axis", "degenerate_proposal", and
    "internal_inconsistency" (an engine bug, which diagnostic_summary() now
    WARNs on). Wired through the joint single- and multi-block paths, the
    registry path, the SPDE grid and CCD paths, and the shared IS cores;
    surfaced by diagnostics(), print.laplace_diagnostics() and
    diagnostic_summary().

  • The inner-Laplace gamma_3 diagnostic now says why it declined too
    (#296). gamma_3 never returns a silently-wrong 0
    (#272), but its NaN carried no reason, so a structurally
    unscorable model -- a coupled multi-process likelihood such as tulpaObs's
    occu_cover, which this formula can never score -- printed as
    control$diagnose_skew = FALSE, attributing an impossibility to a knob the
    user had most likely left at its default TRUE. The reason now travels from
    the point of decline: build_spec_curvature3_fn() reports
    "coupled_likelihood" / "curvature3_unavailable" through an out-parameter
    rather than a second predicate that could drift from it, the per-arm oracles
    travel as a JointCurvature3Oracles carrying "coupled_arm" and which arms
    it applies to, and the R side adds "not_requested", "no_probe_indices",
    "backend_unsupported" and "solve_failed". Reported on the fit as
    inner_skew_declined / inner_skew_arms_declined -- the latter also on a
    PARTIALLY scored joint fit -- and read back by the combined verdict, which
    now distinguishes a layer that was not assessed from one that is unscorable
    by construction (for those models the outer k-hat is the only reliability
    number available, permanently).

  • Fixed: .nl_inner_skew_at_theta() guarded its probe with return(NULL)
    written inside a tryCatch() expression (#298). That returns from
    the ENCLOSING function, so a fit hitting any of those guards had the whole
    res replaced by NULL by a diagnostic that was only meant to decline. The
    probe is now its own function.