Skip to content

Releases: gcol33/tulpaObs

tulpaObs 0.0.233

Choose a tag to compare

@gcol33 gcol33 released this 15 Aug 16:59
  • AGENTS.md added, the Codex-facing counterpart of CLAUDE.md. Same
    contract: rules and interfaces here, the measurements they rest on in
    NOTES_measurements.md. Listed in .Rbuildignore alongside CLAUDE.md, so
    R CMD check does not report a non-standard file at top level.

tulpaObs 0.0.232

Choose a tag to compare

@gcol33 gcol33 released this 15 Aug 16:05
  • The sbc() registry test reads ms_abun()'s ranked quantity, not its
    community mean.
    .sbc_reg_means() carries one branch per community
    family because a community fit's fit$means is the community MEAN alone
    (P-length) while its SBC design ranks the per-species REALIZED coefficient
    mu + b_s (S x P, species-major). ms_abun() was registered without its
    branch, so it fell through to the fit$means short-circuit and returned a
    2-length vector against the registry's 40 -- the exact failure the comment
    above that helper describes. Branch added, mirroring ms_distance()'s:
    as.vector(t(cbind(coef_lambda, coef_p))) against
    .tobs_sbc_ms_abun_names()$cols. All eight community families now have one.

tulpaObs 0.0.231

Choose a tag to compare

@gcol33 gcol33 released this 15 Aug 14:48
  • The sbc() registry's end-to-end composition test slices the pooled
    response at whatever rank the family brings.
    Its slice_site() helper
    enumerated ranks -- 3D array, else 2D matrix -- so ms_dyn_occu(), whose
    y is 4D [site x visit x season x species], fell to the 2D branch and
    errored with "incorrect number of dimensions". .tobs_sbc_pool_4d_species()
    was right: every registered family pools on the first axis and leaves the
    rest alone, so the helper now indexes axis 1 at any rank and the
    rank-enumeration cannot go stale again the next time a family arrives with
    a new response shape.

  • man/dot-tobs_community_em.Rd regenerated, clearing an R CMD check
    codoc WARNING. .tobs_community_em() gained re_aghq/n_quad/
    re_aghq_maxdim (with their roxygen) when the AGHQ variance debias was
    generalized onto it, but document() was not re-run, so the Rd's \usage
    had been three arguments behind the function since then.


Tagged before CI ran green. The run on this commit cleared the codoc WARNING
and the rank error, and reported one remaining failure: ms_abun() was
registered without its .sbc_reg_means() branch, so the sbc() registry
test read its community mean where the registry ranks the per-species
realized coefficient. That is fixed in v0.0.232, the first version in this
sequence with smoke (4936 assertions, 0 failures, 0 errors) and
R CMD check (Status: OK) both passing.

tulpaObs 0.0.230

Choose a tag to compare

@gcol33 gcol33 released this 15 Aug 13:06
  • ms_abun() registered for sbc() (#220, the 27th and
    final family of the original registration scope). Needed TWO upstream
    tulpa engine changes before it could even be attempted:

    1. tulpa#398 (blup_cross): exposes tulpa_re_aghq()'s per-group
      mode/theta cross-Hessian.
    2. A second gap found while wiring this family: tulpa_re_aghq() also
      only ever exposed the per-RE-TERM DIAGONAL of a group's posterior
      covariance (blup_var). ms_abun()'s lambda (abundance) and p
      (detection) arms share a per-species grouping factor with real
      cross-arm posterior covariance -- the same lambda/p identifiability
      ridge tobs()'s penalized-EM exists to break for occupancy psi/p --
      so drawing a species' abundance and detection deviations
      independently would repeat the #226 bug one level deeper (inside a
      species instead of between the community mean and a species). Fixed
      upstream by adding blup_cov_g/blup_cross_g (the full per-group
      joint covariance/cross-Hessian, unsliced by RE term), validated
      against a closed-form joint-Hessian construction on a toy model with
      deliberately collinear RE terms.

    ms_community$Cinv/Bf now thread off blup_cov_g/blup_cross_g
    (R/ms_abun.R), matching the convention every other sbc()-registered
    community family uses. Only reachable via the AGHQ/joint_fd engine
    (control = list(optimizer = "joint_fd", n.quad > 1)) -- the default
    n.quad = 1 Laplace-EM path (a different, faster engine entirely) does
    not expose Cinv/Bf at all, and errors with a pointer rather than
    silently drawing an independent (mu, b_s).

    Registered at reduced n.sim = 15 (distsamp_open's own precedent for the
    identical cost problem, see 0.0.227 below): a n.sim = 100 run at
    standard settings was projected to take >12h; the actual n.sim = 15 run
    completed in 39.9 min. Posterior calibrates cleanly (min p_unif 3.9e-3,
    0/40 quantities below 1e-3); narrow control's minimum p_unif is 2.9e-12
    (comfortably below the 1e-3 pass bar every sibling family uses), though
    one narrow-arm quantity did not fail as comprehensively as siblings'
    (max p_unif 1.3e-2 there, vs ~1e-6 to 1e-15 elsewhere) -- not
    re-investigated given the ~40min cost of a re-run, and does not weaken
    the primary (posterior) calibration signal.

    All 27 originally-scoped families are now registered for sbc().

  • Engine pin raised to tulpa 0.0.198 (Imports floor and Remotes tag
    together, as .github/scripts/check-engine-pin.R requires). That release
    carries both engine changes the ms_abun() registration above depends on:
    blup_cross (gcol33/tulpa#398) and the full per-group joint covariance
    blup_cov_g/blup_cross_g. The floor and the tag had also drifted apart
    from each other (0.0.197 vs v0.0.196), which the pin check fails on.


Tagged before CI ran green. Every workflow run from 2026-08-11 to 2026-08-15
stopped at pak dependency resolution, so the test suite had not executed
against this code. Three defects were sitting behind that, all of them
test-side or in generated documentation: the sbc() registry test's
rank-enumerating slice helper, that test's missing ms_abun()
.sbc_reg_means() branch, and a stale man/dot-tobs_community_em.Rd.
Package behaviour is unaffected.

Use v0.0.232, the first version in this sequence with smoke (4936
assertions, 0 failures, 0 errors) and R CMD check (Status: OK) both
passing.

tulpaObs 0.0.197

Choose a tag to compare

@gcol33 gcol33 released this 11 Aug 11:31

0.0.197 (2026-08-11)

  • Every diagnostic is now one verb, dispatched on the fit (breaking). The
    package exported a parallel tobs_* spelling for diagnostics that already had
    an owner elsewhere, so the same concept carried two names and neither could
    dispatch. Each is now an S3 method on the generic of whichever package owns
    the concept, and the generic is re-exported here, so attaching tulpaObs is
    enough to reach it:

    was now generic from
    tobs_waic(fit) waic(fit) loo
    -- loo(fit) loo
    tobs_cpo(fit) cpo(fit) tulpa
    tobs_dic(fit) dic(fit) tulpa
    tobs_sbc(fit) sbc(fit) tulpa
    tobs_pit_residuals(fit) pit_residuals(fit) tulpa
    tobs_test_uniformity(pit) test_uniformity(pit) tulpa
    tobs_test_dispersion(fit) test_dispersion(fit) tulpa
    tobs_test_outliers(fit) test_outliers(fit) tulpa
    tobs_test_zero_inflation(fit) test_zero_inflation(fit) tulpa
    tobs_ppc(fit) ppc(fit) tulpaObs
    tobs_check(fit) check_model(fit) tulpa

    The old names are removed rather than aliased. Results are unchanged: each
    method is the previous function body, and test_uniformity() runs the same
    Kolmogorov-Smirnov test on the same PIT vector.

  • loo(fit) is new, and returns a genuine psis_loo object, so
    [loo::loo_compare()] and the model-weight machinery read a tobs_fit
    directly. It builds the same pointwise log-likelihood matrix the other
    criteria use and hands it to PSIS with relative effective sample sizes off the
    chain layout -- the builder tobs_stack() already used per member.

  • tobs_check() is now check_model(fit), and it draws the panel its
    generic promises.
    [tulpa::check_model()] errored on a tobs_fit before
    this: its default method builds the panel from fitted() and residuals()
    as numeric vectors, and a latent-state fit has neither (fitted() returns
    list(psi, p, z)), so the response could not be resolved. The method reads
    the same quantities through the family's own doors instead -- pit_residuals(),
    ppc(), test_dispersion(), moran_i() -- printing the roll-up report as
    before and drawing a QQ-uniform, PPC, dispersion and (given coords)
    correlogram panel beside it. plot = FALSE prints the report alone. Nothing
    is simulated twice: the panel plots what the report already computed, which is
    why test_dispersion() now also returns the simulated variances in $sim.

  • ppc() deliberately did not fold into tulpa::pp_check(). That generic
    draws a graphical check while ppc() returns a Bayesian p-value, and a method
    has to honour its generic's contract, so the discrepancy check became its own
    generic.

  • loo moves from Suggests to Imports (it now owns two of the doors), and
    requires tulpa >= 0.0.196 for the generics.

tulpaObs 0.0.196

Choose a tag to compare

@gcol33 gcol33 released this 11 Aug 09:27

0.0.196 (2026-08-11)

  • The information criteria score a random effect the fit carried
    (#215). On the grid-integrated (nested_laplace) route,
    occu_cover() scored every observation-arm random effect at the population
    mean in WAIC, LOO, CPO, PPC and PIT, and an occupancy-arm one likewise. Both
    now reach the criteria through the same per-visit and per-site offset path the
    sampled route uses. This moves existing numbers: elpd_waic -17.939 ->
    -6.406 on a detection-arm RE at sigma_re_p 1.10 over 216 visits, -125.94 ->
    -64.41 on a cover-arm RE over 400 visits, and -209.525 -> -200.180 on an
    occupancy-arm RE at sigma_re 1.57. A fit carrying no random effect is
    identical to the bit. The occupancy term also carries its grouping variable
    and factor levels now, so ranef() labels it like the observation arms and
    predict(newdata = ) matches on it.

  • A bare areal term on occu_cover() loads on occurrence only under nuts,
    as it already did under nested_laplace
    (#217). The same
    input previously fit two different models: the deterministic backend decoupled
    the cover arm while the sampler estimated a copy amplitude off the default
    axis (mean 0.78 on a 36-cell fixture). A term's process is the formula it sits
    in, and copy() is what couples it to the other arm. This changes fitted
    values for existing no-copy spatial NUTS fits.
    Declaring copy(spatial())
    reproduces the previous numbers to every printed digit on all three coupled
    recovery fixtures.

  • occu_cover() samples a second (spatially-varying-coefficient) areal field
    under nuts
    (#214). The sampler carried one field block, so a
    trend surface was confined to the grid-integrated path. Each block now carries
    its own basis, site-to-node map, per-site design weight and its own sampled
    (sigma, rho, alpha). Reported as fit$trend_field / trend_fields, with
    per-block suffixed hyperparameters in hyper_draws. Both surfaces recover with
    zero divergences.

  • A per-source detection formula can carry a structured term
    (#216). An integrated detection-arm spde() field was wired in
    the fitter and unreachable from tobs(). Per-source field correlation with
    truth 0.87-0.93; the fields are reported in fit$spatial_field_det. Gating
    this uncovered a silent misplacement: under method = "nested_laplace" a
    detection-arm term was fit against the occupancy arm, on the single-season path
    as well as the integrated one. Both now error with a pointer.

  • Simulation-based calibration runs on eight more families
    (#207): occu, count, abun, royle_nichols, occu_ttd,
    fp_occu, removal and distance, each verified end to end against a
    deliberately mis-scaled control. A structured term and a visit-level
    observation design are refused rather than approximated.

  • The SBC replicate field is drawn at the width the joint engine fits
    (#213). The generator normalised its ICAR draw to geo-mean
    marginal SD 1 while the joint nested-Laplace engine carries the amplitude
    against the raw Q = D - W, so both reported field SDs were off by one common
    factor while their ratio stayed clean. Ranks return inside the band. Note that
    the joint path reports sigma as a raw amplitude while the sampled path
    reports a geo-mean marginal SD; compare a fit against a simulator on the field,
    not on the reported scalar.

tulpaObs 0.0.188

Choose a tag to compare

@gcol33 gcol33 released this 07 Aug 15:24

0.0.188 (2026-08-07)

  • Pinned to tulpa (>= 0.0.136), which makes a joint nested-Laplace fit
    return the same numbers twice.
    test-occu-cover-parallel-coupling.R
    asserts that two identical parallel occu_cover() fits are bit-identical,
    and it failed once under a loaded parallel suite while passing 200+ paired
    fits in isolation. The cause was upstream: the coupled-cell scatter's chunk
    count, which sets its summation order, was derived from a count of solves in
    flight, and the outer width was clamped against a live free-memory reading,
    so both moved with whatever else the machine was doing (gcol33/tulpa#300).
    Measured from here: forcing the chunked path moves means by 1.4e-15 and
    sds by 1.19e-07, and the fixture's default path was bit-identical to the
    forced serial scatter -- so the chunk count demonstrably changes the answer,
    and whether it changed was race-decided. Both inputs are now read from the
    problem rather than the machine, with no parallelism given up. Nothing in
    this package changed; the engine's exported headers are unchanged from
    0.0.135, so no rebuild is needed.

tulpaObs 0.0.187

Choose a tag to compare

@gcol33 gcol33 released this 07 Aug 13:20

0.0.187 (2026-08-07)

  • Pinned to tulpa (>= 0.0.135). The engine's exported headers
    (inst/include/tulpa/) are unchanged from 0.0.132, so this package's
    compiled backend links against the same ABI and needs no rebuild; the
    bump carries the engine's own nested-Laplace and diagnostics work
    (gcol33/tulpa#294 through #299), including the per-block declaration of
    required fields checked at dispatch. The Imports floor, the Remotes
    tag, and the installed engine are checked against each other by
    .github/scripts/check-engine-pin.R on every CI job, so the three
    cannot drift apart silently.

tulpaObs 0.0.185

Choose a tag to compare

@gcol33 gcol33 released this 07 Aug 09:03

0.0.185 (2026-08-07)

  • Pinned to tulpa (>= 0.0.131) so the auto-recentering outer grid also
    engages at this package's own defaults (gcol33/tulpa#291, #292).
    The
    0.0.130 rescue that 0.0.183 pinned for read pareto_k_mode_u / cov_u,
    fields only the full outer Pareto-k diagnostic populates -- and
    control$diagnose.k defaults to FALSE on every route reaching the joint
    driver -- occu_cover(), occu_multiscale_cover(), and occu()'s
    SVC-bar joint path (#101) -- so the driver's rescue never fired on a
    default fit. It now computes the same (mode, covariance) itself
    whenever the grid has collapsed onto an edge, independent of the
    diagnostic. A collapsed default occu_cover() fit therefore re-centers and
    refits where before it stayed railed.

    The companion upstream change adds the same rescue to fit_st_nested()'s
    tau_spatial x tau_temporal [x rho] tensor grid, which reaches the
    observation families carrying an areal + temporal field on one arm
    (removal(), distance(), fp_occu(), dyn_abun()).

    Both are behaviour-preserving where the grid was not collapsed, and both
    decline whenever a grid-construction knob was set explicitly. The
    Remotes: gcol33/tulpa@v0.0.131 tag moves with the floor, as
    check-engine-pin.R asserts: r-universe builds default-branch HEAD on its
    own poll schedule and currently serves 0.0.130, so the tag is what resolves
    the pinned engine.

0.0.184 (2026-08-07)

  • tobs_cpo(), tobs_ppc() and tobs_pit_residuals() now work on a compact
    (ragged) occu_cover() fit (#185).
    method = "nested_laplace" defaults
    compact = TRUE, so the ragged carrier is the standard representation for the
    joint engine, and a compact fit stores no padded [n_sites x max_visits]
    y / valid grid. The pointwise-log-likelihood path already read the ragged
    carrier, but the PPC and the PIT / LOO-PIT CDF limits still derived their
    per-site detection summary as rowSums(model$y * model$valid), which errored
    with 'x' must be an array of at least two dimensions. LOO / CPO / LOO-PIT and
    the posterior predictive check were therefore unavailable for any real-scale
    occu_cover() fit while tobs_waic() worked.

    All three now read one .occu_cover_visit_view(): the one-row-per-valid-visit
    structure a compact fit stores, and the flattening of a dense fit's padded grid
    (.occu_cover_dense_ragged(), the site-major visit-ascending order the
    pointwise kernel already used). n_valid and any_det are derived there, so
    there is one definition of each. Because both layouts now feed one kernel per
    diagnostic, the dense and compact builds of the same data agree to the bit:
    measured 0.000e+00 across elpd_waic, elpd_loo, the LOO-PIT, the PIT
    residuals, and the PPC fit.y / fit.y.rep / bayesian.p.

  • tobs_ppc() no longer returns NA when a detected visit has a missing
    cover (#185).
    The likelihood gates the cover density on detected AND finite (missing-at-random cover), but the PPC kernel scored the NA into the
    Freeman-Tukey / chi-squared sum, so fit.y came back NA for every draw and
    bayesian.p with it. Verified directly against the pre-change kernel: one NA
    cover at one detected visit took fit.y from 1.788 to NA on all draws.
    The replicate is now drawn at every valid visit (so the RNG stream does not
    depend on which visits happened to be detected) and scored only where a cover
    was observed.

  • Internal: the three ragged occu_cover() kernels -- pointwise
    log-likelihood, PPC, CDF limits -- assemble their per-draw predictors from one
    shared Arms view (src/occu_cover_ragged.h) instead of each writing out the
    column-major design / draw index arithmetic. The pointwise kernel is
    byte-identical after the change (test-occu-cover-ploglik-cpp.R scores it
    against its R oracle). The PPC kernel now takes the arm designs and the
    coefficient draws rather than precomputed [n_sites x (S * max_visits)]
    predictor blocks, so its memory is O(V) in the observations rather than
    O(padded grid x draws).

tulpaObs 0.0.183

Choose a tag to compare

@gcol33 gcol33 released this 06 Aug 19:31

Pins the tulpa v0.0.130 engine.

Auto-recentering outer grid (gcol33/tulpa#289, #290)

Every nested-Laplace family built its outer hyperparameter grid from a fixed default axis, so a fit whose field-SD posterior mode sat above the top node collapsed every outer weight onto that boundary node and reported it only as pareto_k_regime = "collapsed_edge". The motivating case was a tulpaObs one: on the 78 real EVA occu_cover() fits, 10 railed the 5.0 sigma ceiling (measurement upstream, in gcol33/tulpa#289).

The fixed grid is now a starting axis rather than a ceiling. A collapsed fit re-centers using the mode-Hessian its own outer Pareto-k diagnostic already computes, and refits.

It reaches this package through the joint driver occu_cover() and cover() already fit against (single-block icar/bym2 backends and multi-block copy blocks) and through the standalone .NL_REGISTRY path. An explicit sigma_grid / tau_grid still wins; auto-recenter engages only where the grid was left at its default, and is a no-op when the mode already sits inside the old axis. car_proper (its rho axis is unguessable) and MCAR are out of scope upstream, so the correlated cover-field routes keep the old behaviour.

Restored the exact Remotes tag pin

be6b2de dropped the tag on the expectation that r-universe would serve the floor. r-universe builds default-branch HEAD on its own poll schedule, not tags or releases, and currently serves tulpa 0.0.125 -- so an Imports floor above that resolves an engine below the floor and check-engine-pin.R fails, which is the #150 skew reopening. Remotes: gcol33/tulpa@v0.0.130 installs the pinned engine directly.

Cost, as before: the Imports floor and the Remotes tag now have to move together, which check-engine-pin.R asserts.

Also folds in the unreleased 0.0.182, which moved the floor to tulpa (>= 0.0.129) for the outer-grid regime split (gcol33/tulpa#276).

Verification

  • check-engine-pin.R: Imports >= 0.0.130 / Remotes @v0.0.130 / installed 0.0.130, with pak resolving github::gcol33/tulpa@708ddc2
  • Smoke tier against the installed package on tulpa 0.0.130: 3703 passed, 0 failed, 0 errors, 649 skipped
  • Cover recovery files were not run for this release