TracingParameterNodeAtInstant never records scale/bracket parameter reads

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

Nobody has claimed this yet.

Assessment

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

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.

Description

bug

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.

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

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.

More from PolicyEngine/policyengine-core

All issues in PolicyEngine/policyengine-core

Similar issues

More Python issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.