Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 12 additions & 10 deletions rcpchgrowth/constants/reference_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
Reference constants
"""

from .age_constants import (
FORTY_TWO_WEEKS_GESTATION,
TWENTY_FIVE_WEEKS_GESTATION,
TWENTY_THREE_WEEKS_GESTATION,
)

# Trisomy 21 constants
TRISOMY_21 = "trisomy-21"
TRISOMY_21_AAP = "trisomy-21-aap"
Expand Down Expand Up @@ -34,15 +40,13 @@
WHO_2007_CHILD = "who_2007_child" # WHO 2007 child is the reference name for children 5-19 years
WHO_REFERENCES = [WHO_2006_INFANT, WHO_2006_CHILD, WHO_2007_CHILD] # WHO references

WHO_2006_REFERENCE_LOWER_THRESHOLD = ((42 * 7) - (40 * 7)) / 365.25 # 42 weeks as decimal age # 2 weeks as decimal age
WHO_2006_REFERENCE_LOWER_THRESHOLD = FORTY_TWO_WEEKS_GESTATION
WHO_2006_REFERENCE_UPPER_THRESHOLD = 1856/365.25 # 5 years and one month just under as decimal age
WHO_2007_REFERENCE_LOWER_THRESHOLD = (61/30.4375)/365.25 # 5 years and one month as decimal age (61 mths)
WHO_2007_REFERENCE_UPPER_THRESHOLD = 19.0 # 19 years as decimal age

# 23 weeks is the lowest decimal age available on the UK90 charts
UK90_REFERENCE_LOWER_THRESHOLD = (
(23 * 7) - (40 * 7)
) / 365.25 # 23 weeks as decimal age
UK90_REFERENCE_LOWER_THRESHOLD = TWENTY_THREE_WEEKS_GESTATION

# The WHO references change from measuring infants in the lying position to measuring children in the standing position at 2.0 years.
WHO_CHILD_LOWER_THRESHOLD = 2.0 # 2 years as decimal age
Expand All @@ -56,9 +60,7 @@
# 3 weeks of life

WHO_CHILDREN_UPPER_THRESHOLD = 4.0
UK_WHO_INFANT_LOWER_THRESHOLD = (
(42 * 7) - (40 * 7)
) / 365.25 # 42 weeks as decimal age
UK_WHO_INFANT_LOWER_THRESHOLD = FORTY_TWO_WEEKS_GESTATION
UK90_UPPER_THRESHOLD = 20

WHO_NEWBORN_LOWER_THRESHOLD = 0.0
Expand Down Expand Up @@ -111,8 +113,8 @@
]
EXTENDED_WHO_CENTILES_COLLECTION = [1, 3, 5, 10, 15, 50, 85, 90, 95, 97, 99]

UK_90_PRETERM_AGES = [-0.325804244, -0.306639288, -0.287474333, -0.268309377, -0.249144422, -0.229979466, -0.210814511, -0.191649555, -0.1724846, -0.153319644, -0.134154689, -0.114989733, -0.095824778, -0.076659822, -0.057494867, -0.038329911, -0.019164956, 0, 0.019164956, 0.038329911]
WHO_2006_UNDER_TWOS_AGES = [0.038329911, 0.057494867, 0.076659822, 0.083333333, 0.095824778, 0.114989733, 0.134154689, 0.153319644, 0.166666667, 0.1724846, 0.191649555, 0.210814511, 0.229979466, 0.249144422, 0.25, 0.333333333, 0.416666667, 0.5, 0.583333333, 0.666666667, 0.75, 0.833333333, 0.916666667, 1, 1.083333333, 1.166666667, 1.25, 1.333333333, 1.416666667, 1.5, 1.583333333, 1.666666667, 1.75, 1.833333333, 1.916666667, 2]
UK_90_PRETERM_AGES = [TWENTY_THREE_WEEKS_GESTATION, -0.306639288, TWENTY_FIVE_WEEKS_GESTATION, -0.268309377, -0.249144422, -0.229979466, -0.210814511, -0.191649555, -0.1724846, -0.153319644, -0.134154689, -0.114989733, -0.095824778, -0.076659822, -0.057494867, -0.038329911, -0.019164956, 0, 0.019164956, FORTY_TWO_WEEKS_GESTATION]
WHO_2006_UNDER_TWOS_AGES = [FORTY_TWO_WEEKS_GESTATION, 0.057494867, 0.076659822, 0.083333333, 0.095824778, 0.114989733, 0.134154689, 0.153319644, 0.166666667, 0.1724846, 0.191649555, 0.210814511, 0.229979466, 0.249144422, 0.25, 0.333333333, 0.416666667, 0.5, 0.583333333, 0.666666667, 0.75, 0.833333333, 0.916666667, 1, 1.083333333, 1.166666667, 1.25, 1.333333333, 1.416666667, 1.5, 1.583333333, 1.666666667, 1.75, 1.833333333, 1.916666667, 2]
UK_WHO_2006_OVER_TWOS_AGES = [2, 2.083333333, 2.166666667, 2.25, 2.333333333, 2.416666667, 2.5, 2.583333333, 2.666666667, 2.75, 2.833333333, 2.916666667, 3, 3.083333333, 3.166666667, 3.25, 3.333333333, 3.416666667, 3.5, 3.583333333, 3.666666667, 3.75, 3.833333333, 3.916666667, 4]
WHO_2006_OVER_TWOS_AGES = [2, 2.083333333, 2.166666667, 2.25, 2.333333333, 2.416666667, 2.5, 2.583333333, 2.666666667, 2.75, 2.833333333, 2.916666667, 3, 3.083333333, 3.166666667, 3.25, 3.333333333, 3.416666667, 3.5, 3.583333333, 3.666666667, 3.75, 3.833333333, 3.916666667, 4, 4.083333333, 4.166666667, 4.25, 4.333333333, 4.416666667, 4.5, 4.583333333, 4.666666667, 4.75, 4.833333333, 4.916666667, 5.0]
WHO_2007_AGES = [5.0, 5.08,5.17,5.25,5.33,5.42, 5.5,5.58,5.67,5.75,5.83,5.92,6,6.08,6.17,6.25,6.33,6.42, 6.5,6.58,6.67,6.75,6.83,6.92,7,7.08,7.17,7.25,7.33,7.42, 7.5,7.58,7.67,7.75,7.83,7.92,8,8.08,8.17,8.25,8.33,8.42, 8.5,8.58,8.67,8.75,8.83,8.92,9,9.08,9.17,9.25,9.33,9.42, 9.5,9.58,9.67,9.75,9.83,9.92,10,10.08,10.17,10.25,10.33,10.42, 10.5,10.58,10.67,10.75,10.83,10.92,11,11.08,11.17,11.25,11.33,11.42, 11.5,11.58,11.67,11.75,11.83,11.92,12,12.08,12.17,12.25,12.33,12.42, 12.5,12.58,12.67,12.75,12.83,12.92,13,13.08,13.17,13.25,13.33,13.42, 13.5,13.58,13.67,13.75,13.83,13.92,14,14.08,14.17,14.25,14.33,14.42, 14.5,14.58,14.67,14.75,14.83,14.92,15,15.08,15.17,15.25,15.33,15.42, 15.5,15.58,15.67,15.75,15.83,15.92,16,16.08,16.17,16.25,16.33,16.42, 16.5,16.58,16.67,16.75,16.83,16.92,17,17.08,17.17,17.25,17.33,17.42, 17.5,17.58,17.67,17.75,17.83,17.92, 18,18.08,18.17,18.25,18.33,18.42, 18.5,18.58,18.67,18.75,18.83,18.92,19]
Expand All @@ -123,4 +125,4 @@
TURNER_AGES=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
TRISOMY_21_AGES=[0, 0.08, 0.17, 0.25, 0.33, 0.42, 0.5, 0.58, 0.67, 0.75, 0.83, 0.92, 1, 1.08, 1.17, 1.25, 1.33, 1.42, 1.5, 1.58, 1.67, 1.75, 1.83, 1.92, 2, 2.08, 2.17, 2.25, 2.33, 2.42, 2.5, 2.58, 2.67, 2.75, 2.83, 2.92, 3, 3.08, 3.17, 3.25, 3.33, 3.42, 3.5, 3.58, 3.67, 3.75, 3.83, 3.92, 4, 4.08, 4.17, 4.25, 4.33, 4.42, 4.5, 4.58, 4.67, 4.75, 4.83, 4.92, 5, 5.08, 5.17, 5.25, 5.33, 5.42, 5.5, 5.58, 5.67, 5.75, 5.83, 5.92, 6, 6.08, 6.17, 6.25, 6.33, 6.42, 6.5, 6.58, 6.67, 6.75, 6.83, 6.92, 7, 7.08, 7.17, 7.25, 7.33, 7.42, 7.5, 7.58, 7.67, 7.75, 7.83, 7.92, 8, 8.08, 8.17, 8.25, 8.33, 8.42, 8.5, 8.58, 8.67, 8.75, 8.83, 8.92, 9, 9.08, 9.17, 9.25, 9.33, 9.42, 9.5, 9.58, 9.67, 9.75, 9.83, 9.92, 10, 10.08, 10.17, 10.25, 10.33, 10.42, 10.5, 10.58, 10.67, 10.75, 10.83, 10.92, 11, 11.08, 11.17, 11.25, 11.33, 11.42, 11.5, 11.58, 11.67, 11.75, 11.83, 11.92, 12, 12.08, 12.17, 12.25, 12.33, 12.42, 12.5, 12.58, 12.67, 12.75, 12.83, 12.92, 13, 13.08, 13.17, 13.25, 13.33, 13.42, 13.5, 13.58, 13.67, 13.75, 13.83, 13.92, 14, 14.08, 14.17, 14.25, 14.33, 14.42, 14.5, 14.58, 14.67, 14.75, 14.83, 14.92, 15, 15.08, 15.17, 15.25, 15.33, 15.42, 15.5, 15.58, 15.67, 15.75, 15.83, 15.92, 16, 16.08, 16.17, 16.25, 16.33, 16.42, 16.5, 16.58, 16.67, 16.75, 16.83, 16.92, 17, 17.08, 17.17, 17.25, 17.33, 17.42, 17.5, 17.58, 17.67, 17.75, 17.83, 17.92, 18, 18.08, 18.17, 18.25, 18.33, 18.42, 18.5, 18.58, 18.67, 18.75, 18.83, 18.92, 19, 19.08, 19.17, 19.25, 19.33, 19.42, 19.5, 19.58, 19.67, 19.75, 19.83, 19.92, 20]
TRISOMY_21_AAP_INFANT_AGES=[0.083333333, 0.166666667, 0.25, 0.333333333, 0.416666667, 0.5, 0.583333333, 0.666666667, 0.75, 0.833333333, 0.916666667, 1, 1.083333333, 1.166666667, 1.25, 1.333333333, 1.416666667, 1.5, 1.583333333, 1.666666667, 1.75, 1.833333333, 1.916666667, 2, 2.083333333, 2.166666667, 2.25, 2.333333333, 2.416666667, 2.5, 2.583333333, 2.666666667, 2.75, 2.833333333, 2.916666667, 3]
TRISOMY_21_AAP_CHILD_AGES=[3, 3.5, 4, 4.5, 5, 5.5, 6, 6.5, 7, 7.5, 8, 8.5, 9, 9.5, 10, 10.5, 11, 11.5, 12, 12.5, 13, 13.5, 14, 14.5, 15, 15.5, 16, 16.5, 17, 17.5, 18, 18.5, 19, 19.5, 20]
TRISOMY_21_AAP_CHILD_AGES=[3, 3.5, 4, 4.5, 5, 5.5, 6, 6.5, 7, 7.5, 8, 8.5, 9, 9.5, 10, 10.5, 11, 11.5, 12, 12.5, 13, 13.5, 14, 14.5, 15, 15.5, 16, 16.5, 17, 17.5, 18, 18.5, 19, 19.5, 20]
2 changes: 1 addition & 1 deletion rcpchgrowth/global_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ def generate_centile(

def should_default_to_youngest_reference(age: float, reference_name: str):
if reference_name == UK90_PRETERM:
if age == 0.038329911:
if age == FORTY_TWO_WEEKS_GESTATION:
return True
if reference_name == UK_WHO_INFANT:
if age == 2:
Expand Down
119 changes: 119 additions & 0 deletions rcpchgrowth/tests/test_chart_boundary_ages.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
"""Regression tests for valid gestational boundary points in UK-WHO charts."""

from datetime import date

import pytest

from rcpchgrowth import Measurement
from rcpchgrowth.chart_functions import create_chart
from rcpchgrowth.constants import (
BMI,
FORTY_TWO_WEEKS_GESTATION,
HEAD_CIRCUMFERENCE,
HEIGHT,
TWENTY_FIVE_WEEKS_GESTATION,
TWENTY_THREE_WEEKS_GESTATION,
TWENTY_TWO_WEEKS_GESTATION,
UK90_CHILD,
UK90_PRETERM,
UK_WHO,
UK_WHO_CHILD,
UK_WHO_INFANT,
TURNERS,
WEIGHT,
)


@pytest.mark.parametrize(
("measurement_method", "component", "expected_first_age"),
[
(WEIGHT, UK90_PRETERM, TWENTY_THREE_WEEKS_GESTATION),
(HEAD_CIRCUMFERENCE, UK90_PRETERM, TWENTY_THREE_WEEKS_GESTATION),
(HEIGHT, UK90_PRETERM, TWENTY_FIVE_WEEKS_GESTATION),
# UK90 has no preterm BMI values; this seam point is sourced from WHO.
(BMI, UK_WHO_INFANT, FORTY_TWO_WEEKS_GESTATION),
],
)
def test_chart_includes_first_valid_gestational_boundary(
measurement_method,
component,
expected_first_age,
):
chart = create_chart(
reference=UK_WHO,
measurement_method=measurement_method,
sex="female",
)
component_data = next(item[component] for item in chart if component in item)
first_centile = component_data["female"][measurement_method][0]["data"]

assert first_centile[0]["x"] == round(expected_first_age, 4)


@pytest.mark.parametrize("sex", ["female", "male"])
@pytest.mark.parametrize("measurement_method", [HEIGHT, WEIGHT, HEAD_CIRCUMFERENCE, BMI])
def test_both_uk_who_chart_components_include_age_four(measurement_method, sex):
chart = create_chart(
reference=UK_WHO,
measurement_method=measurement_method,
sex=sex,
)
younger_data = next(
item[UK_WHO_CHILD] for item in chart if UK_WHO_CHILD in item
)[sex][measurement_method][0]["data"]
older_data = next(item[UK90_CHILD] for item in chart if UK90_CHILD in item)[
sex
][measurement_method][0]["data"]

assert younger_data[-1]["x"] == 4
assert younger_data[-1]["y"] is not None
assert older_data[0]["x"] == 4
assert older_data[0]["y"] is not None


def test_observation_below_reference_floor_remains_plottable_without_sds():
measurement = Measurement(
birth_date=date(2026, 1, 1),
observation_date=date(2026, 1, 1),
measurement_method=WEIGHT,
observation_value=0.5,
reference=UK_WHO,
sex="female",
gestation_weeks=22,
).measurement
point = measurement["plottable_data"]["centile_data"][
"corrected_decimal_age_data"
]

assert point["x"] == TWENTY_TWO_WEEKS_GESTATION
assert point["y"] == 0.5
assert point["sds"] is None
assert point["centile"] is None
assert point["observation_value_error"] == (
"There is no UK90 reference data below 23 weeks gestation"
)


def test_turner_observation_before_one_year_remains_plottable_without_sds():
measurement = Measurement(
birth_date=date(2025, 1, 1),
observation_date=date(2025, 7, 1),
measurement_method=HEIGHT,
observation_value=60.0,
reference=TURNERS,
sex="female",
).measurement
point = measurement["plottable_data"]["centile_data"][
"chronological_decimal_age_data"
]

assert point["x"] == measurement["measurement_dates"][
"chronological_decimal_age"
]
assert point["x"] < 1
assert point["y"] == 60.0
assert point["sds"] is None
assert point["centile"] is None
assert point["observation_value_error"] == (
"There is no reference data below 1 year."
)
5 changes: 2 additions & 3 deletions rcpchgrowth/tests/test_supported_chart_outputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
REFERENCES,
TRISOMY_21,
TRISOMY_21_AAP,
TWENTY_FIVE_WEEKS_GESTATION,
TURNERS,
UK_WHO,
WHO,
Expand All @@ -23,9 +24,7 @@
HEIGHT,
MALE,
[
# The nominal 25-week grid value is fractionally below the exact
# validation threshold, so the first emitted height point is 26 weeks.
("uk90_preterm", -0.2683, 0.0383),
("uk90_preterm", round(TWENTY_FIVE_WEEKS_GESTATION, 4), 0.0383),
("uk_who_infant", 0.0383, 2),
("uk_who_child", 2, 4),
("uk90_child", 4, 20),
Expand Down
6 changes: 5 additions & 1 deletion spec/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,17 @@ Complete these items before broad formatting, import cleanup, packaging reductio

## High priority

- [ ] **R1 - Resolve the broken public `create_thrive_lines` API.** `rcpchgrowth/dynamic_growth.py:226-294` uses undefined `plt`, has its output-producing plotting code commented out, and returns nothing, while `rcpchgrowth/__init__.py:8` exports it publicly. Decide whether plotting remains supported; either restore it with an optional matplotlib dependency and tests, or remove the export through an explicitly managed breaking change.
- [ ] **R1 - Resolve the broken public `create_thrive_lines` API. (GitHub #21)** `rcpchgrowth/dynamic_growth.py:226-294` uses undefined `plt`, has its output-producing plotting code commented out, and returns nothing, while `rcpchgrowth/__init__.py:8` exports it publicly. Decide whether plotting remains supported; either restore it with an optional matplotlib dependency and tests, or remove the export through an explicitly managed breaking change.
- [x] **R2 - Restore executable notebooks and expose package version metadata consistently.** The package root and measurement provenance now share `rcpchgrowth.__version__`, all four notebooks execute from fresh kernels, and Python 3.13 CI executes them headlessly.
- [x] **R24 - Automate the post-merge release cascade.** A same-repository `release/vX.Y.Z` PR merged into `live` with a merge commit is validated against its exact files, parent commits, supported bump, and synchronized metadata before CI creates or reuses the annotated tag and GitHub Release. The cascade calls the least-privilege reusable package workflow for the Python 3.10-3.13 gate and one exact commit-stamped artifact build, then publishes those verified artifacts from the top-level workflow identity supported by PyPI Trusted Publishing. `workflow_dispatch` accepts a merged release PR number for a non-publishing validation/build dry run or explicit recovery of a failed PyPI publication.

## Medium priority

- [ ] **R25 - Derive accepted age domains from reference metadata and table extents.** Replace duplicated age-limit literals with a central domain model for every reference segment, measurement, and sex. Distinguish observed table extent from clinically supported applicability, including endpoint inclusivity and overlap precedence for hybrid references such as UK-WHO and WHO. Add build-time checks against canonical source material in `rcpch/growth-references`, prohibit extrapolation beyond selected series, and test every first/last accepted endpoint plus `math.nextafter()` immediately outside it. Track implementation in issue #109.
- [ ] **R26 - Ask whether the unused WHO 2007 lower-threshold constant can be removed.** Open a small PR for Simon to review `WHO_2007_REFERENCE_LOWER_THRESHOLD`: it has no repository consumers and its expression `(61 / 30.4375) / 365.25` evaluates to about `0.00549` years rather than the documented 61 months (`61 / 12`, about `5.08333` years). Confirm that no external consumer relies on this wildcard-exported name before removing it; do not silently correct an unused public constant and create a new behavior without an identified consumer.
- [x] **R27 - Route daily UK90 LMS generation to the reference-data project. (GitHub #79)** Daily table generation, source provenance, and validation belong in `rcpch/growth-references`; this Python package should consume an approved generated table. Issue #79 was closed with this decision.
- [ ] **R28 - Add test-coverage reporting. (GitHub #128)** Decide whether CI should publish coverage, enforce a threshold, or both. Start with a report-only baseline and avoid a blocking threshold until maintainers agree that the metric is meaningful for this suite.
- [ ] **R29 - Review the `Measurement.plottable_data` response shape for a future major version. (GitHub #24)** Keep the current public shape until the Python API, downstream server, and chart clients have an agreed migration plan. If simplifying the response, characterize compatibility and remove the redundant nested field only in a coordinated major-version change.
- [ ] **R3 - Decide the future of the unusable `BoneAge` class.** `rcpchgrowth/bone_age.py:12-48` returns a dictionary from `__init__`, so construction raises `TypeError: __init__() should return None`. The class has no internal consumers and is not exported from the package root, although related fields are supported by `Measurement`. Either implement and test a coherent object API or remove the module after checking external compatibility.
- [x] **R4 - Define an explicit runtime package-data allowlist.** Packaging now includes only the 15 JSON resources traced to production loads, and the artifact checkers reject every unapproved package resource.
- [x] **R5 - Exclude the test package from release wheels.** Package discovery excludes `rcpchgrowth.tests` and its subpackages, and the wheel checker rejects packaged test modules.
Expand Down
Loading