This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
TEXAS (texas-psm) is a Python package for Bayesian GDGT–temperature calibration using Stan models. TEXAS stands for "TetraEther indeX of Ammonia oxidizerS" and it is a proxy system model for TEX86 paleothermometer; an organic proxy for past sea surface temperature reconstructions. It implements a two-stage workflow:
- Forward calibration: Fit a generalized logistic curve (Ring Index → temperature) using hierarchical Bayesian Stan models, producing posterior samples stored as
.ncfiles. - Inverse temperature (invT) reconstruction: Predict paleotemperatures from new Ring Index observations by marginalizing over M parameter sets sampled from the forward posterior.
The proxy system is TEX86/Ring Index (isoGDGT-based paleothermometers), with optional non-thermal predictors (GDGT-2/3 ratio, NO3).
# Create conda environment (primary method)
conda env create -f environment.yml
conda activate texas-env
# Install package in editable mode
pip install -e .
# Or install with all extras
pip install -e ".[all]"CmdStan (≥ 2.23.0; TEXAS is developed against 2.36.0) must be installed and discoverable. TEXAS/utils/paths.py::find_cmdstan() searches in priority order: CMDSTAN env var → $CONDA_PREFIX/bin/cmdstan → <sys.prefix>/bin/cmdstan → highest cmdstan-* under /opt/cmdstan/, ~/.cmdstan/, /usr/local/cmdstan/ → cmdstanpy's configured default. A candidate is only accepted if its bin/stanc (stanc.exe on Windows) both exists and is executable. set_cmdstan_path() is always called on the winning path so cmdstanpy's internal state stays consistent. If CMDSTAN points to a broken path (dir exists but stanc missing/unusable), a UserWarning is emitted and the search continues. If nothing is found, a RuntimeError is raised with install instructions. TEXAS.doctor() / texas-doctor diagnoses the whole toolchain (cmdstanpy, CmdStan path/version, C++ compiler, cache dirs) and is encoding-safe (ASCII fallback for Windows cp1252 consoles). TEXAS.install_cmdstan() / texas-install-cmdstan (utils/install.py) is an opt-in one-call installer over cmdstanpy.install_cmdstan() — no-ops if already resolvable, steps aside for conda, and auto-overwrite=Trues a half-built ~/.cmdstan/cmdstan-* dir. TEXAS never installs CmdStan automatically (no install-on-import, no install-on-first-sample).
Development note (editable install): always run
pip install -e .after cloning. A regularpip install texas-psmputs the package in site-packages (no compiled Stan binaries, no local source changes). Without-e,STAN_MODELS_DIRpoints to site-packages and every Stan model must recompile from scratch on first use.
# Run tests
pytest
# Build docs (unified Jupyter Book: guides + autodoc API + tutorial)
jupyter-book build docs/ # output in docs/_build/html/
# Run Streamlit app (from streamlit_app/ directory)
streamlit run main.py
# Build Docker image (for isolated Stan compilation)
docker compose up| Module | Purpose |
|---|---|
stan/compiler.py |
StanCompiler: wraps CmdStanModel with in-memory + disk caching; force=True clears binary; auto-detects stale/cross-environment binaries (exit code 127 → delete + recompile with RuntimeWarning) |
stan/sampler.py |
StanSampler + functional API get_posterior() / sampler_invT_posterior(); auto-detects optional predictors |
stan/io.py |
save_posterior() / load_posterior() / save_invT_posterior() — persists xarray.Dataset as compressed NetCDF |
stan/metadata.py |
Extracts and attaches metadata + prior strings to posterior datasets |
data/builder.py |
build_fwd_data() — builds validated Stan data dict for forward calibration (proxyObs_* keys, auto use_* flags, no3_cutoff auto-calc); build_invT_inputData() + InvTConfig — bridges forward → inverse by sampling M parameter sets from a forward posterior |
data/filter.py / data/screening.py |
Data cleaning and Mahalanobis screening |
models/logistics.py |
Pure-Python logistic / generalized-logistic functions |
models/multivariate.py |
Multivariate variants (GDGT23ratio, NO3 corrections) |
models/calibration.py |
TEX86Calibration + CalibrationRegistry — classical (non-Bayesian) TEX86 calibrations |
ensemble/generator.py |
generate_ensemble() / generate_ensemble_auto() — samples draws from a posterior and computes calibration curve percentiles |
ensemble/detection.py |
detect_model_and_params() — infers suffix, model function, and flags from posterior attributes |
diagnostics.py |
summarize_sampler_diagnostics() — divergences, R-hat, ESS, E-BFMI; attaches as stan_diag_* attrs |
quality.py |
compute_quality_flags() — per-observation flags on a reconstruction (attainable-range, extrapolation, missing predictors, calibration domain, prior dominance); attached as result["flags"] |
plotting/ |
Range utilities and prior distribution plots |
utils/paths.py |
All path constants (STAN_MODELS_DIR, POSTERIOR_CACHE_DIR, INVT_CACHE_DIR, etc.) |
constants.py |
OPTIONAL_PREDICTORS, DEFAULT_SUFFIXES |
Deprecated modules removed (2026-05-31):
stan/auto.py(OpenCL auto-detect invT variant),utils/hw.py(OpenCL/hardware detection), andutils/cache_search.pywere deleted — they were marked deprecated and unused by the live package (onlyauto.pyimportedhw.py). Do not reintroduce references to them.
Model names follow a naming convention: {transform}_{curve}_{params}_{datasources}_{variant}.stan
- Transform prefix:
invT_= inverse temperature model; no prefix = forward calibration model - Curve type:
gen_logi= generalized logistic;logistic= standard logistic;linear= linear - Params:
fixed= fixed upper asymptote;free= free upper asymptote - Data sources:
culmeso= culture+mesocosm;culmesocore= culture+mesocosm+coretop;crtp= coretop-only - Variants:
hier_crtp= hierarchical coretop;multiv= multivariate (GDGT23/NO3);priorApprox= prior approximation;werr= delta-method EIV (heteroscedastic likelihood, no latent vars);werr_ver2= latent-variable EIV with quadrature RI error + process-noise separation (see below);odr= delta-method EIV in the full hierarchical (non-priorApprox) model;marginal_*= marginalized variants;reduce_sum= parallelized withreduce_sum
Stan models pruned to the shipped set (2026-09-03, RESUME.md Phase B1):
src/TEXAS/stan_models/went from 17.stanfiles to 9 — the models the revised manuscript and the public API actually use. The other 8 moved toarchive/submission-2026-04/stan_models/at the repo root, with a README naming each one and why. Nothing was deleted. (A later pass, 2026-09-07, archived the twotruncated_priorinverse models whenconstraint_typeleft the public API — see below — leaving 7 files shipped today.)Archived:
gen_logi_fixed_hier_crtp_multiv_priorApprox_eiv(the initial submission'sGHEAproduction model, still the revision's comparison arm),..._multiv_priorApprox,..._multiv,gen_logi_fixed_culmesocore, the two non-marginalinvT_gen_logi_fixed_{univ,multiv}_unconstrained, and the twoinvT_gen_logi_fixed_{univ,multiv}_marginal_hard_constraint.
pyproject.tomlglobsstan_models/*.stannon-recursively, so the archive never reached the wheel — this is packaging-neutral. To run an archived model, pass an absolute path:StanCompiler.resolve_stan_path()passes absolute paths straight through, soget_posterior(..., stan_file=str(abs_path))works. Themodel_dir=route named in RESUME.md Phase B does not exist throughget_posterior()— it buildsStanCompiler()with no arguments.
_select_invT_stan_file()was narrowed to match.constraint_typenow accepts only"unconstrained"(as of 2026-09-07,"truncated_prior"was archived too and is no longer accepted either — see the shipped-count note above);"hard_constraint"(archived) and"reparameterized"/"soft"(never implemented as Stan models) raiseValueErrornaming the archive, instead of failing later with a missing-file error.model_typeis no longer a public parameter at all —model_type="ensemble"now raisesTypeErrorbefore reaching_select_invT_stan_file, whose own internal, non-publicmodel_typeguard still raisesValueErrorfor anything but"direct". Before this, 40 combinations were constructible and only 9 resolved to a file. The three that still don't are the T₀-shift arm's univariate and truncated-prior variants, which never existed; theFileNotFoundErroratstan/invT.pycatches those and says so.
utils/naming.py::CONSTRAINT_CODESwas deliberately left intact, exactly as the compset decoder still parsesGHEAandculmesocore: it is a name grammar that must keep decoding case ids of reconstructions already on disk and on Zenodo. Narrowing it would orphan those files.No notebook breaks on its defaults — every one reads a cached or downloaded posterior.
SI_code02arecompiles the additive model only on a cold cache orFORCE_RERUN = True. (RESUME.md's Phase B claim that only markdown and commented-out code referenced it was checked and is wrong forSI_code02a, whose cell 30 has a liveget_posterior(..., stan_file=...)call.)tests/test_streamlit_params.pynow scansarchive/submission-2026-04/too, because the additive posteriors remain downloadable and the app still labelsbeta_G23_crtp/beta_NO3_crtp; the two older archive dirs stay excluded, since they declare theQ_crtpandsigma_scaledRI_crtpthat test exists to catch.Two more sub-archives were added later (2026-09-07):
archive/pre-submission/stan_models/(the 15 never-submitted development models plus 4 pre-annotated variants) andarchive/exploratory/gridT-inversion/(revision-cycle work excluded from the resubmission), alongside anarchive/presentations/sibling for conference material — seearchive/README.mdfor what distinguishes all four archive directories.
Posterior variables carry a suffix indicating which dataset they were estimated from:
crtp— coretop only (highest priority for invT reconstruction)culmesocore— culture + mesocosm + coretopculmeso— culture + mesocosmmeso— mesocosm onlycul— culture only
Example: t0_crtp, k_crtp, b_crtp, v_crtp, sigma_proxyObs_crtp.
Q parameter removed (2026-03-24, Python cleanup 2026-03-24): The asymmetry parameter Q has been dropped from all Stan models (both forward and invT). The generalized logistic curve now uses Q=1, so T₀ is the curve's location parameter. T₀ is not the inflection point unless ν=1: the steepest response sits at
T₀ − ln(ν)/k, which for the fitted ν of 2.1–4.0 is 4.2–5.2 °C below T₀ (verified 2026-08-13 across the bounded-T, additive-EIV, univariate, and culmeso posteriors). Do not quote a single thermal sensitivity for this curve — f′ varies ~6× over the sampled range. All existing.stanfiles were edited in-place — thegen_logi_fixed_Q1_culmeso.stanplaceholder has been deleted.ensemble/detection.pyno longer detects Q;plotting/prior_plot.pyno longer lists Q ininclude_groupsor label dicts. Cached.ncposteriors generated before this change containQ_crtp/Q_culmesovariables that are no longer produced; regenerate them.
Stan model bound fixes (2026-03-24):
k_crtp upper=0.5 → removedin all priorApprox models (gen_logi_fixed_hier_crtp_*_priorApprox*.stan): the standalone culmeso model has no upper cap on k, and its posterior mean (~0.57) exceeded the old bound, pinning k against the constraint.b_crtp upper=0.6 → upper=1.0in all priorApprox models: same class of bug; joint models already usedupper=1.v prior T[0, ] → T[0.1, ]ingen_logi_fixed_culmesocore.stan: prior truncation must match thelower=0.1parameter declaration (mismatched truncation gives an incorrect normalizing constant)..gitignorenegation!src/TEXAS/stan_models/*.stanadded so Stan source files created after the binary-glob rule are not silently untracked.
Stan model prior fix (2026-04-08):
sigma_proxyObs_crtp ~ normal(0.01, 0.1) → normal(0, 0.1)in 5 files:gen_logi_fixed_hier_crtp_multiv.stan,gen_logi_fixed_hier_crtp_multiv_priorApprox.stan,gen_logi_fixed_hier_crtp_univ_priorApprox.stan,gen_logi_fixed_culmesocore.stan(and the former_werr.stan, now superseded). The old prior mean (0.01) was ~5× below the posterior (~0.05);normal(0, 0.1)is the conventional half-normal weakly informative prior for scale parameters. Cached.ncposteriors from these models must be regenerated.
EIV Stan model consolidated (2026-04-16, v0.1.5): The sole EIV model is now
gen_logi_fixed_hier_crtp_multiv_priorApprox_eiv.stan. Earlier development variants (_werr.stan,_werr_ver2.stan,_odr.stan, archived variants) have been removed. Cached posteriors were renamed in-place (_werr_ver2→_eiv); no resampling needed.
_eivmodel design — latent-variable EIV with analytical RI SE separation:
sd_proxyObs(per-site RI analytical SE; default Rs = 0.03, Schouten et al. 2013) enters the likelihood in quadrature:total_sd = √(sd_proxyObs² + sigma_proxyObs_crtp²).sigma_proxyObs_crtpis therefore pure process noise (oceanographic scatter, bioturbation) only.sigma_proxyObs_crtpprior scaled tomean(sd_proxyObs) · √(1 − R²_thermal).R2_thermalmust be passed as data (pre-compute from a thermal-only non-EIV coretop run).sampler.pyraisesValueErrorif missing.- G₂/₃ latent variable:
true_gdgt23ratio_crtp ~ normal(0, 2)with normal measurement model. Sites withsd_gdgt23ratio_crtp[i] = 0receive only the prior.- NO₃ latent variable:
true_no3_crtp ~ lognormal(log(0.3), 1.0)with<lower=0, upper=no3_cutoff>— upper bound preventsexp()overflow during HMC. Sites withsd_no3_crtp[i] = 0receive only the prior. No CV-gating.build_fwd_data()always includessd_gdgt23ratio_crtpandsd_no3_crtp(defaulting to zeros), and always includessd_proxyObs(defaulting to 0.03).R2_thermalmust be provided explicitly.
Posteriors are saved as compressed NetCDF (.nc) in:
data/cache/TEXAS_posterior_cache/— forward calibration posteriorsdata/cache/TEXAS_invT_posterior_cache/— inverse temperature posteriors
A third sibling holds derived grids rather than posteriors:
data/cache/TEXAS_kriged_grids_cache/— kriged residual-map grids (.npz), written byplot_residual_maps
Kriged grids got their own folder (2026-09-07): they used to be written loose into the cache root.
utils/paths.pynow declaresKRIGED_CACHE_DIRbeside the two posterior dirs andset_cache_dir()repoints all three. The resolution token is formatted withf"{krige_res:g}", sokrige_res=1andkrige_res=1.0name one file — they used to write two 57 MB copies of one grid. A grid still sitting in the old location is loaded with a printed note (_legacy_grids_cache), so an un-migrated machine keeps working; writes always go to the new folder. The migration is per-machine —data/cache/**is gitignored:python scripts/migrate_kriged_cache.py(dry run), then--apply --delete-superseded.load_or_build_halo_cacheand its halo-only.npzformat were deleted in the same pass (zero callers);krige_halo_allstays —load_or_build_grids_cacheandplot_residual_mapsboth call it.
Legacy forward posterior filenames follow: {model}_{temptype}_{proxy_name}{suffix}.nc
e.g. gen_logi_fixed_hier_crtp_multiv_SST_scaledRI.nc
Optional predictor flags (_gdgt23ratio, _no3_1.5) are appended to temptype before proxy_name.
proxy_name is omitted from the filename only if not set (falls back to old pattern for backward compat).
save_posterior() defaults to the CESM-style case layout described below
(layout="auto", which prefers case naming) — the pattern above is what
layout="legacy" still writes, and what load_posterior() still reads
for old caches and Zenodo downloads pinned to the v0.2.0 record.
CESM-style case naming (2026-08-09):
utils/naming.pyreplaces the concatenated-description filenames (95–122 chars, growing with every new axis) with fixed dot-delimited positions, so tokens stay short:tx.v026.GHEB.sst.sri03.G23-N1p0.001/ <- the case tx.v026.GHEB.sst.sri03.G23-N1p0.001.fwd.nc <- forward posterior tx.v026.GHEB.sst.sri03.G23-N1p0.001.inv.U1482.ud-050126.nc <- a reconstructionPositions: project, version, compset, target temperature, proxy, predictors, run/member. The 4-char compset encodes curve (
Ggen_logi_fixed,Llogistic,Nlinear), training set (Hhier_crtp,Cculmeso,Jculmesocore,Tcrtp), estimator (PpriorApprox,EpriorApprox+EIV,Dfull hierarchical), and predictor structure (Uunivariate,Aadditive β-on-μ,Bbounded-by-construction — the manuscript's "T₀-shift parameterization", γ-on-T₀; the letter names the property, the paper names the mechanism). So..._hier_crtp_multiv_priorApprox_eiv_t0shift→GHEB. Predictors areG23andN+ the cutoff withpfor the decimal point (N1p0= cutoff 1.0), orp0when there are none.
N10→N1p0rename (2026-08-23). The nitrate token wasN+ cutoff×10, so a cutoff of 1.0 µmol/L was writtenN10— misreadable in the one way that matters, since 10 is also the documented value for switching the NO₃ correction off. A token could therefore be read as the opposite of what it means.encode_predictorsnow writesN1p0(_fmt_cutoff,pfor the decimal point since.delimits the fields);N10still parses, so every case id in the cache, in notebooks, and incase_ids.jsonkeeps resolving. Nothing on disk was renamed except the two bundled posteriors.Files on disk were renamed on 2026-08-23 by
scripts/rename_cache_files.py(dry-run by default, refuses on any destination clash,--revertto undo): 191 files across both caches,.npzsiblings moved with their.nc. This is per-machine —data/cache/**is gitignored, so run it on the Windows box too.
resolve_posterior_pathnow normalizes the predictor token on both sides before comparing, and tries both spellings on the exact-path lookups, because an id and the file it names can sit on opposite sides of the rename in either direction — a notebook holdingN10must find a posterior written today, andN1p0must find one in an old cache. Pinned bytests/test_naming.py.Posterior attrs normalized (2026-08-23).
scripts/normalize_posterior_attrs.pyedits cached posteriors' attrs in place through netCDF4 (append mode, so the draws are never rewritten — verified byte-identical, +87 bytes of header):stan_model_name..._eiv_boundedT→..._eiv_t0shifton the 8GHEBfiles,case_idrefreshed to the current predictor token, and the duplicatemodelandversionattrs dropped, andgenerated_bycorrected fromculRI-Bayesian(the project's name years before it was TEXAS) totexas-psm.modelwas arviz echoing CmdStan's own config key —stan_model_name+"_model", identical in all 33 cached files — and it is the reason a rename could leave one name stale while the other was current.versionwasextract_and_update_metadata()'s own default argument, literally"1.0.0"everywhere, set by no caller and read by nothing.sampler.pyandmetadata.pyno longer write either:stan_model_nameis the single model name andtexas_versionthe single version, andtexas_versionis not backfilled onto older files, because which package produced them is not recoverable and absent is the honest answer. The bundled pair additionally carriesbundled_with, which is a different fact — the version that built the bundle, not the one that sampled the draws.filenameis deliberately left as written (it is history, not identity, and legacy stamped lookups match on it), andsuperseded/is not touched at all. Dry-run by default.
boundedT→t0shiftrename (2026-08-15). The variant token in Stan file names, figure names, notebook names, and scripts is nowt0shift, matching the revised manuscript's "T₀-shift parameterization". The legacyboundedTspelling still parses (encode_compsetmaps both toB), so cached posteriors with oldstan_model_nameattrs and legacy.nc/.pklcache files resolve unchanged — nothing indata/cache/was renamed. Renamed Stan models recompile on next use.Notebook switch keys followed on 2026-08-21. They were left alone initially on the grounds that they name runtime branches, not artifacts — which was wrong:
SI_code03_paleo_showcasesinterpolatesMODEL_VARIANTstraight into two output names (figSI_variant_comparison_{variant}_vs_{other}.pdf,data_list_extreme_example_{variant}.pkl), so the branch key is an artifact name.MODEL_VARIANT, theVARIANTS/COMPSET/MUdict keys, andparameterization=inSI_code01now all readt0shift, and those two outputs were renamed on disk to match. Executed cell outputs still show the old spelling until each notebook is re-run; that is a record of what ran, not stale code.The same pass renamed
coretop_maps_boundedT_manifest.csv→coretop_maps_t0shift_manifest.csv(run_coretop_maps.pyalready looked for the new name, so a resumed--arm bndrun had been finding no manifest at all) and deletedcoretop_maps_boundedT_sites.csv, a byte-identical leftover of the arm-independentcoretop_maps_sites.csvthatSI_code04now reads. Inworking-repo/TEXAS-revision:fit_boundedT_comparison.py→fit_t0shift_comparison.py,inspect_boundedT.ipynb→inspect_t0shift.ipynb, and the local..._eiv_boundedT.stan→..._eiv_t0shift.stan. OnlyboundedT-explainer.htmlkeeps the old token, because its prose does too.Proxy codes read as scaled + ri + crenarchaeol ring count:
sri03=scaledRI_cren3(crenarchaeol counted as 3 rings),sri04=scaledRI_cren4(the RI₀₋₄ convention of Zhang et al.),sri=scaledRI,tex=TEX86,tri03=TEXRI_cren3.Token spellings changed 2026-08-11 (
ri3→sri03,none→p0, and the leaf gained its case prefix). The old spellings still parse, so case directories already on disk keep resolving, but they are no longer written.TEXRI_cren3previously shared the coderi3withscaledRI_cren3, which silently collapsed two distinct proxies onto one case id; it is nowtri03.Why each leaf repeats its case. CESM names data output for its case (
b.e12.B1850C5CN.f19_g16.iPETM09x.01.pop.h.1901-2000.climo.nc) and reserves bare names for case control files that never leave the directory. A posterior does leave — decisively, it is published to a Zenodo record whose namespace is flat, where fifteen files namedfwd.nccannot coexist. This is not free: the full path grows from 39 to 72 characters versus a barefwd.nc. What it buys is a leaf that is still self-identifying once detached, and the leaf itself still drops ~100 → ~41 characters against the legacy name.download_posteriors()unpacks a flat Zenodo file into its case directory (utils/download.py::_local_dest) so the local cache has one uniform layout whether a posterior was sampled here or downloaded.
- The case is the forward calibration. An invT model name records the curve and constraint but not the training set or estimator, so a reconstruction is named as a member of its parent case, not as a case of its own. This relies on the
fwd_case/fwd_posterior_nameattrs thatbuild_invT_inputDatanow attaches — invT posteriors written before this carry no provenance and fall back to the legacy flat name automatically.- The run position (
.001) is CESM's ensemble-member field;save_posteriormapsfilename_suffix(e.g. a050126date stamp) onto it, which is what stops two refits of one configuration from colliding.- Dual-read, write-new. Nothing on disk was renamed.
load_posterior()accepts either a case id or a legacy long name and finds the file under either layout (exact-path lookups first, attr-matching scan only as a fallback), so existing caches, Zenodo downloads, and old notebooks keep working.save_posterior(..., layout=)takes"auto"(default, prefers case),"case", or"legacy".Status of the inverse half (audited 2026-08-11) — the forward side is wired, the inverse side is only half-wired. Do not assume otherwise:
naming.inv_relpath()is the documented canonical inverse-name builder and nothing outsidetests/calls it. The production path isio._generate_filename_base(), which reimplements a different leaf format inline (no run number).save_invT_posterior()(stan/io.py:269, exported in__all__) is entirely case-unaware and silently dropsproxy_name, so ascaledRIand aTEX86run of one site overwrite each other.Fixed 2026-08-11.case_from_attrs()cannot recoverfilename_suffix.run_from_attrs()recovers the run token from thefilenameattr, whichsave_posteriorstamps onto every dataset and which keeps its date suffix even after a file is renamed without one. Refits now get distinct runs instead of all collapsing onto.001.save_posteriorstill passes a run explicitly, so a genuinely new fit never inherits a stale stamp.Tracked as Phase 5 in
RESUME.mdonfeat/revision1-validation-groupA.
scripts/migrate_cache_layout.py converts forward posteriors — legacy flat
files and case directories written before 2026-08-11 — onto
<case>/<case>.fwd.nc.
python scripts/migrate_cache_layout.py # dry run: print the plan
python scripts/migrate_cache_layout.py --apply # copy into place, verify each
python scripts/migrate_cache_layout.py --apply --prune # then delete the sources
python scripts/migrate_cache_layout.py --cache /some/other/dirIt is dry-run by default, copies before it deletes, re-opens every copy and checks the case id matches the directory, and exits 1 without touching anything if two files claim one case id — that guard is what makes it safe to run unattended.
This is per-machine, and the plan will differ on each one. data/cache/**
is gitignored, so it does not travel with a clone: the Linux box and the
Windows box hold different posteriors. Never assume a migration done on one
machine has happened on another — run the dry run first and read it. The only
prerequisites are an editable install (pip install -e ., so TEXAS imports)
and xarray; no CmdStan, no compilation, no network.
Inverse posteriors are deliberately skipped. An invT model name records the
curve and constraint but not the training set or estimator, so for any file
without a fwd_case attr the parent case is unrecoverable, and inventing one
would record a guess as provenance. Leave them on the legacy dual-read path.
Nothing is lost if the leaf names are wrong — load_posterior() reads
legacy flat names, <case>/fwd.nc, and <case>/<case>.fwd.nc alike, and old
ri3 / none tokens still parse. Migration is a tidiness step, not a
correctness one.
you ask load_posterior() for |
after migrating | after re-running Stan |
|---|---|---|
| the new case id | ✅ | ✅ |
the unstamped legacy name (..._scaledRI_cren3) |
✅ | ✅ |
a date-stamped legacy name (..._cren3_050126_eiv) |
✅ | ❌ |
The difference is where the old name lives. Migration copies the filename
attr through untouched, so the file still remembers what it was called and
resolve_posterior_path matches on it. A re-run writes a new file and
save_posterior stamps the new leaf name onto filename — nothing on disk
remembers the old one. The unstamped form keeps working either way because it
is reconstructed from attrs by legacy_fwd_name() rather than remembered.
This matters because SI_code3_paleo_showcases.ipynb and
SI_code03_paleo_showcases.ipynb both request a stamped name
(..._scaledRI_cren3_050126_eiv). If you re-run those calibrations rather
than migrating, update the notebooks to the case id. Both behaviors are
pinned by tests in tests/test_naming.py.
Three-tab GUI:
- Predict: upload CSV → run invT reconstruction
- Explore: upload NetCDF posterior → plot distributions
- Compute: run forward calibration in-browser (limited; heavy jobs should use notebooks)
Entry point: streamlit_app/main.py. Config (cache dir resolution, plot defaults): streamlit_app/config.py.
notebooks/manuscripts/— finalized figure-generation notebooks for papersnotebooks/reviewer_response/— revision-cycle analysis notebooks
The docs are a single unified Jupyter Book (Sphinx), migrated from
mkdocs-material on 2026-05-30. docs/_config.yml + docs/_toc.yml define one
book covering the guides, the autodoc API reference, the explainers, and the
interactive tutorial (docs/tutorial/).
- Build:
jupyter-book build docs/(pinjupyter-book<2— v2/MyST does not read thejb-book_toc.yml/_config.ymlformat). Output indocs/_build/html/. - API reference (
docs/api.md): Sphinxautodoc+napoleon(both Google and NumPy docstrings). Optional/heavy deps are mocked viaautodoc_mock_importsin_config.yml, so a core install builds the API page. - Branding:
docs/_static/texas_logo.svg(colorblock TEXAS wordmark) +docs/_static/custom.css(steel-blue accent), derived from the AGU25 poster. - Deploy:
.github/workflows/docs.ymlbuilds the book and publishesdocs/_build/htmltogh-pageson push tomain. Live at https://paleolipidrr.github.io/TEXAS/. There is no longer anmkdocs.yml.
Forward data construction: build_fwd_data() in data/builder.py is the recommended way to build the Stan data dict — it enforces proxyObs_* key naming, validates array shapes, auto-sets use_gdgt23ratio / use_no3 flags, and auto-calculates no3_cutoff via Spearman rank correlation if omitted. For two-stage priorApprox models, pass culmeso_posterior= to extract hyperpriors automatically. Hyperpriors for t0, k, b, v are extracted as raw mean/std from the culmeso posterior. Q is no longer a parameter (fixed to 1 in all models).
Optional predictor auto-detection: auto_detect_predictors() in stan/sampler.py inspects the data dict for GDGT23/NO3 arrays and sets use_gdgt23ratio / use_no3 integer flags for Stan. Also translates legacy scaledRI_* data keys to proxyObs_* with a DeprecationWarning for backward compatibility.
Posterior metadata: After sampling, extract_and_update_metadata() attaches run info (model name, temptype, priors, duration, diagnostic summary) as xr.Dataset.attrs. The proxy_name attr (e.g. "scaledRI", "TEX86") is required at get_posterior() call time and is always written to .nc files via _sanitize_attrs_for_netcdf. Downstream code reads these attrs for decisions (e.g., ensemble/detection.py reads use_gdgt23ratio, use_no3, no3_cutoff from attrs and infers model function from data_vars — it does not read stan_model_name).
Forward → inverse pipeline:
# 1. Forward calibration
data = build_fwd_data(
t_cul=cul_df["SST"].values, proxy_cul=cul_df["scaledRI"].values,
t_meso=meso_df["SST"].values, proxy_meso=meso_df["scaledRI"].values,
t_crtp=crtp_df["SST"].values, proxy_crtp=crtp_df["scaledRI"].values,
gdgt23ratio_crtp=crtp_df["gdgt23ratio"].values,
no3_crtp=crtp_df["no3"].values, # no3_cutoff auto-calculated via Spearman if omitted
)
post, diag = get_posterior(data, "gen_logi_fixed_hier_crtp_multiv", temptype="SST", proxy_name="scaledRI")
save_posterior(post) # → <case_id>/<case_id>.fwd.nc (case layout, the default since v0.2.6)
# 2. Inverse reconstruction
data_inv, kwargs = build_invT_inputData(proxyObs, prior_mu_t, prior_sigma_t, fwd_posterior_name="...")
post_inv, diag = sampler_invT_posterior(data_inv, "invT_gen_logi_fixed_multiv", **kwargs)