Skip to content

Add @brm macro - #6

Draft
nsiccha wants to merge 24 commits into
mainfrom
ns/macro
Draft

Add @brm macro#6
nsiccha wants to merge 24 commits into
mainfrom
ns/macro

Conversation

@nsiccha

@nsiccha nsiccha commented Mar 3, 2026

Copy link
Copy Markdown
Member

@nsiccha

nsiccha commented Mar 3, 2026

Copy link
Copy Markdown
Member Author

I'd have thought github would include the code - but apparently not! Here's another attempt:

https://github.com/PTWaade/BayesianRegressionModels.jl/blob/40d3650e26af91fefa3092b0b96b1340cfb193a6/scripts/macro.jl#L5-L22

@PTWaade

PTWaade commented Mar 3, 2026

Copy link
Copy Markdown
Member

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

@PTWaade

PTWaade commented Mar 3, 2026

Copy link
Copy Markdown
Member

Primnary conversation currently on Slack; I mostly wanted here to distinguihs the linear combinations from the regression submodels
As you can see, there are still things missing, and how to specify the data is hard
I would imagine that poelpe can still write the simple formulas, and that they just expand into this format, but there might be much better ways to do it!

@nsiccha

nsiccha commented Mar 4, 2026

Copy link
Copy Markdown
Member Author

I think these would be the "rules" of the @brm macro:

  • Any assignment operations will broadcast the RHS over the rows of the data frame in question and assign to the LHS such that the LHS can (from that point onwards?) be used as any other column of the data frame (e.g. Age_first, Age_second = ploynomial_expand(Age; order=2))
    • That example is actually a bit more complicated than necessary - behind the scenes, what would have to be happening is that for each row/value of Age, ploynomial_expand(Age; order=2) returns a 2-tuple. If we just broadcasted this over the data frame rows, we'd get a vector of 2-tuples, but we actually want a 2-tuple of vectors, to unsplat(?) into Age_first, Age_second
  • Any sampling statement (~) will (roughly) do one of the following:
    • If the LHS is in the data frame (or can be computed from columns in it?), it's treated as a "likelihood" statement, meaning
      a) if the topmost call on the RHS does not return a Distribution, it will be wrapped in something like Normal(rhs, something_automatic?) before we proceed to b),
      b) if the topmost call on the RHS does return a Distribution, the joint log likelihood will be computed by summing the rowwise (broadcasted) log likelihoods
    • If the LHS is not in the data frame, it's treated as a "regression" statement, i.e. (from that point onwards?) the LHS can be used as any other data frame column, but the values will depend on model parameters via the functional form implied by the RHS formula. Furthermore,
      a) if the topmost call on the RHS returns a Distribution I don't know yet how to handle this?
      b) if the topmost call on the RHS does not return a distribution, "there's no measurement noise", i.e. the value of the LHS will just be "what comes out of the RHS".

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. y ~ Normal(x, 1) parse the arguments to Normal as formulas? I think we may need a way to turn formula parsing on/off via some function/macro call.

@seabbs @penelopeysm @PTWaade

@nsiccha

nsiccha commented Mar 4, 2026

Copy link
Copy Markdown
Member Author

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)
end

to 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)
end

should allow us to do everything that we need - thoughts, @penelopeysm?

@nsiccha

nsiccha commented Mar 4, 2026

Copy link
Copy Markdown
Member Author

log(performance_sd) = ... would obviously not work and would have to be changed to something slightly more clever - but I don't think much more clever.

@PTWaade

PTWaade commented Mar 4, 2026

Copy link
Copy Markdown
Member

So this is awesome work Niko!
Only osme thoughts on the functionality side per your comments with the rules:

I think y ~ x + z should be shorthand for two lines:
y_mean = x + z (a "linear combination")
y ~ Normal(y_mean, sd) (which I have implemented as happening in a Turing submodel).
in addition, the sd must either be the output of a previous line, be a fixed value, or have a prior.

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).
Note that there are two core uses that the TUring submodels cover: evaluating a likelihood, or generating some new values probabilistically.

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 y ~ x + z, which is then split into two parts.

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!

@PTWaade

PTWaade commented Mar 4, 2026

Copy link
Copy Markdown
Member

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 :)

@PTWaade

PTWaade commented Mar 6, 2026

Copy link
Copy Markdown
Member

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.
Notably, it should not include a likelihood.
Rather, it should be used to generate a Turing model that returns the columns of cognitive_parameter1 and cognitive_parameter2. This will be used as a submodel in a larger Turing model, where the cognitive parameters are used for other things.
An arbitrary number of cognitive parmaters must be allowed, and arbitrary regressions must be allowed.

@PTWaade

PTWaade commented Mar 6, 2026

Copy link
Copy Markdown
Member

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.

@PTWaade

PTWaade commented Mar 6, 2026

Copy link
Copy Markdown
Member

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)

@PTWaade

PTWaade commented Mar 6, 2026

Copy link
Copy Markdown
Member

Fourth mock model:

outcome1 ~ monotonic(predictor1) ...

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

@PTWaade

PTWaade commented Mar 6, 2026

Copy link
Copy Markdown
Member

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.

@PTWaade

PTWaade commented Mar 9, 2026

Copy link
Copy Markdown
Member

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

@PTWaade

PTWaade commented Mar 9, 2026

Copy link
Copy Markdown
Member

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

@nsiccha

nsiccha commented Mar 9, 2026

Copy link
Copy Markdown
Member Author

@PTWaade you can make github add syntax highlighting by having "julia" after the first three backticks:

# Use three ` instead of two
``julia
a=1+1
``

@PTWaade

PTWaade commented Mar 9, 2026

Copy link
Copy Markdown
Member

@PTWaade you can make github add syntax highlighting by having "julia" after the first three backticks:

# Use three ` instead of two
``julia
a=1+1
``

Ah - thank you :)

nsiccha and others added 10 commits March 11, 2026 11:00
…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>
nsiccha and others added 11 commits April 20, 2026 22:41
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>
nsiccha added a commit that referenced this pull request Jun 15, 2026
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>
nsiccha added a commit that referenced this pull request Jun 15, 2026
…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>
nsiccha added a commit that referenced this pull request Jun 15, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants