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/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/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/evidence/spec-engine/us-f0-coverage.json b/docs/evidence/spec-engine/us-f0-coverage.json index 01318a3f01..7faef0faae 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": 42217, - "consumed_field_count": 42217, + "configuration_field_count": 42218, + "consumed_field_count": 42218, "generation0_effect_counts": { "legacy_behavior": 38531, - "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": 14043 }, "multiple_primary_use_field_count": 0, - "pointer_inventory_sha256": "c1e2a118a0d53db0bb628b14357a2c86db2956332f5b359c9092764f9c451a95", - "resolved_binding_field_count": 9780, + "pointer_inventory_sha256": "a0da3562ad5559cbbcdf5ee14ebd21db2f067c90e7e470e24d4e3762d7b90848", + "resolved_binding_field_count": 9781, "unused_field_count": 0 }, "inventory_coverage": { @@ -1656,13 +1656,13 @@ "compiler_ir.node_slices" ], "expected": { - "map_sha256": "3cf71bdd3e6df567a4f9eb8d182bea160108cc9941e6ff08b8a0eee4e9fc8ba2", - "protocol_sha256": "15e9b110999c37a0a78ecd001d6b79bbd860b408cf08a1e0ea435690f0a5f041" + "map_sha256": "58741b7c6c8ca8f0ead476ec9893355419c9550d9bdb9287bec57928ce345afb", + "protocol_sha256": "b4c450a618c5ca3116f806d1ae270ae6fb410517404c204e505d10418bc1181f" }, "failures": [], "observed": { - "map_sha256": "3cf71bdd3e6df567a4f9eb8d182bea160108cc9941e6ff08b8a0eee4e9fc8ba2", - "protocol_sha256": "15e9b110999c37a0a78ecd001d6b79bbd860b408cf08a1e0ea435690f0a5f041" + "map_sha256": "58741b7c6c8ca8f0ead476ec9893355419c9550d9bdb9287bec57928ce345afb", + "protocol_sha256": "b4c450a618c5ca3116f806d1ae270ae6fb410517404c204e505d10418bc1181f" }, "status": "covered" }, @@ -1677,7 +1677,7 @@ "compiler_ir.seed_stream_map" ], "expected": { - "implementation_sha256": "15e9b110999c37a0a78ecd001d6b79bbd860b408cf08a1e0ea435690f0a5f041", + "implementation_sha256": "b4c450a618c5ca3116f806d1ae270ae6fb410517404c204e505d10418bc1181f", "protocol": "legacy-v1", "streams": [ "build_model", @@ -1698,7 +1698,7 @@ }, "failures": [], "observed": { - "implementation_sha256": "15e9b110999c37a0a78ecd001d6b79bbd860b408cf08a1e0ea435690f0a5f041", + "implementation_sha256": "b4c450a618c5ca3116f806d1ae270ae6fb410517404c204e505d10418bc1181f", "protocol": "legacy-v1", "streams": [ "build_model", @@ -2599,7 +2599,7 @@ "country": "us", "schema_id": "country_spec", "schema_version": 1, - "spec_sha256": "97f41a9fedba0ed1f26dd5f457997c10ffefe852c75f8fc18c793acdf21ebfd3" + "spec_sha256": "77df608c9b343219cd2717aa08af5198c0ba69b70c61691fb2459f7a886528ba" } }, "report_schema_version": 3, @@ -2609,7 +2609,7 @@ "country": "us", "schema_id": "country_spec", "schema_version": 1, - "spec_sha256": "97f41a9fedba0ed1f26dd5f457997c10ffefe852c75f8fc18c793acdf21ebfd3" + "spec_sha256": "77df608c9b343219cd2717aa08af5198c0ba69b70c61691fb2459f7a886528ba" }, "status": "pass" } diff --git a/docs/us-county-2023-geography.md b/docs/us-county-2023-geography.md new file mode 100644 index 0000000000..f0eaaffac5 --- /dev/null +++ b/docs/us-county-2023-geography.md @@ -0,0 +1,124 @@ +# 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`. +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: + +- `policyengine_us/variables/household/demographic/geographic/county/county_enum.py:315–322` + names Connecticut's eight legacy counties, rather than planning regions. +- `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. +- `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`. +- `policyengine_us/variables/input/geography.py:130–135` + declares `county_fips` as a string household input. The engine's + `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, 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`. +- `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 + +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. 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. + +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 + +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. + +axiom: n/a: data geography infrastructure 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 f86db2dfb8..45da55cb35 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_437 -EXPECTED_RESOLVED_BINDING_FIELD_COUNT = 9_780 -EXPECTED_CONFIGURATION_FIELD_COUNT = 42_217 +EXPECTED_RESOLVED_BINDING_FIELD_COUNT = 9_781 +EXPECTED_CONFIGURATION_FIELD_COUNT = 42_218 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 a463092b96..0b31e123ea 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 @@ -368,9 +368,10 @@ # keeps the US bracket, and the budget search, which the US solve does # not run, changes its probes, so again no US solve changes. #454: the # meps_esi_premiums stage joins the attested sources and the source - # manifest. - "seed_map": "3cf71bdd3e6df567a4f9eb8d182bea160108cc9941e6ff08b8a0eee4e9fc8ba2", - "seed_protocol": "15e9b110999c37a0a78ecd001d6b79bbd860b408cf08a1e0ea435690f0a5f041", + # manifest. #1376: the puma_county_vintage draw module joins the seed + # protocol's attested kernel modules. + "seed_map": "58741b7c6c8ca8f0ead476ec9893355419c9550d9bdb9287bec57928ce345afb", + "seed_protocol": "b4c450a618c5ca3116f806d1ae270ae6fb410517404c204e505d10418bc1181f", "source_manifest": "629a0712659a26b2d6aa6c0e5672421f415d625e0599d6baa0a834d118953c6a", "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/src/microcosm/build/us_runtime/base_pool.py b/packages/microcosm-build/src/microcosm/build/us_runtime/base_pool.py index eb32242c45..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 @@ -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,16 @@ 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. + # _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( + [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/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/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..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 @@ -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,24 @@ 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 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_contract/us/test_spec_engine_coverage_tool.py b/packages/microcosm-build/tests/engine_contract/us/test_spec_engine_coverage_tool.py index 3a0dd0e3d9..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 @@ -82,22 +82,22 @@ 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_217 + assert fields["configuration_field_count"] == 42_218 assert fields["authored_normative_field_count"] == 32_437 - assert fields["resolved_binding_field_count"] == 9_780 - assert fields["consumed_field_count"] == 42_217 + assert fields["resolved_binding_field_count"] == 9_781 + assert fields["consumed_field_count"] == 42_218 assert fields["unused_field_count"] == 0 assert fields["multiple_primary_use_field_count"] == 0 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/shared/test_spec_engine_field_usage.py b/packages/microcosm-build/tests/engine_free/shared/test_spec_engine_field_usage.py index 79d903c887..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 @@ -95,22 +95,22 @@ 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_217 + assert len(field_ledger.fields) == EXPECTED_CONFIGURATION_FIELD_COUNT == 42_218 assert field_ledger.source_counts == { "authored": 32_437, - "resolved_bindings": 9_780, + "resolved_bindings": 9_781, } 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_217 + assert len({field.pointer for field in field_ledger.fields}) == 42_218 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 58dd3f305b..875fd52dac 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 == ( - "44e3f5f2f6c2a83f105d7f1ffa93ca038eb7f6dd722d0c784637f574d33c6fa0" + "bc30e1571ee5c191ca70f9bdfa4d9dcd371b5a5f551518d8945f49f3eea0bcd7" ) second_root = _rich_minimal(tmp_path / "xy", note="second", store="local:b") 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..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 @@ -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,112 @@ def test__given_donor_assigned_geography__then_pool_preserves_it_and_derives_pum assert acs_row["county_fips"].tolist() == ["01001"] +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 + ) + return 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, + ) + + +@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, + 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_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"], 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/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..08266ad6c3 --- /dev/null +++ b/packages/microcosm-build/tests/engine_free/us/test_us_county_vintage_ladders.py @@ -0,0 +1,220 @@ +"""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, + us_puma_ladder_gate, +) +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"} + ) + == () + ) + + +@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, + # 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", + ] 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 628538479f..7240abe15f 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 @@ -162,6 +162,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_spine_blindness.py b/packages/microcosm-build/tests/engine_free/us/test_us_spine_blindness.py index f5f14473c4..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 @@ -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", @@ -300,6 +303,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", @@ -3506,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, ( 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-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 d74db8e007..611a4c3df2 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,8 +343,9 @@ 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, - # and so does microcosm#454's meps_esi_premiums stage. - "spec_sha256": "97f41a9fedba0ed1f26dd5f457997c10ffefe852c75f8fc18c793acdf21ebfd3", + # and so do microcosm#454's meps_esi_premiums stage and + # microcosm#1376's puma_county_vintage draw module. + "spec_sha256": "77df608c9b343219cd2717aa08af5198c0ba69b70c61691fb2459f7a886528ba", }, } 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/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 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 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() diff --git a/tools/spec_engine_coverage.py b/tools/spec_engine_coverage.py index 16a8e339db..557999f172 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 = ( - "c1e2a118a0d53db0bb628b14357a2c86db2956332f5b359c9092764f9c451a95" + "a0da3562ad5559cbbcdf5ee14ebd21db2f067c90e7e470e24d4e3762d7b90848" ) DEFAULT_REPORT_PATH = ( Path(__file__).resolve().parents[1]