Conversation
|
I'd have thought github would include the code - but apparently not! Here's another attempt: |
|
Example inspired by Niko's work and mostly meant to clarify! @brm begin
#Generating the BMI with a very simple formula
@linear_combination begin
BMI = 1
@df dBMI
end
#Checking the BMI against the measured BMI (just a simple likelihood)
@regression_submodel "BMI_measurement_error" begin
DistributionLikelihood(Normal,
# Struggling with this.
# It has to know that it should take the output of the operation above as the BMI mean,
# and take the BMI_measured from the dBMI dataset
dist_args = (μ = BMI, σ = 1),
observations = dBMI.BMI_measured
)
#Something like this could maybe show that it comes from a previous outcome
@previous_outcome BMI
end
#This linear combination generates the mean of the predicted performance
@linear_combination begin
performance_mean = 1 + Age_first * Treatment + Age_second + (1 + Treatment | Subject) + (1 + Age_first | Experimenter)
#This specifies how Age should be expanded. Can also be a custom type with appropriate dispatch.
@expansion (Age_first, Age_second) = Age, PolynomialExpansion(order = 2)
@df dpmean
end
#This linear combination generates the sd of the predicted performance
@linear_combination begin
performance_sd = 1 + Age * BMI + Age:BMI + (1 + Age * BMI | Subject)
#This specified which operator using for the interaction between Age and BMI. Can be a custom type with appropriate syntax
@interaction Age_BMI_max (Age,BMI) MaxOperator()
@df dpsd
end
#This evaluates the likelihood of the perf
@regression_submodel "Performance_likelihood" begin
DistributionLikelihood(Normal,
dist_args = (μ = performance_mean, σ = performance_sd),
observations = dpmean.Performance
)
@previous_outcome performance_mean
@previous_outcome performance_sd inv_link = exp
end
#Configurations
@config begin
## Maybe the configurations for a random effect factor should
#Group specification cannot differ between different linear combinations, and is general for a given random effect factor
@group Subject, by = ClinicalGroup
#Block specifications cannot differn between linear combinations, and is general for a given random effect factor
@block Subject Block1 [performance_mean.Treatment, performance_sd.Age_BMI_max]
@block Subject Block2 [performance_sd.Age, performance_sd.BMI]
end
#Settings that are called in every operation (submodel and linear combination) if not overwritten by specific settings in those operations.
@defaults begin
#Something with priors
end
## ONE THING THAT THIS EXAMPLE DOESN'T HAVE IS A VARIABLE GENERATED BY A SUBMODEL ##
end
|
|
Primnary conversation currently on Slack; I mostly wanted here to distinguihs the linear combinations from the regression submodels |
|
I think these would be the "rules" of the
It's currently a bit unclear how we'd reliably know when the topmost call returns a distribution - but maybe that is actually quite easy. Also, at some point we discussed the question of when to parse terms as formulas and when not to. Should e.g. |
|
I think transforming (something like) this @brm begin
Age_first, Age_second = ploynomial_expand(Age; order=2)
performance_mean ~ 1 + Age_first * Treatment + Age_second + (1 + Treatment | Subject) + (1 + Age_first | Experimenter)
log(performance_sd) ~ 1 + Age * BMI + max(Age, BMI) + (1 + Age * BMI | Subject)
Performance ~ Normal(performance_mean, performance_sd)
endto an expression (somewhat) like this model(__df__) = begin
(; Age, Treatment, Subject, Experimenter, BMI) = data(__df__)
(; performance_mean, performance_sd, Performance) = maybedata(__df__)
(Age_first, Age_second) = (ensurecols)(ploynomial_expand, Age; order = 2)
performance_mean = ((maybedists)(; force = isdata(performance_mean)))(1 + Age_first * Treatment + Age_second + ((1 + Treatment) | Subject) + ((1 + Age_first) | Experimenter))
log(performance_sd) = ((maybedists)(; force = isdata(log(performance_sd))))(1 + Age * BMI + (ensurecols)(max, Age, BMI) + ((1 + Age * BMI) | Subject))
Performance = ((maybedists)(; force = isdata(Performance)))(Normal, performance_mean, performance_sd)
BRM(; Age_first, Age_second, Age, performance_mean, Treatment, Subject, Experimenter, performance_sd, BMI, Performance)
endshould allow us to do everything that we need - thoughts, @penelopeysm? |
|
|
|
So this is awesome work Niko! I think So with that in mind, the way I implemented it differs a bit from the rules you made (so we will change one of the two!) I've made it so that every line (I called them "operations") can either be a linear combination, as above, or a Turing submodel. Both linear combinations and Turing submodels can generate outputs (i.e., the lhs). Outputs must be accessible by subsequent linear combinations and Turing submodels. I split that up in two, so that outputs can be used to update the Predictors design matrix (which can be used by subsequent linear combinations) or be stored in an outputs object (which is passed to subsequent Turing submodels). To me, splitting things up (I used = and ~ for linear combinations and Turing submodels, but happy to do whatever) simplified things a lot. Then it is a higher-layer functionality that its possible to just write Okay so happy to change everything, but just important to align on this point I think, and to make sure the macro and the implementation are both able to handle the full flexibility! |
|
And as I said in Slack, the single distribution likelihood is just one special case of likelihood, but we want to be more general than that, hence the Turing submodel. Otherwise only exciting. Standing by for being prompted :) |
|
Mock model for my specific usecase: cognitive_parameter_1 = predictor1 + predictor2 + (1|p|gr(subjID, by = clinicalgroup))
cognitive_parameter2 = predictor1 + predictor3 + (1|p|gr(subjID, by = clinicalgroup)) This needs to be able to include both random effect groups and correlations across regressions. |
|
Second mock model for my specific usecase: physiological_measurement1 ~ cognitive_state1 + other_predictor + (1|subjID)Here, the cognitive_state1 will be generated by a Turing submodel called before the regression, but should still be used in the regression as a predictor like any other. Must be able to allow as many cognitive_state predictorsd as desired, and also multiple outcomes, and in general any type of regression. |
|
Third mock model for my specific usecase (complex, in the future): Same as the first mock model, but where the clinicalgroup membership is generated in a previous Turing submodel. This is a discrete variable, so a non-gradient sampler must be used for this one (but NUTS or whatever can be used for the regression coefficients) |
|
Fourth mock model:
Here predictor 1 is a categorical variable. It would be ncie to use Paul's approach (which we discussed) where there is a single beta giving the full effect size of going from the lowest to the highest level, and then a simplex defining the differences between each step |
|
Finally, I would like to combine mock models 1 and 2 with a Turing model of my own in-between (so the outcome of mock model 1 is used in my custom Turing mdoel, which in turn generates predictors for mock model 3). It is also in general important that the outcomes (I guess all the lhs of the model) are returned by the regression model so they can be accessed when it is used as a submdoel within a larger Turing model. |
|
Mock Turing model that the brm model will be used inside (dummy code, not optimised). This covers usecases 1 and 2, so brm could be used twice inside the model. Note that, importantly, for the first case here I do not want any likelihood inside the brm model at all - I just want to calculate the linear combinations of predictors and coeffiicents, and then handle the likleihood in the larger model. @model function full_cognitive_model(cognitive_model, behavioural_data, conditioned_regression_model, post_behavioural_model)
#This should give a set of different cognitive parameters (i.e. columns) for each participant in an experiment (i.e., each row)
#If the regression model outputs more columns than needed, there coukld be a tiny wrapper here selecting the appropriate columns
cognitive_parameters ~ to_submodel(conditioned_regression_model)
#Initialise storage for cognitive state
cognitive_states = []
#Go through each row (i.e., each subject)
for (subject_idx, subject_parameters) in enumerate(zip(cognitive_parameters...))
#Cognitive modelling is essentially agent-based modelling
agent = initialise_agent(cognitive_model, subject_parameters)
#Extract the empirical observations and actions for a participant
subject_observations, subject_actions = behavioura_data[i]
#For each trial in the experiment
for (subject_action_t, subject_observation_t)
#Generate a probability disitribution over the action at that timestep
predicted_action_dist_t = agent.cognitive_model!(subject_observation_t)
#Evaluate the probability of the real action (sample a new one if it is missing)
subject_action_t ~ predicted_action_dist_t
#Store the action (since some models depend on their previous actions)
store_action!(agent, subject_action_t)
end
#Store the trajectory of cognitive states (which are updated on each trial)
push!(cognitive_states, get_cognitive_state_history(agent))
end
#Optionally continue with a different model (such as a regression relating cognitive states to neuroimaging data).
physiological_data ~ to_submodel(post_behavioural_model(cognitive_states, otherstuff))
end
```julia |
|
I think we also want to be able to do something like: a ~ custom_submodel_1()
b ~ a + z
p ~ custom_submodel_2(a, b, z)i.e., where a custom submodel generates some value (not via a regression) which is used in a different submodel (which is also not a regression). In other words, we might want to allow storing and passing values in another way than through updatecols!, so that we don't waste time updating columns in a design matrix when not necessary,a nd so that we allow for non-tabular values too |
|
@PTWaade you can make github add syntax highlighting by having "julia" after the first three backticks: |
Ah - thank you :) |
…than brms for the primal computation and 20 times (Enzyme) or 30 times (Mooncake) slower for the gradient computation
The @brm macro and the VBRMI implementation now live alongside the new BRMMacroWeb module under web-macro/src/, so Revise tracks edits without a restart. scripts/Benchmarking/main.jl is updated to include them from the new location; the other (untracked) entry points still on disk are left to fix themselves up. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…fects BRMMacroWeb is a small HTMXObjects app that walks the @brm pipeline stage by stage (Meta.parse → parse! → _brm let-block → eval → VBRMI → Chairmarks benchmark) against a synthetic dataset. Each stage renders incrementally and the VBRMI stage runs a finite-difference gradient sanity check that flags any parameter that fails to influence the log density — useful for catching the kinds of bugs fixed below. vimpl.jl fixes: - growblock!! used to return view(g, :, 1), aliasing every parameter in a block to the first column. Now returns view(g, :, idxs) so each growblock!! call gets its own freshly-appended slot. - _cat_lookup / _cat_re_lookup add treatment-coded categorical predictors at population level and as random slopes inside a grouping factor (e.g. (cohort | group)). - _re_lookup / _gc_idx wire random-effects views back to a length-N per-row lookup keyed by the row's group code, so (... | group) formulas materialize correctly instead of erroring with a DimensionMismatch. The default formula in BRMMacroWeb exercises Normal (with distributional regression on the scale), Poisson, Binomial (using a plain positional `n` argument instead of brms's trials() sidecar) and Bernoulli likelihoods in one model, sharing random-effects blocks across linear predictors. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…afety whitelist BRMMacroWeb.jl: - Styled HTML rendering for BRMI/VBRMI cards: deterministic per-symbol colors (HSL from hash), data columns normal-weight, parameters bold, likelihood statements underlined. Replaces the plain-text `sprint(show)` rendering with a recursive `_html_expr` tree walker that produces colored <span> nests. - VBRMI card shows BRMI-level symbolic expressions (from the parent BRMI) with type/shape annotations, not raw Broadcasted internals. - Logdensity + finite-difference gradient check collapsed into a single <details> with a green/red summary line. - TODO page (/todo) with nav_sidebar navigation (HTMXObjects `page` property for auto fragment/full-page wrapping). - File-backed TODO state: each item is a .jl file under web-macro/todos/ with `# key: value` header + `#= markdown =#` body + raw formula. Status (open/done/deprioritized) and formula edits persist to disk. - Per-todo article cards with status-colored left border, done/deprioritize pills, collapsible details for done/deprioritized items. - Inline pipeline results: "Try in pipeline" posts to /stage/vbrmi and swaps the VBRMI output into a div inside the card, no page navigation. - 25 TODO items across 3 tiers with verification formulas where applicable. - Formula safety whitelist: _check_formula_safety! walks the parsed AST before eval and rejects any function call not in _ALLOWED_CALLS (math, distributions, DSL operators). Blocks macros, shell commands, eval, include, run, ccall, etc. vimpl.jl: - vbroadcasted(::Number; meta) fallback so literal numbers (e.g. the 2 in a^2) pass through to Base.broadcasted as scalars. macro.jl: - _show_top renamed from _show_op, _leaf_column helper for walking through link-function wrappers to find the innermost NamedColumn. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
`slope ~ 0 + ztime` now skips the intercept — `vmeta_sampling_rhs(0; group)` returns `(meta, 0)` without calling `growblock!!`, so no parameter is allocated. The literal `0` broadcasts as a scalar in the parent `+`. Needed by the QT pipeline's two-formula pattern where the slope formula uses `0 + ztime` (no intercept, just the time covariate). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
web-macro/todos/bruno-*.jl files contain client-project model references that must stay local. The pattern is broad enough to cover any future Bruno-related TODO files. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- drop `Block` struct; `meta.blocks` is now NamedTuple[Symbol → Tuple of
`Part{F,D}`], with each Part carrying its marker-fn dispatch tag and a
NamedTuple of buffers.
- split LKJ-Cholesky into `chol` + `grouped_normal` Parts sharing one L;
`finalize` threads the expansion per-part.
- add `simplex` primitive Part (log-scale stick-breaking, `alpha=1.0` kwarg on
`lprior!`), and user-template block at the bottom defining `mo1(c)` and
`mo(c)` = β·mo1(c), reusing the shared `_mo_contrast!` + `_scale_by_beta`
helpers.
- extract helpers: `push_parts!!`, `_scale_by_beta`, `_level_index` (with a
`CategoricalArrays` fast path + Dict-fallback warning).
- rename `page` → `__page__` in AppContext to match HTMXObjects naming.
- Wire AppData via struct-body `__appdata__ = APPDATA` singleton. - Rewrite `@get stage` with Treebars `polling_fetchindex` so pipeline runs off the request thread and the UI polls for progress. - Add dataset_namespace/dataset_container/dataset_extras hooks so per-TODO extras (e.g. bruno-ext) can be spliced into the data NamedTuple without touching macro.jl. - Split `render_output` -> `render_pipeline`, stream per-step Chairmarks `@be` benches (logdensity, full lprior!, each Part, each materialized column) so allocations can be localized. - Add formula safety whitelist before `Meta.parse`/`eval`. - TODO page with file-backed entries and mark/formula persistence. - `@inline` lprior!/llikelihood! leaves + container folds so the heterogeneous-tuple `foldl` specializes to zero allocations. - .gitignore: also exclude `web-macro/src/bruno-*.jl`. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Status toggles and textarea edits persisted from the /todo page (1.1 Bernoulli/Binomial formula switches c1 -> c2; several items reopened). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
sbimpl walks a BRMI directly and emits a @slic model that transpiles to Stan. Covers population fixed effects, `mo(c)` / `mo1(c)` monotonic terms, treatment-coded categorical predictors (K-1 betas), LKJ-correlated random effects `(1 + x + ... | g)` with per-group merging, and a Normal likelihood. Pipeline branches after :brmi -- :bench stays on VBRMI, :stan_code shortcircuits into SBBRMI without paying for materialization. TODO cards sb.1..sb.6 scaffold the roadmap for remaining features (distributional, ranefs done, categorical done, non-Normal likelihoods, submodels). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ed LHS Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…fset/gp) Updates existing TODOs to mark I(), scale/center/standardize, `||` zerocorr, and `a:b` cont x cont interactions as done, and adds new TODO examples for `offset(x)` and HSGP `gp(x; k, c)`. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Staged current state of the web module so diffs against the upcoming review pass are easy to read: - rename todos/ -> examples/ (status/formula edits already written back) - add brm-macro.css + html_expr.jl (styled BRMI/VBRMI cards) - Project.toml: BridgeStan + WarmupHMC direct deps - BRMMacroWeb.jl: AppContext + polling_fetchindex + sbimpl stages Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Avoid collisions with `LinearAlgebra.I` and `Distributions.scale` imports in vimpl. `protect` follows GLM.jl naming; `zscale` clarifies the z-transform semantics that `standardize` already aliases. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…lays - Stan path: param_constrain(generated_unc) -> (long, wide, summary) DF triple, shared with Pathfinder/warmup fits via `constrain_draws` + `dfs_from_constrained` helpers (deduplicates ~60 lines of inline DataFrame plumbing across three callsites). - SBC setup: take one draw from the prior-predictive `generated` matrix, fold `*_gen` entries back into the Stan data dict as `*` (observed), and fit `fit_instance = StanModel(lib, bridgestan_data( fit_data_dict))`. Pathfinder / full warmup then sample `p(theta | y_sim)`; ground-truth `fit_truth_unc` stays available. - Treebars progress nesting: `pathfinder` / `posterior_warmup` are IPs that pass `progress=__status__` to WarmupHMC; `compute_steps` calls `fetchindex!(progress, ip, args...)` so the IP's substatus attaches under the step's phase. Kwargs (rng, maxiters, n_draws) supported natively on IP signatures. - StanProblem wrap: BridgeStan.StanModel lacks `logdensity_and_gradient`; IPs wrap with `StanLogDensityProblems.StanProblem(instance)` at call. - Shared `posterior_plots(long, wide, summary; id_prefix, kind, truth)` builds the 8-tab PI / LR / ECDF / Hist tabset. Used by `stan_generate`, `stan_fit_pathfinder`, `stan_fit_warmup`. `plot_fit` removed (redundant with the two fit steps). - Pre-aggregated summary path: `bands=[:q025=>:q975, :q10=>:q90, :q25=>:q75]` feeds `pointinterval(bands=..., orientation=:vertical)` and `lineribbon(bands=...)`; `Statistics.quantile/median` compute once per (param, index) group. - Truth overlays: `truth_df` (fit_draw_idx column constrained, one row per (param, index)) layers black `Scatter` over PI/LR and index-colored `VLines` over ECDF/Hist. Same overlay on prior-predictive and fit plots so user can see which draw was picked and how well the fit recovers it. - PI/LR share identical mapping `(:index, :median, row=:param)` with `indep_y`; ECDF/Hist share `(:value; row=:param, color=:index)` with `indep_x`. Picker tabs wrap each spec via `with_plot_caption`; PI/LR pickers list only `:param` in dims to avoid the pinned catch-all combo. - Project deps: +AlgebraOfGraphics, +AlgebraOfVega, +JSON, +Statistics (web-macro); +AlgebraOfVega, +WarmupHMC (web-macro/app). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ixes - New pipeline step 6b' StanShapes between StanEval and StanGenerate: `param_shapes_df` (one row per base param, :n_indices count), rendered as a sortable table. - Truth overlay support in `posterior_plots`: filled black Scatter on PI/LR, VLines on ECDF/Hist. Pass-through via optional `truth=...` kwarg; threaded through stan_generate, stan_fit_pathfinder, stan_fit_warmup bundles. - Histogram: `bins=30, datalimits=extrema` for per-facet local binning (workaround for `linkxaxes=:none` + faceted histogram sharing x-axis). - Scatter fill: `filled=true` kwarg so dots render filled instead of Vega's hollow `point` default. - `nonnumeric` on :Int :index to keep ECDF/hist color discrete. - ExampleEntry: added explicit `__parent__ = nothing` field so positional `ExampleEntry(path; __parent__)` works after DO constructor tightening. - Reverted earlier explicit `examples_dir = __appdata__.examples_dir` back to `(; examples_dir) = __appdata__` destructure (DO b8cb6c8 fix makes the tuple-destructure LHS now registers as a parent property for inline-child auto-forwarding). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ink hardcoded
- Example card's formula form now renders the full 15-button pipeline
stage set (1. Parse through 6d'. StanFit Warmup), each GETing
/pipeline/stage/:id with the card's formula+label included so bruno-
prefixed examples pick up `dataset_extras(::Val{:bruno}, df)` extras.
- SB repro button added to card form too; shareable URL now carries
label, so the sb_repro page for a bruno example fires the same
namespace dispatch as the main pipeline page.
- Permalink hardcoded to `/examples/$slug` (the `__parent__/slug`
form was yielding `/slug` without the `/examples` prefix; reason
pending HTMXO agent investigation).
- `@get index(slug::AbstractString="")` — widened slug type so URL-
parsed SubString{String} matches (was String-only → MethodError on
/examples/{slug}).
- Removed `filled=true` kwarg on scatter overlays — AoV now defaults
Scatter marks to filled.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…round - Permalink `href=string(__parent__/slug)` now that HTMXO bc32014 + 6ae7866 land (per-include __prefix__ threading + :index-always-collapses path). - Histogram workaround `datalimits=extrema` removed — AoV now handles per-facet bin extents natively when `facet=(; linkxaxes=:none)`. `bins=30` kept (pending AoV kwarg-forwarding fix). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Factor sb_repro's render into `_sb_repro_html(run, formula_str)` helper so
both verb paths share it.
- New `@get sb_repro_example(; name::AbstractString="")` on PipelineRoutes:
looks up an ExampleEntry by slug via `__parent__.examples.example_store.
find_by_slug(name)`, reads that entry's label/formula, and emits the same
bug-report HTML/markdown. No persist side effect (reads from disk only).
- New example `examples/sb-bug-popefs-tp-size.jl` (label "Bruno SB bug:
popefs TP matrix size scope", tier 2, open) with the minimal formula that
trips the `pop_loc_loc_n_covariates` TP-size scope bug, for external
agents to reproduce via the above route.
Acceptance:
curl -s -H 'Accept: text/plain' \\
'http://localhost:.../pipeline/sb_repro_example?name=sb-bug-popefs-tp-size' \\
| head -40
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
6 PK predictors + stubbed twocmt_superposition term + addprop obs family all transpile in one shot. Shared |p| ranef block (6x6 LKJ matrix) correctly slices b_p_subject[subject_idx, k] per predictor. Gap found: _sb_emit_cat! names categoricals cat_<covariate> without per-predictor scope; same Int column in multiple predictors collides in SLIC info. Fix: use Float64 for binary covariates (male, diseased) -- matches deployed model's continuous matrix-multiply treatment. Generated Stan confirms: - all 6 predictors compose fixed+ranef from shared b_p_subject |p| block - amount_pk = log_Vc (stub proxy) correctly typed as per-obs vector - pk_conc ~ normal(amount_pk, addprop(amount_pk, sigma_add, sigma_prop)) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rified Extends #6 skeleton (1f97ce9) from 6 PK predictors to the full 13-param roster (4 PK + 7 PD + 2 source) with source-verified covariate structure. Key findings: - Transpile: PASSED (lkj_corr_cholesky(1.0) at 13×13, standard not stable) - b_p_subject: 13 columns intact in parameters block — block stays 13-wide - PD-param disposition: b_p_subject[·,5..11] ranef slices computed in GQ (StanBlocks §9: unconsumed-with-prior params → generated_quantities) Fixed-effect betas and predictor vectors for PD params → GQ as RNG draws - Differential covariate structure confirmed: log_Vc/log_k10 emit 5-coef (intercept+male+zage+zweight+diseased), log_k12..log_theta2_csf emit 2-coef (intercept+diseased), log_abs_rate/log_abs_mode emit 1-coef Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…m5f:twocmt-ode-term) Verification scaffolding for the vs2-pkcore BRM port (test/probe_*.jl, standalone, not wired into runtests). Verifies BRM-side floor/param/ranef/seam machinery with a stub term, independent of the real twocmt_superposition term: - probe_pkcore_skeleton (#6): 6-predictor PK-core skeleton transpiles green - probe_fullparam_skeleton (#8): full 13-param correlated block transpiles green - probe_kwarg_seam: option-(ii) param-feed kwarg seam - probe_addprop_family: add+prop obs-family seam These are the Task C / #6 / #8 artifacts cited as verified in the port todo tree; merging so the verification lives in-tree, not orphaned on the retired child branch. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Discuss https://github.com/PTWaade/BayesianRegressionModels.jl/blob/ns/macro/scripts/macro.jl, @penelopeysm @PTWaade @seabbs.