PolicyEngine / PolicyEngine/policyengine-core

TracingParameterNodeAtInstant never records scale/bracket parameter reads

Open
#542 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug
Dominant language
Python
Stars
22
Forks
30
Avg merge
14h 33m
Merged PRs (30d)
7

Description

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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in tracing_parameter_node_at_instant.py:49 at TracingParameterNodeAtInstant.get_traced_child, then inspect how ParameterScaleAtInstant children are represented and how record_parameter_access is used for vectorial reads. Reproduce a scale access such as p.base.calc(age) or p.max[child_count]. Done means scale reads are recorded at the scale node without breaking existing node and leaf tracing.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
devtools
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
74/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.