Skip to content

v0.7.1

Choose a tag to compare

@github-actions github-actions released this 15 May 16:05
· 70 commits to main since this release

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_name normalizes by replacing non-alphanumeric characters with underscores. The map is not injective (poly(x, 2)_1 → poly_x_2_1).
  • Presentation labels. FixedColumn.term and FixedColumn.name are 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 holds

API 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_formula documents the shape and contents of the complement.
  • test_backward_decodes_structure_from_module exercises 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