Skip to content

GeneralLogLinear does not satisfy the LifeModel contract, and is excluded from the type ratchet #345

Description

@derrynknife

LifeModel.__init__ declares:

phi_param_map: dict[str, int]
phi_bounds: tuple[tuple[int | None, int | None], ...]

GeneralLogLinear passes callables of Z for both, because its parameterisation depends on the covariate dimension — there is one beta_i per stress column, so neither the map nor the bounds can be known until the data arrives:

lambda Z: {"beta_" + str(i): i for i in range(Z.shape[1])}
lambda Z: (((None, None),) * Z.shape[1])

This is already known and deliberate — accelerated_life/__init__.py excludes it from LIFE_MODELS for exactly this reason, so it cannot be rebuilt from a name and cannot be deserialised. It is also not exported from surpyval, and nothing in the package consumes it.

Consequences

Every other accelerated-life module is now in the type ratchet (#143). general_log_linear is the one exclusion, and it needs two # type: ignore[arg-type] comments to typecheck at all, because the values genuinely are not the declared types.

Fixed in passing

The two arguments were swapped: the bounds lambda sat in the phi_param_map slot and the param-map lambda in the phi_bounds slot. Verified before the fix:

phi_param_map(Z) -> ((None, None), (None, None), (None, None))   <- bounds-shaped
phi_bounds(Z)    -> {'beta_0': 0, 'beta_1': 1, 'beta_2': 2}       <- a param map

Nothing consumed either, so the swap had no observable effect, but it would have bitten whoever finished the model.

Options

  1. Widen the contract. Let LifeModel accept either the static values or callables of Z, resolved once the covariate dimension is known. Everything else keeps its current shape, and GeneralLogLinear becomes a first-class life model that can join LIFE_MODELS and the ratchet.
  2. Give it a separate base. A DimensionDependentLifeModel with phi_param_map(Z) / phi_bounds(Z) as real abstract methods, and have the fitter branch on which base it is. Honest about there being two kinds, at the cost of a second hierarchy.
  3. Remove it. It is unexported, unfinished and unused; multi-stress fitting is otherwise unavailable, so this closes off a capability rather than deleting dead weight.

I would lean towards 1 — the callables already work, they just are not declared — but it touches every life model's constructor signature, so it is worth deciding rather than drifting into.

Context

  • surpyval/univariate/regression/accelerated_life/general_log_linear.py
  • surpyval/univariate/regression/accelerated_life/lifemodel.py
  • surpyval/univariate/regression/accelerated_life/__init__.py (the LIFE_MODELS exclusion comment)
  • Ratchet list in pyproject.toml under [[tool.mypy.overrides]]

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions