PolicyEngine / PolicyEngine/policyengine-core
TracingParameterNodeAtInstant never records scale/bracket parameter reads
Nobody has claimed this yet.
- 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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