From 55a28e02cccd48bf6cd74671e05d24668f3ae00e Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 23 Oct 2026 21:18:10 -0400 Subject: [PATCH 01/10] docs: investigate US county vintage contracts and implementation plan --- docs/us-county-2023-geography.md | 90 ++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 docs/us-county-2023-geography.md diff --git a/docs/us-county-2023-geography.md b/docs/us-county-2023-geography.md new file mode 100644 index 0000000000..13157d7917 --- /dev/null +++ b/docs/us-county-2023-geography.md @@ -0,0 +1,90 @@ +# US household county geography, 2023 + +Investigation for microcosm#922 item 3, against base `dec45a439`. + +## Engine and downstream contracts + +`uv.lock:1692–1707` pins **policyengine-us 2.2.1**, with wheel SHA-256 +`0993a6c73fcdfbe171a796aca9d302741bd8e00ab83388ad81c15a08740318c6`. +Inspection of that exact cached wheel, without importing the engine, finds: + +- [`county_enum.py:315–322`](https://github.com/PolicyEngine/policyengine-us/blob/2.2.1/policyengine_us/variables/household/demographic/geographic/county/county_enum.py#L315) + names Connecticut's eight legacy counties, rather than planning regions. +- [`county.py:36–69`](https://github.com/PolicyEngine/policyengine-us/blob/2.2.1/policyengine_us/variables/household/demographic/geographic/county/county.py#L36) + joins `county_fips` to the engine county dataset, converts the result to + that enum, and falls back to the first county in the state on an unrecognized + code. Replacing Connecticut's legacy codes would therefore change rules + evaluation rather than merely relabel geography. +- [`county_helpers.py:12–23`](https://github.com/PolicyEngine/policyengine-us/blob/2.2.1/policyengine_us/tools/geography/county_helpers.py#L12) + explicitly loads `county_fips_2020.csv.gz`. Its Connecticut rows are + `09001,09003,09005,09007,09009,09011,09013,09015`. +- [`geography.py:130–135`](https://github.com/PolicyEngine/policyengine-us/blob/2.2.1/policyengine_us/variables/input/geography.py#L130) + declares `county_fips` as a string household input. The engine's + [`ct_c4k_region.py:22–30`](https://github.com/PolicyEngine/policyengine-us/blob/2.2.1/policyengine_us/variables/gov/states/ct/oec/c4k/ct_c4k_region.py#L22) + uses county enum names to approximate Care 4 Kids regions. The unrecognized + planning-region codes would fall back to Fairfield, affecting eligibility. +- `packages/microcosm-frame/src/microcosm/frame/adapters/policyengine_us.py:670–682` + documents the engine's default county-based SPM measurement and its use of + stored `county_fips`. +- `packages/microcosm-build/src/microcosm/build/us_runtime/geography_ladder.py:443–447,500–525` + uses `county_fips` for NYC shares and requires the legacy county to match the + block prefix. `puma_ladder.py:464–468,517–536` also checks county/state and NYC + geography. These contracts must keep their current interpretation. +- `packages/microcosm-build/src/microcosm/build/us_runtime/base_pool.py:529–547` + checks donor `county_fips` against the legacy tract prefix during pooling. + +**Decision:** add the five-digit text household column `county_fips_2023`. +Preserve `county_fips` for the locked engine, and preserve 2020 block/tract IDs. +Register the new column as deliberately stored geography metadata, since it +is not a PE-US input (`packages/microcosm-data/src/microcosm/data/stored_inputs.py:320`). + +## Reusing the existing mapping + +PR #1072 already supplies the correct route: + +- `block_ladder_sources.py:225–275` parses the 2020 BAF MCD block-to-town layer. +- `block_ladder_sources.py:393–515` parses the official town-to-planning-region + crosswalk and joins blocks through their towns, refusing missing towns. +- `tools/build_us_block_ladder_artifact.py:67–82,285–310` identifies the Census + source URLs and applies that join for CBSA assignment. + +These paths are under `packages/microcosm-build/src/microcosm/build/us_runtime/` +unless prefixed with `tools/`. Read-only inspection of the existing cached +sources finds 169 defined towns, nine planning regions, and 49,845 defined-town +blocks. All 880 corresponding 2020 tracts resolve to exactly one planning +region. Undefined coastal-water geography is outside that support. A compact +tract table can therefore reuse the existing block/town mapping without +deriving boundaries or guessing from legacy counties. + +The PUMA ladder's default county-only path needs a population-weighted tract +draw conditional on the already drawn PUMA and legacy county. This draw runs +after the existing draws and supplies only the new county column. With +`assign_tract=True`, the new county comes directly from the assigned tract. + +## Implementation and validation plan + +Package the compact crosswalk, original Census town crosswalk, and provenance +JSON recording source URLs and SHA-256 digests. Add a reproducible generator +that uses the existing parsers and cached sources. Wire both household ladder +assignments with minimal changes. + +Engine-free checks cover national support of 3,144 county-equivalents in the +50 states plus DC, one region per CT town, non-CT identity, one non-null value +per household, and CT population conservation across county vintages. Include +Hypothesis mapping invariants and exercise both PUMA tract modes. Run focused +tests with at most four workers, the test-plan verifier, and `uv run ruff check .`. + +No full ladder NPZ is present in this workspace or the existing geography +cache. Use source-derived fixtures for CI and provide an optional read-only +check against an existing national ladder. Do not rebuild a ladder or build +or publish a release. + +## Release-tool follow-up + +The task excludes edits to `tools/build_us_acs_local_release.py` and +`tools/us_acs_local_cd_surface.py`. In particular, +`tools/build_us_acs_local_release.py:1203–1211` has a lean household checkpoint +allowlist containing only `county_fips`; that checkpoint will need to retain +`county_fips_2023` before the local county-target lane can use the new geography. +County calibration/target joins must explicitly select the 2023 column while +engine computation continues to use the legacy column. From f227ddc1cf26a90781a16f6a7d6db1ece5a97861 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 23 Oct 2026 21:24:24 -0400 Subject: [PATCH 02/10] feat: add provenance-pinned 2023 county geography crosswalk --- .../build/us_runtime/county_vintage.py | 308 ++ .../data/ct_2020_tract_to_2023_county.csv | 881 ++++ ...0_tract_to_2023_county.csv.provenance.json | 152 + .../data/ct_cou_to_cousub_crosswalk.txt | 190 + .../engine_free/us/test_us_county_vintage.py | 248 + .../us_county_vintage_tract_support.csv | 4016 +++++++++++++++++ ..._vintage_tract_support.csv.provenance.json | 32 + tools/build_us_county_vintage_crosswalk.py | 182 + 8 files changed, 6009 insertions(+) create mode 100644 packages/microcosm-build/src/microcosm/build/us_runtime/county_vintage.py create mode 100644 packages/microcosm-build/src/microcosm/build/us_runtime/data/ct_2020_tract_to_2023_county.csv create mode 100644 packages/microcosm-build/src/microcosm/build/us_runtime/data/ct_2020_tract_to_2023_county.csv.provenance.json create mode 100644 packages/microcosm-build/src/microcosm/build/us_runtime/data/ct_cou_to_cousub_crosswalk.txt create mode 100644 packages/microcosm-build/tests/engine_free/us/test_us_county_vintage.py create mode 100644 packages/microcosm-build/tests/fixtures/us_county_vintage_tract_support.csv create mode 100644 packages/microcosm-build/tests/fixtures/us_county_vintage_tract_support.csv.provenance.json create mode 100644 tools/build_us_county_vintage_crosswalk.py diff --git a/packages/microcosm-build/src/microcosm/build/us_runtime/county_vintage.py b/packages/microcosm-build/src/microcosm/build/us_runtime/county_vintage.py new file mode 100644 index 0000000000..204e4a00e3 --- /dev/null +++ b/packages/microcosm-build/src/microcosm/build/us_runtime/county_vintage.py @@ -0,0 +1,308 @@ +"""2023 county-equivalents on the US ladder's 2020 block geography. + +Connecticut's nine planning regions replace its eight historical counties. +The existing Census BAF town assignment and county-to-town crosswalk show +that every town-defined 2020 CT tract lies in exactly one planning region. +The packaged tract crosswalk preserves that exact join without shipping a +second block ladder. Undefined coastal-water blocks cannot host households +and are refused. All other states retain their 2020 county FIPS. +""" + +from __future__ import annotations + +import csv +import hashlib +import io +import json +from collections.abc import Iterable, Mapping +from functools import cache +from importlib.resources import files +from numbers import Integral +from types import MappingProxyType +from typing import Any + +import numpy as np + +from microcosm.build.us_runtime.block_ladder_sources import ( + CT_STATE_FIPS, + parse_ct_planning_region_crosswalk, +) +from microcosm.calibrate.geography_constants import US_STATE_FIPS_TO_POSTAL + +COUNTY_FIPS_2023_COLUMN = "county_fips_2023" +CT_TRACT_COUNTY_CROSSWALK_RESOURCE = "ct_2020_tract_to_2023_county.csv" +CT_TOWN_COUNTY_CROSSWALK_RESOURCE = "ct_cou_to_cousub_crosswalk.txt" +COUNTY_VINTAGE_PROVENANCE_RESOURCE = ( + CT_TRACT_COUNTY_CROSSWALK_RESOURCE + ".provenance.json" +) + + +def load_county_vintage_provenance() -> dict[str, Any]: + """Read the packaged source URLs, digests and CT coverage evidence.""" + + return json.loads( + files("microcosm.build.us_runtime.data") + .joinpath(COUNTY_VINTAGE_PROVENANCE_RESOURCE) + .read_text(encoding="utf-8") + ) + + +@cache +def _ct_crosswalks() -> tuple[Mapping[str, str], Mapping[str, str], frozenset[str]]: + resource_dir = files("microcosm.build.us_runtime.data") + provenance = load_county_vintage_provenance() + tract_bytes = resource_dir.joinpath(CT_TRACT_COUNTY_CROSSWALK_RESOURCE).read_bytes() + town_bytes = resource_dir.joinpath(CT_TOWN_COUNTY_CROSSWALK_RESOURCE).read_bytes() + for name, payload, expected in ( + ( + CT_TRACT_COUNTY_CROSSWALK_RESOURCE, + tract_bytes, + provenance["output_sha256"], + ), + ( + CT_TOWN_COUNTY_CROSSWALK_RESOURCE, + town_bytes, + provenance["source_files"]["ct_planning_region_crosswalk"]["sha256"], + ), + ): + if hashlib.sha256(payload).hexdigest() != expected: + raise ValueError(f"County-vintage resource {name} has a SHA-256 mismatch.") + reader = csv.DictReader(io.StringIO(tract_bytes.decode("utf-8"))) + if reader.fieldnames != ["tract_geoid", COUNTY_FIPS_2023_COLUMN, "population_2020"]: + raise ValueError("County-vintage tract crosswalk has an invalid header.") + tracts: dict[str, str] = {} + for row in reader: + tract = row["tract_geoid"] + county = row[COUNTY_FIPS_2023_COLUMN] + if ( + len(tract) != 11 + or not tract.isascii() + or not tract.isdigit() + or not tract.startswith(CT_STATE_FIPS) + or len(county) != 5 + or not county.isascii() + or not county.isdigit() + or not county.startswith(CT_STATE_FIPS) + or tract in tracts + ): + raise ValueError(f"Invalid county-vintage crosswalk row: {row}.") + tracts[tract] = county + towns = parse_ct_planning_region_crosswalk( + town_bytes.decode("utf-8-sig").splitlines(keepends=True) + ) + coverage = provenance["coverage"] + if len(tracts) != coverage["tracts"] or len(towns) != coverage["towns"]: + raise ValueError("County-vintage crosswalk coverage disagrees with provenance.") + if set(tracts.values()) != set(towns.values()): + raise ValueError("County-vintage tract and town region rosters disagree.") + return ( + MappingProxyType(tracts), + MappingProxyType(towns), + frozenset(provenance["excluded_zero_population_blocks"]), + ) + + +def load_ct_planning_region_by_tract() -> Mapping[str, str]: + """Return the immutable, hash-checked 2020 CT tract → 2023 county map.""" + + return _ct_crosswalks()[0] + + +def load_ct_planning_region_by_town() -> Mapping[str, str]: + """Return the existing Census crosswalk's immutable CT town → county map.""" + + return _ct_crosswalks()[1] + + +def county_fips_2023_from_blocks(values: Iterable[object]) -> np.ndarray: + """Map 2020 block GEOIDs to five-digit 2023 county-equivalent strings. + + Inputs are a one-dimensional iterable of 15-digit strings or integer + GEOIDs. Unknown CT tracts and town-undefined water blocks fail closed; + every populated 2020 Connecticut block is covered. + """ + + return _county_fips_2023(values, width=15) + + +def county_fips_2023_from_tracts(values: Iterable[object]) -> np.ndarray: + """Map 2020 tract GEOIDs to five-digit 2023 county-equivalent strings. + + Every town-defined Connecticut tract maps unambiguously in the source + BAF. This includes all populated CT tracts. Inputs are 11-digit strings + or integer GEOIDs, with leading zeros restored for integer inputs. + """ + + return _county_fips_2023(values, width=11) + + +def county_fips_2023_assignment_failures( + values: Iterable[object], + legacy_counties: Iterable[object], + *, + blocks: Iterable[object] | None = None, + tracts: Iterable[object] | None = None, +) -> tuple[str, ...]: + """Check stored calibration counties, returning release-gate failure messages. + + A county-only PUMA assignment can check region membership and non-CT + identity. Block- or tract-anchored assignments additionally check each + stored county against the exact source crosswalk. + """ + + failures: list[str] = [] + arrays: list[np.ndarray] = [] + for label, items in ( + (COUNTY_FIPS_2023_COLUMN, values), + ("county_fips", legacy_counties), + ): + if isinstance(items, (str, bytes)): + return (f"{label}: expected a one-dimensional county vector.",) + try: + array = np.asarray(list(items), dtype=object) + except TypeError: + return (f"{label}: expected a one-dimensional county vector.",) + if array.ndim != 1: + return (f"{label}: expected a one-dimensional county vector.",) + arrays.append(array) + counties, legacy = arrays + if len(counties) != len(legacy): + return ( + f"{COUNTY_FIPS_2023_COLUMN}: {len(counties)} values for " + f"{len(legacy)} household counties.", + ) + valid = np.asarray( + [ + isinstance(value, str) + and len(value) == 5 + and value.isascii() + and value.isdigit() + for value in counties + ], + dtype=bool, + ) + if not valid.all(): + failures.append( + f"{COUNTY_FIPS_2023_COLUMN}: {int((~valid).sum())}/{len(counties)} " + "row(s) are not five-digit text codes." + ) + ct_mask = np.asarray( + [ + isinstance(value, str) and value.startswith(CT_STATE_FIPS) + for value in legacy + ], + dtype=bool, + ) + if ct_mask.any(): + try: + regions = frozenset(load_ct_planning_region_by_town().values()) + except (OSError, ValueError) as exc: + failures.append( + f"{COUNTY_FIPS_2023_COLUMN}: cannot load planning-region source: {exc}" + ) + else: + invalid_ct = ( + valid + & ct_mask + & np.asarray( + [ + value not in regions if isinstance(value, str) else False + for value in counties + ], + dtype=bool, + ) + ) + if invalid_ct.any(): + failures.append( + f"{COUNTY_FIPS_2023_COLUMN}: {int(invalid_ct.sum())} Connecticut " + "row(s) do not name a 2023 planning region." + ) + non_ct_mismatch = np.asarray( + [ + valid[index] + and not ct_mask[index] + and (not isinstance(legacy[index], str) or value != legacy[index]) + for index, value in enumerate(counties) + ], + dtype=bool, + ) + if non_ct_mismatch.any(): + failures.append( + f"{COUNTY_FIPS_2023_COLUMN}: {int(non_ct_mismatch.sum())} non-CT " + "row(s) disagree with county_fips." + ) + for label, source, mapper in ( + ("block_geoid", blocks, county_fips_2023_from_blocks), + ("tract_geoid", tracts, county_fips_2023_from_tracts), + ): + if source is None: + continue + try: + expected = mapper(source) + except (OSError, TypeError, ValueError) as exc: + failures.append(f"{COUNTY_FIPS_2023_COLUMN}: cannot resolve {label}: {exc}") + continue + if len(expected) != len(counties): + failures.append( + f"{COUNTY_FIPS_2023_COLUMN}: {label} has {len(expected)} values " + f"for {len(counties)} household counties." + ) + continue + mismatch = np.asarray( + [ + valid[index] and value != expected[index] + for index, value in enumerate(counties) + ], + dtype=bool, + ) + if mismatch.any(): + failures.append( + f"{COUNTY_FIPS_2023_COLUMN}: {int(mismatch.sum())} row(s) " + f"disagree with {label}'s 2023 county-equivalent." + ) + return tuple(failures) + + +def _county_fips_2023(values: Iterable[object], *, width: int) -> np.ndarray: + if isinstance(values, (str, bytes)): + raise ValueError("County-vintage GEOIDs must be a one-dimensional iterable.") + try: + array = np.asarray(list(values), dtype=object) + except TypeError as exc: + raise ValueError( + "County-vintage GEOIDs must be a one-dimensional iterable." + ) from exc + if array.ndim != 1: + raise ValueError("County-vintage GEOIDs must be a one-dimensional iterable.") + result = np.empty(len(array), dtype=object) + ct_crosswalk: Mapping[str, str] | None = None + excluded: frozenset[str] = frozenset() + for index, value in enumerate(array): + if isinstance(value, Integral) and not isinstance(value, (bool, np.bool_)): + geoid = f"{int(value):0{width}d}" + elif isinstance(value, str): + geoid = value + else: + raise ValueError(f"Expected a {width}-digit GEOID, got {value!r}.") + if ( + len(geoid) != width + or not geoid.isascii() + or not geoid.isdigit() + or geoid[:2] not in US_STATE_FIPS_TO_POSTAL + ): + raise ValueError(f"Expected a {width}-digit US GEOID, got {value!r}.") + if not geoid.startswith(CT_STATE_FIPS): + result[index] = geoid[:5] + continue + if ct_crosswalk is None: + ct_crosswalk, _, excluded = _ct_crosswalks() + if width == 15 and geoid in excluded: + raise ValueError(f"Connecticut block {geoid} has no defined town.") + county = ct_crosswalk.get(geoid[:11]) + if county is None: + raise ValueError( + f"Connecticut tract {geoid[:11]} is absent from the " + "2023 county-equivalent crosswalk." + ) + result[index] = county + return result diff --git a/packages/microcosm-build/src/microcosm/build/us_runtime/data/ct_2020_tract_to_2023_county.csv b/packages/microcosm-build/src/microcosm/build/us_runtime/data/ct_2020_tract_to_2023_county.csv new file mode 100644 index 0000000000..6cd5a19893 --- /dev/null +++ b/packages/microcosm-build/src/microcosm/build/us_runtime/data/ct_2020_tract_to_2023_county.csv @@ -0,0 +1,881 @@ +tract_geoid,county_fips_2023,population_2020 +09001010101,09190,4401 +09001010102,09190,4276 +09001010201,09190,3395 +09001010202,09190,5904 +09001010300,09190,4370 +09001010400,09190,5716 +09001010500,09190,6570 +09001010600,09190,1884 +09001010700,09190,3573 +09001010800,09190,3135 +09001010900,09190,5279 +09001011000,09190,5477 +09001011100,09190,5049 +09001011200,09190,1703 +09001011300,09190,2786 +09001020101,09190,2862 +09001020102,09190,3988 +09001020200,09190,3754 +09001020301,09190,3829 +09001020302,09190,3379 +09001020400,09190,3611 +09001020500,09190,4813 +09001020600,09190,4842 +09001020700,09190,4170 +09001020800,09190,2873 +09001020900,09190,5257 +09001021000,09190,3559 +09001021100,09190,6414 +09001021200,09190,4319 +09001021300,09190,4781 +09001021401,09190,3079 +09001021402,09190,3947 +09001021501,09190,3955 +09001021502,09190,2821 +09001021601,09190,3559 +09001021602,09190,4536 +09001021701,09190,3268 +09001021702,09190,4732 +09001021801,09190,4720 +09001021802,09190,5156 +09001021900,09190,6341 +09001022000,09190,3058 +09001022101,09190,3586 +09001022102,09190,3943 +09001022201,09190,3060 +09001022202,09190,4581 +09001022300,09190,6311 +09001022400,09190,2366 +09001030100,09190,3733 +09001030200,09190,3316 +09001030300,09190,4425 +09001030400,09190,4060 +09001030500,09190,5965 +09001035101,09190,3663 +09001035102,09190,4259 +09001035200,09190,3306 +09001035300,09190,4114 +09001035400,09190,5280 +09001042500,09190,3678 +09001042600,09190,4053 +09001042700,09190,5585 +09001042800,09190,4761 +09001042900,09190,1593 +09001043000,09190,3233 +09001043100,09190,4545 +09001043200,09190,3081 +09001043300,09190,3389 +09001043400,09190,4980 +09001043500,09190,2713 +09001043600,09190,3187 +09001043700,09190,3602 +09001043800,09190,8083 +09001043900,09190,5456 +09001044000,09190,6241 +09001044100,09190,3479 +09001044200,09190,4092 +09001044300,09190,4264 +09001044400,09190,3536 +09001044500,09190,4056 +09001044600,09190,3577 +09001045101,09190,4094 +09001045102,09190,5885 +09001045200,09190,2720 +09001045300,09190,2513 +09001045400,09190,3291 +09001050100,09190,4070 +09001050200,09190,4096 +09001050301,09190,3977 +09001050302,09190,4425 +09001050400,09190,2444 +09001050500,09190,4751 +09001050600,09190,3378 +09001055100,09190,5837 +09001055200,09190,4517 +09001060100,09120,4551 +09001060200,09120,4211 +09001060300,09120,3986 +09001060400,09120,4494 +09001060500,09120,3092 +09001060600,09120,2235 +09001060700,09120,8087 +09001060800,09120,2089 +09001060900,09120,2546 +09001061000,09120,4060 +09001061100,09120,3826 +09001061200,09120,2342 +09001061300,09120,3031 +09001061400,09120,3004 +09001061500,09120,4936 +09001061600,09120,5022 +09001070100,09120,5178 +09001070200,09120,3984 +09001070300,09120,1359 +09001070400,09120,1712 +09001070500,09120,1684 +09001070600,09120,2795 +09001070900,09120,2792 +09001071000,09120,3850 +09001071100,09120,4623 +09001071200,09120,5302 +09001071300,09120,3547 +09001071400,09120,3876 +09001071600,09120,2514 +09001071900,09120,4714 +09001072000,09120,3699 +09001072100,09120,6399 +09001072200,09120,3944 +09001072300,09120,5404 +09001072400,09120,2833 +09001072500,09120,5843 +09001072600,09120,7746 +09001072700,09120,3849 +09001072800,09120,5722 +09001072900,09120,4849 +09001073000,09120,1998 +09001073100,09120,4666 +09001073200,09120,3765 +09001073300,09120,3430 +09001073400,09120,3882 +09001073500,09120,3613 +09001073600,09120,2318 +09001073700,09120,5173 +09001073800,09120,2375 +09001073900,09120,3542 +09001074000,09120,2262 +09001074300,09120,4652 +09001074400,09120,4673 +09001080100,09120,4998 +09001080200,09120,4446 +09001080400,09120,6073 +09001080500,09120,3107 +09001080600,09120,2533 +09001080700,09120,1980 +09001080800,09120,4929 +09001080900,09120,4897 +09001081000,09120,4359 +09001081100,09120,4767 +09001081200,09120,5247 +09001081300,09120,5019 +09001090100,09120,3305 +09001090200,09120,8152 +09001090300,09120,4681 +09001090400,09120,6714 +09001090500,09120,4814 +09001090600,09120,3578 +09001090700,09120,5583 +09001100100,09120,5583 +09001100200,09120,6749 +09001100300,09120,6493 +09001105100,09120,4220 +09001105200,09120,3385 +09001110100,09140,2682 +09001110201,09140,4763 +09001110202,09140,5078 +09001110301,09140,6428 +09001110302,09140,3355 +09001110400,09140,5051 +09001110500,09140,5561 +09001110601,09140,2907 +09001110602,09140,5044 +09001200100,09190,4039 +09001200200,09190,5788 +09001200301,09190,5263 +09001200302,09190,5268 +09001205100,09190,4413 +09001205200,09190,7015 +09001205300,09190,6100 +09001210101,09190,2773 +09001210102,09190,3246 +09001210201,09190,3533 +09001210202,09190,3437 +09001210300,09190,6114 +09001210401,09190,4594 +09001210402,09190,4433 +09001210501,09190,3207 +09001210502,09190,6528 +09001210600,09190,6623 +09001210701,09190,5877 +09001210702,09190,3880 +09001210800,09190,6099 +09001210900,09190,5921 +09001211000,09190,4051 +09001211100,09190,1190 +09001211201,09190,3630 +09001211202,09190,2769 +09001211300,09190,3549 +09001211400,09190,5064 +09001220100,09190,4912 +09001220200,09190,4873 +09001220300,09190,3794 +09001230100,09190,7351 +09001230200,09190,1914 +09001230300,09190,3490 +09001230400,09190,5579 +09001230501,09190,3942 +09001230502,09190,4897 +09001240100,09190,4294 +09001240200,09190,4471 +09001245100,09190,2879 +09001245200,09190,4463 +09001245300,09190,5362 +09001245400,09190,3404 +09001245500,09190,2952 +09001245600,09190,5973 +09001257100,09190,3527 +09001257200,09120,4087 +09001990000,09190,0 +09003330100,09160,1901 +09003400101,09110,2928 +09003400102,09110,4299 +09003400200,09110,5997 +09003400300,09110,6951 +09003405100,09140,3164 +09003405200,09140,4225 +09003405300,09140,6105 +09003405401,09140,3421 +09003405402,09140,4257 +09003405500,09140,5266 +09003405600,09140,7398 +09003405700,09140,2061 +09003405801,09140,3176 +09003405802,09140,4172 +09003405900,09140,4647 +09003406001,09140,4113 +09003406002,09140,4754 +09003406100,09140,4074 +09003410101,09160,5285 +09003410102,09160,4234 +09003415300,09110,2521 +09003415400,09110,6179 +09003415500,09110,3094 +09003415600,09110,4198 +09003415700,09110,3256 +09003415800,09110,2873 +09003415900,09110,1647 +09003416000,09110,4537 +09003416100,09110,4955 +09003416200,09110,3025 +09003416300,09110,3810 +09003416400,09110,3152 +09003416500,09110,4833 +09003416600,09110,3150 +09003416700,09110,6877 +09003416800,09110,3079 +09003417100,09110,1857 +09003417200,09110,1702 +09003417300,09110,2045 +09003417400,09110,3012 +09003417500,09110,4333 +09003420400,09110,3021 +09003420500,09110,5321 +09003420601,09110,2596 +09003420602,09110,3224 +09003420700,09110,3363 +09003430100,09110,4141 +09003430201,09110,3373 +09003430202,09110,4616 +09003430203,09110,4024 +09003430301,09110,3855 +09003430302,09110,2878 +09003430400,09110,5092 +09003430501,09110,1705 +09003430502,09110,5157 +09003430602,09110,3451 +09003430603,09110,2170 +09003430604,09110,3039 +09003460100,09110,3441 +09003460202,09110,4820 +09003460203,09110,3313 +09003460204,09110,6190 +09003460301,09110,5025 +09003460302,09110,3923 +09003462101,09110,6151 +09003462102,09110,4391 +09003462201,09110,5439 +09003462202,09110,2951 +09003464101,09110,5185 +09003464102,09110,4939 +09003466101,09110,5675 +09003466102,09110,5496 +09003466201,09110,2685 +09003466202,09110,3678 +09003466300,09110,4608 +09003466400,09110,2375 +09003468101,09110,6912 +09003468102,09110,3991 +09003470100,09110,5214 +09003471100,09110,3554 +09003471200,09110,3171 +09003471300,09110,5161 +09003471400,09110,5665 +09003471500,09110,3984 +09003473100,09110,6162 +09003473400,09110,2050 +09003473501,09110,4638 +09003473502,09110,3397 +09003473601,09110,3286 +09003473602,09110,2374 +09003473700,09110,5748 +09003473800,09110,1837 +09003476100,09110,4734 +09003476200,09110,2723 +09003476300,09110,5156 +09003477101,09110,4144 +09003477103,09110,5657 +09003477104,09110,2959 +09003477200,09110,2992 +09003480300,09110,2173 +09003480400,09110,3466 +09003480500,09110,3256 +09003480600,09110,4938 +09003480700,09110,2175 +09003480800,09110,4177 +09003480900,09110,2989 +09003481000,09110,3411 +09003481100,09110,3686 +09003481200,09110,3734 +09003481300,09110,2807 +09003484100,09110,5665 +09003484200,09110,5525 +09003487100,09110,7158 +09003487201,09110,4581 +09003487202,09110,3517 +09003487300,09110,1434 +09003487400,09110,2621 +09003487500,09110,7607 +09003490100,09110,5577 +09003490303,09110,4579 +09003490304,09110,4031 +09003492100,09110,3354 +09003492200,09110,4263 +09003492300,09110,6118 +09003492400,09110,3007 +09003492500,09110,3436 +09003492600,09110,7120 +09003494100,09110,6293 +09003494201,09110,4687 +09003494202,09110,2792 +09003494300,09110,4034 +09003494400,09110,4747 +09003494500,09110,4366 +09003494600,09110,3617 +09003496100,09110,2683 +09003496200,09110,4747 +09003496300,09110,3650 +09003496400,09110,2939 +09003496500,09110,2801 +09003496600,09110,2781 +09003496700,09110,3674 +09003496800,09110,3616 +09003496900,09110,6179 +09003497000,09110,4279 +09003497100,09110,4041 +09003497200,09110,2478 +09003497300,09110,5478 +09003497400,09110,4068 +09003497500,09110,3972 +09003497600,09110,2246 +09003497700,09110,4451 +09003500100,09110,3720 +09003500200,09110,2179 +09003500300,09110,1937 +09003500400,09110,1862 +09003500500,09110,1533 +09003500900,09110,2027 +09003501200,09110,2552 +09003501300,09110,1707 +09003501400,09110,2627 +09003501500,09110,3783 +09003501700,09110,1512 +09003501800,09110,2898 +09003502100,09110,2832 +09003502300,09110,5910 +09003502400,09110,5582 +09003502500,09110,1768 +09003502600,09110,3743 +09003502700,09110,5001 +09003502800,09110,2971 +09003502900,09110,2776 +09003503000,09110,2978 +09003503101,09110,1677 +09003503102,09110,2851 +09003503300,09110,2617 +09003503500,09110,1616 +09003503700,09110,2450 +09003503800,09110,2462 +09003503900,09110,4772 +09003504000,09110,3365 +09003504100,09110,1918 +09003504200,09110,4713 +09003504300,09110,2022 +09003504500,09110,3478 +09003504800,09110,4576 +09003504900,09110,4342 +09003510100,09110,2224 +09003510200,09110,2367 +09003510300,09110,4185 +09003510400,09110,5614 +09003510500,09110,3139 +09003510600,09110,5051 +09003510700,09110,4915 +09003510800,09110,3110 +09003510900,09110,3699 +09003511000,09110,3581 +09003511100,09110,3742 +09003511200,09110,3455 +09003511300,09110,3542 +09003511400,09110,2421 +09003514101,09110,3160 +09003514103,09110,4038 +09003514104,09110,4244 +09003514200,09110,3382 +09003514300,09110,4363 +09003514400,09110,4899 +09003514500,09110,4481 +09003514600,09110,5258 +09003514700,09110,4403 +09003514800,09110,3577 +09003514900,09110,2987 +09003515000,09110,2899 +09003515101,09110,2525 +09003515102,09110,6054 +09003515200,09110,3443 +09003520100,09110,5668 +09003520201,09110,3883 +09003520202,09110,4253 +09003520301,09110,4732 +09003520302,09110,4467 +09003520400,09110,7794 +09003520501,09110,4362 +09003524100,09110,6133 +09003524200,09110,6658 +09003524300,09110,5329 +09003524400,09110,3157 +09003524501,09110,3422 +09003524502,09110,2229 +09003524600,09110,3198 +09003524700,09110,3402 +09003980001,09110,0 +09003980002,09110,0 +09003980003,09110,0 +09003980100,09110,889 +09005250100,09190,1662 +09005253100,09190,3473 +09005253200,09190,7397 +09005253400,09190,6216 +09005253500,09190,5990 +09005260200,09160,3211 +09005261100,09160,4194 +09005262100,09160,2680 +09005263200,09160,1567 +09005265100,09160,1351 +09005266100,09160,3019 +09005267100,09160,3646 +09005268100,09160,2260 +09005290100,09160,3647 +09005293100,09160,1361 +09005296100,09160,3150 +09005298300,09160,2729 +09005298400,09160,2755 +09005300100,09160,2610 +09005300400,09160,2151 +09005300500,09160,3431 +09005303100,09160,2256 +09005306100,09160,6658 +09005310100,09160,4253 +09005310200,09160,2844 +09005310300,09160,1801 +09005310400,09160,2578 +09005310500,09160,1806 +09005310601,09160,3454 +09005310602,09160,4427 +09005310700,09160,4379 +09005310801,09160,2385 +09005310803,09160,4802 +09005310804,09160,2786 +09005320101,09160,1388 +09005320102,09160,4551 +09005320200,09160,4285 +09005342100,09140,3385 +09005349100,09140,4792 +09005349200,09140,2650 +09005360100,09140,5285 +09005360200,09140,7720 +09005360300,09140,3271 +09005360400,09140,5829 +09005362101,09140,4136 +09005362102,09140,5587 +09005425300,09140,3986 +09005425400,09140,4429 +09005425500,09140,3256 +09005425601,09160,1588 +09005425602,09160,1080 +09005425700,09190,5039 +09007541100,09130,2549 +09007541200,09130,5427 +09007541301,09130,2047 +09007541302,09130,4123 +09007541401,09130,3333 +09007541402,09130,5639 +09007541500,09130,3324 +09007541600,09130,1475 +09007541700,09130,3664 +09007542000,09130,4482 +09007542100,09130,3571 +09007542200,09130,1628 +09007550100,09130,5759 +09007550201,09130,3877 +09007550202,09130,3081 +09007560100,09130,5789 +09007560200,09130,3595 +09007570100,09130,5007 +09007570200,09130,3458 +09007570300,09130,5760 +09007580100,09130,4217 +09007585100,09130,7152 +09007590101,09130,3925 +09007590102,09130,4527 +09007595101,09130,3573 +09007595102,09130,5302 +09007600100,09130,3749 +09007610100,09130,2146 +09007610200,09130,3689 +09007610300,09130,4150 +09007610400,09130,3200 +09007620100,09130,4415 +09007630101,09130,4185 +09007630102,09130,2548 +09007640100,09130,6174 +09007670100,09130,4725 +09007670200,09130,5756 +09007680101,09130,3161 +09007680102,09130,3608 +09007680200,09130,6455 +09009120100,09140,6375 +09009120200,09140,5950 +09009125100,09140,4367 +09009125200,09140,5888 +09009125300,09140,5167 +09009125400,09140,3496 +09009130101,09140,5136 +09009130102,09140,3177 +09009130201,09140,3566 +09009130202,09140,4869 +09009140101,09170,1306 +09009140102,09170,2526 +09009140200,09170,543 +09009140300,09170,2750 +09009140400,09170,3554 +09009140500,09170,3775 +09009140600,09170,4747 +09009140700,09170,4275 +09009140800,09170,3892 +09009140900,09170,4478 +09009141000,09170,3873 +09009141100,09170,2805 +09009141200,09170,4678 +09009141301,09170,6316 +09009141400,09170,4989 +09009141500,09170,7614 +09009141600,09170,5139 +09009141800,09170,4523 +09009141900,09170,5569 +09009142000,09170,3451 +09009142100,09170,1189 +09009142200,09170,1671 +09009142300,09170,5392 +09009142400,09170,5916 +09009142500,09170,5938 +09009142601,09170,5890 +09009142604,09170,2948 +09009142605,09170,4149 +09009142700,09170,6743 +09009142800,09170,4751 +09009150100,09170,4332 +09009150200,09170,3385 +09009150300,09170,3894 +09009150400,09170,4118 +09009150500,09170,4165 +09009150600,09170,6513 +09009150700,09170,4331 +09009150800,09170,3847 +09009150900,09170,3965 +09009151000,09170,4090 +09009151100,09170,6341 +09009151200,09170,3063 +09009154101,09170,3089 +09009154102,09170,4918 +09009154200,09170,6767 +09009154500,09170,4738 +09009154600,09170,4226 +09009154700,09170,6187 +09009154800,09170,4954 +09009154900,09170,3805 +09009155000,09170,5487 +09009155100,09170,3421 +09009157100,09170,2604 +09009157200,09170,3865 +09009157300,09170,3524 +09009157400,09170,4287 +09009160100,09170,3576 +09009160200,09170,5511 +09009161100,09170,5297 +09009165100,09170,4067 +09009165200,09170,2577 +09009165300,09170,2257 +09009165400,09170,5504 +09009165500,09170,4904 +09009165600,09170,5935 +09009165700,09170,4557 +09009165801,09170,5370 +09009165802,09170,4103 +09009165900,09170,8178 +09009166002,09170,6894 +09009166003,09170,2473 +09009166004,09170,4350 +09009167101,09170,3847 +09009167102,09170,4289 +09009167201,09170,4183 +09009167202,09170,4332 +09009167301,09170,7602 +09009170100,09170,1679 +09009170200,09170,1900 +09009170300,09170,2453 +09009170400,09170,1772 +09009170500,09170,5442 +09009170600,09170,2516 +09009170700,09170,2672 +09009170800,09170,5925 +09009170900,09170,2705 +09009171000,09170,2045 +09009171100,09170,5597 +09009171201,09170,4709 +09009171202,09170,3420 +09009171300,09170,3427 +09009171400,09170,2100 +09009171500,09170,3634 +09009171600,09170,4998 +09009171700,09170,3856 +09009175100,09170,3409 +09009175200,09170,2481 +09009175300,09170,4237 +09009175400,09170,4444 +09009175501,09170,1441 +09009175502,09170,6158 +09009175600,09170,5203 +09009175700,09170,1956 +09009175800,09170,4200 +09009175900,09170,4911 +09009176000,09170,5956 +09009180101,09170,2117 +09009180102,09170,4031 +09009180200,09170,5723 +09009180300,09170,2470 +09009180400,09170,2613 +09009180500,09170,4386 +09009180601,09170,3049 +09009180602,09170,3534 +09009184101,09170,5254 +09009184200,09170,4276 +09009184300,09170,4265 +09009184400,09170,3224 +09009184500,09170,2540 +09009184600,09170,2740 +09009184700,09170,5974 +09009186100,09170,6813 +09009186200,09170,6731 +09009190100,09170,3289 +09009190200,09170,3421 +09009190301,09170,6616 +09009190302,09170,5398 +09009190303,09170,3349 +09009194101,09170,2045 +09009194102,09170,2991 +09009194201,09170,7520 +09009194202,09170,5135 +09009341100,09140,6000 +09009343101,09140,5128 +09009343102,09140,4000 +09009343200,09140,6022 +09009343300,09140,6918 +09009343400,09140,6665 +09009344100,09140,4656 +09009344200,09140,2918 +09009345100,09140,6858 +09009345201,09140,7148 +09009345202,09140,4065 +09009345300,09140,6330 +09009345400,09140,7118 +09009346101,09140,6722 +09009346102,09140,5984 +09009347100,09140,5712 +09009347200,09140,3689 +09009348111,09140,2455 +09009348122,09140,4198 +09009348123,09140,5686 +09009348124,09140,2534 +09009348125,09140,5006 +09009350101,09140,4697 +09009350200,09140,3695 +09009350300,09140,2235 +09009350400,09140,2846 +09009350500,09140,2693 +09009350800,09140,6010 +09009350900,09140,2180 +09009351000,09140,4107 +09009351100,09140,4341 +09009351200,09140,4146 +09009351300,09140,5557 +09009351400,09140,4087 +09009351500,09140,4975 +09009351601,09140,3541 +09009351602,09140,6201 +09009351700,09140,3015 +09009351800,09140,4403 +09009351900,09140,2840 +09009352000,09140,4996 +09009352100,09140,4359 +09009352200,09140,2582 +09009352300,09140,3106 +09009352400,09140,3461 +09009352500,09140,3084 +09009352600,09140,5450 +09009352701,09140,3415 +09009352702,09140,5996 +09009352800,09140,6385 +09009361100,09140,6418 +09009361200,09140,5446 +09009361300,09140,4278 +09009361401,09170,4458 +09009361402,09170,4175 +09009361500,09170,7992 +09011650100,09130,2352 +09011660101,09130,3420 +09011660103,09130,2766 +09011660104,09130,1442 +09011690300,09180,6805 +09011690400,09180,2186 +09011690500,09180,2774 +09011690700,09180,1402 +09011690800,09180,3458 +09011690900,09180,4963 +09011693300,09180,5672 +09011693400,09180,4286 +09011693500,09180,3460 +09011693600,09180,2886 +09011693700,09180,3267 +09011695201,09180,4975 +09011695202,09180,5854 +09011696100,09180,5094 +09011696200,09180,4768 +09011696300,09180,2741 +09011696401,09180,6168 +09011696500,09180,2931 +09011696600,09180,3861 +09011696701,09180,5902 +09011696800,09180,4016 +09011697000,09180,4644 +09011700100,09180,4788 +09011701100,09180,8050 +09011701200,09180,7355 +09011702100,09180,4504 +09011702300,09180,2419 +09011702400,09180,3352 +09011702500,09180,4679 +09011702600,09180,1115 +09011702700,09180,4707 +09011702800,09180,4031 +09011702900,09180,2352 +09011703000,09180,3645 +09011705101,09180,4202 +09011705102,09180,4193 +09011705200,09180,3407 +09011705300,09180,3747 +09011705400,09180,2786 +09011707100,09180,5149 +09011708100,09150,2570 +09011709100,09180,6004 +09011709200,09180,5398 +09011710100,09180,4195 +09011711100,09180,2967 +09011712100,09180,1863 +09011713100,09180,2429 +09011714101,09180,3782 +09011714103,09180,7096 +09011714104,09180,4677 +09011715100,09180,4213 +09011716101,09180,4975 +09011716102,09180,5753 +09011870100,09180,7142 +09011870200,09180,5792 +09011870300,09180,5779 +09011870501,09180,4315 +09011870502,09180,3243 +09011870701,09180,2072 +09011870703,09180,1749 +09011870704,09180,4144 +09011980000,09180,1823 +09013526101,09110,4418 +09013526102,09110,4680 +09013528100,09110,3151 +09013529100,09110,4858 +09013530100,09110,2286 +09013530200,09110,6259 +09013530301,09110,5532 +09013530302,09110,6240 +09013530400,09110,4118 +09013530500,09110,3236 +09013530600,09110,2544 +09013533102,09110,5562 +09013533103,09110,4026 +09013533104,09110,4975 +09013535101,09110,5533 +09013535102,09110,4153 +09013535200,09110,6740 +09013538100,09110,1300 +09013538201,09110,5007 +09013538202,09110,3948 +09013840100,09110,5566 +09013850100,09110,5586 +09013850200,09110,6649 +09013860100,09110,5272 +09013881100,09110,4977 +09013881200,09110,9522 +09013881300,09110,5349 +09013881500,09110,6044 +09013890100,09110,3696 +09013890202,09110,4949 +09013890203,09150,785 +09013890204,09110,2827 +09015800300,09180,5958 +09015800400,09180,3910 +09015800501,09180,3804 +09015800502,09180,3149 +09015800600,09180,4187 +09015800700,09180,3417 +09015815000,09150,2151 +09015820000,09150,1728 +09015825000,09150,1576 +09015830100,09150,4191 +09015900100,09150,4422 +09015900200,09150,4767 +09015901101,09150,2640 +09015901102,09150,5581 +09015902200,09150,1649 +09015902500,09150,4266 +09015903101,09150,3302 +09015903102,09150,3527 +09015903200,09150,2395 +09015904101,09150,4761 +09015904102,09150,2797 +09015904400,09150,4112 +09015904500,09150,6082 +09015905101,09150,4751 +09015905102,09150,3699 +09015906100,09150,5045 +09015907100,09150,4217 +09015907200,09150,5302 +09015907300,09150,5454 +09015908100,09150,3578 diff --git a/packages/microcosm-build/src/microcosm/build/us_runtime/data/ct_2020_tract_to_2023_county.csv.provenance.json b/packages/microcosm-build/src/microcosm/build/us_runtime/data/ct_2020_tract_to_2023_county.csv.provenance.json new file mode 100644 index 0000000000..ab6632531b --- /dev/null +++ b/packages/microcosm-build/src/microcosm/build/us_runtime/data/ct_2020_tract_to_2023_county.csv.provenance.json @@ -0,0 +1,152 @@ +{ + "artifact": "ct_2020_tract_to_2023_county.csv", + "coverage": { + "ambiguous_tracts": 0, + "planning_regions": [ + "09110", + "09120", + "09130", + "09140", + "09150", + "09160", + "09170", + "09180", + "09190" + ], + "populated_blocks": 42008, + "town_defined_blocks": 49845, + "towns": 169, + "tracts": 880 + }, + "excluded_blocks_reason": "County subdivisions not defined (coastal water). No populated 2020 block has this town code; the block mapping refuses these blocks even if their tract contains town-defined blocks.", + "excluded_zero_population_blocks": [ + "090019900000001", + "090019900000002", + "090019900000003", + "090019900000004", + "090019900000005", + "090019900000006", + "090019900000007", + "090019900000008", + "090019900000010", + "090079901000001", + "090079901000002", + "090079901000003", + "090079901000004", + "090079901000005", + "090079901000006", + "090091404002017", + "090091428001010", + "090091428003039", + "090091504003000", + "090091505003009", + "090091505003013", + "090091509001028", + "090091509001029", + "090091510001000", + "090091510001015", + "090091510003012", + "090091545002000", + "090091547002000", + "090091547002001", + "090091547002011", + "090091548003009", + "090091550004000", + "090091550004012", + "090091551003011", + "090091901003019", + "090091901003023", + "090091901003024", + "090091902002021", + "090091902002023", + "090091941011034", + "090091941011035", + "090091941022013", + "090091941023019", + "090099900000001", + "090099900000002", + "090099900000003", + "090099900000004", + "090099900000005", + "090099900000006", + "090099900000007", + "090099900000008", + "090099900000009", + "090099900000010", + "090099900000011", + "090099900000012", + "090099900000013", + "090099900000014", + "090099900000015", + "090099900000016", + "090099900000017", + "090099900000018", + "090099900000019", + "090099900000020", + "090099900000021", + "090099900000022", + "090099900000023", + "090099900000024", + "090099900000025", + "090099900000026", + "090099900000027", + "090099900000028", + "090099900000029", + "090099900000030", + "090099900000031", + "090099900000032", + "090099900000033", + "090119901000001", + "090119901000002", + "090119901000003", + "090119901000004", + "090119901000005" + ], + "generator": "tools/build_us_county_vintage_crosswalk.py", + "method": "Reuse microcosm#1072's exact 2020 BAF MCD block-to-town join to the Census CT 2022 planning-region crosswalk. Assert every town-defined 2020 tract belongs to exactly one region, then retain that compact tract mapping. The 2023 county-equivalents use those nine regions; non-Connecticut blocks retain their original county prefix.", + "output_sha256": "dbf5b07d190e6d6d8d15b719f108d42baa9e82e3722fae8caaf38125eea51a8e", + "population_conservation": { + "source_county_population": { + "09001": 957419, + "09003": 899498, + "09005": 185186, + "09007": 164245, + "09009": 864835, + "09011": 268555, + "09013": 149788, + "09015": 116418 + }, + "source_population": 3605944, + "target_county_population": { + "09110": 976248, + "09120": 325778, + "09130": 174225, + "09140": 450376, + "09150": 95348, + "09160": 112503, + "09170": 570487, + "09180": 280430, + "09190": 620549 + }, + "target_population": 3605944 + }, + "schema_version": 1, + "source_files": { + "baf2020_ct": { + "member": "BlockAssign_ST09_CT_MCD.txt", + "member_sha256": "fe56bf38a30921e68360edc4e0ffabd2dae64ade6072761ed887c907bfadb653", + "sha256": "1125c49564224cced90844ab2345df9a2c191a0084c5029ff53ddc9c320c54ba", + "url": "https://www2.census.gov/geo/docs/maps-data/data/baf2020/BlockAssign_ST09_CT.zip" + }, + "ct_planning_region_crosswalk": { + "sha256": "13ad002564e30e6dcd1df112fdaa985f2cd0cdb908f7f3017cd2723db294cff8", + "url": "https://www2.census.gov/geo/docs/reference/ct_change/ct_cou_to_cousub_crosswalk.txt" + }, + "pl94171_ct": { + "sha256": "c4a2d42a79c52de10ea9397cca43db59c2df38e6d6e66b4396a0de8bd3f66780", + "url": "https://www2.census.gov/programs-surveys/decennial/2020/data/01-Redistricting_File--PL_94-171/Connecticut/ct2020.pl.zip" + } + }, + "source_vintage": "2020_tabulation_blocks", + "target_vintage": "2023_county_equivalents" +} diff --git a/packages/microcosm-build/src/microcosm/build/us_runtime/data/ct_cou_to_cousub_crosswalk.txt b/packages/microcosm-build/src/microcosm/build/us_runtime/data/ct_cou_to_cousub_crosswalk.txt new file mode 100644 index 0000000000..86cbb99161 --- /dev/null +++ b/packages/microcosm-build/src/microcosm/build/us_runtime/data/ct_cou_to_cousub_crosswalk.txt @@ -0,0 +1,190 @@ +"STATEFP +(INCITS38)"|"OLD_COUNTYFP +(INCITS31)"|OLD_COUNTY_NAMELSAD|"NEW_COUNTYFP +(INCITS31)"|NEW_COUNTY_NAMELSAD|COUSUBFP|OLD_COUSUB_GEOID|NEW_COUSUB_GEOID|COUSUB_NAMELSAD|"COUSUBNS +(INCITS446)"|COUSUB_LSAD|COUSUB_FUNCSTAT|COUSUB_CLASSFP +09|001|Fairfield County|120|Greater Bridgeport Planning Region|08070|0900108070|0912008070|Bridgeport town|00213396|43|C|T5 +09|001|Fairfield County|120|Greater Bridgeport Planning Region|23890|0900123890|0912023890|Easton town|00213427|43|A|T1 +09|001|Fairfield County|120|Greater Bridgeport Planning Region|26620|0900126620|0912026620|Fairfield town|00213429|43|A|T1 +09|001|Fairfield County|120|Greater Bridgeport Planning Region|48620|0900148620|0912048620|Monroe town|00213463|43|A|T1 +09|001|Fairfield County|120|Greater Bridgeport Planning Region|74190|0900174190|0912074190|Stratford town|00213514|43|A|T1 +09|001|Fairfield County|120|Greater Bridgeport Planning Region|77200|0900177200|0912077200|Trumbull town|00213518|43|A|T1 +09|001|Fairfield County|120|Greater Bridgeport Planning Region|00000|0900100000|0912000000|County subdivisions not defined|00000000|00|F|Z9 +09|001|Fairfield County|140|Naugatuck Valley Planning Region|68170|0900168170|0914068170|Shelton town|00213504|43|C|T5 +09|001|Fairfield County|190|Western Connecticut Planning Region|00000|0900100000|0919000000|County subdivisions not defined|00000000|00|F|Z9 +09|001|Fairfield County|190|Western Connecticut Planning Region|56060|0900156060|0919056060|Norwalk town|00213481|43|C|T5 +09|001|Fairfield County|190|Western Connecticut Planning Region|04720|0900104720|0919004720|Bethel town|00213390|43|A|T1 +09|001|Fairfield County|190|Western Connecticut Planning Region|08980|0900108980|0919008980|Brookfield town|00213399|43|A|T1 +09|001|Fairfield County|190|Western Connecticut Planning Region|18500|0900118500|0919018500|Danbury town|00213415|43|C|T5 +09|001|Fairfield County|190|Western Connecticut Planning Region|18850|0900118850|0919018850|Darien town|00213416|43|A|T1 +09|001|Fairfield County|190|Western Connecticut Planning Region|33620|0900133620|0919033620|Greenwich town|00213435|43|A|T1 +09|001|Fairfield County|190|Western Connecticut Planning Region|50580|0900150580|0919050580|New Canaan town|00213468|43|A|T1 +09|001|Fairfield County|190|Western Connecticut Planning Region|50860|0900150860|0919050860|New Fairfield town|00213469|43|A|T1 +09|001|Fairfield County|190|Western Connecticut Planning Region|52980|0900152980|0919052980|Newtown town|00213475|43|A|T1 +09|001|Fairfield County|190|Western Connecticut Planning Region|63970|0900163970|0919063970|Ridgefield town|00213496|43|A|T1 +09|001|Fairfield County|190|Western Connecticut Planning Region|68310|0900168310|0919068310|Sherman town|00213505|43|A|T1 +09|001|Fairfield County|190|Western Connecticut Planning Region|73070|0900173070|0919073070|Stamford town|00213511|43|C|T5 +09|001|Fairfield County|190|Western Connecticut Planning Region|83430|0900183430|0919083430|Weston town|00213531|43|A|T1 +09|001|Fairfield County|190|Western Connecticut Planning Region|83500|0900183500|0919083500|Westport town|00213532|43|A|T1 +09|001|Fairfield County|190|Western Connecticut Planning Region|86370|0900186370|0919086370|Wilton town|00213535|43|A|T1 +09|001|Fairfield County|190|Western Connecticut Planning Region|63480|0900163480|0919063480|Redding town|00213495|43|A|T1 +09|003|Hartford County|110|Capitol Planning Region|12270|0900312270|0911012270|Canton town|00213404|43|A|T1 +09|003|Hartford County|110|Capitol Planning Region|32640|0900332640|0911032640|Granby town|00213434|43|A|T1 +09|003|Hartford County|110|Capitol Planning Region|45820|0900345820|0911045820|Marlborough town|00213457|43|A|T1 +09|003|Hartford County|110|Capitol Planning Region|02060|0900302060|0911002060|Avon town|00213385|43|A|T1 +09|003|Hartford County|110|Capitol Planning Region|04300|0900304300|0911004300|Berlin town|00213388|43|A|T1 +09|003|Hartford County|110|Capitol Planning Region|05910|0900305910|0911005910|Bloomfield town|00213392|43|A|T1 +09|003|Hartford County|110|Capitol Planning Region|22070|0900322070|0911022070|East Granby town|00213421|43|A|T1 +09|003|Hartford County|110|Capitol Planning Region|22630|0900322630|0911022630|East Hartford town|00213424|43|A|T1 +09|003|Hartford County|110|Capitol Planning Region|25990|0900325990|0911025990|Enfield town|00212332|43|A|T1 +09|003|Hartford County|110|Capitol Planning Region|27600|0900327600|0911027600|Farmington town|00213430|43|A|T1 +09|003|Hartford County|110|Capitol Planning Region|31240|0900331240|0911031240|Glastonbury town|00213432|43|A|T1 +09|003|Hartford County|110|Capitol Planning Region|37070|0900337070|0911037070|Hartford town|00213442|43|C|T5 +09|003|Hartford County|110|Capitol Planning Region|44700|0900344700|0911044700|Manchester town|00213455|43|A|T1 +09|003|Hartford County|110|Capitol Planning Region|50440|0900350440|0911050440|New Britain town|00213467|43|C|T5 +09|003|Hartford County|110|Capitol Planning Region|52140|0900352140|0911052140|Newington town|00213472|43|A|T1 +09|003|Hartford County|110|Capitol Planning Region|60120|0900360120|0911060120|Plainville town|00213488|43|A|T1 +09|003|Hartford County|110|Capitol Planning Region|68940|0900368940|0911068940|Simsbury town|00213506|43|A|T1 +09|003|Hartford County|110|Capitol Planning Region|70550|0900370550|0911070550|Southington town|00213508|43|A|T1 +09|003|Hartford County|110|Capitol Planning Region|71390|0900371390|0911071390|South Windsor town|00213509|43|A|T1 +09|003|Hartford County|110|Capitol Planning Region|82590|0900382590|0911082590|West Hartford town|00213529|43|A|T1 +09|003|Hartford County|110|Capitol Planning Region|84900|0900384900|0911084900|Wethersfield town|00213533|43|A|T1 +09|003|Hartford County|110|Capitol Planning Region|87000|0900387000|0911087000|Windsor town|00212354|43|A|T1 +09|003|Hartford County|110|Capitol Planning Region|87070|0900387070|0911087070|Windsor Locks town|00212355|43|A|T1 +09|003|Hartford County|110|Capitol Planning Region|24800|0900324800|0911024800|East Windsor town|00212329|43|A|T1 +09|003|Hartford County|110|Capitol Planning Region|65370|0900365370|0911065370|Rocky Hill town|00213497|43|A|T1 +09|003|Hartford County|110|Capitol Planning Region|74540|0900374540|0911074540|Suffield town|00212351|43|A|T1 +09|003|Hartford County|140|Naugatuck Valley Planning Region|08490|0900308490|0914008490|Bristol town|00213398|43|C|T5 +09|003|Hartford County|160|Northwest Hills Planning Region|10100|0900310100|0916010100|Burlington town|00213401|43|A|T1 +09|003|Hartford County|160|Northwest Hills Planning Region|37140|0900337140|0916037140|Hartland town|00213443|43|A|T1 +09|005|Litchfield County|140|Naugatuck Valley Planning Region|75730|0900575730|0914075730|Thomaston town|00213515|43|A|T1 +09|005|Litchfield County|140|Naugatuck Valley Planning Region|80490|0900580490|0914080490|Watertown town|00213527|43|A|T1 +09|005|Litchfield County|140|Naugatuck Valley Planning Region|87910|0900587910|0914087910|Woodbury town|00213540|43|A|T1 +09|005|Litchfield County|140|Naugatuck Valley Planning Region|60750|0900560750|0914060750|Plymouth town|00213489|43|A|T1 +09|005|Litchfield County|140|Naugatuck Valley Planning Region|04930|0900504930|0914004930|Bethlehem town|00213391|43|A|T1 +09|005|Litchfield County|160|Northwest Hills Planning Region|43370|0900543370|0916043370|Litchfield town|00213452|43|A|T1 +09|005|Litchfield County|160|Northwest Hills Planning Region|51350|0900551350|0916051350|New Hartford town|00213470|43|A|T1 +09|005|Litchfield County|160|Northwest Hills Planning Region|53470|0900553470|0916053470|Norfolk town|00213476|43|A|T1 +09|005|Litchfield County|160|Northwest Hills Planning Region|54030|0900554030|0916054030|North Canaan town|00213478|43|A|T1 +09|005|Litchfield County|160|Northwest Hills Planning Region|67960|0900567960|0916067960|Sharon town|00213503|43|A|T1 +09|005|Litchfield County|160|Northwest Hills Planning Region|76570|0900576570|0916076570|Torrington town|00213517|43|C|T5 +09|005|Litchfield County|160|Northwest Hills Planning Region|79510|0900579510|0916079510|Warren town|00213523|43|A|T1 +09|005|Litchfield County|160|Northwest Hills Planning Region|79720|0900579720|0916079720|Washington town|00213524|43|A|T1 +09|005|Litchfield County|160|Northwest Hills Planning Region|86440|0900586440|0916086440|Winchester town|00213536|43|A|T1 +09|005|Litchfield County|160|Northwest Hills Planning Region|02760|0900502760|0916002760|Barkhamsted town|00213386|43|A|T1 +09|005|Litchfield County|160|Northwest Hills Planning Region|65930|0900565930|0916065930|Roxbury town|00213498|43|A|T1 +09|005|Litchfield County|160|Northwest Hills Planning Region|66420|0900566420|0916066420|Salisbury town|00213500|43|A|T1 +09|005|Litchfield County|160|Northwest Hills Planning Region|10940|0900510940|0916010940|Canaan town|00213402|43|A|T1 +09|005|Litchfield County|160|Northwest Hills Planning Region|16050|0900516050|0916016050|Colebrook town|00213410|43|A|T1 +09|005|Litchfield County|160|Northwest Hills Planning Region|17240|0900517240|0916017240|Cornwall town|00213412|43|A|T1 +09|005|Litchfield County|160|Northwest Hills Planning Region|32290|0900532290|0916032290|Goshen town|00213433|43|A|T1 +09|005|Litchfield County|160|Northwest Hills Planning Region|37280|0900537280|0916037280|Harwinton town|00213444|43|A|T1 +09|005|Litchfield County|160|Northwest Hills Planning Region|40290|0900540290|0916040290|Kent town|00213446|43|A|T1 +09|005|Litchfield County|160|Northwest Hills Planning Region|49460|0900549460|0916049460|Morris town|00213465|43|A|T1 +09|005|Litchfield County|190|Western Connecticut Planning Region|52630|0900552630|0919052630|New Milford town|00213474|43|A|T1 +09|005|Litchfield County|190|Western Connecticut Planning Region|08210|0900508210|0919008210|Bridgewater town|00213397|43|A|T1 +09|007|Middlesex County|130|Lower Connecticut River Valley Planning Region|81680|0900781680|0913081680|Westbrook town|00213528|43|A|T1 +09|007|Middlesex County|130|Lower Connecticut River Valley Planning Region|00000|0900700000|0913000000|County subdivisions not defined|00000000|00|F|Z9 +09|007|Middlesex County|130|Lower Connecticut River Valley Planning Region|19130|0900719130|0913019130|Deep River town|00213417|43|A|T1 +09|007|Middlesex County|130|Lower Connecticut River Valley Planning Region|22280|0900722280|0913022280|East Haddam town|02378251|43|A|T1 +09|007|Middlesex County|130|Lower Connecticut River Valley Planning Region|22490|0900722490|0913022490|East Hampton town|00213423|43|A|T1 +09|007|Middlesex County|130|Lower Connecticut River Valley Planning Region|26270|0900726270|0913026270|Essex town|00213428|43|A|T1 +09|007|Middlesex County|130|Lower Connecticut River Valley Planning Region|35230|0900735230|0913035230|Haddam town|00213439|43|A|T1 +09|007|Middlesex County|130|Lower Connecticut River Valley Planning Region|40710|0900740710|0913040710|Killingworth town|00213448|43|A|T1 +09|007|Middlesex County|130|Lower Connecticut River Valley Planning Region|47080|0900747080|0913047080|Middlefield town|00213460|43|A|T1 +09|007|Middlesex County|130|Lower Connecticut River Valley Planning Region|57320|0900757320|0913057320|Old Saybrook town|00213484|43|A|T1 +09|007|Middlesex County|130|Lower Connecticut River Valley Planning Region|61800|0900761800|0913061800|Portland town|00213491|43|A|T1 +09|007|Middlesex County|130|Lower Connecticut River Valley Planning Region|47360|0900747360|0913047360|Middletown town|00213461|43|C|T5 +09|007|Middlesex County|130|Lower Connecticut River Valley Planning Region|14300|0900714300|0913014300|Chester town|00213407|43|A|T1 +09|007|Middlesex County|130|Lower Connecticut River Valley Planning Region|18080|0900718080|0913018080|Cromwell town|00213414|43|A|T1 +09|007|Middlesex County|130|Lower Connecticut River Valley Planning Region|20810|0900720810|0913020810|Durham town|00213419|43|A|T1 +09|007|Middlesex County|130|Lower Connecticut River Valley Planning Region|15350|0900715350|0913015350|Clinton town|00213408|43|A|T1 +09|009|New Haven County|140|Naugatuck Valley Planning Region|14160|0900914160|0914014160|Cheshire town|00213406|43|A|T1 +09|009|New Haven County|140|Naugatuck Valley Planning Region|19550|0900919550|0914019550|Derby town|00213418|43|C|T5 +09|009|New Haven County|140|Naugatuck Valley Planning Region|67610|0900967610|0914067610|Seymour town|00213502|43|A|T1 +09|009|New Haven County|140|Naugatuck Valley Planning Region|49950|0900949950|0914049950|Naugatuck town|00213466|43|C|T5 +09|009|New Haven County|140|Naugatuck Valley Planning Region|87560|0900987560|0914087560|Wolcott town|00213538|43|A|T1 +09|009|New Haven County|140|Naugatuck Valley Planning Region|03250|0900903250|0914003250|Beacon Falls town|00213387|43|A|T1 +09|009|New Haven County|140|Naugatuck Valley Planning Region|46940|0900946940|0914046940|Middlebury town|00213459|43|A|T1 +09|009|New Haven County|140|Naugatuck Valley Planning Region|80070|0900980070|0914080070|Waterbury town|00213525|43|C|T5 +09|009|New Haven County|140|Naugatuck Valley Planning Region|58300|0900958300|0914058300|Oxford town|00213486|43|A|T1 +09|009|New Haven County|140|Naugatuck Valley Planning Region|69640|0900969640|0914069640|Southbury town|00213507|43|A|T1 +09|009|New Haven County|140|Naugatuck Valley Planning Region|62290|0900962290|0914062290|Prospect town|00213493|43|A|T1 +09|009|New Haven County|140|Naugatuck Valley Planning Region|01220|0900901220|0914001220|Ansonia town|00213383|43|C|T5 +09|009|New Haven County|170|South Central Connecticut Planning Region|22910|0900922910|0917022910|East Haven town|00213425|43|A|T1 +09|009|New Haven County|170|South Central Connecticut Planning Region|87700|0900987700|0917087700|Woodbridge town|00213539|43|A|T1 +09|009|New Haven County|170|South Central Connecticut Planning Region|35650|0900935650|0917035650|Hamden town|00213440|43|A|T1 +09|009|New Haven County|170|South Central Connecticut Planning Region|46520|0900946520|0917046520|Meriden town|00213458|43|C|T5 +09|009|New Haven County|170|South Central Connecticut Planning Region|52070|0900952070|0917052070|New Haven town|00213471|43|C|T5 +09|009|New Haven County|170|South Central Connecticut Planning Region|53890|0900953890|0917053890|North Branford town|00213477|43|A|T1 +09|009|New Haven County|170|South Central Connecticut Planning Region|54870|0900954870|0917054870|North Haven town|00213479|43|A|T1 +09|009|New Haven County|170|South Central Connecticut Planning Region|57600|0900957600|0917057600|Orange town|00213485|43|A|T1 +09|009|New Haven County|170|South Central Connecticut Planning Region|78740|0900978740|0917078740|Wallingford town|00213522|43|A|T1 +09|009|New Haven County|170|South Central Connecticut Planning Region|82870|0900982870|0917082870|West Haven town|00213530|43|C|T5 +09|009|New Haven County|170|South Central Connecticut Planning Region|47535|0900947535|0917047535|Milford town|00213462|43|C|T5 +09|009|New Haven County|170|South Central Connecticut Planning Region|00000|0900900000|0917000000|County subdivisions not defined|00000000|00|F|Z9 +09|009|New Haven County|170|South Central Connecticut Planning Region|44560|0900944560|0917044560|Madison town|00213454|43|A|T1 +09|009|New Haven County|170|South Central Connecticut Planning Region|04580|0900904580|0917004580|Bethany town|00213389|43|A|T1 +09|009|New Haven County|170|South Central Connecticut Planning Region|07310|0900907310|0917007310|Branford town|00213395|43|A|T1 +09|009|New Haven County|170|South Central Connecticut Planning Region|34950|0900934950|0917034950|Guilford town|00213438|43|A|T1 +09|011|New London County|130|Lower Connecticut River Valley Planning Region|44210|0901144210|0913044210|Lyme town|00213453|43|A|T1 +09|011|New London County|130|Lower Connecticut River Valley Planning Region|57040|0901157040|0913057040|Old Lyme town|00213483|43|A|T1 +09|011|New London County|150|Northeastern Connecticut Planning Region|78600|0901178600|0915078600|Voluntown town|00213521|43|A|T1 +09|011|New London County|180|Southeastern Connecticut Planning Region|06820|0901106820|0918006820|Bozrah town|00213394|43|A|T1 +09|011|New London County|180|Southeastern Connecticut Planning Region|15910|0901115910|0918015910|Colchester town|00213409|43|A|T1 +09|011|New London County|180|Southeastern Connecticut Planning Region|23400|0901123400|0918023400|East Lyme town|00213426|43|A|T1 +09|011|New London County|180|Southeastern Connecticut Planning Region|29910|0901129910|0918029910|Franklin town|00213431|43|A|T1 +09|011|New London County|180|Southeastern Connecticut Planning Region|33900|0901133900|0918033900|Griswold town|00213436|43|A|T1 +09|011|New London County|180|Southeastern Connecticut Planning Region|34250|0901134250|0918034250|Groton town|00213437|43|A|T1 +09|011|New London County|180|Southeastern Connecticut Planning Region|48900|0901148900|0918048900|Montville town|00213464|43|A|T1 +09|011|New London County|180|Southeastern Connecticut Planning Region|55500|0901155500|0918055500|North Stonington town|00213480|43|A|T1 +09|011|New London County|180|Southeastern Connecticut Planning Region|56270|0901156270|0918056270|Norwich town|00213482|43|C|T5 +09|011|New London County|180|Southeastern Connecticut Planning Region|62150|0901162150|0918062150|Preston town|00213492|43|A|T1 +09|011|New London County|180|Southeastern Connecticut Planning Region|66210|0901166210|0918066210|Salem town|00213499|43|A|T1 +09|011|New London County|180|Southeastern Connecticut Planning Region|71670|0901171670|0918071670|Sprague town|00213510|43|A|T1 +09|011|New London County|180|Southeastern Connecticut Planning Region|80280|0901180280|0918080280|Waterford town|00213526|43|A|T1 +09|011|New London County|180|Southeastern Connecticut Planning Region|00000|0901100000|0918000000|County subdivisions not defined|00000000|00|F|Z9 +09|011|New London County|180|Southeastern Connecticut Planning Region|42390|0901142390|0918042390|Lebanon town|00213449|43|A|T1 +09|011|New London County|180|Southeastern Connecticut Planning Region|43230|0901143230|0918043230|Lisbon town|00213451|43|A|T1 +09|011|New London County|180|Southeastern Connecticut Planning Region|42600|0901142600|0918042600|Ledyard town|00213450|43|A|T1 +09|011|New London County|180|Southeastern Connecticut Planning Region|73770|0901173770|0918073770|Stonington town|00213513|43|A|T1 +09|011|New London County|180|Southeastern Connecticut Planning Region|52350|0901152350|0918052350|New London town|00213473|43|C|T5 +09|013|Tolland County|110|Capitol Planning Region|06260|0901306260|0911006260|Bolton town|00213393|43|A|T1 +09|013|Tolland County|110|Capitol Planning Region|25360|0901325360|0911025360|Ellington town|00212330|43|A|T1 +09|013|Tolland County|110|Capitol Planning Region|37910|0901337910|0911037910|Hebron town|00213445|43|A|T1 +09|013|Tolland County|110|Capitol Planning Region|69220|0901369220|0911069220|Somers town|00212346|43|A|T1 +09|013|Tolland County|110|Capitol Planning Region|72090|0901372090|0911072090|Stafford town|00212349|43|A|T1 +09|013|Tolland County|110|Capitol Planning Region|76290|0901376290|0911076290|Tolland town|00212667|43|A|T1 +09|013|Tolland County|110|Capitol Planning Region|78250|0901378250|0911078250|Vernon town|00213520|43|A|T1 +09|013|Tolland County|110|Capitol Planning Region|85950|0901385950|0911085950|Willington town|00213534|43|A|T1 +09|013|Tolland County|110|Capitol Planning Region|01080|0901301080|0911001080|Andover town|00213382|43|A|T1 +09|013|Tolland County|110|Capitol Planning Region|16400|0901316400|0911016400|Columbia town|00213411|43|A|T1 +09|013|Tolland County|110|Capitol Planning Region|17800|0901317800|0911017800|Coventry town|00213413|43|A|T1 +09|013|Tolland County|110|Capitol Planning Region|44910|0901344910|0911044910|Mansfield town|00213456|43|A|T1 +09|013|Tolland County|150|Northeastern Connecticut Planning Region|77830|0901377830|0915077830|Union town|00213519|43|A|T1 +09|015|Windham County|150|Northeastern Connecticut Planning Region|36000|0901536000|0915036000|Hampton town|00213441|43|A|T1 +09|015|Windham County|150|Northeastern Connecticut Planning Region|73420|0901573420|0915073420|Sterling town|00213512|43|A|T1 +09|015|Windham County|150|Northeastern Connecticut Planning Region|75870|0901575870|0915075870|Thompson town|00213516|43|A|T1 +09|015|Windham County|150|Northeastern Connecticut Planning Region|88190|0901588190|0915088190|Woodstock town|00213541|43|A|T1 +09|015|Windham County|150|Northeastern Connecticut Planning Region|62710|0901562710|0915062710|Putnam town|00213494|43|A|T1 +09|015|Windham County|150|Northeastern Connecticut Planning Region|40500|0901540500|0915040500|Killingly town|00213447|43|A|T1 +09|015|Windham County|150|Northeastern Connecticut Planning Region|01430|0901501430|0915001430|Ashford town|00213384|43|A|T1 +09|015|Windham County|150|Northeastern Connecticut Planning Region|09190|0901509190|0915009190|Brooklyn town|00213400|43|A|T1 +09|015|Windham County|150|Northeastern Connecticut Planning Region|12130|0901512130|0915012130|Canterbury town|00213403|43|A|T1 +09|015|Windham County|150|Northeastern Connecticut Planning Region|13810|0901513810|0915013810|Chaplin town|00213405|43|A|T1 +09|015|Windham County|150|Northeastern Connecticut Planning Region|21860|0901521860|0915021860|Eastford town|00213420|43|A|T1 +09|015|Windham County|150|Northeastern Connecticut Planning Region|59980|0901559980|0915059980|Plainfield town|00213487|43|A|T1 +09|015|Windham County|150|Northeastern Connecticut Planning Region|61030|0901561030|0915061030|Pomfret town|00213490|43|A|T1 +09|015|Windham County|150|Northeastern Connecticut Planning Region|67400|0901567400|0915067400|Scotland town|00213501|43|A|T1 +09|015|Windham County|180|Southeastern Connecticut Planning Region|86790|0901586790|0918086790|Windham town|00213537|43|A|T1 + + +GLOSSARY +STATEFP = State FIPS Code / ANSI INCITS 38 Code +COUNTYFP = County FIPS Code / ANSI INCITS 31 Code +COUSUBFP = County Subdivision FIPS Code +LSAD = Legal Statistical Area Descriptor Code +COUSUB_GEOID = Nationally Unique Geographic Identifier, State FIPS Code + County FIPS Code + County Subdivision FIPS Code +COUSUBNS = County Subdivision National Standard (NS) Code / ANSI INCITS 446 Code +FUNCSTAT = Functional Status +CLASSFP = FIPS Class Code diff --git a/packages/microcosm-build/tests/engine_free/us/test_us_county_vintage.py b/packages/microcosm-build/tests/engine_free/us/test_us_county_vintage.py new file mode 100644 index 0000000000..c253ab25c2 --- /dev/null +++ b/packages/microcosm-build/tests/engine_free/us/test_us_county_vintage.py @@ -0,0 +1,248 @@ +"""2020 geography resolves to complete, population-conserving 2023 counties.""" + +from __future__ import annotations + +import csv +import hashlib +import json +import os +from pathlib import Path + +import numpy as np +import pandas as pd +import pytest +from hypothesis import given, settings +from hypothesis import strategies as st + +from microcosm.build.us_runtime.county_vintage import ( + COUNTY_FIPS_2023_COLUMN, + county_fips_2023_from_blocks, + county_fips_2023_from_tracts, + load_ct_planning_region_by_town, + load_ct_planning_region_by_tract, +) +from microcosm.calibrate.geography_constants import US_STATE_FIPS_TO_POSTAL +from test_support.paths import paths_for + +_PATHS = paths_for("microcosm-build") +_DATA = _PATHS.package / "src/microcosm/build/us_runtime/data" +_NATIONAL_SUPPORT = _PATHS.tests / "fixtures/us_county_vintage_tract_support.csv" +_CT_REGIONS = {f"09{county}" for county in range(110, 191, 10)} +_CT_OLD_COUNTIES = {f"09{county:03d}" for county in range(1, 16, 2)} + + +def _ct_tract_rows() -> list[dict[str, str]]: + with (_DATA / "ct_2020_tract_to_2023_county.csv").open(newline="") as source: + return list(csv.DictReader(source)) + + +def test_original_crosswalk_assigns_all_169_towns_to_one_planning_region() -> None: + with (_DATA / "ct_cou_to_cousub_crosswalk.txt").open( + encoding="utf-8-sig", newline="" + ) as source: + rows = csv.reader(source, delimiter="|") + header = next(rows) + columns = {name.split("\n", 1)[0]: index for index, name in enumerate(header)} + regions_by_town: dict[str, set[str]] = {} + for row in rows: + if not row or not any(row): + break + if row[columns["COUSUBFP"]] == "00000": + continue + town = row[columns["OLD_COUSUB_GEOID"]] + region = row[columns["STATEFP"]] + row[columns["NEW_COUNTYFP"]] + regions_by_town.setdefault(town, set()).add(region) + + assert len(regions_by_town) == 169 + assert all(len(regions) == 1 for regions in regions_by_town.values()) + assert load_ct_planning_region_by_town() == { + town: next(iter(regions)) for town, regions in regions_by_town.items() + } + assert set(load_ct_planning_region_by_town().values()) == _CT_REGIONS + + +def test_real_fairfield_blocks_follow_three_different_planning_regions() -> None: + # Existing BAF examples: Bridgeport, Shelton and Stamford all have the + # same 2020 county prefix, so county-only replacement cannot resolve them. + blocks = ["090010701001000", "090011101001000", "090010201011000"] + + assert COUNTY_FIPS_2023_COLUMN == "county_fips_2023" + assert {block[:5] for block in blocks} == {"09001"} + assert county_fips_2023_from_blocks(blocks).tolist() == [ + "09120", + "09140", + "09190", + ] + assert county_fips_2023_from_tracts([block[:11] for block in blocks]).tolist() == [ + "09120", + "09140", + "09190", + ] + + +@pytest.mark.parametrize("as_integer", [False, True]) +def test_every_source_tract_gets_one_value_with_non_ct_identity(as_integer) -> None: + # This is the full county support universe, not a rebuilt ladder artifact: + # one Census tract per non-CT county and every town-defined CT tract. + with _NATIONAL_SUPPORT.open(newline="") as source: + tracts = [row["tract_geoid"] for row in csv.DictReader(source)] + values = [int(tract) for tract in tracts] if as_integer else tracts + counties = county_fips_2023_from_tracts(values) + legacy = np.asarray([tract[:5] for tract in tracts]) + non_ct = np.asarray([not tract.startswith("09") for tract in tracts]) + + assert len(tracts) == len(set(tracts)) == 4015 + assert len(set(legacy)) == 3143 + assert {tract[:2] for tract in tracts} == set(US_STATE_FIPS_TO_POSTAL) + assert counties.shape == (len(tracts),) + assert not pd.isna(counties).any() + assert all(isinstance(county, str) and len(county) == 5 for county in counties) + np.testing.assert_array_equal(counties[non_ct], legacy[non_ct]) + assert len(set(counties)) == 3144 + assert set(counties[~non_ct]) == _CT_REGIONS + assert not set(counties).intersection(_CT_OLD_COUNTIES) + + +def test_national_support_fixture_matches_its_provenance_digest() -> None: + provenance = json.loads( + _NATIONAL_SUPPORT.with_suffix(".csv.provenance.json").read_text() + ) + + assert ( + hashlib.sha256(_NATIONAL_SUPPORT.read_bytes()).hexdigest() + == provenance["output_sha256"] + ) + source = provenance["source_files"]["tract_to_puma"] + assert source["url"].startswith("https://www2.census.gov/") + assert len(source["sha256"]) == 64 + + +def test_ct_tract_mapping_covers_population_and_conserves_county_totals() -> None: + rows = _ct_tract_rows() + tracts = [row["tract_geoid"] for row in rows] + population = np.asarray( + [int(row["population_2020"]) for row in rows], dtype=np.int64 + ) + legacy = np.asarray([tract[:5] for tract in tracts]) + counties = county_fips_2023_from_tracts(tracts) + legacy_totals = pd.Series(population).groupby(legacy).sum() + region_totals = pd.Series(population).groupby(counties).sum() + + assert len(tracts) == len(set(tracts)) == 880 + assert set(legacy_totals.index) == _CT_OLD_COUNTIES + assert set(region_totals.index) == _CT_REGIONS + assert legacy_totals.sum() == region_totals.sum() == population.sum() == 3_605_944 + assert not pd.isna(counties).any() + assert load_ct_planning_region_by_tract() == dict( + zip(tracts, counties, strict=True) + ) + + +@pytest.mark.parametrize( + ("mapper", "valid"), + [ + (county_fips_2023_from_blocks, "090010701001000"), + (county_fips_2023_from_tracts, "09001070100"), + ], +) +def test_mapping_preserves_empty_vectors_duplicates_and_input_order( + mapper, valid +) -> None: + assert mapper([]).shape == (0,) + assert mapper([valid, int(valid), valid]).tolist() == ["09120"] * 3 + assert mapper(iter([valid, valid])).tolist() == ["09120"] * 2 + + +@pytest.mark.parametrize("invalid", [None, np.nan, pd.NA, "", "invalid", True, 1.5]) +@pytest.mark.parametrize( + "mapper", [county_fips_2023_from_blocks, county_fips_2023_from_tracts] +) +def test_mapping_refuses_missing_or_invalid_geography(mapper, invalid) -> None: + with pytest.raises(ValueError): + mapper([invalid]) + + +@pytest.mark.parametrize( + ("mapper", "unknown"), + [ + (county_fips_2023_from_blocks, "090019999991000"), + (county_fips_2023_from_tracts, "09001999999"), + ], +) +def test_mapping_refuses_unknown_ct_tracts_instead_of_returning_null(mapper, unknown): + with pytest.raises(ValueError, match="Connecticut|tract"): + mapper([unknown]) + + +def test_mapping_refuses_undefined_water_block_even_when_its_tract_is_mapped(): + # The original BAF assigns no town to this coastal-water block. Other + # blocks in tract 09001990000 have towns, so a tract-prefix-only lookup + # would silently give this uninhabitable block a planning region. + assert "09001990000" in load_ct_planning_region_by_tract() + with pytest.raises(ValueError, match="no defined town"): + county_fips_2023_from_blocks(["090019900000001"]) + + +@given( + rows=st.lists( + st.tuples( + st.sampled_from( + [ + ("090010701001000", "09120"), + ("090011101001000", "09140"), + ("090010201011000", "09190"), + ("090091401011000", "09170"), + ("010010201001000", "01001"), + ("020130001001000", "02013"), + ("110010062021000", "11001"), + ("360610001001000", "36061"), + ] + ), + st.integers(min_value=0, max_value=10**9), + st.booleans(), + ), + max_size=100, + ) +) +@settings(max_examples=75, deadline=None) +def test_mapping_is_complete_order_preserving_and_conserves_generated_mass(rows): + blocks = [int(pair[0]) if integral else pair[0] for pair, _, integral in rows] + expected = [pair[1] for pair, _, _ in rows] + population = np.asarray([mass for _, mass, _ in rows], dtype=np.int64) + + counties = county_fips_2023_from_blocks(blocks) + assert counties.tolist() == expected + assert not pd.isna(counties).any() + np.testing.assert_array_equal( + county_fips_2023_from_blocks(blocks[::-1]), counties[::-1] + ) + assert ( + county_fips_2023_from_tracts([pair[0][:11] for pair, _, _ in rows]).tolist() + == expected + ) + assert pd.Series(population).groupby(counties).sum().sum() == population.sum() + + +def test_existing_full_ladder_has_3144_counties_without_rebuilding() -> None: + artifact = os.environ.get("MICROCOSM_US_BLOCK_LADDER_ARTIFACT") + if not artifact: + pytest.skip( + "Set MICROCOSM_US_BLOCK_LADDER_ARTIFACT to an existing national NPZ." + ) + from microcosm.build.us_runtime.geography_ladder import load_us_block_ladder + + ladder = load_us_block_ladder(Path(artifact)) + states = {f"{int(state):02d}" for state in np.unique(ladder.block_geoid // 10**13)} + assert states == set(US_STATE_FIPS_TO_POSTAL) + counties = county_fips_2023_from_blocks(ladder.block_geoid) + legacy = np.asarray([f"{int(block) // 10**10:05d}" for block in ladder.block_geoid]) + is_ct = (ladder.block_geoid // 10**13) == 9 + + assert not pd.isna(counties).any() + assert len(set(counties)) == 3144 + np.testing.assert_array_equal(counties[~is_ct], legacy[~is_ct]) + ct_population = pd.Series(ladder.population[is_ct]) + assert ( + ct_population.groupby(legacy[is_ct]).sum().sum() + == ct_population.groupby(counties[is_ct]).sum().sum() + ) diff --git a/packages/microcosm-build/tests/fixtures/us_county_vintage_tract_support.csv b/packages/microcosm-build/tests/fixtures/us_county_vintage_tract_support.csv new file mode 100644 index 0000000000..e8a87df67a --- /dev/null +++ b/packages/microcosm-build/tests/fixtures/us_county_vintage_tract_support.csv @@ -0,0 +1,4016 @@ +tract_geoid +01001020100 +01003010100 +01005950100 +01007010001 +01009050103 +01011952100 +01013952700 +01015000200 +01017953800 +01019955701 +01021060101 +01023956700 +01025957500 +01027958900 +01029959500 +01031010100 +01033020100 +01035960200 +01037961000 +01039961600 +01041963400 +01043964100 +01045020000 +01047956101 +01049960101 +01051030101 +01053969801 +01055000200 +01057020000 +01059972900 +01061050100 +01063060000 +01065040000 +01067030100 +01069040100 +01071950101 +01073000100 +01075030000 +01077010100 +01079979100 +01081040201 +01083020101 +01085780800 +01087231400 +01089000203 +01091972901 +01093964001 +01095030101 +01097000200 +01099075600 +01101000100 +01103000100 +01105686800 +01107050000 +01109188600 +01111000100 +01113030200 +01115040104 +01117030102 +01119011301 +01121010101 +01123961900 +01125010101 +01127020100 +01129043900 +01131034700 +01133965501 +02013000100 +02016000100 +02020000101 +02050000100 +02060000100 +02063000200 +02066000100 +02068000100 +02070000100 +02090000100 +02100000100 +02105000400 +02110000100 +02122000100 +02130000101 +02150000100 +02158000100 +02164000100 +02170000101 +02180000100 +02185000100 +02188000100 +02195000200 +02198000100 +02220000100 +02230000100 +02240000100 +02275000300 +02282000100 +02290000100 +04001942600 +04003000101 +04005000100 +04007000100 +04009940500 +04011960100 +04012020101 +04013010102 +04015940400 +04017940008 +04019000100 +04021000201 +04023966000 +04025000205 +04027000100 +05001480100 +05003960100 +05005950100 +05007020102 +05009790100 +05011950100 +05013480100 +05015950100 +05017080100 +05019953601 +05021950100 +05023480100 +05025970100 +05027950100 +05029950100 +05031000101 +05033020101 +05035030101 +05037950100 +05039970100 +05041950100 +05043490100 +05045030101 +05047950100 +05049550101 +05051010301 +05053470100 +05055480100 +05057480101 +05059020101 +05061950100 +05063490101 +05065960100 +05067480100 +05069000102 +05071951700 +05073470101 +05075470100 +05077470100 +05079960300 +05081030101 +05083950100 +05085020102 +05087960100 +05089960100 +05091020100 +05093010100 +05095950100 +05097953000 +05099090100 +05101180100 +05103950101 +05105952700 +05107480100 +05109953300 +05111490100 +05113950100 +05115950700 +05117460100 +05119000500 +05121960100 +05123960100 +05125010101 +05127950100 +05129970100 +05131000100 +05133080100 +05135470100 +05137950100 +05139950100 +05141460100 +05143010101 +05145070100 +05147490100 +05149952301 +06001400100 +06003010000 +06005000101 +06007000102 +06009000121 +06011000100 +06013301000 +06015000101 +06017030201 +06019000100 +06021010101 +06023000100 +06025010101 +06027000100 +06029000102 +06031000100 +06033000100 +06035040100 +06037101110 +06039000102 +06041101100 +06043000101 +06045010100 +06047000201 +06049000100 +06051000101 +06053000101 +06055200201 +06057000102 +06059001101 +06061020104 +06063000100 +06065030101 +06067000100 +06069000100 +06071000103 +06073000100 +06075010101 +06077000101 +06079010016 +06081600100 +06083000101 +06085500100 +06087100100 +06089010100 +06091010000 +06093000100 +06095250103 +06097150100 +06099000101 +06101050101 +06103000100 +06105000101 +06107000101 +06109001100 +06111000302 +06113010102 +06115040100 +08001007801 +08003960000 +08005004951 +08007940400 +08009964600 +08011966701 +08013012101 +08014030000 +08015000100 +08017960600 +08019014701 +08021974800 +08023972600 +08025969601 +08027970101 +08029964600 +08031000102 +08033000100 +08035013904 +08037000100 +08039961100 +08041000102 +08043978100 +08045951600 +08047013801 +08049000100 +08051963601 +08053973100 +08055960600 +08057955600 +08059009806 +08061960100 +08063962100 +08065961701 +08067940300 +08069000100 +08071000100 +08073961700 +08075965900 +08077000200 +08079973600 +08081000300 +08083941100 +08085966100 +08087000100 +08089968000 +08091967601 +08093000101 +08095967600 +08097000101 +08099000100 +08101000100 +08103951100 +08105976700 +08107000100 +08109977600 +08111972600 +08113968101 +08115968300 +08117000101 +08119010104 +08121924100 +08123000100 +08125963100 +09001010101 +09001010102 +09001010201 +09001010202 +09001010300 +09001010400 +09001010500 +09001010600 +09001010700 +09001010800 +09001010900 +09001011000 +09001011100 +09001011200 +09001011300 +09001020101 +09001020102 +09001020200 +09001020301 +09001020302 +09001020400 +09001020500 +09001020600 +09001020700 +09001020800 +09001020900 +09001021000 +09001021100 +09001021200 +09001021300 +09001021401 +09001021402 +09001021501 +09001021502 +09001021601 +09001021602 +09001021701 +09001021702 +09001021801 +09001021802 +09001021900 +09001022000 +09001022101 +09001022102 +09001022201 +09001022202 +09001022300 +09001022400 +09001030100 +09001030200 +09001030300 +09001030400 +09001030500 +09001035101 +09001035102 +09001035200 +09001035300 +09001035400 +09001042500 +09001042600 +09001042700 +09001042800 +09001042900 +09001043000 +09001043100 +09001043200 +09001043300 +09001043400 +09001043500 +09001043600 +09001043700 +09001043800 +09001043900 +09001044000 +09001044100 +09001044200 +09001044300 +09001044400 +09001044500 +09001044600 +09001045101 +09001045102 +09001045200 +09001045300 +09001045400 +09001050100 +09001050200 +09001050301 +09001050302 +09001050400 +09001050500 +09001050600 +09001055100 +09001055200 +09001060100 +09001060200 +09001060300 +09001060400 +09001060500 +09001060600 +09001060700 +09001060800 +09001060900 +09001061000 +09001061100 +09001061200 +09001061300 +09001061400 +09001061500 +09001061600 +09001070100 +09001070200 +09001070300 +09001070400 +09001070500 +09001070600 +09001070900 +09001071000 +09001071100 +09001071200 +09001071300 +09001071400 +09001071600 +09001071900 +09001072000 +09001072100 +09001072200 +09001072300 +09001072400 +09001072500 +09001072600 +09001072700 +09001072800 +09001072900 +09001073000 +09001073100 +09001073200 +09001073300 +09001073400 +09001073500 +09001073600 +09001073700 +09001073800 +09001073900 +09001074000 +09001074300 +09001074400 +09001080100 +09001080200 +09001080400 +09001080500 +09001080600 +09001080700 +09001080800 +09001080900 +09001081000 +09001081100 +09001081200 +09001081300 +09001090100 +09001090200 +09001090300 +09001090400 +09001090500 +09001090600 +09001090700 +09001100100 +09001100200 +09001100300 +09001105100 +09001105200 +09001110100 +09001110201 +09001110202 +09001110301 +09001110302 +09001110400 +09001110500 +09001110601 +09001110602 +09001200100 +09001200200 +09001200301 +09001200302 +09001205100 +09001205200 +09001205300 +09001210101 +09001210102 +09001210201 +09001210202 +09001210300 +09001210401 +09001210402 +09001210501 +09001210502 +09001210600 +09001210701 +09001210702 +09001210800 +09001210900 +09001211000 +09001211100 +09001211201 +09001211202 +09001211300 +09001211400 +09001220100 +09001220200 +09001220300 +09001230100 +09001230200 +09001230300 +09001230400 +09001230501 +09001230502 +09001240100 +09001240200 +09001245100 +09001245200 +09001245300 +09001245400 +09001245500 +09001245600 +09001257100 +09001257200 +09001990000 +09003330100 +09003400101 +09003400102 +09003400200 +09003400300 +09003405100 +09003405200 +09003405300 +09003405401 +09003405402 +09003405500 +09003405600 +09003405700 +09003405801 +09003405802 +09003405900 +09003406001 +09003406002 +09003406100 +09003410101 +09003410102 +09003415300 +09003415400 +09003415500 +09003415600 +09003415700 +09003415800 +09003415900 +09003416000 +09003416100 +09003416200 +09003416300 +09003416400 +09003416500 +09003416600 +09003416700 +09003416800 +09003417100 +09003417200 +09003417300 +09003417400 +09003417500 +09003420400 +09003420500 +09003420601 +09003420602 +09003420700 +09003430100 +09003430201 +09003430202 +09003430203 +09003430301 +09003430302 +09003430400 +09003430501 +09003430502 +09003430602 +09003430603 +09003430604 +09003460100 +09003460202 +09003460203 +09003460204 +09003460301 +09003460302 +09003462101 +09003462102 +09003462201 +09003462202 +09003464101 +09003464102 +09003466101 +09003466102 +09003466201 +09003466202 +09003466300 +09003466400 +09003468101 +09003468102 +09003470100 +09003471100 +09003471200 +09003471300 +09003471400 +09003471500 +09003473100 +09003473400 +09003473501 +09003473502 +09003473601 +09003473602 +09003473700 +09003473800 +09003476100 +09003476200 +09003476300 +09003477101 +09003477103 +09003477104 +09003477200 +09003480300 +09003480400 +09003480500 +09003480600 +09003480700 +09003480800 +09003480900 +09003481000 +09003481100 +09003481200 +09003481300 +09003484100 +09003484200 +09003487100 +09003487201 +09003487202 +09003487300 +09003487400 +09003487500 +09003490100 +09003490303 +09003490304 +09003492100 +09003492200 +09003492300 +09003492400 +09003492500 +09003492600 +09003494100 +09003494201 +09003494202 +09003494300 +09003494400 +09003494500 +09003494600 +09003496100 +09003496200 +09003496300 +09003496400 +09003496500 +09003496600 +09003496700 +09003496800 +09003496900 +09003497000 +09003497100 +09003497200 +09003497300 +09003497400 +09003497500 +09003497600 +09003497700 +09003500100 +09003500200 +09003500300 +09003500400 +09003500500 +09003500900 +09003501200 +09003501300 +09003501400 +09003501500 +09003501700 +09003501800 +09003502100 +09003502300 +09003502400 +09003502500 +09003502600 +09003502700 +09003502800 +09003502900 +09003503000 +09003503101 +09003503102 +09003503300 +09003503500 +09003503700 +09003503800 +09003503900 +09003504000 +09003504100 +09003504200 +09003504300 +09003504500 +09003504800 +09003504900 +09003510100 +09003510200 +09003510300 +09003510400 +09003510500 +09003510600 +09003510700 +09003510800 +09003510900 +09003511000 +09003511100 +09003511200 +09003511300 +09003511400 +09003514101 +09003514103 +09003514104 +09003514200 +09003514300 +09003514400 +09003514500 +09003514600 +09003514700 +09003514800 +09003514900 +09003515000 +09003515101 +09003515102 +09003515200 +09003520100 +09003520201 +09003520202 +09003520301 +09003520302 +09003520400 +09003520501 +09003524100 +09003524200 +09003524300 +09003524400 +09003524501 +09003524502 +09003524600 +09003524700 +09003980001 +09003980002 +09003980003 +09003980100 +09005250100 +09005253100 +09005253200 +09005253400 +09005253500 +09005260200 +09005261100 +09005262100 +09005263200 +09005265100 +09005266100 +09005267100 +09005268100 +09005290100 +09005293100 +09005296100 +09005298300 +09005298400 +09005300100 +09005300400 +09005300500 +09005303100 +09005306100 +09005310100 +09005310200 +09005310300 +09005310400 +09005310500 +09005310601 +09005310602 +09005310700 +09005310801 +09005310803 +09005310804 +09005320101 +09005320102 +09005320200 +09005342100 +09005349100 +09005349200 +09005360100 +09005360200 +09005360300 +09005360400 +09005362101 +09005362102 +09005425300 +09005425400 +09005425500 +09005425601 +09005425602 +09005425700 +09007541100 +09007541200 +09007541301 +09007541302 +09007541401 +09007541402 +09007541500 +09007541600 +09007541700 +09007542000 +09007542100 +09007542200 +09007550100 +09007550201 +09007550202 +09007560100 +09007560200 +09007570100 +09007570200 +09007570300 +09007580100 +09007585100 +09007590101 +09007590102 +09007595101 +09007595102 +09007600100 +09007610100 +09007610200 +09007610300 +09007610400 +09007620100 +09007630101 +09007630102 +09007640100 +09007670100 +09007670200 +09007680101 +09007680102 +09007680200 +09009120100 +09009120200 +09009125100 +09009125200 +09009125300 +09009125400 +09009130101 +09009130102 +09009130201 +09009130202 +09009140101 +09009140102 +09009140200 +09009140300 +09009140400 +09009140500 +09009140600 +09009140700 +09009140800 +09009140900 +09009141000 +09009141100 +09009141200 +09009141301 +09009141400 +09009141500 +09009141600 +09009141800 +09009141900 +09009142000 +09009142100 +09009142200 +09009142300 +09009142400 +09009142500 +09009142601 +09009142604 +09009142605 +09009142700 +09009142800 +09009150100 +09009150200 +09009150300 +09009150400 +09009150500 +09009150600 +09009150700 +09009150800 +09009150900 +09009151000 +09009151100 +09009151200 +09009154101 +09009154102 +09009154200 +09009154500 +09009154600 +09009154700 +09009154800 +09009154900 +09009155000 +09009155100 +09009157100 +09009157200 +09009157300 +09009157400 +09009160100 +09009160200 +09009161100 +09009165100 +09009165200 +09009165300 +09009165400 +09009165500 +09009165600 +09009165700 +09009165801 +09009165802 +09009165900 +09009166002 +09009166003 +09009166004 +09009167101 +09009167102 +09009167201 +09009167202 +09009167301 +09009170100 +09009170200 +09009170300 +09009170400 +09009170500 +09009170600 +09009170700 +09009170800 +09009170900 +09009171000 +09009171100 +09009171201 +09009171202 +09009171300 +09009171400 +09009171500 +09009171600 +09009171700 +09009175100 +09009175200 +09009175300 +09009175400 +09009175501 +09009175502 +09009175600 +09009175700 +09009175800 +09009175900 +09009176000 +09009180101 +09009180102 +09009180200 +09009180300 +09009180400 +09009180500 +09009180601 +09009180602 +09009184101 +09009184200 +09009184300 +09009184400 +09009184500 +09009184600 +09009184700 +09009186100 +09009186200 +09009190100 +09009190200 +09009190301 +09009190302 +09009190303 +09009194101 +09009194102 +09009194201 +09009194202 +09009341100 +09009343101 +09009343102 +09009343200 +09009343300 +09009343400 +09009344100 +09009344200 +09009345100 +09009345201 +09009345202 +09009345300 +09009345400 +09009346101 +09009346102 +09009347100 +09009347200 +09009348111 +09009348122 +09009348123 +09009348124 +09009348125 +09009350101 +09009350200 +09009350300 +09009350400 +09009350500 +09009350800 +09009350900 +09009351000 +09009351100 +09009351200 +09009351300 +09009351400 +09009351500 +09009351601 +09009351602 +09009351700 +09009351800 +09009351900 +09009352000 +09009352100 +09009352200 +09009352300 +09009352400 +09009352500 +09009352600 +09009352701 +09009352702 +09009352800 +09009361100 +09009361200 +09009361300 +09009361401 +09009361402 +09009361500 +09011650100 +09011660101 +09011660103 +09011660104 +09011690300 +09011690400 +09011690500 +09011690700 +09011690800 +09011690900 +09011693300 +09011693400 +09011693500 +09011693600 +09011693700 +09011695201 +09011695202 +09011696100 +09011696200 +09011696300 +09011696401 +09011696500 +09011696600 +09011696701 +09011696800 +09011697000 +09011700100 +09011701100 +09011701200 +09011702100 +09011702300 +09011702400 +09011702500 +09011702600 +09011702700 +09011702800 +09011702900 +09011703000 +09011705101 +09011705102 +09011705200 +09011705300 +09011705400 +09011707100 +09011708100 +09011709100 +09011709200 +09011710100 +09011711100 +09011712100 +09011713100 +09011714101 +09011714103 +09011714104 +09011715100 +09011716101 +09011716102 +09011870100 +09011870200 +09011870300 +09011870501 +09011870502 +09011870701 +09011870703 +09011870704 +09011980000 +09013526101 +09013526102 +09013528100 +09013529100 +09013530100 +09013530200 +09013530301 +09013530302 +09013530400 +09013530500 +09013530600 +09013533102 +09013533103 +09013533104 +09013535101 +09013535102 +09013535200 +09013538100 +09013538201 +09013538202 +09013840100 +09013850100 +09013850200 +09013860100 +09013881100 +09013881200 +09013881300 +09013881500 +09013890100 +09013890202 +09013890203 +09013890204 +09015800300 +09015800400 +09015800501 +09015800502 +09015800600 +09015800700 +09015815000 +09015820000 +09015825000 +09015830100 +09015900100 +09015900200 +09015901101 +09015901102 +09015902200 +09015902500 +09015903101 +09015903102 +09015903200 +09015904101 +09015904102 +09015904400 +09015904500 +09015905101 +09015905102 +09015906100 +09015907100 +09015907200 +09015907300 +09015908100 +10001040100 +10003000200 +10005050101 +11001000101 +12001000201 +12003040101 +12005000201 +12007000100 +12009060103 +12011010102 +12013010100 +12015010100 +12017450103 +12019030103 +12021000101 +12023110201 +12027010101 +12029970101 +12031000101 +12033000100 +12035060103 +12037970101 +12039020101 +12041950100 +12043000100 +12045960101 +12047960101 +12049970101 +12051000101 +12053040101 +12055960101 +12057000101 +12059960100 +12061050101 +12063210100 +12065250103 +12067960100 +12069030102 +12071000303 +12073000200 +12075970101 +12077950100 +12079110100 +12081000101 +12083000101 +12085000100 +12086000107 +12087970400 +12089050102 +12091020101 +12093910101 +12095010201 +12097040801 +12099000102 +12101030101 +12103020105 +12105010300 +12107950100 +12109020200 +12111380100 +12113010100 +12115000102 +12117020101 +12119910100 +12121970100 +12123950100 +12125960100 +12127080101 +12129010101 +12131950102 +12133970102 +13001950100 +13003960100 +13005970100 +13007960100 +13009970101 +13011970100 +13013180103 +13015960101 +13017960100 +13019970100 +13021010100 +13023790100 +13025960101 +13027960200 +13029920101 +13031110100 +13033950101 +13035150101 +13037950100 +13039010100 +13043950100 +13045910101 +13047030100 +13049010101 +13051000601 +13053020100 +13055010100 +13057090101 +13059000100 +13061960300 +13063040202 +13065970100 +13067030104 +13069010100 +13071970101 +13073030103 +13075960100 +13077170101 +13079070100 +13081010100 +13083040102 +13085970101 +13087970100 +13089020100 +13091960100 +13093970100 +13095000101 +13097080104 +13099090100 +13101880100 +13103030100 +13105000100 +13107970101 +13109970100 +13111050100 +13113140102 +13115000100 +13117130101 +13119890101 +13121000100 +13123080100 +13125010100 +13127000101 +13129970101 +13131950100 +13133950100 +13135050105 +13137000100 +13139000101 +13141480300 +13143010100 +13145120198 +13147960101 +13149970100 +13151070109 +13153020106 +13155950100 +13157010104 +13159010100 +13161960101 +13163960100 +13165960100 +13167960100 +13169030101 +13171970100 +13173950100 +13175950100 +13177020101 +13179010101 +13181970100 +13183970101 +13185010101 +13187960103 +13189950100 +13191110101 +13193000100 +13195020100 +13197920100 +13199970501 +13201950100 +13205090100 +13207050101 +13209950100 +13211010100 +13213010100 +13215000200 +13217100101 +13219030101 +13221960100 +13223120104 +13225040101 +13227050101 +13229960100 +13231010101 +13233010100 +13235950100 +13237960101 +13239960300 +13241970101 +13243790100 +13245000100 +13247060101 +13249960100 +13251970200 +13253200100 +13255160101 +13257970101 +13259950100 +13261950100 +13263960100 +13265010200 +13267950100 +13269950100 +13271950101 +13273120200 +13275960100 +13277960100 +13279970101 +13281960100 +13283960100 +13285960101 +13287970201 +13289060101 +13291000101 +13293010100 +13295020101 +13297110101 +13299950100 +13301970400 +13303950100 +13305970100 +13307960100 +13309780100 +13311950101 +13313000101 +13315960100 +13317010101 +13319960200 +13321950100 +15001020100 +15003000107 +15005031900 +15007040104 +15009030100 +16001000101 +16003950100 +16005000200 +16007950100 +16009940000 +16011940000 +16013960101 +16015950201 +16017950100 +16019970100 +16021970100 +16023970100 +16025970100 +16027020100 +16029960100 +16031950100 +16033950100 +16035940001 +16037960200 +16039960100 +16041970100 +16043970100 +16045960100 +16047960101 +16049940000 +16051960100 +16053970100 +16055000101 +16057005101 +16059970100 +16061940001 +16063950100 +16065950101 +16067970100 +16069940000 +16071960100 +16073950101 +16075960100 +16077960100 +16079960200 +16081960101 +16083000200 +16085970100 +16087970100 +17001000100 +17003957600 +17005951200 +17007010101 +17009970400 +17011964700 +17013951200 +17015960100 +17017960100 +17019000200 +17021958100 +17023060100 +17025971900 +17027900100 +17029000100 +17031010100 +17033880100 +17035972400 +17037000100 +17039971400 +17041952000 +17043840000 +17045070100 +17047956900 +17049950100 +17051950500 +17053961600 +17055040100 +17057952800 +17059972700 +17061973600 +17063000102 +17065973100 +17067953700 +17069970900 +17071973300 +17073030100 +17075950100 +17077010100 +17079977300 +17081050100 +17083010100 +17085020101 +17087977600 +17089850101 +17091010100 +17093890103 +17095000100 +17097860103 +17099961701 +17101880700 +17103000100 +17105960100 +17107952900 +17109010100 +17111870103 +17113000105 +17115000200 +17117956000 +17119400101 +17121951600 +17123961100 +17125956300 +17127970100 +17129010100 +17131040100 +17133600102 +17135957300 +17137951400 +17139976900 +17141960700 +17143000200 +17145030100 +17147954500 +17149952400 +17151971200 +17153971000 +17155954500 +17157950500 +17159977900 +17161020101 +17163500400 +17165955100 +17167000100 +17169970100 +17171970600 +17173959100 +17175951400 +17177000100 +17179020100 +17181950100 +17183000100 +17185957200 +17187870100 +17189950100 +17191954900 +17193958000 +17195000100 +17197880105 +17199020100 +17201000101 +17203030100 +18001030100 +18003000100 +18005010100 +18007100100 +18009975100 +18011810100 +18013974600 +18015959300 +18017950900 +18019050100 +18021040100 +18023950100 +18025951900 +18027954300 +18029080101 +18031969000 +18033020100 +18035000300 +18037953200 +18039000100 +18041954000 +18043070200 +18045957600 +18047960100 +18049953000 +18051050100 +18053000100 +18055954701 +18057110101 +18059410100 +18061060100 +18063210103 +18065975500 +18067000200 +18069961300 +18071967501 +18073100400 +18075962700 +18077966000 +18079960200 +18081610101 +18083955000 +18085960900 +18087970100 +18089010100 +18091040100 +18093950400 +18095000300 +18097310104 +18099020101 +18101950100 +18103952000 +18105000100 +18107956700 +18109510101 +18111100400 +18113971700 +18115965700 +18117951300 +18119955500 +18121030100 +18123952200 +18125953900 +18127050104 +18129040100 +18131958900 +18133956000 +18135951400 +18137968401 +18139974100 +18141000100 +18143966700 +18145710100 +18147952701 +18149953600 +18151970800 +18153050101 +18155965700 +18157000100 +18159020100 +18161960700 +18163000100 +18165020100 +18167000300 +18169102200 +18171951001 +18173030100 +18175967200 +18177000200 +18179040100 +18181958100 +18183050100 +19001960100 +19003950100 +19005960100 +19007950100 +19009070100 +19011960100 +19013000100 +19015020100 +19017004000 +19019950100 +19021960100 +19023070100 +19025950100 +19027960100 +19029190100 +19031450100 +19033950102 +19035080101 +19037070100 +19039960100 +19041080100 +19043070100 +19045000100 +19047070100 +19049050100 +19051080100 +19053960100 +19055950100 +19057000200 +19059450200 +19061000100 +19063070100 +19065080100 +19067480100 +19069360100 +19071970100 +19073080100 +19075960100 +19077950100 +19079960100 +19081270100 +19083480100 +19085290100 +19087970100 +19089960100 +19091970100 +19093090100 +19095960100 +19097950100 +19099040100 +19101090100 +19103000100 +19105070100 +19107080100 +19109950100 +19111490100 +19113000101 +19115450100 +19117950100 +19119950100 +19121060101 +19123950100 +19125030101 +19127950100 +19129040100 +19131560100 +19133960100 +19135070100 +19137960100 +19139050100 +19141490100 +19143460100 +19145490100 +19147960100 +19149970100 +19151780100 +19153000101 +19155021200 +19157370100 +19159950100 +19161080100 +19163010101 +19165960100 +19167070100 +19169000101 +19171290100 +19173180100 +19175190100 +19177950100 +19179960100 +19181020101 +19183960101 +19185070100 +19187000101 +19189680100 +19191950100 +19193000100 +19195690100 +19197680100 +20001952600 +20003953600 +20005081600 +20007968100 +20009971100 +20011955600 +20013480600 +20015020101 +20017960600 +20019964600 +20021958100 +20023950200 +20025967100 +20027458100 +20029977100 +20031966100 +20033967600 +20035493100 +20037956600 +20039951300 +20041084100 +20043020100 +20045000100 +20047969600 +20049965100 +20051072600 +20053086600 +20055960100 +20057961600 +20059954101 +20061000100 +20063955300 +20065952300 +20067963600 +20069962600 +20071958100 +20073965600 +20075958600 +20077961600 +20079030100 +20081463100 +20083461100 +20085082600 +20087020101 +20089576300 +20091050000 +20093959100 +20095961100 +20097969100 +20099950100 +20101956600 +20103070100 +20105086100 +20107955101 +20109954600 +20111000101 +20113788100 +20115489500 +20117040701 +20119966600 +20121100100 +20123176600 +20125950100 +20127963600 +20129964600 +20131480100 +20133951600 +20135956300 +20137951700 +20139010100 +20141474100 +20143085600 +20145970200 +20147475100 +20149000101 +20151968600 +20153950600 +20155000100 +20157978100 +20159967100 +20161000201 +20163974600 +20165972300 +20167973800 +20169000101 +20171957100 +20173000100 +20175965600 +20177000400 +20179952800 +20181453600 +20183475800 +20185470600 +20187964100 +20189965100 +20191962100 +20193953100 +20195955800 +20197483100 +20199954100 +20201978600 +20203957600 +20205097100 +20207096600 +20209040100 +21001970100 +21003920100 +21005950101 +21007950100 +21009950100 +21011970100 +21013960100 +21015070101 +21017030101 +21019030200 +21021930100 +21023950100 +21025920200 +21027960100 +21029020101 +21031930100 +21033920100 +21035010100 +21037050100 +21039960100 +21041950100 +21043960100 +21045950100 +21047200100 +21049020101 +21051950100 +21053970100 +21055930100 +21057950100 +21059000100 +21061920200 +21063920101 +21065920100 +21067000101 +21069920100 +21071920100 +21073070101 +21075960100 +21077960102 +21079970101 +21081920101 +21083020100 +21085950101 +21087930100 +21089040100 +21091960100 +21093000100 +21095970100 +21097950100 +21099970100 +21101020101 +21103090100 +21105970100 +21107970100 +21109960101 +21111000201 +21113060102 +21115960100 +21117060300 +21119960100 +21121930100 +21123960101 +21125970100 +21127930101 +21129950100 +21131920101 +21133950100 +21135930100 +21137920101 +21139040100 +21141960100 +21143960101 +21145030100 +21147960100 +21149970100 +21151010101 +21153970100 +21155970200 +21157950101 +21159950100 +21161960100 +21163970100 +21165960100 +21167960100 +21169960100 +21171930100 +21173920100 +21175950100 +21177960100 +21179930100 +21181960100 +21183920100 +21185030100 +21187970100 +21189930100 +21191930101 +21193970300 +21195930100 +21197970101 +21199930101 +21201970100 +21203950100 +21205950100 +21207960101 +21209040101 +21211040101 +21213970100 +21215080101 +21217920100 +21219950100 +21221970100 +21223100100 +21225950100 +21227010100 +21229930100 +21231920101 +21233960100 +21235920100 +21237930100 +21239050103 +22001960101 +22003950100 +22005030102 +22007050100 +22009030100 +22011960101 +22013970100 +22015010400 +22017020500 +22019000100 +22021000100 +22023970101 +22025000100 +22027950100 +22029000100 +22031950101 +22033000100 +22035000100 +22037951300 +22039950101 +22041950100 +22043020100 +22045030101 +22047952600 +22049970100 +22051020204 +22053000100 +22055000100 +22057020100 +22059970100 +22061960100 +22063040101 +22065960100 +22067950100 +22069000100 +22071000100 +22073000100 +22075050100 +22077951900 +22079010101 +22081960100 +22083970100 +22085000100 +22087030103 +22089060100 +22091951100 +22093040100 +22095070100 +22097960100 +22099020101 +22101040100 +22103040103 +22105953200 +22107000100 +22109000103 +22111960100 +22113950101 +22115950100 +22117950101 +22119031100 +22121020100 +22123000100 +22125951703 +22127960100 +23001010100 +23003950100 +23005000100 +23007970101 +23009965100 +23011010100 +23013970200 +23015975100 +23017965100 +23019000200 +23021960301 +23023970100 +23025965301 +23027041000 +23029955100 +23031005100 +24001000100 +24003701101 +24005400100 +24009860103 +24011955000 +24013501001 +24015030100 +24017850101 +24019970100 +24021740200 +24023000100 +24025301102 +24027601103 +24029950100 +24031700101 +24033800102 +24035810100 +24037875000 +24039930101 +24041960100 +24043000100 +24045000100 +24047950000 +24510010100 +25001010100 +25003900100 +25005600100 +25007200100 +25009201100 +25011040100 +25013800101 +25015820101 +25017300100 +25019950100 +25021400100 +25023500101 +25025000101 +25027700100 +26001000100 +26003000100 +26005030200 +26007000101 +26009960100 +26011970100 +26013000100 +26015010100 +26017280300 +26019000101 +26021000300 +26023950100 +26025000200 +26027001000 +26029000100 +26031960100 +26033970100 +26035000100 +26037010104 +26039960100 +26041970100 +26043950100 +26045020101 +26047970100 +26049000100 +26051000101 +26053950200 +26055550101 +26057000100 +26059050101 +26061000100 +26063950100 +26065000100 +26067030101 +26069000101 +26071000100 +26073000100 +26075000100 +26077000100 +26079950201 +26081000100 +26083000100 +26085960100 +26087330000 +26089970100 +26091060100 +26093710100 +26095960100 +26097950100 +26099206700 +26101000100 +26103000100 +26105950100 +26107960100 +26109960100 +26111290100 +26113960101 +26115830100 +26117970100 +26119910100 +26121000100 +26123970100 +26125120000 +26127010300 +26129950100 +26131970100 +26133970100 +26135970201 +26137950100 +26139020100 +26141950100 +26143970100 +26145000100 +26147620000 +26149040100 +26151970100 +26153000100 +26155030100 +26157000100 +26159010101 +26161400100 +26163500100 +26165380100 +27001770100 +27003050107 +27005450100 +27007450101 +27009020100 +27011950100 +27013170100 +27015960101 +27017070100 +27019090101 +27021940001 +27023950300 +27025110100 +27027020300 +27029000100 +27031480101 +27033270100 +27035950100 +27037060101 +27039950100 +27041450100 +27043460100 +27045960100 +27047180100 +27049080101 +27051070100 +27053000101 +27055020100 +27057070100 +27059130100 +27061480101 +27063480100 +27065480100 +27067770900 +27069090100 +27071790100 +27073180100 +27075370101 +27077460300 +27079950101 +27081201001 +27083360100 +27085950100 +27087940100 +27089080100 +27091790100 +27093560100 +27095170400 +27097780100 +27099000100 +27101900100 +27103480100 +27105105100 +27107960100 +27109000100 +27111960102 +27113090100 +27115950100 +27117460100 +27119020100 +27121970100 +27123030100 +27125010100 +27127750100 +27129790100 +27131070101 +27133570100 +27135970100 +27137000100 +27139080100 +27141030103 +27143170198 +27145000301 +27147960100 +27149480100 +27151960100 +27153790100 +27155460100 +27157490100 +27159480100 +27161790100 +27163070103 +27165950100 +27167950100 +27169670101 +27171100101 +27173970100 +28001000101 +28003950100 +28005950100 +28007060100 +28009950101 +28011950100 +28013950100 +28015950100 +28017950100 +28019950100 +28021950100 +28023950100 +28025950100 +28027950100 +28029950101 +28031950100 +28033070101 +28035000200 +28037950101 +28039950101 +28041950101 +28043950101 +28045030101 +28047000600 +28049000100 +28051950100 +28053950100 +28055950100 +28057950100 +28059040103 +28061950101 +28063950101 +28065950101 +28067950101 +28069030100 +28071950100 +28073020101 +28075000200 +28077960100 +28079040100 +28081950101 +28083950100 +28085950100 +28087000102 +28089030104 +28091950100 +28093950101 +28095950101 +28097950100 +28099010101 +28101050100 +28103950100 +28105950101 +28107950101 +28109950101 +28111950101 +28113950102 +28115950101 +28117950101 +28119950100 +28121020101 +28123020101 +28125950100 +28127950101 +28129950100 +28131020100 +28133950100 +28135950100 +28137950100 +28139950100 +28141950101 +28143950101 +28145950100 +28147950101 +28149950101 +28151000100 +28153950101 +28155950100 +28157950100 +28159950100 +28161950100 +28163950100 +29001950100 +29003010100 +29005950100 +29007950100 +29009960101 +29011960100 +29013070100 +29015460100 +29017950100 +29019000200 +29021000100 +29023950101 +29025950100 +29027070100 +29029950101 +29031880100 +29033960100 +29035960100 +29037060001 +29039870101 +29041470100 +29043020101 +29045950100 +29047020201 +29049960100 +29051010300 +29053950100 +29055450101 +29057480100 +29059480100 +29061470100 +29063080101 +29065960100 +29067950100 +29069360100 +29071800101 +29073960100 +29075960100 +29077000101 +29079960100 +29081950100 +29083950100 +29085470100 +29087960100 +29089960100 +29091090101 +29093950100 +29095000300 +29097010100 +29099700107 +29101960101 +29103960100 +29105960101 +29107090100 +29109470100 +29111970100 +29113810100 +29115490100 +29117480100 +29119070101 +29121960100 +29123960100 +29125880100 +29127960100 +29129470100 +29131962501 +29133950100 +29135385100 +29137960100 +29139970100 +29141470100 +29143960100 +29145020100 +29147470100 +29149480100 +29151490100 +29153470101 +29155470100 +29157470100 +29159480100 +29161890100 +29163460100 +29165030002 +29167960101 +29169470101 +29171960100 +29173470100 +29175490100 +29177080001 +29179380100 +29181870100 +29183310100 +29185480100 +29186960101 +29187950103 +29189210101 +29195090100 +29197470100 +29199480100 +29201780100 +29203470100 +29205450100 +29207470100 +29209090101 +29211480100 +29213480106 +29215480101 +29217950100 +29219820104 +29221460101 +29223690100 +29225470101 +29227960100 +29229490100 +29510101100 +30001000100 +30003000100 +30005000100 +30007000100 +30009000100 +30011000300 +30013000100 +30015010200 +30017961300 +30019020300 +30021000100 +30023000300 +30025000100 +30027030100 +30029000101 +30031000101 +30033000100 +30035940200 +30037000100 +30039961701 +30041040100 +30043962201 +30045000100 +30047000100 +30049000100 +30051050100 +30053000100 +30055954000 +30057000101 +30059000100 +30061964500 +30063000100 +30065000100 +30067000100 +30069000100 +30071060200 +30073977000 +30075000100 +30077000100 +30079000100 +30081000100 +30083070100 +30085080100 +30087000100 +30089000100 +30091090200 +30093000101 +30095966400 +30097967000 +30099000100 +30101000100 +30103963500 +30105100100 +30107000100 +30109000100 +30111000200 +31001965400 +31003979600 +31005958300 +31007954000 +31009972400 +31011960100 +31013951100 +31015975800 +31017975000 +31019968900 +31021963200 +31023967600 +31025965600 +31027977100 +31029961900 +31031955800 +31033954800 +31035962100 +31037964600 +31039972700 +31041971700 +31043010101 +31045950600 +31047968000 +31049955400 +31051977600 +31053963600 +31055000200 +31057962300 +31059091600 +31061964600 +31063961100 +31065963900 +31067964600 +31069952100 +31071973200 +31073967600 +31075956300 +31077970900 +31079000100 +31081969100 +31083964200 +31085961500 +31087962700 +31089974000 +31091956700 +31093970500 +31095963600 +31097967500 +31099966600 +31101000100 +31103975400 +31105954500 +31107976200 +31109000100 +31111959700 +31113957500 +31115972800 +31117957900 +31119960600 +31121966600 +31123952500 +31125966100 +31127968100 +31129960000 +31131966600 +31133967800 +31135959300 +31137967000 +31139979100 +31141965100 +31143960000 +31145963100 +31147964500 +31149974600 +31151960601 +31153010103 +31155968100 +31157952900 +31159960100 +31161951600 +31163970100 +31165950100 +31167962100 +31169963100 +31171957100 +31173940100 +31175971300 +31177050101 +31179978600 +31181965000 +31183973600 +31185969600 +32001950100 +32003000101 +32005001000 +32007950200 +32009950100 +32011000100 +32013010501 +32015000301 +32017950100 +32019960103 +32021970700 +32023960100 +32027960100 +32029970200 +32031000103 +32033970100 +32510000100 +33001965100 +33003955101 +33005970100 +33007950100 +33009960101 +33011000103 +33013003001 +33015003301 +33017080100 +33019975100 +34001000100 +34003001001 +34005700102 +34007600200 +34009020101 +34011010101 +34013000100 +34015500100 +34017000101 +34019010100 +34021000100 +34023000101 +34025800101 +34027040101 +34029710101 +34031116500 +34033020100 +34035050100 +34037371000 +34039030200 +34041030600 +35001000107 +35003976401 +35005000201 +35006941500 +35007950500 +35009000100 +35011960100 +35013000102 +35015000100 +35017964100 +35019961600 +35021000100 +35023970000 +35025000100 +35027960200 +35028000100 +35029000100 +35031940301 +35033955201 +35035000100 +35037958601 +35039000100 +35041000100 +35043010503 +35045000100 +35047957200 +35049000102 +35051962200 +35053940000 +35055940000 +35057963201 +35059950200 +35061940300 +36001000100 +36003940200 +36005000100 +36007000100 +36009940000 +36011040101 +36013030100 +36015000100 +36017970101 +36019100101 +36021000100 +36023970100 +36025970101 +36027010000 +36029000110 +36031960100 +36033940000 +36035970100 +36037940100 +36039080100 +36041950100 +36043010100 +36045060101 +36047000100 +36049950101 +36051030100 +36053030101 +36055000200 +36057070200 +36059300100 +36061000100 +36063020100 +36065020102 +36067000100 +36069050101 +36071000100 +36073040200 +36075020101 +36077590101 +36079010100 +36081000101 +36083040100 +36085000300 +36087010101 +36089490100 +36091060102 +36093020101 +36095740100 +36097950100 +36099950100 +36101960100 +36103110101 +36105950100 +36107020101 +36109000100 +36111950100 +36113070100 +36115080100 +36117020103 +36119000101 +36121970100 +36123150101 +37001020100 +37003040100 +37005950100 +37007920100 +37009970200 +37011930100 +37013930101 +37015960100 +37017950101 +37019020101 +37021000100 +37023020100 +37025040501 +37027030100 +37029950101 +37031970101 +37033930100 +37035010101 +37037020103 +37039930101 +37041930101 +37043950101 +37045950101 +37047930100 +37049960101 +37051000200 +37053110101 +37055970101 +37057060101 +37059080101 +37061090101 +37063000101 +37065020200 +37067000100 +37069060100 +37071030103 +37073970100 +37075920100 +37077970101 +37079950101 +37081010100 +37083930100 +37085070100 +37087920101 +37089930100 +37091950101 +37093970104 +37095920101 +37097060100 +37099940200 +37101040101 +37103920100 +37105030101 +37107010100 +37109070100 +37111970101 +37113970100 +37115010100 +37117970100 +37119000101 +37121950100 +37123960101 +37125950100 +37127010200 +37129010100 +37131920101 +37133000103 +37135010701 +37137950101 +37139960100 +37141920104 +37143920101 +37145920100 +37147000101 +37149920101 +37151030100 +37153970100 +37155960101 +37157040101 +37159050201 +37161960101 +37163970101 +37165010101 +37167930101 +37169070101 +37171930101 +37173940100 +37175960100 +37177960100 +37179020100 +37181960100 +37183050100 +37185950101 +37187950100 +37189920100 +37191000102 +37193960101 +37195000100 +37197050101 +37199960101 +38001965600 +38003967900 +38005940100 +38007963100 +38009952300 +38011965200 +38013953300 +38015010100 +38017000100 +38019950900 +38021973200 +38023954500 +38025962200 +38027959200 +38029966500 +38031959600 +38033962900 +38035010100 +38037965900 +38039968600 +38041964700 +38043966800 +38045972100 +38047972500 +38049955600 +38051972900 +38053940100 +38055940100 +38057961600 +38059020100 +38061940300 +38063959000 +38065961200 +38067950100 +38069956100 +38071957600 +38073968900 +38075952900 +38077970700 +38079941800 +38081974000 +38083960200 +38085940800 +38087965000 +38089963300 +38091968700 +38093967000 +38095951500 +38097970100 +38099957800 +38101010100 +38103959800 +38105953400 +39001770100 +39003010100 +39005970101 +39007000101 +39009972600 +39011040100 +39013010100 +39015951201 +39017000100 +39019720100 +39021010100 +39023000200 +39025040101 +39027964300 +39029950100 +39031960900 +39033974100 +39035101101 +39037500100 +39039958100 +39041010100 +39043040100 +39045030100 +39047925800 +39049000110 +39051040100 +39053953500 +39055310100 +39057200101 +39059977100 +39061000200 +39063000100 +39065000100 +39067975600 +39069000100 +39071954400 +39073964900 +39075976301 +39077915400 +39079957200 +39081000200 +39083006700 +39085200100 +39087050100 +39089750700 +39091003800 +39093010200 +39095000201 +39097040101 +39099800501 +39101000100 +39103400100 +39105964100 +39107967200 +39109300100 +39111966600 +39113000100 +39115968800 +39117965000 +39119911000 +39121968300 +39123050100 +39125960100 +39127965801 +39129020100 +39131952200 +39133600102 +39135400100 +39137030100 +39139000400 +39141955500 +39143960800 +39145002100 +39147962500 +39149971400 +39151700100 +39153501100 +39155920300 +39157020100 +39159050100 +39161020100 +39163953000 +39165030101 +39167020101 +39169000100 +39171950100 +39173020100 +39175938000 +40001376600 +40003955600 +40005587600 +40007951600 +40009966100 +40011958600 +40013795600 +40015161601 +40017300100 +40019892100 +40021977600 +40023966900 +40025950100 +40027200100 +40029388100 +40031000100 +40033871100 +40035373100 +40037020101 +40039950800 +40041375601 +40043959100 +40045952600 +40047000101 +40049681100 +40051000100 +40053956400 +40055967100 +40057967800 +40059952100 +40061279100 +40063484600 +40065968100 +40067371600 +40069660198 +40071000100 +40073958100 +40075963600 +40077087100 +40079040102 +40081961100 +40083600101 +40085094100 +40087400103 +40089098200 +40091779601 +40093955100 +40095094698 +40097040100 +40099790600 +40101000100 +40103956600 +40105172100 +40107080600 +40109100100 +40111000100 +40113940001 +40115574100 +40117957100 +40119010101 +40121485601 +40123088600 +40125500200 +40127097600 +40129960000 +40131050101 +40133583100 +40135030101 +40137000101 +40139950600 +40141070100 +40143000200 +40145030101 +40147000100 +40149964900 +40151954201 +40153953100 +41001950100 +41003000101 +41005020101 +41007950100 +41009970201 +41011000101 +41013950101 +41015950100 +41017000100 +41019010000 +41021960100 +41023960100 +41025960101 +41027950100 +41029000100 +41031940000 +41033360100 +41035970100 +41037960100 +41039000100 +41041950100 +41043020101 +41045940000 +41047000200 +41049970101 +41051000101 +41053005100 +41055950100 +41057960101 +41059940000 +41061970100 +41063960100 +41065970100 +41067030103 +41069960100 +41071030101 +42001030101 +42003010301 +42005950100 +42007600601 +42009960100 +42011000100 +42013010101 +42015950100 +42017100102 +42019902100 +42021000100 +42023960100 +42025020102 +42027010100 +42029300101 +42031160101 +42033330100 +42035030100 +42037050100 +42039110100 +42041010100 +42043020100 +42045400301 +42047950100 +42049000100 +42051260100 +42053530100 +42055010100 +42057960100 +42059970101 +42061950100 +42063960100 +42065950100 +42067070101 +42069100200 +42071000100 +42073000100 +42075000100 +42077000101 +42079200100 +42081000100 +42083420100 +42085030100 +42087960100 +42089300101 +42091200103 +42093050100 +42095010100 +42097080100 +42099030100 +42101000101 +42103950102 +42105950101 +42107000100 +42109070100 +42111020101 +42113960101 +42115032000 +42117950100 +42119090102 +42121200000 +42123970100 +42125704100 +42127960100 +42129800100 +42131400100 +42133000100 +44001030100 +44003020101 +44005040101 +44007000101 +44009041500 +45001950100 +45003020100 +45005970201 +45007000200 +45009960101 +45011970101 +45013000100 +45015020101 +45017950100 +45019000100 +45021970101 +45023020100 +45025950101 +45027960100 +45029970100 +45031010100 +45033970100 +45035010100 +45037970201 +45039960100 +45041000101 +45043920100 +45045000100 +45047970101 +45049920100 +45051010100 +45053950100 +45055970100 +45057010100 +45059920102 +45061920100 +45063020100 +45065920100 +45067950100 +45069960100 +45071950100 +45073030100 +45075010101 +45077010100 +45079000100 +45081960100 +45083020301 +45085000100 +45087030100 +45089970100 +45091060102 +46003973600 +46005956600 +46007941000 +46009967600 +46011958600 +46013951300 +46015973100 +46017940200 +46019967600 +46021964100 +46023940200 +46025955800 +46027965700 +46029954100 +46031941000 +46033965100 +46035962600 +46037952700 +46039953600 +46041941500 +46043969600 +46045962100 +46047964100 +46049961100 +46051953100 +46053971100 +46055960100 +46057955100 +46059975600 +46061964100 +46063968700 +46065977700 +46067968600 +46069976700 +46071941200 +46073974100 +46075091600 +46077958100 +46079960100 +46081966101 +46083010103 +46085940100 +46087964600 +46089963100 +46091950800 +46093020200 +46095940300 +46097961600 +46099000100 +46101959600 +46102941000 +46103010201 +46105968300 +46107000100 +46109940400 +46111962100 +46115000100 +46117960100 +46119979100 +46121940100 +46123971600 +46125965100 +46127020100 +46129965100 +46135966100 +46137941600 +47001020100 +47003950100 +47005963000 +47007953000 +47009010100 +47011010100 +47013950100 +47015960100 +47017962000 +47019070100 +47021070102 +47023970101 +47025970100 +47027955000 +47029920100 +47031970100 +47033961000 +47035970101 +47037010103 +47039955001 +47041920101 +47043060100 +47045964001 +47047060300 +47049965000 +47051960100 +47053966100 +47055920100 +47057500100 +47059090100 +47061955000 +47063100100 +47065000400 +47067960500 +47069950100 +47071920100 +47073050100 +47075930100 +47077975000 +47079969001 +47081950100 +47083120100 +47085130100 +47087960100 +47089070101 +47091956000 +47093000100 +47095960100 +47097050100 +47099960100 +47101970100 +47103975000 +47105060100 +47107970102 +47109930100 +47111970100 +47113000100 +47115050101 +47117955000 +47119010100 +47121960100 +47123925001 +47125100100 +47127930100 +47129110100 +47131965000 +47133950100 +47135930100 +47137925101 +47139950100 +47141000100 +47143975000 +47145030100 +47147080101 +47149040101 +47151975000 +47153060102 +47155080101 +47157000100 +47159975000 +47161110201 +47163040200 +47165020101 +47167040100 +47169090100 +47171080100 +47173040101 +47175925000 +47177930100 +47179060100 +47181950100 +47183968000 +47185935000 +47187050102 +47189030102 +48001950100 +48003950100 +48005000102 +48007950101 +48009020100 +48011950100 +48013960101 +48015760100 +48017950101 +48019000101 +48021950101 +48023950301 +48025950100 +48027020101 +48029110100 +48031950101 +48033950100 +48035950100 +48037010100 +48039660100 +48041000103 +48043950300 +48045950200 +48047950100 +48049950100 +48051970100 +48053960101 +48055960102 +48057000100 +48059030101 +48061010101 +48063950101 +48065950100 +48067950101 +48069950100 +48071710100 +48073950100 +48075950100 +48077030200 +48079950100 +48081950100 +48083950300 +48085030101 +48087950300 +48089750100 +48091310100 +48093950101 +48095950300 +48097000100 +48099010101 +48101950100 +48103950100 +48105950100 +48107950100 +48109950300 +48111950100 +48113000100 +48115950401 +48117950300 +48119950100 +48121020109 +48123970100 +48125950300 +48127950201 +48129950200 +48131950100 +48133950100 +48135000100 +48137950300 +48139060103 +48141000106 +48143950100 +48145000200 +48147950100 +48149970100 +48151950300 +48153950500 +48155950100 +48157670101 +48159950101 +48161000101 +48163950101 +48165950100 +48167720100 +48169950101 +48171950100 +48173950100 +48175960100 +48177000100 +48179950100 +48181000101 +48183000201 +48185180101 +48187210100 +48189950100 +48191950500 +48193950100 +48195950100 +48197950100 +48199030100 +48201100001 +48203020103 +48205950200 +48207950300 +48209010100 +48211950300 +48213950101 +48215020103 +48217960100 +48219950100 +48221160100 +48223950100 +48225950101 +48227950100 +48229950300 +48231960100 +48233950200 +48235950100 +48237950100 +48239950100 +48241950101 +48243950100 +48245000101 +48247950200 +48249950101 +48251130100 +48253020101 +48255970100 +48257050204 +48259970101 +48261950100 +48263950100 +48265960101 +48267950100 +48269950100 +48271950100 +48273020101 +48275950100 +48277000101 +48279950100 +48281950100 +48283950301 +48285000100 +48287000100 +48289950101 +48291700100 +48293970100 +48295950200 +48297950100 +48299970100 +48301950100 +48303000100 +48305950400 +48307950300 +48309000100 +48311950100 +48313000100 +48315950100 +48317950100 +48319950100 +48321730100 +48323950204 +48325000101 +48327950300 +48329000100 +48331950100 +48333950100 +48335950200 +48337950100 +48339690101 +48341950100 +48343950100 +48345950100 +48347950100 +48349970100 +48351950100 +48353950100 +48355000500 +48357950100 +48359950100 +48361020200 +48363000100 +48365950100 +48367140101 +48369950200 +48371950100 +48373210103 +48375010100 +48377950100 +48379950101 +48381020100 +48383950100 +48385950100 +48387950100 +48389950200 +48391950200 +48393950100 +48395960100 +48397040101 +48399950100 +48401950101 +48403950100 +48405950100 +48407200103 +48409010201 +48411950100 +48413950300 +48415950100 +48417950300 +48419950100 +48421950200 +48423000100 +48425000101 +48427950101 +48429950200 +48431950100 +48433950300 +48435950300 +48437950200 +48439100101 +48441010100 +48443950100 +48445950100 +48447950300 +48449950100 +48451000100 +48453000101 +48455950100 +48457950100 +48459950101 +48461950100 +48463950100 +48465950202 +48467950100 +48469000100 +48471790101 +48473680100 +48475950100 +48477170100 +48479000101 +48481740100 +48483950100 +48485010100 +48487950300 +48489950300 +48491020106 +48493000102 +48495950200 +48497150101 +48499950100 +48501950100 +48503950200 +48505950303 +48507950100 +49001100100 +49003960100 +49005000101 +49007000100 +49009960100 +49011125102 +49013940300 +49015976200 +49017000300 +49019000200 +49021110100 +49023010100 +49025130100 +49027974100 +49029970101 +49031960100 +49033950100 +49035100100 +49037942000 +49039972101 +49041975100 +49043964101 +49045130600 +49047940201 +49049000102 +49051940501 +49053270101 +49055979100 +49057200100 +50001960100 +50003970401 +50005957000 +50007000100 +50009950100 +50011010101 +50013020100 +50015953000 +50017959000 +50019951100 +50021962100 +50023954000 +50025967000 +50027965000 +51001090101 +51003010100 +51005070100 +51007930101 +51009010100 +51011040101 +51013100100 +51015070100 +51017920101 +51019030101 +51021040100 +51023040100 +51025930100 +51027010100 +51029930101 +51031020101 +51033030100 +51035080100 +51036600100 +51037930101 +51041100106 +51043010101 +51045050100 +51047930101 +51049930101 +51051040100 +51053840101 +51057950600 +51059415100 +51061930101 +51063920102 +51065020101 +51067020102 +51069050100 +51071930100 +51073100101 +51075400101 +51077060101 +51079030101 +51081880101 +51083930100 +51085320100 +51087200106 +51089010100 +51091970100 +51093280101 +51095080102 +51097950400 +51099040100 +51101950101 +51103030100 +51105950100 +51107610101 +51109950101 +51111930100 +51113930101 +51115951301 +51117930101 +51119950900 +51121020100 +51125950101 +51127700100 +51131930100 +51133020100 +51135000101 +51137110104 +51139030100 +51141030101 +51143010100 +51145500101 +51147930101 +51149850100 +51153900100 +51155210101 +51157950100 +51159040100 +51161030100 +51163930101 +51165010100 +51167030100 +51169030100 +51171040101 +51173030100 +51175200100 +51177020104 +51179010103 +51181860100 +51183870100 +51185020100 +51187020101 +51191010101 +51193010101 +51195930700 +51197050101 +51199050203 +51510200102 +51520020100 +51530930601 +51540000201 +51550020001 +51570830100 +51580060100 +51590000100 +51595890100 +51600300100 +51610500100 +51620090100 +51630000100 +51640070101 +51650010103 +51660000101 +51670820100 +51678930501 +51680000100 +51683910100 +51685920100 +51690000100 +51700030100 +51710000100 +51720960100 +51730810100 +51735340100 +51740210200 +51750010101 +51760010201 +51770000100 +51775010100 +51790000100 +51800065100 +51810040000 +51820003100 +51830370100 +51840000101 +53001950100 +53003960100 +53005010100 +53007960100 +53009000300 +53011040101 +53013960200 +53015000400 +53017950101 +53019940000 +53021020101 +53023970300 +53025010100 +53027000201 +53029970100 +53031950202 +53033000101 +53035080101 +53037975101 +53039950101 +53041970100 +53043960100 +53045940000 +53047940100 +53049950200 +53051970100 +53053060200 +53055960101 +53057940201 +53059950100 +53061040100 +53063000201 +53065941000 +53067010100 +53069950100 +53071920000 +53073000101 +53075000100 +53077000100 +54001965500 +54003971101 +54005958200 +54007967900 +54009031102 +54011000101 +54013962600 +54015957900 +54017965000 +54019020101 +54021967701 +54023969400 +54025950101 +54027968201 +54029020600 +54031970101 +54033030100 +54035963200 +54037972201 +54039000100 +54041967200 +54043955401 +54045956102 +54047953600 +54049020100 +54051020200 +54053954801 +54055000900 +54057010100 +54059957100 +54061010101 +54063950100 +54065970701 +54067950100 +54069000200 +54071970400 +54073962100 +54075960101 +54077963801 +54079020100 +54081000200 +54083965900 +54085962300 +54087962800 +54089000500 +54091964600 +54093965200 +54095961800 +54097966600 +54099005100 +54101970101 +54103004900 +54105030101 +54107000100 +54109002800 +55001950100 +55003940000 +55005000100 +55007960100 +55009000100 +55011960100 +55013970400 +55015020100 +55017010100 +55019950100 +55021970100 +55023960100 +55025000100 +55027960100 +55029100100 +55031020300 +55033970100 +55035000100 +55037190101 +55039040100 +55041950100 +55043960100 +55045960100 +55047100100 +55049950100 +55051180100 +55053960100 +55055100100 +55057100100 +55059000101 +55061960100 +55063000100 +55065970100 +55067960101 +55069960100 +55071000100 +55073000100 +55075960100 +55077960100 +55078940101 +55079000101 +55081950100 +55083100300 +55085970101 +55087010100 +55089610101 +55091950100 +55093960100 +55095960100 +55097960100 +55099970100 +55101000200 +55103970100 +55105000100 +55107960100 +55109120100 +55111000101 +55113100300 +55115100100 +55117000100 +55119960100 +55121100100 +55123960100 +55125940000 +55127000102 +55129950100 +55131400102 +55133200101 +55135100100 +55137960100 +55139000100 +55141010100 +56001962700 +56003962600 +56005000101 +56007967600 +56009956400 +56011950200 +56013000100 +56015957700 +56017967800 +56019955100 +56021000200 +56023978001 +56025000200 +56027957200 +56029965100 +56031959100 +56033000100 +56035000101 +56037970500 +56039967601 +56041975201 +56043000200 +56045951100 diff --git a/packages/microcosm-build/tests/fixtures/us_county_vintage_tract_support.csv.provenance.json b/packages/microcosm-build/tests/fixtures/us_county_vintage_tract_support.csv.provenance.json new file mode 100644 index 0000000000..e5dea42c8a --- /dev/null +++ b/packages/microcosm-build/tests/fixtures/us_county_vintage_tract_support.csv.provenance.json @@ -0,0 +1,32 @@ +{ + "block_vintage": "2020_tabulation_blocks", + "county_vintage": "2020", + "diagnostics": { + "connecticut_tract_count": 880, + "fixture_tract_count": 4015, + "legacy_county_count": 3143, + "source_50_states_dc_tract_count": 84414, + "state_count": 51 + }, + "kind": "us_county_vintage_tract_support_fixture", + "limitation": "Complete county support coverage; this fixture does not certify an actual full ladder NPZ artifact.", + "output_sha256": "b0fdf899c37ecb9431df1cbd0bc428192f9a59d35fb4592ffc638995de256c32", + "schema_version": 1, + "selection": { + "connecticut": "Every source tract represented by town-defined 2020 BAF blocks in the packaged CT tract crosswalk.", + "excluded_connecticut_reason": "The three tracts contain only undefined-town water blocks, have zero population, and cannot host a household.", + "excluded_connecticut_tracts": [ + "09007990100", + "09009990000", + "09011990100" + ], + "non_connecticut": "Lexicographically first real 2020 tract for each 2020 county-equivalent." + }, + "source_files": { + "tract_to_puma": { + "sha256": "5262f460b2c8dbe86b00549e916317b1791fdc595c9af72ab18f6f6a67040531", + "url": "https://www2.census.gov/geo/docs/maps-data/data/rel2020/2020_Census_Tract_to_2020_PUMA.txt" + } + }, + "state_scope": "50 states and District of Columbia" +} diff --git a/tools/build_us_county_vintage_crosswalk.py b/tools/build_us_county_vintage_crosswalk.py new file mode 100644 index 0000000000..011457e087 --- /dev/null +++ b/tools/build_us_county_vintage_crosswalk.py @@ -0,0 +1,182 @@ +"""Build the compact 2020 CT tract → 2023 county map from cached Census sources. + +Reuses the BAF/crosswalk parsers and source URLs used by the block ladder's +Connecticut CBSA join (microcosm#1072). This reads CT source files only and +never builds or publishes a ladder. For example:: + + uv run python tools/build_us_county_vintage_crosswalk.py \ + --cache-dir ~/.cache/populace-us-geography \ + --out-dir packages/microcosm-build/src/microcosm/build/us_runtime/data +""" + +from __future__ import annotations + +import argparse +import csv +import hashlib +import io +import json +import zipfile +from collections import defaultdict +from pathlib import Path + +from build_us_block_ladder_artifact import ( + BAF2020_URL_TEMPLATE, + CT_PLANNING_REGION_CROSSWALK_URL, + PL94171_URL_TEMPLATE, +) + +from microcosm.build.us_runtime.block_ladder_sources import ( + ct_planning_region_by_block, + parse_baf_county_subdivision_file, + parse_ct_planning_region_crosswalk, + parse_pl_geo_blocks, +) +from microcosm.build.us_runtime.county_vintage import ( + COUNTY_FIPS_2023_COLUMN, + COUNTY_VINTAGE_PROVENANCE_RESOURCE, + CT_TOWN_COUNTY_CROSSWALK_RESOURCE, + CT_TRACT_COUNTY_CROSSWALK_RESOURCE, +) + + +def build_crosswalk(cache_dir: Path, out_dir: Path) -> dict: + """Derive and write the crosswalk, source copy and provenance from CT inputs.""" + + baf_path = cache_dir / "BlockAssign_ST09_CT.zip" + town_path = cache_dir / CT_TOWN_COUNTY_CROSSWALK_RESOURCE + pl_path = cache_dir / "ct2020.pl.zip" + baf_member = "BlockAssign_ST09_CT_MCD.txt" + with zipfile.ZipFile(baf_path) as archive: + baf_bytes = archive.read(baf_member) + town_bytes = town_path.read_bytes() + towns = parse_ct_planning_region_crosswalk( + town_bytes.decode("utf-8-sig").splitlines(keepends=True) + ) + cousub_by_block = parse_baf_county_subdivision_file( + baf_bytes.decode("latin-1").splitlines(), label=baf_member + ) + with zipfile.ZipFile(pl_path) as archive: + population = parse_pl_geo_blocks( + archive.read("ctgeo2020.pl").decode("latin-1").splitlines(), + state_fips="09", + ) + # The maintained join refuses a populated block without a defined town. + populated_regions = ct_planning_region_by_block( + population, + cousub_by_block=cousub_by_block, + planning_region_by_cousub=towns, + ) + defined_blocks = { + block: town for block, town in cousub_by_block.items() if town in towns + } + regions = ct_planning_region_by_block( + defined_blocks, + cousub_by_block=defined_blocks, + planning_region_by_cousub=towns, + ) + by_tract: dict[str, set[str]] = defaultdict(set) + for block, region in regions.items(): + by_tract[f"{block:015d}"[:11]].add(region) + ambiguous = { + tract: sorted(values) for tract, values in by_tract.items() if len(values) != 1 + } + if ambiguous: + raise ValueError( + f"Connecticut tracts span multiple planning regions: {ambiguous}" + ) + old_totals: dict[str, int] = defaultdict(int) + new_totals: dict[str, int] = defaultdict(int) + tract_population: dict[str, int] = defaultdict(int) + for block, count in population.items(): + old_totals[f"{block:015d}"[:5]] += count + new_totals[populated_regions[block]] += count + tract_population[f"{block:015d}"[:11]] += count + if sum(old_totals.values()) != sum(new_totals.values()): + raise ValueError( + "Connecticut county translation failed population conservation." + ) + output = io.StringIO(newline="") + writer = csv.writer(output, lineterminator="\n") + writer.writerow(["tract_geoid", COUNTY_FIPS_2023_COLUMN, "population_2020"]) + writer.writerows( + (tract, next(iter(values)), tract_population[tract]) + for tract, values in sorted(by_tract.items()) + ) + payload = output.getvalue().encode("utf-8") + provenance = { + "schema_version": 1, + "artifact": CT_TRACT_COUNTY_CROSSWALK_RESOURCE, + "output_sha256": hashlib.sha256(payload).hexdigest(), + "source_vintage": "2020_tabulation_blocks", + "target_vintage": "2023_county_equivalents", + "method": ( + "Reuse microcosm#1072's exact 2020 BAF MCD block-to-town join to the " + "Census CT 2022 planning-region crosswalk. Assert every town-defined " + "2020 tract belongs to exactly one region, then retain that compact " + "tract mapping. The 2023 county-equivalents use those nine regions; " + "non-Connecticut blocks retain their original county prefix." + ), + "source_files": { + "baf2020_ct": { + "url": BAF2020_URL_TEMPLATE.format(fips="09", usps="CT"), + "sha256": hashlib.sha256(baf_path.read_bytes()).hexdigest(), + "member": baf_member, + "member_sha256": hashlib.sha256(baf_bytes).hexdigest(), + }, + "ct_planning_region_crosswalk": { + "url": CT_PLANNING_REGION_CROSSWALK_URL, + "sha256": hashlib.sha256(town_bytes).hexdigest(), + }, + "pl94171_ct": { + "url": PL94171_URL_TEMPLATE.format( + dirname="Connecticut", usps_lower="ct" + ), + "sha256": hashlib.sha256(pl_path.read_bytes()).hexdigest(), + }, + }, + "coverage": { + "towns": len(towns), + "tracts": len(by_tract), + "planning_regions": sorted(set(towns.values())), + "town_defined_blocks": len(regions), + "populated_blocks": len(population), + "ambiguous_tracts": len(ambiguous), + }, + "population_conservation": { + "source_population": sum(old_totals.values()), + "target_population": sum(new_totals.values()), + "source_county_population": dict(sorted(old_totals.items())), + "target_county_population": dict(sorted(new_totals.items())), + }, + "excluded_zero_population_blocks": [ + f"{block:015d}" + for block in sorted(set(cousub_by_block) - set(defined_blocks)) + ], + "excluded_blocks_reason": ( + "County subdivisions not defined (coastal water). No populated " + "2020 block has this town code; the block mapping refuses these " + "blocks even if their tract contains town-defined blocks." + ), + "generator": "tools/build_us_county_vintage_crosswalk.py", + } + out_dir.mkdir(parents=True, exist_ok=True) + (out_dir / CT_TRACT_COUNTY_CROSSWALK_RESOURCE).write_bytes(payload) + (out_dir / CT_TOWN_COUNTY_CROSSWALK_RESOURCE).write_bytes(town_bytes) + (out_dir / COUNTY_VINTAGE_PROVENANCE_RESOURCE).write_text( + json.dumps(provenance, indent=2, sort_keys=True) + "\n" + ) + return provenance + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--cache-dir", type=Path, required=True) + parser.add_argument("--out-dir", type=Path, required=True) + args = parser.parse_args() + provenance = build_crosswalk(args.cache_dir, args.out_dir) + print(json.dumps(provenance["coverage"], sort_keys=True)) + + +if __name__ == "__main__": + main() From 51af907f4aea44413675e1e589167ac59d589818 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 23 Oct 2026 21:28:42 -0400 Subject: [PATCH 03/10] feat: assign and validate 2023 counties in both US ladders --- changelog.d/us-county-2023-geography.added.md | 5 + .../build/us_runtime/geography_ladder.py | 20 ++- .../build/us_runtime/puma_county_vintage.py | 55 +++++++ .../microcosm/build/us_runtime/puma_ladder.py | 30 +++- .../us/test_us_county_vintage_gate.py | 74 ++++++++++ .../us/test_us_county_vintage_ladders.py | 135 ++++++++++++++++++ .../us/test_us_geography_ladder.py | 1 + .../engine_free/us/test_us_l0_refit_export.py | 1 + .../us/test_us_puma_county_vintage.py | 90 ++++++++++++ .../engine_free/us/test_us_puma_ladder.py | 1 + .../us/test_us_stored_input_register.py | 5 +- .../src/microcosm/data/stored_inputs.py | 7 + 12 files changed, 420 insertions(+), 4 deletions(-) create mode 100644 changelog.d/us-county-2023-geography.added.md create mode 100644 packages/microcosm-build/src/microcosm/build/us_runtime/puma_county_vintage.py create mode 100644 packages/microcosm-build/tests/engine_free/us/test_us_county_vintage_gate.py create mode 100644 packages/microcosm-build/tests/engine_free/us/test_us_county_vintage_ladders.py create mode 100644 packages/microcosm-build/tests/engine_free/us/test_us_puma_county_vintage.py diff --git a/changelog.d/us-county-2023-geography.added.md b/changelog.d/us-county-2023-geography.added.md new file mode 100644 index 0000000000..c99b08b80e --- /dev/null +++ b/changelog.d/us-county-2023-geography.added.md @@ -0,0 +1,5 @@ +US block and PUMA household ladders now carry `county_fips_2023` for county +calibration on the 3,144-area 2023 geography, including Connecticut's nine +planning regions. The existing `county_fips` remains on the locked engine's +2020 geography. Packaged Census crosswalks carry source digests and population +conservation evidence, and geography gates validate the additional column. diff --git a/packages/microcosm-build/src/microcosm/build/us_runtime/geography_ladder.py b/packages/microcosm-build/src/microcosm/build/us_runtime/geography_ladder.py index d893dae7d4..3237643d07 100644 --- a/packages/microcosm-build/src/microcosm/build/us_runtime/geography_ladder.py +++ b/packages/microcosm-build/src/microcosm/build/us_runtime/geography_ladder.py @@ -9,6 +9,10 @@ carried in the ladder artifact. One national dataset, filter by geography, at any grain (microcosm #275; no per-area files, the standing rule). +``county_fips_2023`` separately supplies calibration geography, mapping +Connecticut to its nine planning regions through the packaged Census town +crosswalk. ``county_fips`` retains the 2020 prefix required by the engine. + Vintage discipline follows the country-spec geography-spine schema (``vintage_policy: "error"``): the ladder artifact records one vintage per derived layer, the loader refuses an artifact that omits any of them, and the @@ -39,6 +43,11 @@ from microcosm.build.us_runtime.congressional_district_geography import ( CONGRESSIONAL_DISTRICT_GEOID_COLUMN, ) +from microcosm.build.us_runtime.county_vintage import ( + COUNTY_FIPS_2023_COLUMN, + county_fips_2023_assignment_failures, + county_fips_2023_from_blocks, +) from microcosm.frame import Frame #: Derived (crosswalk-sourced) layers the ladder artifact must carry a @@ -53,11 +62,12 @@ ) #: Household spine columns the ladder assignment writes, in write order. -#: Every name is a policyengine-us household input variable. +#: The 2023 county is calibration metadata; other names are engine inputs. US_GEOGRAPHY_LADDER_COLUMNS = ( "block_geoid", "tract_geoid", "county_fips", + COUNTY_FIPS_2023_COLUMN, "place_fips", "sldu", "sldl", @@ -359,6 +369,7 @@ def assign_us_geography_ladder( assigned["block_geoid"] = block_str assigned["tract_geoid"] = np.array([value[:11] for value in block_str.tolist()]) assigned["county_fips"] = np.array([value[:5] for value in block_str.tolist()]) + assigned[COUNTY_FIPS_2023_COLUMN] = county_fips_2023_from_blocks(block_values) assigned["place_fips"] = _fips_strings(ladder.place_fips[assigned_index], width=5) assigned["sldu"] = ladder.sldu[assigned_index].astype(object) assigned["sldl"] = ladder.sldl[assigned_index].astype(object) @@ -500,6 +511,13 @@ def us_geography_ladder_gate( non_text = non_text_county_fips_failure( household["county_fips"], column="county_fips" ) + failures.extend( + county_fips_2023_assignment_failures( + household[COUNTY_FIPS_2023_COLUMN], + household["county_fips"], + blocks=household["block_geoid"], + ) + ) if non_text is not None: failures.append(non_text) block = household["block_geoid"].astype(str).to_numpy() diff --git a/packages/microcosm-build/src/microcosm/build/us_runtime/puma_county_vintage.py b/packages/microcosm-build/src/microcosm/build/us_runtime/puma_county_vintage.py new file mode 100644 index 0000000000..a10caaa6da --- /dev/null +++ b/packages/microcosm-build/src/microcosm/build/us_runtime/puma_county_vintage.py @@ -0,0 +1,55 @@ +"""Add 2023 county geography after the PUMA ladder's original draws.""" + +from __future__ import annotations + +import numpy as np + +from microcosm.build.us_runtime.block_ladder_sources import CT_STATE_FIPS +from microcosm.build.us_runtime.county_vintage import county_fips_2023_from_tracts +from microcosm.build.us_runtime.puma_ladder_sources import COUNTY_FROM_TRACT_DIVISOR + + +def county_fips_2023_for_puma_assignment( + row_puma: np.ndarray, + county_values: np.ndarray, + *, + tract_values: np.ndarray | None, + tract_overlap: tuple[np.ndarray, np.ndarray, np.ndarray], + rng: np.random.Generator, +) -> np.ndarray: + """Resolve the county vintage without changing an assigned legacy county. + + An assigned tract determines its planning region directly. Without a + stored tract, Connecticut rows draw one conditionally on their PUMA and + already assigned 2020 county, using the existing tract population overlap. + Call this after the ladder's original draws so those inputs keep their + existing seed behavior. Other states consume no additional random draws. + """ + + if tract_values is not None: + return county_fips_2023_from_tracts(tract_values) + + result = np.asarray([f"{value:05d}" for value in county_values], dtype=object) + ct_mask = county_values // 1000 == int(CT_STATE_FIPS) + if not ct_mask.any(): + return result + + tract_puma, tract_geoid, tract_population = tract_overlap + tract_county = tract_geoid // COUNTY_FROM_TRACT_DIVISOR + ct_pairs = np.unique( + np.column_stack((row_puma[ct_mask], county_values[ct_mask])), axis=0 + ) + for puma, county in ct_pairs: + rows = np.flatnonzero(ct_mask & (row_puma == puma) & (county_values == county)) + support = (tract_puma == puma) & (tract_county == county) + if not support.any(): + raise ValueError( + "No tract overlap for Connecticut PUMA " + f"{puma:07d} and assigned 2020 county {county:05d}." + ) + populations = tract_population[support] + sampled = rng.choice( + tract_geoid[support], size=len(rows), p=populations / populations.sum() + ) + result[rows] = county_fips_2023_from_tracts(sampled) + return result diff --git a/packages/microcosm-build/src/microcosm/build/us_runtime/puma_ladder.py b/packages/microcosm-build/src/microcosm/build/us_runtime/puma_ladder.py index 018964ad64..fce85688f1 100644 --- a/packages/microcosm-build/src/microcosm/build/us_runtime/puma_ladder.py +++ b/packages/microcosm-build/src/microcosm/build/us_runtime/puma_ladder.py @@ -24,6 +24,10 @@ state / congressional district / county for local analysis because every record carries them. +``county_fips_2023`` separately supplies calibration geography. Connecticut +uses the assigned tract's planning region, or a further tract draw conditional +on the already assigned PUMA and 2020 county when the tract rung is omitted. + Vintage discipline follows the country-spec geography-spine schema (``vintage_policy: "error"``): the artifact records one vintage per derived layer, the loader refuses an artifact that omits any of them, and the @@ -55,10 +59,17 @@ from microcosm.build.us_runtime.congressional_district_geography import ( CONGRESSIONAL_DISTRICT_GEOID_COLUMN, ) +from microcosm.build.us_runtime.county_vintage import ( + COUNTY_FIPS_2023_COLUMN, + county_fips_2023_assignment_failures, +) from microcosm.build.us_runtime.geography_ladder import ( US_NYC_COUNTY_FIPS, non_text_county_fips_failure, ) +from microcosm.build.us_runtime.puma_county_vintage import ( + county_fips_2023_for_puma_assignment, +) from microcosm.build.us_runtime.puma_ladder_sources import ( COUNTY_FROM_TRACT_DIVISOR, PUMA_GEOID_STATE_DIVISOR, @@ -73,12 +84,13 @@ ) #: Household spine columns the launch assignment writes (state / CD / county -#: filtering surface), in write order. ``puma`` is the anchor; the other two -#: are policyengine-us household inputs. +#: filtering surface), in write order. ``puma`` is the anchor; district and +#: legacy county are engine inputs, and the 2023 county is calibration metadata. US_PUMA_LADDER_COLUMNS = ( "puma", CONGRESSIONAL_DISTRICT_GEOID_COLUMN, "county_fips", + COUNTY_FIPS_2023_COLUMN, ) #: The finer tract rung, written only when ``assign_tract=True``. @@ -379,6 +391,13 @@ def assign_us_puma_ladder( assigned["county_fips"] = np.array( [f"{value:05d}" for value in county_values.tolist()], dtype=object ) + assigned[COUNTY_FIPS_2023_COLUMN] = county_fips_2023_for_puma_assignment( + row_puma, + county_values, + tract_values=tract_values, + tract_overlap=ladder._overlap("tract"), + rng=rng, + ) if tract_values is not None: assigned[US_PUMA_LADDER_TRACT_COLUMN] = np.array( [f"{value:011d}" for value in tract_values.tolist()], dtype=object @@ -517,6 +536,13 @@ def us_puma_ladder_gate( non_text = non_text_county_fips_failure( household["county_fips"], column="county_fips" ) + failures.extend( + county_fips_2023_assignment_failures( + household[COUNTY_FIPS_2023_COLUMN], + household["county_fips"], + tracts=household[US_PUMA_LADDER_TRACT_COLUMN] if assign_tract else None, + ) + ) if non_text is not None: failures.append(non_text) puma = household["puma"].astype(str).to_numpy() diff --git a/packages/microcosm-build/tests/engine_free/us/test_us_county_vintage_gate.py b/packages/microcosm-build/tests/engine_free/us/test_us_county_vintage_gate.py new file mode 100644 index 0000000000..cc529fd78e --- /dev/null +++ b/packages/microcosm-build/tests/engine_free/us/test_us_county_vintage_gate.py @@ -0,0 +1,74 @@ +"""Stored 2023 county metadata must remain complete and geographically valid.""" + +import numpy as np +import pytest + +from microcosm.build.us_runtime.county_vintage import ( + county_fips_2023_assignment_failures, +) + + +@pytest.mark.parametrize( + "source", + [ + {}, + {"blocks": ["090010701001000", "360610001001000"]}, + {"tracts": ["09001070100", "36061000100"]}, + ], +) +def test_valid_assignment_passes_with_or_without_source_geography(source): + assert ( + county_fips_2023_assignment_failures( + ["09120", np.str_("36061")], ["09001", "36061"], **source + ) + == () + ) + + +@pytest.mark.parametrize( + "invalid", [None, np.nan, 9120, b"09120", "", "0912", "invalid"] +) +def test_gate_refuses_absent_or_non_text_counties(invalid): + failures = county_fips_2023_assignment_failures([invalid], ["09001"]) + assert any("five-digit text" in failure for failure in failures) + + +@pytest.mark.parametrize("invalid", ["09001", "09100", "09200", "36061"]) +def test_ct_rows_must_name_a_current_planning_region(invalid): + failures = county_fips_2023_assignment_failures([invalid], ["09001"]) + assert any("planning region" in failure for failure in failures) + + +def test_non_ct_rows_must_preserve_the_legacy_county(): + failures = county_fips_2023_assignment_failures(["36001"], ["36061"]) + assert any("non-CT" in failure for failure in failures) + + +@pytest.mark.parametrize( + "source", [{"blocks": ["090010701001000"]}, {"tracts": ["09001070100"]}] +) +def test_a_valid_but_wrong_ct_region_fails_the_source_join(source): + failures = county_fips_2023_assignment_failures(["09190"], ["09001"], **source) + assert any("disagree" in failure for failure in failures) + + +@pytest.mark.parametrize( + "source", [{"blocks": ["090019999991000"]}, {"tracts": ["09001999999"]}] +) +def test_unknown_source_geography_becomes_a_gate_failure(source): + failures = county_fips_2023_assignment_failures(["09120"], ["09001"], **source) + assert any("cannot resolve" in failure for failure in failures) + + +@pytest.mark.parametrize("counties", ["09120", None, [["09120"]]]) +def test_gate_refuses_non_vector_metadata(counties): + assert county_fips_2023_assignment_failures(counties, ["09001"]) + + +def test_gate_refuses_mismatched_vector_lengths(): + assert county_fips_2023_assignment_failures([], ["09001"]) + assert county_fips_2023_assignment_failures(["09120"], ["09001"], tracts=[]) + + +def test_empty_assignment_passes(): + assert county_fips_2023_assignment_failures([], [], blocks=[]) == () diff --git a/packages/microcosm-build/tests/engine_free/us/test_us_county_vintage_ladders.py b/packages/microcosm-build/tests/engine_free/us/test_us_county_vintage_ladders.py new file mode 100644 index 0000000000..8045d11b4b --- /dev/null +++ b/packages/microcosm-build/tests/engine_free/us/test_us_county_vintage_ladders.py @@ -0,0 +1,135 @@ +"""Both household ladders carry separate engine and calibration counties.""" + +import numpy as np +import pandas as pd +import pytest + +from microcosm.build.us_runtime.county_vintage import ( + COUNTY_FIPS_2023_COLUMN, + county_fips_2023_from_blocks, + county_fips_2023_from_tracts, + load_ct_planning_region_by_tract, +) +from microcosm.build.us_runtime.geography_ladder import ( + UsBlockLadder, + assign_us_geography_ladder, +) +from microcosm.build.us_runtime.puma_ladder import ( + UsPumaLadder, + assign_us_puma_ladder, +) +from microcosm.data.stored_inputs import undefined_stored_inputs + + +def _puma_ladder() -> UsPumaLadder: + hartford = int( + next(t for t in load_ct_planning_region_by_tract() if t.startswith("09003")) + ) + return UsPumaLadder( + puma=np.array([600100, 900100]), + puma_population=np.array([2.0, 6.0]), + cd_overlap_puma=np.array([600100, 900100]), + cd_overlap_cd=np.array([601, 901]), + cd_overlap_population=np.array([2.0, 6.0]), + county_overlap_puma=np.array([600100, 900100, 900100]), + county_overlap_county=np.array([6037, 9001, 9003]), + county_overlap_population=np.array([2.0, 4.0, 2.0]), + tract_overlap_puma=np.array([600100, 900100, 900100, 900100]), + tract_overlap_tract=np.array([6037000100, 9001070100, 9001110100, hartford]), + tract_overlap_population=np.array([2.0, 1.0, 3.0, 2.0]), + metadata={}, + ) + + +@pytest.mark.parametrize("assign_tract", [False, True]) +def test_puma_assignment_carries_complete_counties_and_conserves_mass( + assign_tract, +) -> None: + household = pd.DataFrame( + { + "household_id": np.arange(600), + "state_fips": [6] * 100 + [9] * 500, + "puma": [600100] * 100 + [900100] * 500, + "population_weight": np.arange(1, 601), + }, + index=np.arange(600) + 1000, + ) + assigned = assign_us_puma_ladder( + household, _puma_ladder(), seed=7, assign_tract=assign_tract + ) + + assert not assigned[COUNTY_FIPS_2023_COLUMN].isna().any() + assert assigned.index.equals(household.index) + pd.testing.assert_series_equal( + assigned["population_weight"], household["population_weight"] + ) + non_ct = assigned["state_fips"] != 9 + assert ( + assigned.loc[non_ct, COUNTY_FIPS_2023_COLUMN] + == assigned.loc[non_ct, "county_fips"] + ).all() + assert set(assigned.loc[~non_ct, "county_fips"]) == {"09001", "09003"} + ct = assigned.loc[~non_ct] + assert ( + ct.groupby("county_fips")["population_weight"].sum().sum() + == ct.groupby(COUNTY_FIPS_2023_COLUMN)["population_weight"].sum().sum() + == household.loc[~non_ct, "population_weight"].sum() + ) + if assign_tract: + np.testing.assert_array_equal( + assigned[COUNTY_FIPS_2023_COLUMN], + county_fips_2023_from_tracts(assigned["tract_geoid"]), + ) + assert (assigned["county_fips"] == assigned["tract_geoid"].str[:5]).all() + else: + assert "tract_geoid" not in assigned + # The planning region must respect the drawn legacy county as well + # as the PUMA: Hartford support cannot leak into Fairfield rows. + fairfield = ct["county_fips"] == "09001" + assert set(ct.loc[fairfield, COUNTY_FIPS_2023_COLUMN]) == {"09120", "09140"} + + +def test_block_assignment_derives_2023_county_without_changing_2020_ids() -> None: + blocks = np.array([90010701001000, 90011101001000, 90010201011000, 60370001001000]) + ladder = UsBlockLadder( + block_geoid=blocks, + population=np.ones(4), + congressional_district_geoid=np.array([901, 902, 903, 601]), + sldu=np.array(["001"] * 4), + sldl=np.array(["001"] * 4), + place_fips=np.ones(4, dtype=int), + cbsa_code=np.ones(4, dtype=int), + metadata={}, + ) + household = pd.DataFrame( + { + "household_id": [1, 2, 3, 4], + "state_fips": [9, 9, 9, 6], + "congressional_district_geoid": [901, 902, 903, 601], + "population_weight": [30, 20, 10, 40], + } + ) + assigned = assign_us_geography_ladder(household, ladder, seed=3) + + assert assigned["county_fips"].tolist() == ["09001"] * 3 + ["06037"] + np.testing.assert_array_equal( + assigned[COUNTY_FIPS_2023_COLUMN], county_fips_2023_from_blocks(blocks) + ) + assert assigned[COUNTY_FIPS_2023_COLUMN].tolist() == [ + "09120", + "09140", + "09190", + "06037", + ] + assert (assigned["tract_geoid"] == assigned["block_geoid"].str[:11]).all() + assert assigned["population_weight"].equals(household["population_weight"]) + assert not assigned[COUNTY_FIPS_2023_COLUMN].isna().any() + + +def test_new_county_column_is_registered_stored_geography() -> None: + assert ( + undefined_stored_inputs( + [COUNTY_FIPS_2023_COLUMN], engine_variables={"county_fips"} + ) + == () + ) diff --git a/packages/microcosm-build/tests/engine_free/us/test_us_geography_ladder.py b/packages/microcosm-build/tests/engine_free/us/test_us_geography_ladder.py index 8413ed5452..f60e8d02e0 100644 --- a/packages/microcosm-build/tests/engine_free/us/test_us_geography_ladder.py +++ b/packages/microcosm-build/tests/engine_free/us/test_us_geography_ladder.py @@ -392,6 +392,7 @@ def _gated_household() -> tuple[pd.DataFrame, np.ndarray]: "48001000100", ], "county_fips": ["36061", "36001", "06037", "48001"], + "county_fips_2023": ["36061", "36001", "06037", "48001"], "place_fips": ["51000", "", "44000", ""], "sldu": ["027", "046", "024", "001"], "sldl": ["075", "109", "051", "001"], diff --git a/packages/microcosm-build/tests/engine_free/us/test_us_l0_refit_export.py b/packages/microcosm-build/tests/engine_free/us/test_us_l0_refit_export.py index ce708331ae..814ac22268 100644 --- a/packages/microcosm-build/tests/engine_free/us/test_us_l0_refit_export.py +++ b/packages/microcosm-build/tests/engine_free/us/test_us_l0_refit_export.py @@ -161,6 +161,7 @@ def _us_frame(**person_extra: object) -> Frame: "block_geoid": ["360610001001000", "060370001001000"], "tract_geoid": ["36061000100", "06037000100"], "county_fips": ["36061", "06037"], + "county_fips_2023": ["36061", "06037"], "place_fips": ["51000", "44000"], "sldu": ["027", "024"], "sldl": ["075", "051"], diff --git a/packages/microcosm-build/tests/engine_free/us/test_us_puma_county_vintage.py b/packages/microcosm-build/tests/engine_free/us/test_us_puma_county_vintage.py new file mode 100644 index 0000000000..184bfaeb8c --- /dev/null +++ b/packages/microcosm-build/tests/engine_free/us/test_us_puma_county_vintage.py @@ -0,0 +1,90 @@ +"""PUMA planning-region draws preserve the assigned legacy county and PUMA.""" + +import numpy as np +import pytest + +from microcosm.build.us_runtime.county_vintage import county_fips_2023_from_tracts +from microcosm.build.us_runtime.puma_county_vintage import ( + county_fips_2023_for_puma_assignment, +) + + +def _overlap() -> tuple[np.ndarray, np.ndarray, np.ndarray]: + # Bridgeport and Shelton share Fairfield County but have different regions. + # Stamford is in a separate PUMA here, so its large population cannot leak + # into the first pair's conditional draw. + return ( + np.asarray([900100, 900100, 900200]), + np.asarray([9001070100, 9001110100, 9001020101]), + np.asarray([1.0, 3.0, 1000.0]), + ) + + +def test_stored_tract_resolves_directly_without_random_draws() -> None: + tracts = np.asarray([9001070100, 9001110100, 6037000100]) + rng = np.random.default_rng(19) + untouched = np.random.default_rng(19) + + actual = county_fips_2023_for_puma_assignment( + np.asarray([900100, 900100, 600100]), + tracts // 1_000_000, + tract_values=tracts, + tract_overlap=_overlap(), + rng=rng, + ) + + np.testing.assert_array_equal(actual, county_fips_2023_from_tracts(tracts)) + assert rng.random() == untouched.random() + + +def test_non_ct_identity_consumes_no_random_draws() -> None: + rng = np.random.default_rng(42) + untouched = np.random.default_rng(42) + + actual = county_fips_2023_for_puma_assignment( + np.asarray([100100, 600100, 3600100]), + np.asarray([1001, 6037, 36061]), + tract_values=None, + tract_overlap=_overlap(), + rng=rng, + ) + + assert actual.tolist() == ["01001", "06037", "36061"] + assert rng.random() == untouched.random() + + +def test_ct_conditional_draw_respects_pair_population_and_is_reproducible() -> None: + puma = np.asarray([900100] * 20_000 + [900200] * 100 + [600100]) + county = np.asarray([9001] * 20_100 + [6037]) + original_county = county.copy() + + def draw() -> np.ndarray: + return county_fips_2023_for_puma_assignment( + puma, + county, + tract_values=None, + tract_overlap=_overlap(), + rng=np.random.default_rng(24), + ) + + actual = draw() + + np.testing.assert_array_equal(actual, draw()) + np.testing.assert_array_equal(county, original_county) + assert len(actual) == len(puma) + assert set(actual[:20_000]) == {"09120", "09140"} + assert 0.72 < np.mean(actual[:20_000] == "09140") < 0.78 + assert set(actual[20_000:20_100]) == {"09190"} + assert actual[-1] == "06037" + assert all(isinstance(value, str) and len(value) == 5 for value in actual) + + +def test_missing_conditional_pair_fails_closed() -> None: + with pytest.raises(ValueError, match="0900300.*09001"): + county_fips_2023_for_puma_assignment( + np.asarray([900300]), + np.asarray([9001]), + tract_values=None, + tract_overlap=_overlap(), + rng=np.random.default_rng(0), + ) diff --git a/packages/microcosm-build/tests/engine_free/us/test_us_puma_ladder.py b/packages/microcosm-build/tests/engine_free/us/test_us_puma_ladder.py index 849ef40cd3..dab67ef235 100644 --- a/packages/microcosm-build/tests/engine_free/us/test_us_puma_ladder.py +++ b/packages/microcosm-build/tests/engine_free/us/test_us_puma_ladder.py @@ -427,6 +427,7 @@ def _gated_household() -> tuple[pd.DataFrame, np.ndarray]: "puma": ["3603801", "3600100", "0603701", "4800100"], "congressional_district_geoid": [3612, 3620, 653, 4802], "county_fips": ["36061", "36001", "06037", "48001"], + "county_fips_2023": ["36061", "36001", "06037", "48001"], } ) weights = np.asarray([2.6, 3.6, 73.8, 20.0]) diff --git a/packages/microcosm-build/tests/engine_free/us/test_us_stored_input_register.py b/packages/microcosm-build/tests/engine_free/us/test_us_stored_input_register.py index e25bc2619a..f303146113 100644 --- a/packages/microcosm-build/tests/engine_free/us/test_us_stored_input_register.py +++ b/packages/microcosm-build/tests/engine_free/us/test_us_stored_input_register.py @@ -6,6 +6,7 @@ import pytest # noqa: E402 +from microcosm.build.us_runtime.county_vintage import COUNTY_FIPS_2023_COLUMN from microcosm.data import contract # noqa: E402 from microcosm.data.contract import ( # noqa: E402 US_FISCAL_STORED_INPUTS_STAGE, @@ -60,10 +61,11 @@ def test_every_register_entry_is_a_named_producer_column(): | tail | _ACS_NATIVE_AMOUNTS | {"puma_geoid"} + | {COUNTY_FIPS_2023_COLUMN} | set(US_ELIGIBILITY_INPUTS_PARENT_ID_COLUMNS) ) assert len(_ACS_NATIVE_AMOUNTS) == 6 - assert len(US_STORED_NON_VARIABLE_COLUMNS) == 30 + 6 + 6 + 6 + 1 + 2 + assert len(US_STORED_NON_VARIABLE_COLUMNS) == 30 + 6 + 6 + 6 + 1 + 2 + 1 def test_every_register_reason_names_where_its_column_comes_from(): @@ -88,6 +90,7 @@ def test_every_register_reason_names_where_its_column_comes_from(): }, **{column: "_ACS_NATIVE_INPUT_CONTRACTS" for column in _ACS_NATIVE_AMOUNTS}, "puma_geoid": "microcosm.build.us_runtime.acs_pums", + COUNTY_FIPS_2023_COLUMN: "microcosm.build.us_runtime.county_vintage", _TAX_UNIT_ROLE_COLUMN: "microcosm.frame.units._TAX_UNIT_ROLE_COLUMN", TAX_UNIT_FILING_STATUS_COLUMN: ( "microcosm.frame.units.TAX_UNIT_FILING_STATUS_COLUMN" diff --git a/packages/microcosm-data/src/microcosm/data/stored_inputs.py b/packages/microcosm-data/src/microcosm/data/stored_inputs.py index 5193e0cfcc..205df13e4d 100644 --- a/packages/microcosm-data/src/microcosm/data/stored_inputs.py +++ b/packages/microcosm-data/src/microcosm/data/stored_inputs.py @@ -319,6 +319,13 @@ #: that rule against the installed engine. US_STORED_NON_VARIABLE_COLUMNS: Mapping[str, str] = MappingProxyType( { + "county_fips_2023": ( + "The household's five-digit 2023 county-equivalent FIPS, produced " + "by microcosm.build.us_runtime.county_vintage and the block/PUMA " + "ladder assignments for county calibration. Connecticut uses its " + "nine planning regions. The separate county_fips input retains " + "2020 counties for policyengine-us 2.2.1 county-based rules." + ), **{ f"{entity}_source_id": _SUPPORT_SOURCE_ID.format(entity=entity) for entity in _US_ENTITIES From 5f0095501b7ea9f74edbe1a64485334ed7c910fa Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 23 Oct 2026 21:35:45 -0400 Subject: [PATCH 04/10] test: preserve seeded PUMA geography and document validation --- docs/agent-guide.md | 6 ++ docs/us-county-2023-geography.md | 68 ++++++++++++++----- .../us/test_us_county_vintage_ladders.py | 49 +++++++++++++ 3 files changed, 106 insertions(+), 17 deletions(-) diff --git a/docs/agent-guide.md b/docs/agent-guide.md index 83a859fff0..ad882204d3 100644 --- a/docs/agent-guide.md +++ b/docs/agent-guide.md @@ -335,6 +335,12 @@ and the ACS local-area `stored_inputs` gate, each naming the built-with engine on which an SPM unit the built-with engine measures has no classified adult, and binds the `spm_measurement_composition` gate the publisher requires (microcosm#1263; `microcosm.build.us_runtime.spm_measurement_gate`). +US household ladders carry both `county_fips` (2020 counties for the locked +policyengine-us rules) and `county_fips_2023` (3,144 county-equivalents for +calibration, including Connecticut's nine planning regions). The latter is +registered non-engine metadata. Geography gates require both columns; older +artifacts need the new mapping before they pass. County target joins must +choose the 2023 column explicitly. See [the vintage investigation](us-county-2023-geography.md). A source-enrichment publisher claim widens the range consumers accept, not what was checked: the bundle's `compatibility.claim_measurement` names the one version the probe measured. diff --git a/docs/us-county-2023-geography.md b/docs/us-county-2023-geography.md index 13157d7917..f0eaaffac5 100644 --- a/docs/us-county-2023-geography.md +++ b/docs/us-county-2023-geography.md @@ -6,23 +6,27 @@ Investigation for microcosm#922 item 3, against base `dec45a439`. `uv.lock:1692–1707` pins **policyengine-us 2.2.1**, with wheel SHA-256 `0993a6c73fcdfbe171a796aca9d302741bd8e00ab83388ad81c15a08740318c6`. -Inspection of that exact cached wheel, without importing the engine, finds: +The [locked wheel](https://files.pythonhosted.org/packages/5e/09/86ac3968d2a18e28fce3ba1186fc21728e9f299b96a806d6f204f1e0aaed/policyengine_us-2.2.1-py3-none-any.whl) +was independently downloaded and hash-verified. Paths below refer to files in +that wheel; there is no matching GitHub version tag. Inspection without +importing the engine finds: -- [`county_enum.py:315–322`](https://github.com/PolicyEngine/policyengine-us/blob/2.2.1/policyengine_us/variables/household/demographic/geographic/county/county_enum.py#L315) +- `policyengine_us/variables/household/demographic/geographic/county/county_enum.py:315–322` names Connecticut's eight legacy counties, rather than planning regions. -- [`county.py:36–69`](https://github.com/PolicyEngine/policyengine-us/blob/2.2.1/policyengine_us/variables/household/demographic/geographic/county/county.py#L36) +- `policyengine_us/variables/household/demographic/geographic/county/county.py:36–69` joins `county_fips` to the engine county dataset, converts the result to that enum, and falls back to the first county in the state on an unrecognized code. Replacing Connecticut's legacy codes would therefore change rules evaluation rather than merely relabel geography. -- [`county_helpers.py:12–23`](https://github.com/PolicyEngine/policyengine-us/blob/2.2.1/policyengine_us/tools/geography/county_helpers.py#L12) +- `policyengine_us/tools/geography/county_helpers.py:12–23` explicitly loads `county_fips_2020.csv.gz`. Its Connecticut rows are `09001,09003,09005,09007,09009,09011,09013,09015`. -- [`geography.py:130–135`](https://github.com/PolicyEngine/policyengine-us/blob/2.2.1/policyengine_us/variables/input/geography.py#L130) +- `policyengine_us/variables/input/geography.py:130–135` declares `county_fips` as a string household input. The engine's - [`ct_c4k_region.py:22–30`](https://github.com/PolicyEngine/policyengine-us/blob/2.2.1/policyengine_us/variables/gov/states/ct/oec/c4k/ct_c4k_region.py#L22) + `policyengine_us/variables/gov/states/ct/oec/c4k/ct_c4k_region.py:22–30` uses county enum names to approximate Care 4 Kids regions. The unrecognized - planning-region codes would fall back to Fairfield, affecting eligibility. + planning-region codes would fall back to Fairfield, changing the program's + regional computation. - `packages/microcosm-frame/src/microcosm/frame/adapters/policyengine_us.py:670–682` documents the engine's default county-based SPM measurement and its use of stored `county_fips`. @@ -61,23 +65,51 @@ draw conditional on the already drawn PUMA and legacy county. This draw runs after the existing draws and supplies only the new county column. With `assign_tract=True`, the new county comes directly from the assigned tract. -## Implementation and validation plan +## Implementation -Package the compact crosswalk, original Census town crosswalk, and provenance -JSON recording source URLs and SHA-256 digests. Add a reproducible generator -that uses the existing parsers and cached sources. Wire both household ladder -assignments with minimal changes. +The compact crosswalk, original Census town crosswalk, and provenance JSON +are packaged under `us_runtime/data/`. The loader checks SHA-256 digests. +`tools/build_us_county_vintage_crosswalk.py` reproduces them from the existing +cached CT BAF, town crosswalk, and P.L. 94-171 archives using the maintained +parsers. Its normal CLI was run successfully without building a ladder. + +Both household ladder assignments now write `county_fips_2023`. PUMA's +conditional draw is isolated in `puma_county_vintage.py`. The new stored-input +register entry is bound to its producer by tests. Both geography gates require +the additional column and check completeness, text format, valid CT regions, +non-CT identity, and agreement with assigned block/tract geography when present. +Consequently, older artifacts without the new column fail these updated gates +and the default L0 export's required-geography check. + +## Validation Engine-free checks cover national support of 3,144 county-equivalents in the 50 states plus DC, one region per CT town, non-CT identity, one non-null value -per household, and CT population conservation across county vintages. Include -Hypothesis mapping invariants and exercise both PUMA tract modes. Run focused -tests with at most four workers, the test-plan verifier, and `uv run ruff check .`. +per household, and CT population conservation across county vintages. Hypothesis +checks generated mapping invariants and both PUMA tract modes are exercised. +The source-backed crosswalk conserves Connecticut's 3,605,944 people across +all 42,008 populated blocks, eight legacy counties and nine planning regions. + +The primary engine-free run passed **275 tests**, with **one skip**, serially +on Python 3.14.7. It covered the four new mapping/assignment/gate modules and +the existing block/PUMA ladder, stored-input register, L0 export, base-pool, +and block/PUMA source suites. `uv run ruff check .`, +`python3 tools/ci_test_plan.py verify`, and `git diff --check` passed. +Two additional seeded regression tests passed against outputs captured from +the original `dec45a439` PUMA implementation: PUMA, CD, legacy county and optional +tract assignments remain unchanged. Across both invocations: **277 passed, +one skipped**. No full ladder NPZ is present in this workspace or the existing geography cache. Use source-derived fixtures for CI and provide an optional read-only -check against an existing national ladder. Do not rebuild a ladder or build -or publish a release. +check against an existing national ladder. + +The skipped test is +`test_us_county_vintage.py::test_existing_full_ladder_has_3144_counties_without_rebuilding`. +To run it, set `MICROCOSM_US_BLOCK_LADDER_ARTIFACT` to an existing national +block-ladder NPZ and invoke that pytest node. The passing fixture check covers +the complete county support universe using 4,015 actual Census tracts; it does +not certify a full NPZ artifact. No ladder or release was built or published. ## Release-tool follow-up @@ -88,3 +120,5 @@ allowlist containing only `county_fips`; that checkpoint will need to retain `county_fips_2023` before the local county-target lane can use the new geography. County calibration/target joins must explicitly select the 2023 column while engine computation continues to use the legacy column. + +axiom: n/a: data geography infrastructure diff --git a/packages/microcosm-build/tests/engine_free/us/test_us_county_vintage_ladders.py b/packages/microcosm-build/tests/engine_free/us/test_us_county_vintage_ladders.py index 8045d11b4b..6d469f2188 100644 --- a/packages/microcosm-build/tests/engine_free/us/test_us_county_vintage_ladders.py +++ b/packages/microcosm-build/tests/engine_free/us/test_us_county_vintage_ladders.py @@ -133,3 +133,52 @@ def test_new_county_column_is_registered_stored_geography() -> None: ) == () ) + + +@pytest.mark.parametrize("assign_tract", [False, True]) +def test_existing_seeded_puma_columns_match_the_pre_change_assignment(assign_tract): + # Captured from the unchanged assign_us_puma_ladder at base dec45a439, + # using this exact ladder/household fixture and seed. The added conditional + # draws must leave all previously assigned geography byte-for-byte stable. + household = pd.DataFrame( + { + "household_id": np.arange(12), + "state_fips": [6] * 2 + [9] * 10, + "puma": [600100] * 2 + [900100] * 10, + } + ) + assigned = assign_us_puma_ladder( + household, _puma_ladder(), seed=7, assign_tract=assign_tract + ) + + assert assigned["puma"].tolist() == ["0600100"] * 2 + ["0900100"] * 10 + assert assigned["congressional_district_geoid"].tolist() == [601] * 2 + [901] * 10 + assert assigned["county_fips"].tolist() == [ + "06037", + "06037", + "09001", + "09001", + "09003", + "09003", + "09001", + "09003", + "09001", + "09001", + "09001", + "09001", + ] + if assign_tract: + assert assigned["tract_geoid"].tolist() == [ + "06037000100", + "06037000100", + "09001110100", + "09001110100", + "09003330100", + "09003330100", + "09001110100", + "09003330100", + "09001110100", + "09001070100", + "09001110100", + "09001070100", + ] From a788815ab358c6499af6fd84c17dc4924dfdbfab Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 23 Oct 2026 22:23:47 -0400 Subject: [PATCH 05/10] fix: derive pooled donor 2023 counties from preserved tracts --- .../microcosm/build/us_runtime/base_pool.py | 9 ++ .../microcosm/build/us_runtime/puma_ladder.py | 11 +++ .../tests/engine_free/us/test_us_base_pool.py | 84 ++++++++++++++++++- .../us/test_us_county_vintage_ladders.py | 36 ++++++++ 4 files changed, 138 insertions(+), 2 deletions(-) diff --git a/packages/microcosm-build/src/microcosm/build/us_runtime/base_pool.py b/packages/microcosm-build/src/microcosm/build/us_runtime/base_pool.py index eb32242c45..a26f7cc9dc 100644 --- a/packages/microcosm-build/src/microcosm/build/us_runtime/base_pool.py +++ b/packages/microcosm-build/src/microcosm/build/us_runtime/base_pool.py @@ -9,6 +9,10 @@ import pandas as pd from microcosm.build.us_runtime.acs_pums import ACS_2024_1YR_SPINE +from microcosm.build.us_runtime.county_vintage import ( + COUNTY_FIPS_2023_COLUMN, + county_fips_2023_from_tracts, +) from microcosm.build.us_runtime.puf_support import ( support_channel_column, support_clone_index_column, @@ -684,6 +688,11 @@ def _assign_pooled_puma_ladder( assert preserved_cd is not None and preserved_county is not None assigned.loc[donor_rows, "congressional_district_geoid"] = preserved_cd assigned.loc[donor_rows, "county_fips"] = preserved_county + # The sampled county is overwritten above; the retained tract also + # determines the donor's calibration county, including CT regions. + assigned.loc[donor_rows, COUNTY_FIPS_2023_COLUMN] = ( + county_fips_2023_from_tracts(assigned.loc[donor_rows, "tract_geoid"]) + ) assigned.drop(columns=[_PUMA_LADDER_ANCHOR_COLUMN], inplace=True) tables["household"] = assigned diff --git a/packages/microcosm-build/src/microcosm/build/us_runtime/puma_ladder.py b/packages/microcosm-build/src/microcosm/build/us_runtime/puma_ladder.py index fce85688f1..40b2d46371 100644 --- a/packages/microcosm-build/src/microcosm/build/us_runtime/puma_ladder.py +++ b/packages/microcosm-build/src/microcosm/build/us_runtime/puma_ladder.py @@ -543,6 +543,17 @@ def us_puma_ladder_gate( tracts=household[US_PUMA_LADDER_TRACT_COLUMN] if assign_tract else None, ) ) + if not assign_tract and US_PUMA_LADDER_TRACT_COLUMN in household.columns: + # Pooled donors retain tracts even when fresh ACS rows have none. + tracts = household[US_PUMA_LADDER_TRACT_COLUMN] + retained = household.loc[tracts.notna() & tracts.ne("")] + failures.extend( + county_fips_2023_assignment_failures( + retained[COUNTY_FIPS_2023_COLUMN], + retained["county_fips"], + tracts=retained[US_PUMA_LADDER_TRACT_COLUMN], + ) + ) if non_text is not None: failures.append(non_text) puma = household["puma"].astype(str).to_numpy() diff --git a/packages/microcosm-build/tests/engine_free/us/test_us_base_pool.py b/packages/microcosm-build/tests/engine_free/us/test_us_base_pool.py index e7a543539e..bd0330b6c4 100644 --- a/packages/microcosm-build/tests/engine_free/us/test_us_base_pool.py +++ b/packages/microcosm-build/tests/engine_free/us/test_us_base_pool.py @@ -17,6 +17,11 @@ spine_column, with_optional_acs_spine, ) +from microcosm.build.us_runtime.county_vintage import ( + COUNTY_FIPS_2023_COLUMN, + county_fips_2023_from_tracts, + load_ct_planning_region_by_tract, +) from microcosm.build.us_runtime.puf_support import ( support_channel_column, support_clone_index_column, @@ -75,6 +80,8 @@ def _base_frame( def _acs_frame( *, + state_fips: int = 1, + puma: str = "0100101", support_metadata: bool = False, conflicting_support_metadata: bool = False, weight_kind: WeightKind = WeightKind.DESIGN, @@ -94,8 +101,8 @@ def _acs_frame( "household": pd.DataFrame( { "household_id": [1], - "state_fips": [1], - "puma": ["0100101"], + "state_fips": [state_fips], + "puma": [puma], } ), "tax_unit": pd.DataFrame({"tax_unit_id": [10]}), @@ -296,9 +303,11 @@ def test__given_mixed_spines__then_ladder_keeps_acs_puma_and_assigns_asec() -> N def _assigned_geography_base_frame( *, + state_fips: int = 1, tracts: list[str] | None = None, congressional_districts: list[int] | None = None, counties: list[str] | None = None, + calibration_counties: list[str] | None = None, drop_columns: list[str] | None = None, ) -> Frame: """A base frame whose donor households carry block-ladder geography.""" @@ -306,12 +315,15 @@ def _assigned_geography_base_frame( base = _base_frame() tables = {entity: base.table(entity).copy() for entity in base.entities} household = tables["household"] + household["state_fips"] = state_fips household["tract_geoid"] = tracts or ["01001000100", "01003000100"] household["congressional_district_geoid"] = congressional_districts or [ 101, 102, ] household["county_fips"] = counties or ["01001", "01003"] + if calibration_counties is not None: + household[COUNTY_FIPS_2023_COLUMN] = calibration_counties for column in drop_columns or []: household.drop(columns=[column], inplace=True) return Frame( @@ -358,6 +370,74 @@ def test__given_donor_assigned_geography__then_pool_preserves_it_and_derives_pum assert acs_row["county_fips"].tolist() == ["01001"] +@pytest.mark.parametrize("carry_county_2023", [False, True]) +@pytest.mark.parametrize( + ("puma", "tracts", "counties"), + [ + ("0100101", ["01001000100", "01003000100"], ["01001", "01003"]), + ("0920301", ["09013890203", "09013890202"], ["09013", "09013"]), + ], + ids=["non_ct_mixed_county", "tolland_planning_region_split"], +) +def test__given_preserved_donors__then_2023_county_follows_each_tract( + puma: str, tracts: list[str], counties: list[str], carry_county_2023: bool +) -> None: + state = int(puma[:2]) + district = state * 100 + 1 + county_values, county_populations = np.unique( + np.asarray(counties, dtype=np.int64), return_counts=True + ) + ladder = UsPumaLadder( + puma=np.asarray([int(puma)]), + puma_population=np.asarray([2.0]), + cd_overlap_puma=np.asarray([int(puma)]), + cd_overlap_cd=np.asarray([district]), + cd_overlap_population=np.asarray([2.0]), + county_overlap_puma=np.full(len(county_values), int(puma)), + county_overlap_county=county_values, + county_overlap_population=county_populations.astype(float), + tract_overlap_puma=np.full(len(tracts), int(puma)), + tract_overlap_tract=np.asarray(tracts, dtype=np.int64), + tract_overlap_population=np.ones(len(tracts)), + metadata=_test_puma_ladder().metadata, + ) + expected = county_fips_2023_from_tracts(tracts) + base = _assigned_geography_base_frame( + state_fips=state, + tracts=tracts, + congressional_districts=[district, district + 1], + counties=counties, + calibration_counties=expected.tolist() if carry_county_2023 else None, + ) + if state == 9: + # Read the pinned packaged CSV through its hash-checked loader. The + # same legacy Tolland county spans two 2023 planning regions. + crosswalk = load_ct_planning_region_by_tract() + assert crosswalk["09013890203"] == "09150" + assert crosswalk[tracts[0]] != crosswalk[tracts[1]] + assert expected.tolist() == [crosswalk[tract] for tract in tracts] + + result = with_optional_acs_spine( + base, + _acs_frame(state_fips=state, puma=puma), + puma_ladder=ladder, + geography_seed=1, + expected_congressional_district_vintage="119th_congress", + ) + + household = result.table("household") + donor = household[household[spine_column("household")].eq(ASEC_PUF_SPINE)] + assert donor["puma"].tolist() == [puma, puma] + assert donor["tract_geoid"].tolist() == tracts + assert donor["county_fips"].tolist() == counties + assert donor["congressional_district_geoid"].tolist() == [district, district + 1] + np.testing.assert_array_equal(donor[COUNTY_FIPS_2023_COLUMN], expected) + if state != 9: + assert donor[COUNTY_FIPS_2023_COLUMN].tolist() == counties + acs = household[household[spine_column("household")].eq(ACS_2024_1YR_SPINE)] + assert acs["tract_geoid"].isna().all() + + def test__given_partial_donor_assigned_geography_columns__then_pool_fails() -> None: base = _assigned_geography_base_frame( drop_columns=["congressional_district_geoid", "county_fips"], diff --git a/packages/microcosm-build/tests/engine_free/us/test_us_county_vintage_ladders.py b/packages/microcosm-build/tests/engine_free/us/test_us_county_vintage_ladders.py index 6d469f2188..08266ad6c3 100644 --- a/packages/microcosm-build/tests/engine_free/us/test_us_county_vintage_ladders.py +++ b/packages/microcosm-build/tests/engine_free/us/test_us_county_vintage_ladders.py @@ -17,6 +17,7 @@ from microcosm.build.us_runtime.puma_ladder import ( UsPumaLadder, assign_us_puma_ladder, + us_puma_ladder_gate, ) from microcosm.data.stored_inputs import undefined_stored_inputs @@ -135,6 +136,41 @@ def test_new_county_column_is_registered_stored_geography() -> None: ) +@pytest.mark.parametrize("empty_tract", ["", None, np.nan, pd.NA]) +def test_puma_gate_checks_retained_donor_tracts_with_tractless_acs_rows( + empty_tract, +) -> None: + donor_tract = "09013890203" + donor_region = load_ct_planning_region_by_tract()[donor_tract] + household = pd.DataFrame( + { + "state_fips": [9, 36, 9, 36], + "puma": ["0920301", "3603801", "0920301", "3600100"], + "congressional_district_geoid": [902, 3612, 902, 3620], + "county_fips": ["09013", "36061", "09013", "36001"], + COUNTY_FIPS_2023_COLUMN: [donor_region, "36061", "09110", "36001"], + "tract_geoid": [donor_tract, "36061000100", empty_tract, empty_tract], + }, + index=[1000, 2000, 3000, 4000], + ) + weights = np.array([48.0, 1.0, 49.0, 2.0]) + + result = us_puma_ladder_gate(household, weights, assign_tract=False) + assert result.passed, result.failures + + household.loc[1000, COUNTY_FIPS_2023_COLUMN] = next( + region + for region in load_ct_planning_region_by_tract().values() + if region != donor_region + ) + result = us_puma_ladder_gate(household, weights, assign_tract=False) + + assert not result.passed + assert result.failures == ( + "county_fips_2023: 1 row(s) disagree with tract_geoid's 2023 county-equivalent.", + ) + + @pytest.mark.parametrize("assign_tract", [False, True]) def test_existing_seeded_puma_columns_match_the_pre_change_assignment(assign_tract): # Captured from the unchanged assign_us_puma_ladder at base dec45a439, From 012dd5369a29580fbe4302c09158b19e2c87a6ad Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 23 Oct 2026 22:27:40 -0400 Subject: [PATCH 06/10] test: mark 2023 county metadata as producer-bound --- test_support/microcosm_data/stored_inputs.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/test_support/microcosm_data/stored_inputs.py b/test_support/microcosm_data/stored_inputs.py index 4206bdf943..4951f454d5 100644 --- a/test_support/microcosm_data/stored_inputs.py +++ b/test_support/microcosm_data/stored_inputs.py @@ -154,10 +154,13 @@ def _column_strategies(st): #: Register entries that postdate the files examined for #1026 and rest on a -#: live producer instead: the parent ids (microcosm#884), bound to +#: live producer instead (microcosm-data cannot import the build). The parent +#: ids (microcosm#884) are bound to #: eligibility_inputs.US_ELIGIBILITY_INPUTS_PARENT_ID_COLUMNS by -#: test_us_stored_input_register.py (microcosm-data cannot import the build). -_PRODUCER_BOUND_ENTRIES = frozenset({"parent_1_id", "parent_2_id"}) +#: test_us_stored_input_register.py. That test also binds county_fips_2023 to +#: county_vintage; test_us_county_vintage_ladders.py proves the PUMA and block +#: assignments write it. +_PRODUCER_BOUND_ENTRIES = frozenset({"county_fips_2023", "parent_1_id", "parent_2_id"}) #: The role columns policyengine-core's ``build_from_dataset`` reads to build From b3dc8adab276b65f08622546d0a29c696fa25780 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 24 Oct 2026 00:01:31 -0400 Subject: [PATCH 07/10] Re-pin the seed-protocol attestation for the 2023 county draw module Adding puma_county_vintage to the seed protocol's direct kernel modules moves the seed-protocol and seed-map digests, the field-usage counts, the pointer inventory digest, the loader golden vector, the US pool-tool spec_sha256 and the regenerated us-f0-coverage.json. Each takes its observed value, per the documented re-pin procedure. The spine-blindness allowlist names the two county-vintage modules, and a changelog fragment records the new county_fips_2023 column. Co-Authored-By: Claude Opus 5.5 --- changelog.d/1376-us-county-fips-2023.added.md | 1 + docs/evidence/spec-engine/us-f0-coverage.json | 32 +++++++++---------- .../build/spec_engine/field_usage.py | 8 ++--- .../build/spec_engine/inventory_coverage.py | 4 +-- .../src/microcosm/build/spec_engine/seeds.py | 1 + .../us/test_spec_engine_coverage_tool.py | 6 ++-- .../shared/test_spec_engine_field_usage.py | 6 ++-- .../shared/test_spec_engine_loader.py | 2 +- .../engine_free/us/test_us_spine_blindness.py | 6 ++++ .../us/test_us_multispine_pool_tool.py | 2 +- tools/spec_engine_coverage.py | 2 +- 11 files changed, 39 insertions(+), 31 deletions(-) create mode 100644 changelog.d/1376-us-county-fips-2023.added.md diff --git a/changelog.d/1376-us-county-fips-2023.added.md b/changelog.d/1376-us-county-fips-2023.added.md new file mode 100644 index 0000000000..31caaf7b9c --- /dev/null +++ b/changelog.d/1376-us-county-fips-2023.added.md @@ -0,0 +1 @@ +US household ladders write `county_fips_2023`, each household's 2023 county-equivalent, beside the engine's legacy `county_fips`: Connecticut households take the planning region of their assigned 2020 tract, or, without a tract rung, a tract drawn after the ladder's original draws conditional on their PUMA and 2020 county; every other state's code is unchanged. The new draw module is attested by the seed protocol, so the seed-protocol and seed-map digests, the configuration field counts and pointer inventory, the loader golden vector, the US pool-tool `spec_sha256` and `docs/evidence/spec-engine/us-f0-coverage.json` are re-pinned at their observed values (microcosm#1376, for the LWI County Files). diff --git a/docs/evidence/spec-engine/us-f0-coverage.json b/docs/evidence/spec-engine/us-f0-coverage.json index 52fe0adb4f..69723fd01c 100644 --- a/docs/evidence/spec-engine/us-f0-coverage.json +++ b/docs/evidence/spec-engine/us-f0-coverage.json @@ -736,8 +736,8 @@ "legacy_sinks": [], "mode": "compiler_semantic", "pointer_class": "all", - "pointer_count": 826, - "pointer_sha256": "7ff2d5d1c2fd8026d17a57244f969dc0e9625a9e47304b693ad15df329041282", + "pointer_count": 827, + "pointer_sha256": "525ed8621baa2c6e0c0970a6cec85b3b250f8aadfe93750a08deb0d9f240f15e", "rationale": null, "relative_sink_prefix": null, "source_prefix": "/resolved/seed_protocol", @@ -772,21 +772,21 @@ "verifier": "vintages" } ], - "configuration_field_count": 42184, - "consumed_field_count": 42184, + "configuration_field_count": 42185, + "consumed_field_count": 42185, "generation0_effect_counts": { "legacy_behavior": 38498, - "no_generation0_effect": 3686 + "no_generation0_effect": 3687 }, "mode_counts": { - "compiler_semantic": 27717, + "compiler_semantic": 27718, "front_end_validation": 354, "identity_only": 103, "legacy_behavior": 14010 }, "multiple_primary_use_field_count": 0, - "pointer_inventory_sha256": "8f655670b9794adae905ede64bdceba6395eeb8d642b6c5896d5c516f1f6a6a7", - "resolved_binding_field_count": 9780, + "pointer_inventory_sha256": "d3e9c8453bc25cafc34e015b862bc5504f9f53085a19fce618e1ba1f20e4b8b6", + "resolved_binding_field_count": 9781, "unused_field_count": 0 }, "inventory_coverage": { @@ -1656,13 +1656,13 @@ "compiler_ir.node_slices" ], "expected": { - "map_sha256": "0db599df419dd16779097361dc72afdefb8df2abf8b134078da9c52d31d8e017", - "protocol_sha256": "2b7bdc1d6b0da1bdaf54b4cac29f10f2d3c8bed2e177729c2c89e6b33a099cc5" + "map_sha256": "52d1e39cef478e0f8567f647c1afa4169bcd972cf547b23cdbf20a085f398b59", + "protocol_sha256": "c16455b9a979f213acd21c737b9f26dbfc262437656464e893cb01cae69e11cd" }, "failures": [], "observed": { - "map_sha256": "0db599df419dd16779097361dc72afdefb8df2abf8b134078da9c52d31d8e017", - "protocol_sha256": "2b7bdc1d6b0da1bdaf54b4cac29f10f2d3c8bed2e177729c2c89e6b33a099cc5" + "map_sha256": "52d1e39cef478e0f8567f647c1afa4169bcd972cf547b23cdbf20a085f398b59", + "protocol_sha256": "c16455b9a979f213acd21c737b9f26dbfc262437656464e893cb01cae69e11cd" }, "status": "covered" }, @@ -1677,7 +1677,7 @@ "compiler_ir.seed_stream_map" ], "expected": { - "implementation_sha256": "2b7bdc1d6b0da1bdaf54b4cac29f10f2d3c8bed2e177729c2c89e6b33a099cc5", + "implementation_sha256": "c16455b9a979f213acd21c737b9f26dbfc262437656464e893cb01cae69e11cd", "protocol": "legacy-v1", "streams": [ "build_model", @@ -1698,7 +1698,7 @@ }, "failures": [], "observed": { - "implementation_sha256": "2b7bdc1d6b0da1bdaf54b4cac29f10f2d3c8bed2e177729c2c89e6b33a099cc5", + "implementation_sha256": "c16455b9a979f213acd21c737b9f26dbfc262437656464e893cb01cae69e11cd", "protocol": "legacy-v1", "streams": [ "build_model", @@ -2599,7 +2599,7 @@ "country": "us", "schema_id": "country_spec", "schema_version": 1, - "spec_sha256": "19460fc168a6f9ed44e580d0e36777cce78bb33cb52b75630ee86203fa795684" + "spec_sha256": "f3c090fab738e376c425f902f4300e227805151fb885acf2d874a458453f8309" } }, "report_schema_version": 3, @@ -2609,7 +2609,7 @@ "country": "us", "schema_id": "country_spec", "schema_version": 1, - "spec_sha256": "19460fc168a6f9ed44e580d0e36777cce78bb33cb52b75630ee86203fa795684" + "spec_sha256": "f3c090fab738e376c425f902f4300e227805151fb885acf2d874a458453f8309" }, "status": "pass" } diff --git a/packages/microcosm-build/src/microcosm/build/spec_engine/field_usage.py b/packages/microcosm-build/src/microcosm/build/spec_engine/field_usage.py index 56f39195cf..e56bad175f 100644 --- a/packages/microcosm-build/src/microcosm/build/spec_engine/field_usage.py +++ b/packages/microcosm-build/src/microcosm/build/spec_engine/field_usage.py @@ -27,8 +27,8 @@ from .schemas import load_schema_registry EXPECTED_AUTHORED_FIELD_COUNT = 32_404 -EXPECTED_RESOLVED_BINDING_FIELD_COUNT = 9_780 -EXPECTED_CONFIGURATION_FIELD_COUNT = 42_184 +EXPECTED_RESOLVED_BINDING_FIELD_COUNT = 9_781 +EXPECTED_CONFIGURATION_FIELD_COUNT = 42_185 class FieldUsageError(AssertionError): @@ -453,8 +453,8 @@ def _path_inventory(rows: Sequence[tuple[str, object]]) -> tuple[int, str]: "6a781915fd491d2c4b16d2b7d482f69cf362c904130093c59f9629f7a319269b", ), "resolved_seed_protocol": ( - 826, - "7ff2d5d1c2fd8026d17a57244f969dc0e9625a9e47304b693ad15df329041282", + 827, + "525ed8621baa2c6e0c0970a6cec85b3b250f8aadfe93750a08deb0d9f240f15e", ), "resolved_seed_site_bindings": ( 277, diff --git a/packages/microcosm-build/src/microcosm/build/spec_engine/inventory_coverage.py b/packages/microcosm-build/src/microcosm/build/spec_engine/inventory_coverage.py index 4a20e69687..9a4562d87a 100644 --- a/packages/microcosm-build/src/microcosm/build/spec_engine/inventory_coverage.py +++ b/packages/microcosm-build/src/microcosm/build/spec_engine/inventory_coverage.py @@ -367,8 +367,8 @@ # moves both again: calibrate gains budget_search_bracket, whose default # keeps the US bracket, and the budget search, which the US solve does # not run, changes its probes, so again no US solve changes. - "seed_map": "0db599df419dd16779097361dc72afdefb8df2abf8b134078da9c52d31d8e017", - "seed_protocol": "2b7bdc1d6b0da1bdaf54b4cac29f10f2d3c8bed2e177729c2c89e6b33a099cc5", + "seed_map": "52d1e39cef478e0f8567f647c1afa4169bcd972cf547b23cdbf20a085f398b59", + "seed_protocol": "c16455b9a979f213acd21c737b9f26dbfc262437656464e893cb01cae69e11cd", "source_manifest": "f4f8bfeb79268a4043a843b47cec1d1c0e58e9f5dd6bc23417ada95446dd7b8e", "take_up": "9522ce40f7dea569312dd7a7beb474e5a5afadd2f1e10cb5876534c3ef623d35", "tail": "ac92829c88a1a4fb6460d61190918d5d99c6c377fc8dd8f62f02b332d09bf59c", diff --git a/packages/microcosm-build/src/microcosm/build/spec_engine/seeds.py b/packages/microcosm-build/src/microcosm/build/spec_engine/seeds.py index e837a009e5..cae6e3b72f 100644 --- a/packages/microcosm-build/src/microcosm/build/spec_engine/seeds.py +++ b/packages/microcosm-build/src/microcosm/build/spec_engine/seeds.py @@ -327,6 +327,7 @@ def _site( "microcosm.build.us_runtime.puf_qrf_chain", "microcosm.build.us_runtime.puf_source_agi", "microcosm.build.us_runtime.puf_support", + "microcosm.build.us_runtime.puma_county_vintage", "microcosm.build.us_runtime.puma_ladder", "microcosm.build.us_runtime.retirement_contributions", "microcosm.build.us_runtime.retirement_distributions", diff --git a/packages/microcosm-build/tests/engine_contract/us/test_spec_engine_coverage_tool.py b/packages/microcosm-build/tests/engine_contract/us/test_spec_engine_coverage_tool.py index 7be8062b02..582b19f3f9 100644 --- a/packages/microcosm-build/tests/engine_contract/us/test_spec_engine_coverage_tool.py +++ b/packages/microcosm-build/tests/engine_contract/us/test_spec_engine_coverage_tool.py @@ -82,10 +82,10 @@ def test_us_coverage_is_exact_complete_and_honest( assert_coverage_complete(coverage_report) assert coverage_report["status"] == "pass" fields = coverage_report["field_usage"] - assert fields["configuration_field_count"] == 42_184 + assert fields["configuration_field_count"] == 42_185 assert fields["authored_normative_field_count"] == 32_404 - assert fields["resolved_binding_field_count"] == 9_780 - assert fields["consumed_field_count"] == 42_184 + assert fields["resolved_binding_field_count"] == 9_781 + assert fields["consumed_field_count"] == 42_185 assert fields["unused_field_count"] == 0 assert fields["multiple_primary_use_field_count"] == 0 assert fields["claim_count"] == 49 diff --git a/packages/microcosm-build/tests/engine_free/shared/test_spec_engine_field_usage.py b/packages/microcosm-build/tests/engine_free/shared/test_spec_engine_field_usage.py index c5ec18bb51..53cedc7d00 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_spec_engine_field_usage.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_spec_engine_field_usage.py @@ -95,10 +95,10 @@ def _mutated_bundle( def test_exact_complete_ledger_has_one_primary_mode_per_pointer(field_ledger) -> None: - assert len(field_ledger.fields) == EXPECTED_CONFIGURATION_FIELD_COUNT == 42_184 + assert len(field_ledger.fields) == EXPECTED_CONFIGURATION_FIELD_COUNT == 42_185 assert field_ledger.source_counts == { "authored": 32_404, - "resolved_bindings": 9_780, + "resolved_bindings": 9_781, } assert field_ledger.mode_counts == { "legacy_behavior": 14_010, @@ -110,7 +110,7 @@ def test_exact_complete_ledger_has_one_primary_mode_per_pointer(field_ledger) -> "legacy_behavior": 38_498, "no_generation0_effect": 3_686, } - assert len({field.pointer for field in field_ledger.fields}) == 42_184 + assert len({field.pointer for field in field_ledger.fields}) == 42_185 def test_eligibility_concepts_are_validation_not_generation0_behavior( diff --git a/packages/microcosm-build/tests/engine_free/shared/test_spec_engine_loader.py b/packages/microcosm-build/tests/engine_free/shared/test_spec_engine_loader.py index 2d4a494760..483b41db8d 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_spec_engine_loader.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_spec_engine_loader.py @@ -16,7 +16,7 @@ def test_semantic_hash_has_golden_vector_and_surface_separation(tmp_path) -> Non # budget_search_bracket move it, and the fit kernels' bytes, so the QRF # chain step's caller-owned uniforms move it too. assert first.spec_sha256 == ( - "4f71334334c67cc17d1aab5b02a838def57bcc54ac4413b89b70b8ae3086d9ee" + "443cc405ad6671568b76054aeea5822316da3b28bf879a1bff373615f8131cf2" ) second_root = _rich_minimal(tmp_path / "xy", note="second", store="local:b") diff --git a/packages/microcosm-build/tests/engine_free/us/test_us_spine_blindness.py b/packages/microcosm-build/tests/engine_free/us/test_us_spine_blindness.py index 77338f1173..b49087120b 100644 --- a/packages/microcosm-build/tests/engine_free/us/test_us_spine_blindness.py +++ b/packages/microcosm-build/tests/engine_free/us/test_us_spine_blindness.py @@ -261,6 +261,9 @@ "congressional_district_geography.py", "congressional_district_vintage.py", "congressional_district_vintage_crosswalk.py", + # 2020 tract/county -> 2023 county-equivalent metadata and its gate; + # writes calibration geography only, no population treatment. + "county_vintage.py", "cps_carried.py", "demographics.py", "education_assistance_source.py", @@ -295,6 +298,9 @@ "puf_qrf_worker.py", "puf_source_agi.py", "puf_support.py", + # 2023 county for the PUMA ladder's rows, drawn after its original + # draws (CT only, without a tract); no population treatment. + "puma_county_vintage.py", "puma_ladder.py", "puma_ladder_sources.py", "reform_coverage_smoke.py", diff --git a/packages/microcosm-build/tests/engine_workflow/us/test_us_multispine_pool_tool.py b/packages/microcosm-build/tests/engine_workflow/us/test_us_multispine_pool_tool.py index ed52466467..566c71d484 100644 --- a/packages/microcosm-build/tests/engine_workflow/us/test_us_multispine_pool_tool.py +++ b/packages/microcosm-build/tests/engine_workflow/us/test_us_multispine_pool_tool.py @@ -343,7 +343,7 @@ def capture_equality(expected: object, actual: object) -> None: # retain_best_objective and microcosm#1301's budget_search_bracket # move it. It covers microcosm.fit.qrf's attested bytes too, so # fit_draw_next's caller-owned uniforms (microcosm#1332) move it. - "spec_sha256": "19460fc168a6f9ed44e580d0e36777cce78bb33cb52b75630ee86203fa795684", + "spec_sha256": "f3c090fab738e376c425f902f4300e227805151fb885acf2d874a458453f8309", }, } diff --git a/tools/spec_engine_coverage.py b/tools/spec_engine_coverage.py index c066d573ce..f4facc681a 100644 --- a/tools/spec_engine_coverage.py +++ b/tools/spec_engine_coverage.py @@ -41,7 +41,7 @@ REPORT_SCHEMA_VERSION = 3 EXPECTED_POINTER_INVENTORY_SHA256 = ( - "8f655670b9794adae905ede64bdceba6395eeb8d642b6c5896d5c516f1f6a6a7" + "d3e9c8453bc25cafc34e015b862bc5504f9f53085a19fce618e1ba1f20e4b8b6" ) DEFAULT_REPORT_PATH = ( Path(__file__).resolve().parents[1] From bb18273d03773b2077b246b6bed295395135057a Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 24 Oct 2026 00:34:55 -0400 Subject: [PATCH 08/10] Pin the 78-module pool import graph and the new field's coverage class CI on the merged head: the pool build tool now reaches county_vintage.py, puma_county_vintage.py and block_ladder_sources.py (which both import for CT_STATE_FIPS), all already classified, so the pinned graph is 78 modules. The added configuration field is compiler_semantic with no generation-0 effect, so those counts move to 27_718 and 3_687; every other asserted count matches the regenerated us-f0-coverage.json. Co-Authored-By: Claude Opus 5.5 --- .../us/test_spec_engine_coverage_tool.py | 4 ++-- .../tests/engine_free/us/test_us_spine_blindness.py | 12 +++++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/microcosm-build/tests/engine_contract/us/test_spec_engine_coverage_tool.py b/packages/microcosm-build/tests/engine_contract/us/test_spec_engine_coverage_tool.py index e8aae2e206..646d57b651 100644 --- a/packages/microcosm-build/tests/engine_contract/us/test_spec_engine_coverage_tool.py +++ b/packages/microcosm-build/tests/engine_contract/us/test_spec_engine_coverage_tool.py @@ -91,13 +91,13 @@ def test_us_coverage_is_exact_complete_and_honest( assert fields["claim_count"] == 49 assert fields["mode_counts"] == { "legacy_behavior": 14_043, - "compiler_semantic": 27_717, + "compiler_semantic": 27_718, "front_end_validation": 354, "identity_only": 103, } assert fields["generation0_effect_counts"] == { "legacy_behavior": 38_531, - "no_generation0_effect": 3_686, + "no_generation0_effect": 3_687, } inventory = coverage_report["inventory_coverage"] diff --git a/packages/microcosm-build/tests/engine_free/us/test_us_spine_blindness.py b/packages/microcosm-build/tests/engine_free/us/test_us_spine_blindness.py index 25777c9706..0e0decad2f 100644 --- a/packages/microcosm-build/tests/engine_free/us/test_us_spine_blindness.py +++ b/packages/microcosm-build/tests/engine_free/us/test_us_spine_blindness.py @@ -3512,11 +3512,13 @@ def test_pool_build_tool_import_graph_is_source_spine_blind() -> None: # plus asec_sources.py, whose default ASEC pool the education sidecar # loader now defaults to, plus esi_premiums.py, whose output family # operator_boundary registers so a raw source frame that already - # carries it is refused (#454). All five are classified in - # _OTHER_US_RUNTIME_MODULES and scanned below like every other - # reached module. - assert len(runtime_graph) == 75, ( - f"{tool.name} must reach the pinned 75-module runtime graph; " + # carries it is refused (#454). 78 adds county_vintage.py and + # puma_county_vintage.py, which write county_fips_2023, and + # block_ladder_sources.py, whose CT_STATE_FIPS they import (#1376). + # All are classified in _OTHER_US_RUNTIME_MODULES and scanned below + # like every other reached module. + assert len(runtime_graph) == 78, ( + f"{tool.name} must reach the pinned 78-module runtime graph; " f"reached {len(runtime_graph)}" ) assert not missing_modules, ( From 62ea8e8f2dbbeba945ac61a0235ed238573bcfb0 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 24 Oct 2026 01:24:58 -0400 Subject: [PATCH 09/10] Map pooled donors' 2023 county from integer-typed tracts too Integer tract GEOIDs become floats once pooled beside tractless ACS rows, and the 2023 county mapper rejected them. The donor tracts are already validated as integral, so they are normalized to 11-digit text before mapping. Regression: integer Tolland tracts still land in 09150 and 09110. Co-Authored-By: Claude Opus 5.5 --- .../microcosm/build/us_runtime/base_pool.py | 7 +- .../tests/engine_free/us/test_us_base_pool.py | 68 +++++++++++++++---- 2 files changed, 59 insertions(+), 16 deletions(-) diff --git a/packages/microcosm-build/src/microcosm/build/us_runtime/base_pool.py b/packages/microcosm-build/src/microcosm/build/us_runtime/base_pool.py index a26f7cc9dc..7cacda4d22 100644 --- a/packages/microcosm-build/src/microcosm/build/us_runtime/base_pool.py +++ b/packages/microcosm-build/src/microcosm/build/us_runtime/base_pool.py @@ -690,8 +690,13 @@ def _assign_pooled_puma_ladder( assigned.loc[donor_rows, "county_fips"] = preserved_county # The sampled county is overwritten above; the retained tract also # determines the donor's calibration county, including CT regions. + # _derived_donor_puma validated the tracts as integral, but pooling + # beside tractless ACS rows can leave them as floats. + tracts = pd.to_numeric(assigned.loc[donor_rows, "tract_geoid"]) assigned.loc[donor_rows, COUNTY_FIPS_2023_COLUMN] = ( - county_fips_2023_from_tracts(assigned.loc[donor_rows, "tract_geoid"]) + county_fips_2023_from_tracts( + [f"{tract:011d}" for tract in tracts.astype(np.int64).tolist()] + ) ) assigned.drop(columns=[_PUMA_LADDER_ANCHOR_COLUMN], inplace=True) tables["household"] = assigned diff --git a/packages/microcosm-build/tests/engine_free/us/test_us_base_pool.py b/packages/microcosm-build/tests/engine_free/us/test_us_base_pool.py index bd0330b6c4..9b0700128e 100644 --- a/packages/microcosm-build/tests/engine_free/us/test_us_base_pool.py +++ b/packages/microcosm-build/tests/engine_free/us/test_us_base_pool.py @@ -370,24 +370,15 @@ def test__given_donor_assigned_geography__then_pool_preserves_it_and_derives_pum assert acs_row["county_fips"].tolist() == ["01001"] -@pytest.mark.parametrize("carry_county_2023", [False, True]) -@pytest.mark.parametrize( - ("puma", "tracts", "counties"), - [ - ("0100101", ["01001000100", "01003000100"], ["01001", "01003"]), - ("0920301", ["09013890203", "09013890202"], ["09013", "09013"]), - ], - ids=["non_ct_mixed_county", "tolland_planning_region_split"], -) -def test__given_preserved_donors__then_2023_county_follows_each_tract( - puma: str, tracts: list[str], counties: list[str], carry_county_2023: bool -) -> None: - state = int(puma[:2]) - district = state * 100 + 1 +def _single_puma_ladder( + puma: str, tracts: list[str], counties: list[str], district: int +) -> UsPumaLadder: + """A one-PUMA ladder whose tracts and counties are the given donors'.""" + county_values, county_populations = np.unique( np.asarray(counties, dtype=np.int64), return_counts=True ) - ladder = UsPumaLadder( + return UsPumaLadder( puma=np.asarray([int(puma)]), puma_population=np.asarray([2.0]), cd_overlap_puma=np.asarray([int(puma)]), @@ -401,6 +392,23 @@ def test__given_preserved_donors__then_2023_county_follows_each_tract( tract_overlap_population=np.ones(len(tracts)), metadata=_test_puma_ladder().metadata, ) + + +@pytest.mark.parametrize("carry_county_2023", [False, True]) +@pytest.mark.parametrize( + ("puma", "tracts", "counties"), + [ + ("0100101", ["01001000100", "01003000100"], ["01001", "01003"]), + ("0920301", ["09013890203", "09013890202"], ["09013", "09013"]), + ], + ids=["non_ct_mixed_county", "tolland_planning_region_split"], +) +def test__given_preserved_donors__then_2023_county_follows_each_tract( + puma: str, tracts: list[str], counties: list[str], carry_county_2023: bool +) -> None: + state = int(puma[:2]) + district = state * 100 + 1 + ladder = _single_puma_ladder(puma, tracts, counties, district) expected = county_fips_2023_from_tracts(tracts) base = _assigned_geography_base_frame( state_fips=state, @@ -438,6 +446,36 @@ def test__given_preserved_donors__then_2023_county_follows_each_tract( assert acs["tract_geoid"].isna().all() +def test__given_integer_donor_tracts__then_2023_county_still_follows_each_tract() -> ( + None +): + # Integer tract GEOIDs become floats once pooled beside tractless ACS + # rows; the 2023 county must still come from each validated tract, + # including Tolland's split across two planning regions. + puma = "0920301" + tracts = ["09013890203", "09013890202"] + counties = ["09013", "09013"] + district = 901 + base = _assigned_geography_base_frame( + state_fips=9, + tracts=[int(tract) for tract in tracts], + congressional_districts=[district, district + 1], + counties=counties, + ) + + result = with_optional_acs_spine( + base, + _acs_frame(state_fips=9, puma=puma), + puma_ladder=_single_puma_ladder(puma, tracts, counties, district), + geography_seed=1, + expected_congressional_district_vintage="119th_congress", + ) + + household = result.table("household") + donor = household[household[spine_column("household")].eq(ASEC_PUF_SPINE)] + assert donor[COUNTY_FIPS_2023_COLUMN].tolist() == ["09150", "09110"] + + def test__given_partial_donor_assigned_geography_columns__then_pool_fails() -> None: base = _assigned_geography_base_frame( drop_columns=["congressional_district_geoid", "county_fips"], From a13de090971d27d0793883761117e9c558eea51a Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 24 Oct 2026 02:01:01 -0400 Subject: [PATCH 10/10] Move the shared field-usage test's mode counts to the 42,218-field ledger bb18273d0 moved the configuration total to 42_218 and corrected the engine-contract coverage test, but this engine-free test still counted 27_717 compiler-semantic and 3_686 no-generation-0-effect fields, so both breakdowns summed to 42_217. The new county field is one of each. Co-Authored-By: Claude Opus 5.5 --- .../tests/engine_free/shared/test_spec_engine_field_usage.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/microcosm-build/tests/engine_free/shared/test_spec_engine_field_usage.py b/packages/microcosm-build/tests/engine_free/shared/test_spec_engine_field_usage.py index 21d209b2c0..5132ba2e09 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_spec_engine_field_usage.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_spec_engine_field_usage.py @@ -102,13 +102,13 @@ def test_exact_complete_ledger_has_one_primary_mode_per_pointer(field_ledger) -> } assert field_ledger.mode_counts == { "legacy_behavior": 14_043, - "compiler_semantic": 27_717, + "compiler_semantic": 27_718, "front_end_validation": 354, "identity_only": 103, } assert field_ledger.generation0_effect_counts == { "legacy_behavior": 38_531, - "no_generation0_effect": 3_686, + "no_generation0_effect": 3_687, } assert len({field.pointer for field in field_ledger.fields}) == 42_218