Releases: gcol33/tulpa
Release list
tulpa 0.0.198
-
tulpa_re_aghq()returns the mode/theta cross-Hessian block (#398).
Addsblup_cross(Bf) to the return value: the per-group-d^2 ell_g / d theta dbblock 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/dthetacorrection) -- 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_scoreoverb(O(d) oracle calls, independent ofn_theta).
Declines to NA (never a silent 0) when the oracle'stheta_scorehas no
analytic implementation, as on the R-closure bridge (make_site/
make_group) --REGroupOraclegainshas_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_varonly 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-existingblup_var.
tulpa 0.0.197
- 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-onlydispatch_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'ssqrtproducedNaN, 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_densewithJointPDMode::LM, mirroring the
single-species driver and the batch driver's own sparse branch exactly.
tulpa 0.0.196
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
defaultmethod, andsbc("prior_predictive", ...)/sbc("posterior", ...)
keep working unchanged throughsbc.character(). A call passing the
experiment BY NAME (sbc(experiment = "posterior")) has to drop the name or
useobject =, since the first argument is what dispatch reads. -
dic()andcpo()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()andloo::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
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
outsiderule --extendmirrors a half-cell beyond the outer coordinate --
so it is an extrapolation rather than a bound the design supports.
theta_ci_outside_nodesrecords 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 = 3is 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 theextendrule 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 atspan = 4(0.450 / 0.415 against
0.370) and ties atspan = 2.5; and 6 reaches 0.520 only on a doubled width.
Under the shippedsd_clamp_policy = "decline"a lower ceiling also is not
free -- dropping to 1.5 abandons the placement on 32-39% of fits.spanand
n_ptsare kept with it: nine nodes moves 50% coverage FURTHER from nominal
while costing proportionally more inner solves.
tulpa 0.0.194
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_choleskyand its siblings were defined TWICE, differently:
gpu_backend.hcompiled stubs returningFALSEin the#elsebranch of
#ifdef TULPA_ENABLE_CUDA-- which neitherMakevarsever defined -- while
gpu_nngp_laplace.hincludedgpu_cuda.hdirectly and compiled the real
ones. Twoinlinedefinitions 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.his now included from exactly one place, and the NNGP kernels
reach it throughgpu_backend.hlike 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 returnsFALSE
when absent, which is what makes "use CUDA if available" expressible as a
default.TULPA_DISABLE_CUDAbuilds 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
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_1width 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 asigma_1trial 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,cancellationand, for the
barycentre,out_of_box, tallied per cell on both sides of the refinement
gate.n_axes_declinedis 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_boxturning out to be unreachable
through the closed form by construction, since its error bound sits under the
in-box slack.
tulpa 0.0.150
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 atlog_mass_ratioas 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 oneiidblock, 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: thesigma_1interval 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 intervalwidth, 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_infowith the two masses it is formed from andmax_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 gis 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 normoffsetsays 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_gaindeclines to
NA where-His 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:
misfitis
non-quadraticity,offset/mode_gainare off-centring,log_mass_ratiois
mass correction, andskew_maxreadsmisfitand 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, soouter_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)
andouter_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 intest-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
dnodeon 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_declinedonlocal_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.offsetis 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 shapeskew_maxreads.
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-Laplacegamma_3uses, 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 convergedm = 13reference 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-dominatedm = 3configuration reads 0.126, so it keeps its cloud.
control$local_ccd$skew_maxoverrides the threshold;$local_ccd_infogains
misfit,skew_max,cells_declined,misfit_declinedand
`n_c...
tulpa 0.0.136
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
Cchunks and reduces them in a
fixed chunk order, which makes the reduce independent of which thread ran
each chunk.Citself, though, came fromteam / act, whereactwas a
count of the solves in flight at that instant (an atomicfetch_add). The
chunk count sets the chunk boundaries, the boundaries set the summation
order, and floating-point addition is not associative -- soCmoving with
the machine's load moved the answer. It is now read from the cell index:
n_grid - k_gridbounds how many peers a cell can have and estimates the
same tail width from grid geometry alone.n_outeralso replaces
omp_get_num_threads(), so an OMP dynamic team adjustment cannot move it
either. -
The outer width
n_outerwas clamped against a liveavailable_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()reportsn_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
- A prior block missing its required fields now errors instead of segfaulting
the session (#299). Each.NL_REGISTRYentry 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.Rwalks
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
-
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 oftau_lower,
tau_upper,n_grid_spatial,n_grid_temporal,n_grid_rho,rho_lower,
rho_upperincontrol, socontrol = 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_gridargument, 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_temporalone each, andn_grid_rho/rho_lower/rho_upperthe
ar1autocorrelation axis. A pinned axis keeps exactly the nodes its knobs
built and is named in the newouter_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_sigmathe caller merely supplied (#297). The escalation
that exists for a runaway, near-separation mode engaged only when
prior_sigmawasNULL, so a wrapper stamping aprior_sigmaof its own
turned attempt 2 into a second geometry recenter while the fit still reported
outer_grid_recenter_attempts = 2as if the full escalation had run. The
suppression is now decided by provenance -- anauto_grid()-marked spec, or
one equal by value to the engine's ownPC(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 carriesouter_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 readingpareto_kacross
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'srho_car-- read the
quadrature ESS instead, permanently),"draws_too_few","grid_too_small",
"no_varying_axis","degenerate_proposal", and
"internal_inconsistency"(an engine bug, whichdiagnostic_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 bydiagnostics(),print.laplace_diagnostics()and
diagnostic_summary(). -
The inner-Laplace
gamma_3diagnostic now says why it declined too
(#296).gamma_3never returns a silently-wrong0
(#272), but itsNaNcarried 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 defaultTRUE. 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 aJointCurvature3Oraclescarrying"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 withreturn(NULL)
written inside atryCatch()expression (#298). That returns from
the ENCLOSING function, so a fit hitting any of those guards had the whole
resreplaced byNULLby a diagnostic that was only meant to decline. The
probe is now its own function.