Skip to content

TracingParameterNodeAtInstant never records scale/bracket parameter reads #542

Description

@PavelMakarchuk

Summary

TracingParameterNodeAtInstant.get_traced_child (tracing_parameter_node_at_instant.py:49) only calls record_parameter_access when the child is an ndarray or one of ALLOWED_PARAM_TYPES (float, int, bool, None, list):

if isinstance(child, (numpy.ndarray,) + parameters.ALLOWED_PARAM_TYPES):
    self.tracer.record_parameter_access(name, period, self.branch_name, child)

A ParameterScaleAtInstant child (SingleAmountTaxScale, MarginalRateTaxScale, …) is neither a node (so it is not re-wrapped) nor an allowed leaf, so it is returned raw and never recorded. Any formula that reads a bracket parameter through .calc() or [index] on the scale leaves no trace of it:

p = parameters(period).gov.irs.credits.ctc.amount
return qualifying_child * p.base.calc(age)      # gov.irs.credits.ctc.amount.base never recorded

Same for p.max[child_count] (EITC), p.rates.calc(income) (income tax brackets), and every state bracket schedule. Verified on policyengine-core 3.30.2 / policyengine-us 1.808.0.

Why it matters

The tracer is the only exact record of which parameters a formula reads (static analysis misses vectorised and alias-based reads). Bracket schedules are a large share of reform-relevant parameters, so a parameter dependency map built from the tracer is missing them entirely today.

Workaround

original = TracingParameterNodeAtInstant.get_traced_child
def get_traced_child(self, child, key):
    is_node = isinstance(child, (ParameterNodeAtInstant, VectorialParameterNodeAtInstant))
    is_leaf = isinstance(child, ALLOWED_PARAM_TYPES) or hasattr(child, "shape")
    if not is_node and not is_leaf:
        name = self.parameter_node_at_instant._name if not isinstance(key, str) \
            else f"{self.parameter_node_at_instant._name}.{key}"
        self.tracer.record_parameter_access(name, self.parameter_node_at_instant._instant_str, self.branch_name, None)
    return original(self, child, key)

Proposed fix

Record the access for any child that is not a parameter node, using the scale's node name and value=None (or the scale object itself). Recording at the scale node is the right granularity: rate[status].zone1 is already recorded as rate for vectorial reads, and a bracket read is best described as a read of the whole scale.

Context

Found alongside the cached-at-instant bug (#541) while building a traced parameter → variable dependency map for PolicyEngine/policyengine-app-v2#1180. Note this only reproduces once that bug is worked around, since yearly formulas otherwise record nothing at all.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions