Conversation
The Universal Credit child limit and higher first child element, the Child Tax Credit child limit and the Pension Credit first child addition turn on whether a child was born before 6 April 2017. They compared birth_year (period less whole age) with 2017, which in survey data treats the whole cohort born October 2016 to October 2017 as born after the cutoff. Add date_of_birth (YYYYMMDD) from age and months_since_last_birthday, which state_pension_age now reads too; compare it with new born_before date parameters; order UC and Pension Credit children by date of birth; derive birth_year from it. Household situations are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Sep 30, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of #1904 found that a situation setting date_of_birth for some people gives the others 0: state_pension_age then read a birth date of 0 (a 35-year-old came out over State Pension age), unset siblings ranked first and counted as born before 6 April 2017. A date_of_birth input that disagreed with the age grid also gave a State Pension age measured from two different births. utils.dates.birth_instant now resolves the instant of birth once: the date_of_birth input where given (not 0, and not the day age and months_since_last_birthday already place), otherwise the instant from age and months_since_last_birthday. state_pension_age and months_since_state_pension_age share it, and the cutoffs, child order and birth_year read birth_day. Without an input nothing changes. Adds YAML regressions (partial input, exact State Pension age from a date, an unset sibling), a Hypothesis test that a date input matches the same birthday placed by age, UC flags in the household differential test, and changelog wording limited to whole ages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Round-2 review of #1904: an input was accepted as any positive integer and normalised silently (20170229 became 1 March, 2017 read as a date in year 1, making a child over State Pension age), and nothing checked it against age (a child given only a date kept the default age). birth_instant now raises a ValueError naming the input for a non-date, or for a date whose legal age on 6 October is not floor(age). Inputs that agree with age are unaffected, and data never carries the input. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 1, 2026
Open
Collaborator
Author
|
Coordination note from #2051 (Child Tax Credit family element, #2050), which adds a fourth reader of the 6 April 2017 cutoff on main.
|
Collaborator
Author
|
Follow-up from #2051's review: when
#2051's |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1903
Stacked on #1899, which adds
months_since_last_birthday. I'll retarget this tomainonce #1899 merges; the diff here is this change only.Summary
Four rules turn on whether a child was born before 6 April 2017:
uc_is_child_born_before_child_limituc_individual_child_elementis_CTC_child_limit_exemptchild_minimum_guarantee_additionAll four compared
birth_year(period less whole age) with 2017. So everyone aged 9 in 2026-27 counted as born in 2017, whether born in January (before the cutoff) or May (after it). In survey data, the whole cohort born from October 2016 to October 2017 was treated as born after the cutoff, when about half of it was born before.This PR compares an exact date of birth with 6 April 2017:
date_of_birth(new, YYYYMMDD). Each person was bornageyears andmonths_since_last_birthdaymonths before 6 October of the fiscal year. The day of birth is the one starting at or after that instant, so their legal age on 6 October isage. It uses Set State Pension age from date of birth, including the rise to 67 #1899's date grid. It can also be set directly, withageas the person's age on 6 October. Every date-of-birth rule then reads one instant of birth through a shared helper,utils.dates.birth_instant, so adate_of_birthinput setsstate_pension_age,months_since_state_pension_age,is_SP_ageand each cutoff consistently. Age-based rules still readage. A situation that setsdate_of_birthfor some people gives the others 0, which the helper treats as not given; see the review section. Without an input,state_pension_ageis exactly Set State Pension age from date of birth, including the rise to 67 #1899's.born_beforedate parameters replace the year parameters:gov.dwp.universal_credit.elements.child.limit.born_beforegov.dwp.tax_credits.child_tax_credit.limit.born_beforegov.dwp.pension_credit.guarantee_credit.child.first.born_before, which was a hard-coded2017.Each is 20170406 and cites its provision.
Children are ordered by date of birth. UC Regs reg 24B(1) orders children "by reference to the date of birth ... taking the earliest date first", and Sch IIA para 10 names the "eldest".
uc_child_indexand the Pension Credit child rank now sort bydate_of_birthrather than whole age. This only breaks ties between children of the same whole age, but once a cutoff can fall between two such siblings the order matters. With the old ranking, a family with children born 5 and 6 April 2017 could list the later-born child second, and it would get the child element under the limit.birth_yearis now the calendar year ofdate_of_birth. Its only model readers were the three cutoffs above.Household situations with whole ages are unchanged. A whole age in a situation sits in the middle of the year of age, which is 6 April of the year the person turned that age. So "born before 6 April 2017" gives exactly what
period - age < 2017gave, andbirth_yearis stillperiod - age. This is tested for every year from 2015 to 2035 and every age up to 19. Only survey data, which spreads birthdays over the year (#1899), moves, along with fractional ages. A fractional age is now read as the exact age on 6 October, as #1899 already does for State Pension age: in 2025, an age of 8.25 places the birth on 6 July 2017, after the cutoff, where the old truncated birth year said 2016. A date of birth can now also be set directly (date_of_birth: 20170405) for a year, withageas the age on 6 October. A value that isn't a calendar date, or that gives a different age, raises an error rather than being silently normalised.Invariants (stated and tested)
policyengine_uk/tests/test_date_of_birth.py:Households with whole ages match the birth-year rule. Exhaustively, for 2015-2035 and ages 0-19:
period - age < 2017;is_CTC_child_limit_exemptagrees, and so doesuc_is_child_born_before_child_limitfor under-16s;birth_year == period - age.This differential test pins "whole-age situations unchanged".
The date of birth follows the legal age (Hypothesis). For any whole age 0-15, any position in the year (including instants within a day) and any year 2015-2040:
date_of_birthequals adatetimereference that shares no code withutils/dates.py, though it re-implements the same grid rule;ageon 6 October;birth_yearis its year;Ordering refines age order (Hypothesis).
uc_child_indexis a permutation of 1..n. A child born earlier is always numbered first, and so is an older whole age.Microdata shares match the statute. In a data-built simulation with weights, for 2022, 2024, 2025, 2026 and 2028 and ages 5-12:
birth_yearthe year beforeperiod - ageequals the share of birthdays from 7 October to 31 December;date_of_birth < 20170406.A date input matches the same birthday placed by age (Hypothesis). For any births from 1935 to 2020 and any year from 2021 to 2040, setting
date_of_birth(with the legal age) gives the samebirth_year,is_SP_age, UC and CTC flags as setting the matchingmonths_since_last_birthday, andstate_pension_ageagrees to within 1e-4 years. A person left without the input in the same situation readsdate_of_birth0 but gets the results they would with no input at all.Boundary YAML tests (born 5 against 6 April 2017):
demographic/date_of_birth.yamlageandmonths_since_last_birthday, a date set for one person only (the parent keeps State Pension age 68, not over it), and a date setting State Pension age exactly (5 January 1960: 66, attained 5 January 2026, 9.032 months before 6 October)uc_is_child_born_before_child_limit.yamltax_credits.yamlCTC_child_element(2024-25)child_minimum_guarantee_addition.yamlAs a mutation check, reverting either ranking to
-agefails exactly the two same-age ordering tests.Microsimulation impact
Enhanced FRS 2024-25 (policyengine-uk-data-private, snapshot ace89433). Each column is a full microsimulation of each code version:
c46893e2, and were repeated at2466f445after Set State Pension age from date of birth, including the rise to 67 #1899 merged main.df94b5f8, thenef35551d(the review fix, below).Every figure tabulated below is identical between the two pairs, to within £0.001bn and 1k people. Two things moved between the pairs at once: #1899 merged main, which brought in #1901's Housing Benefit pension-age claims, and this branch added the review fix. So the pairs alone don't isolate the fix. The code does, though: with no
date_of_birthinput,birth_instantreturns the same instant #1899 used. The one untabulated row that moved is relative poverty after housing costs in 2024-25: -192k people rather than -186k, because in the first pair 9 households entered it and in the second none did. That comes from #1901's change to the base.This branch minus base
period - age= 2017), levelchild_countis infinite from 6 April 2026), and the remaining effect is the higher first child element (reg 43) for families whose eldest child is in that half.The scripts, CSVs, household-crossing counts (
flips_output*.txt), poverty lines (lines_*.txt) and aPROVENANCE.mdwith commits and the dataset blob are in~/PolicyEngine/_pr_worktrees/dob-cutoffs-axiom/impact/. Every figure is from a real run; none is scaled or interpolated.Caveats
CTC_family_elementpays it to every CTC claimant. That is a separate, pre-existing gap for 2017-18 to 2024-25, and it's queued as a follow-up; the Axiom side is in Encode TCA 2002 s.9(2)(a), (3A), (3B) and CTC Regs 2002 regs 7(2)–(2A) and 9 (family element and child limit by date of birth, 2017-18 to 2024-25) TheAxiomFoundation/rulespec-uk#372.Removed parameters
gov.dwp.universal_credit.elements.child.limit.start_yearandgov.dwp.tax_credits.child_tax_credit.limit.start_yearheld the year 2017. They are replaced by theborn_beforedates, and theremovedfragment says how to write a reform now. In the local checkouts of policyengine.py, policyengine-api, policyengine-api-v2, policyengine-app-v2, microcosm and policyengine-uk-data, the only other mention ispolicyengine-app-v2's mock metadata (app/src/mocks/UK_Metadata.json). A saved policy that changes either parameter will stop loading, rather than silently doing nothing. The fragment type isremoved(a minor release), following #1899's removal of the State Pension age year parameters and this repo'sgithub-prs.md; downstream packages pin exact versions.Independent review
An Opus 5.5 reviewer (Subfleet,
review/standard) requested changes. Each finding and what was done:date_of_birthset for one person gave the others 0, so a 35-year-old parent got State Pension age −1925 and counted as over it; unset siblings ranked first and counted as born before the cutoffutils.dates.birth_instantnow resolves every rule's birth instant: the input where given, otherwise fromageandmonths_since_last_birthday. There are YAML regressions and a Hypothesis property.state_pension_ageandmonths_since_state_pension_agenow share one instant, which gives 66, attained 5 January 2026.removedvsbreakingremoved, following #1899's precedent (see above).age_exemptionreform never lifts the limitA round-2 review approved, on the condition that the tests pass (they do; see "Tests run"). Its one should-fix: a
date_of_birthinput was never checked, so20170229became 1 March, and2017was read as a date in year 1, which put a child over State Pension age. It also wasn't checked againstage. Such inputs now raise aValueError(test_an_invalid_date_of_birth_is_rejectedandtest_a_date_of_birth_that_contradicts_age_is_rejected). Its wording nits, the confounded before/after comparison and the BHC/AHC mix-up in the poverty sentence, are corrected above.Tests run
policyengine-core testonbaseline/demographic,baseline/finance/benefit/family,baseline/gov/dwpandreforms/parametric/two_child_limit: 422 passed.test_date_of_birth.py(9 passed at the head). Set State Pension age from date of birth, including the rise to 67 #1899'stest_state_pension_age.pypassed 24 of 24 atef35551d, including the day-by-day statute reference; the later commit only adds input checks, which never run without adate_of_birthinput.main.axiom: uk/regulation/uksi/2002/1792/schedule/IIA encoded-correct (takes the eldest child's born-before-6-April-2017 status as an input judgment; companion tests
post_2017_child_has_no_eldest_child_increaseandmixed_children_with_pre_2017_disabled_and_severely_disabled_amounts) | TheAxiomFoundation/rulespec-uk#371 queued (UC Regs reg 24A(3) and 24B, SI 2014/1230 reg 43, reg 36 first-child row) | TheAxiomFoundation/rulespec-uk#372 queued (TCA 2002 s.9(2)(a), (3A), (3B); CTC Regs regs 7(2A) and 9)🤖 Generated with Claude Code