Repository navigation
v0.7.1
A small but conceptually load-bearing refactor of quivers.formulas.compile.FormulaToQVRModule: the lens complement is now FormulaData instead of the entire Formula.
The motivation
QVR is strictly more expressive than the brms / lme4 formula surface: every formula has an injective embedding into a QVR program, and the structural fields of the formula (which columns exist, intercept flag, random-effect group / slope structure, family choice, response identifier) are recoverable from the emitted Module by inspection. The previous lens declared C = Formula and set backward = const c — the lazy function-lens idiom. GetPut held, but it said "we don't know how to read the Module back," which was conceptually a lie.
The only fields that genuinely can't be recovered from the Module:
- Per-row data arrays. Predictor and response values flow through the host-data channel at fit time; they are never in the program AST.
- Original identifiers.
_qvr_namenormalizes by replacing non-alphanumeric characters with underscores. The map is not injective (poly(x, 2)_1→poly_x_2_1). - Presentation labels.
FixedColumn.termandFixedColumn.nameare display metadata, not load-bearing for compilation. - The original formula string. The lens emits a canonical AST that does not record user whitespace.
Those fields ride in FormulaData. The rest is decoded.
The change
The new _decode_module(module) walks the canonical emitted shape — object Resp : N, group ObjectDecls, the program block's intercept / beta_* / sigma_* / alpha_* latents with their _per_row LetSteps, the final indexed-score observe step — and recovers the structural fields.
forward(formula) now produces (Module, FormulaData). backward(module, complement) calls the decoder for structure and fuses the result with the complement for data and presentation.
from quivers.formulas import FormulaToQVRModule, formula_from_data
from quivers.formulas.family import families
f = formula_from_data("y ~ x + (1 | g)", df)
lens = FormulaToQVRModule(families["gaussian"])
module, complement = lens.forward(f)
# complement is FormulaData, not Formula:
assert isinstance(complement, FormulaData)
# Structural fields are NOT in the complement; they live in `module`:
assert "x" in complement.fixed_column_data # data, yes
assert complement.formula == "y ~ x + (1 | g)" # presentation, yes
# But: which columns exist, what's an intercept, where the random
# effects scope — all decoded from `module` by `backward`.
recovered = lens.backward(module, complement)
assert recovered == f # GetPut holdsAPI impact
Additive only at the surface — the public symbol FormulaToQVRModule keeps its name and call surface, and the standard usage module, complement = lens.forward(f) continues to work. The lens type parameter changes from Lens[Formula, Module, Formula] to Lens[Formula, Module, FormulaData]. New public symbol FormulaData in quivers.formulas.formula.
Tests
All 10 parametrized formulas in TestLensLaws.test_get_put_law continue to pass. Two new tests:
test_complement_is_formula_data_not_formuladocuments the shape and contents of the complement.test_backward_decodes_structure_from_moduleexercises the decoder by stripping the structural metadata from the complement and asserting backward still recovers the right fixed-column / random-term structure from the Module alone.
Full suite: 2022 passed, 62 skipped, 10 deselected.
Full Changelog: v0.7.0...v0.7.1