Skip to content

Fix epacriteriaairpollutants - #2206

Open
shourya116 wants to merge 12 commits into
datacommonsorg:masterfrom
shourya116:fix-epacriteriaairpollutants
Open

shourya116 wants to merge 12 commits into
datacommonsorg:masterfrom
shourya116:fix-epacriteriaairpollutants

Conversation

@shourya116

@shourya116 shourya116 commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Resolves two consecutive execution failures of the EPACriteriaAirPollutants import pipeline:

  1. Incident 1 (Validation Gate Failure): Blocked at ImportStatus.VALIDATION due to 18.1M missing measurement method references (NO2_1hour_2010), 875K fatal observationAbout reference errors from an unregistered Mexican cross-border monitor (epa/800060001), and differ summary DATA_ERROR.
  2. Incident 2 (Differ OOM Crash / Exit Code 50002): Cloud Batch VM kernel freeze caused by the Python in-memory differ (mcf_file_util.load_mcf_nodes) attempting to load 121.83 GB of MCF text (305M nodes) into Python dictionaries on a 128 GiB RAM VM.

Key Changes

1. Data Extraction & Pipeline Reliability (scripts/us_epa/airdata/air_quality.py)

  • Geographic Scoping: Filter out non-US cross-border monitors (State Code '80' for Mexico and 'CC' for Canada) that lack Data Commons place entities, eliminating ~875K fatal lint errors.
  • Unit Normalization: Map observation units via UNIT_MAP (MicrogramsPerCubicMeter_lc, MicrogramsPerCubicMeter_25C, PartsPerMillion, PartsPerBillion) with fallback to camel case.
  • HTTP Connection Pooling & Retries: Wrap downloads in requests.Session() with urllib3.util.Retry (10 retries, exponential backoff factor 2, retry status codes [429, 500, 502, 503, 504], and 120s timeout) to prevent dropped connections during multi-year archive extraction.

2. Schema Declarations & Zero-Error Pre-declarations (scripts/us_epa/airdata/EPA_AirQuality.mcf)

  • Declare missing Measurement Method enums (NO2_1hour_2010, Ozone_8hour_2015, SO2_1hour_2010, PM25_24hour_2012, PM10_24hour_2006, etc.).
  • Declare custom units (MicrogramsPerCubicMeter_lc, MicrogramsPerCubicMeter_25C).
  • Pre-declare all 8,404 EPA monitoring station entities (AirQualitySite) to satisfy strict zero lint errors and zero missing references in local resolution mode.

3. Automation & Differ Guardrail (scripts/us_epa/airdata/manifest.json)

  • Set "config_override": {"invoke_differ_tool": false} to safely bypass the in-memory differ on the 121+ GB MCF output and prevent VM memory exhaustion.
  • Wire "validation_config_file": "validation_config.json".
  • Set "node_mcf": "EPA_AirQuality.mcf" under import_inputs.
  • Specify resource limits (cpu: 32, memory: 128, disk: 512).

4. Import Validation Configuration (scripts/us_epa/airdata/validation_config.json)

  • Disable check_deleted_records_percent to prevent DATA_ERROR when the differ tool is bypassed.
  • Strictly enforce threshold: 0 for check_missing_refs_count and check_lint_error_count.
  • Add automated date validation rules:
    • check_max_date_consistent (MAX_DATE_CONSISTENT): Verifies uniform MaxDate across all 18 air quality StatVars.
    • check_date_freshness (SQL_VALIDATOR): Enforces that maximum observation date meets freshness requirements (latest_date >= '2024').

5. Unit Tests & Test Data (scripts/us_epa/airdata/air_quality_test.py, test_data/test_import.csv)

  • Added unit tests for cross-border monitor filtering (test_filter_cross_border_monitors).
  • Added unit tests for unit mapping (test_unit_mapping).
  • Updated expected CSV fixtures with normalized unit representations.

Verification and Test Run Proof

Cloud Batch Staging Execution

  • Cloud Batch Job: epacriteriaairpollutants-shouryasingh-20260914-075748
  • GCS Staging Version: gs://datcom-import-test/scripts/us_epa/airdata/EPACriteriaAirPollutants/2026_09_14T01_01_32_581690_07_00
  • Execution Latencies:
    • Script (CSV Extraction): ~28 mins (64,802,212 rows / 16.45 GB)
    • GenMCF (Transformation): ~3.16 hours (305,269,438 nodes / 121.83 GB)
    • Differ: Safely bypassed (0 min, 0 RAM consumed)
    • Validation: ~1 sec
    • Total Wall Clock Time: ~3.7 hours (Exit Code: 0)
  • Pipeline Status: Successfully promoted to ImportStatus.STAGING.

Validation Report (validation_output.csv)

Validation Check Status Details
check_empty_import PASSED 305,269,438 nodes, 64,802,212 rows
check_missing_refs_count PASSED 0 missing references (threshold: 0)
check_lint_error_count PASSED 0 fatal lint errors (threshold: 0)
check_max_date_consistent PASSED Uniform 2025-12-31 across all 18 StatVars
check_date_freshness PASSED latest_date >= '2024' satisfied (2025-12-31)

Local Unit Tests

  • python3 -m unittest discover -v -s scripts/us_epa/airdata -p "*_test.py": 7/7 tests passed.

References

- Pre-declare 47 unindexed EPA Air Quality monitoring stations in EPA_AirQuality.mcf
- Standardize units and add retry logic in air_quality.py
- Add node_mcf, validation_config_file, and resource limits to manifest.json
- Add validation_config.json with strict zero lint error threshold
@google-cla

google-cla Bot commented Sep 6, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a diagnostic runbook, configures validation rules, and updates the air_quality.py script to handle unit mapping, filter out cross-border monitors, and use a robust HTTP session with retries for downloading data. Feedback on the changes includes addressing a critical runtime AttributeError when configuring retries, relaxing the lint error threshold in validation_config.json to prevent pipeline failures from new stations, and resolving a potential KeyError and eager evaluation issue when parsing observation units.

Comment thread scripts/us_epa/airdata/air_quality.py Outdated
Comment thread scripts/us_epa/airdata/validation_config.json
Comment thread scripts/us_epa/airdata/air_quality.py Outdated
shourya116 and others added 10 commits September 6, 2026 19:06
…ource_files, add date validation rules, and add unit tests
…force strict zero validation thresholds

- Restore all 8,408 EPA monitoring station declarations in EPA_AirQuality.mcf to eliminate remote DC API calls, missing observationAbout errors, and location reference warnings
- Restore node_mcf in manifest.json and keep differ disabled
- Set strict zero threshold for check_missing_refs_count and check_lint_error_count in validation_config.json
- Format air_quality_test.py with yapf

@abhishekjaisw abhishekjaisw left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review scope

  • Target: PR #2206 (7cdda42598c5cce772ee173325c655b016f6d083)
  • Reviewed: EPA_AirQuality.mcf, air_quality.py, air_quality_test.py, manifest.json, test_data/test_import.csv, validation_config.json
  • Skipped: None

Summary of Progression

Thank you for updating the PR description with the detailed incident summaries and staging verification (epacriteriaairpollutants-shouryasingh-20260914-075748 -> gs://datcom-import-test/scripts/us_epa/airdata/EPACriteriaAirPollutants/2026_09_14T01_01_32_581690_07_00, achieving ImportStatus.STAGING with 0 missing refs and 0 lint errors), and for adding unit tests (7/7 passing).

Three remaining schema/automation items need attention before merging (see inline comments):

  1. [P1] Remove duplicate canonical StatVars & Units from EPA_AirQuality.mcf (lines 1–158): Inspecting input0/genmcf/instance_mcf_nodes.mcf from the Sep 14 run confirms that genmcf ingests all 18 canonical StatVars and 2 units into instance_mcf_nodes.mcf (lines 97460 and 114284). Because genmcf's built-in schema cache already resolves canonical Google3 StatVars (manual_epa_stat_vars.mcf) and units (unit.mcf) locally without node_mcf, lines 1–158 should be removed so only the 21 MeasurementMethodEnum nodes remain in EPA_AirQuality.mcf.
  2. [P1] Dynamically generate AirQualitySite nodes (EPA_AirQuality_sites.mcf) instead of hardcoding 8,404 stations in Git: Pre-populating AirQualitySite nodes via node_mcf is necessary so genmcf populates local_nodes before validating table_mcf rows—preventing remote DC API lookups and achieving 0 missing refs and 0 lint errors. However, hardcoding 8,404 static stations in Git paired with threshold: 0 means future automated cron refreshes (15 22 10 1,7 *) will fail whenever EPA adds a single new monitoring station. Having air_quality.py dynamically write unique AirQualitySite nodes to EPA_AirQuality_sites.mcf at runtime and setting "node_mcf": "EPA_AirQuality*.mcf" in manifest.json preserves threshold: 0 automatically on every future refresh without 50,000+ lines of static Git bloat.
  3. [P2] Use dynamic year expression in check_date_freshness: Replace static '2024' with CAST(SUBSTR(latest_date, 1, 4) AS INT) >= YEAR(CURRENT_DATE) - 1.

Positive findings

  • scripts/us_epa/airdata/air_quality.py:221-236 - Resilient HTTP Session with Exponential Backoff and Retries ✓
    • Finding: Good - Configuring requests.Session with urllib3.util.Retry (10 retries, backoff factor 2, status forcelist [429, 500, 502, 503, 504]) and 120s timeout provides strong fault tolerance across 270+ annual archive downloads.
  • scripts/us_epa/airdata/air_quality.py:138-140 & air_quality_test.py:63-151 - Foreign Monitor Filtering and Unit Mapping with Unit Tests ✓
    • Finding: Good - Filtering out cross-border monitors (State Code in ('80', 'CC')) and normalizing unit strings (UNIT_MAP) cleanly resolves 875K fatal observationAbout errors, verified by unit tests (7/7 passing).
  • scripts/us_epa/airdata/manifest.json:25-33 - Appropriate Resource Sizing, Differ Bypass, and PR Documentation ✓
    • Finding: Good - Allocating 32 vCPUs / 128 GiB RAM / 512 GB disk, setting invoke_differ_tool: false, and documenting the RCA and staging proof in the PR description prevents the 121.83 GB MCF in-memory diffing OOM kernel freeze.

Coverage

File Status Result
scripts/us_epa/airdata/EPA_AirQuality.mcf Reviewed Two P1 findings
scripts/us_epa/airdata/manifest.json Reviewed Two P1 findings
scripts/us_epa/airdata/validation_config.json Reviewed One P1 finding, One P2 finding
scripts/us_epa/airdata/air_quality.py Reviewed Positive findings
scripts/us_epa/airdata/air_quality_test.py Reviewed Positive findings
scripts/us_epa/airdata/test_data/test_import.csv Reviewed No findings

Verification and limitations

  • Checks run: Executed all 7 unit tests in scripts/us_epa/airdata (7/7 OK) and verified yapf formatting (0 diffs) in detached worktree at 7cdda42598c5cce772ee173325c655b016f6d083; audited GCS staging artifacts (instance_mcf_nodes.mcf, report.json, import_summary.json) for 2026_09_10T13_01_42_117377_07_00 and 2026_09_14T01_01_32_581690_07_00; verified Google3 schema declarations via Code Search.
  • Checks not run: Live download of 1980–2025 EPA AQS archives (~3.7h runtime).
  • Limitations: Note that inline comments for EPA_AirQuality.mcf are attached to manifest.json and validation_config.json because EPA_AirQuality.mcf (+50,525 lines) exceeds GitHub's diff size limit for inline comments.

"template_mcf": "EPA_AirQuality.tmcf",
"cleaned_csv": "EPA_AirQuality.csv"
"cleaned_csv": "EPA_AirQuality.csv",
"node_mcf": "EPA_AirQuality.mcf"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Redundant declaration of existing canonical StatVars and Units in EPA_AirQuality.mcf (node_mcf) causes double-ingestion as instance nodes

(Comment attached to manifest.json because EPA_AirQuality.mcf exceeds GitHub's diff size limit for inline comments.)

Finding: EPA_AirQuality.mcf lines 1–146 declare 18 StatisticalVariable nodes (Mean_Concentration_AirPollutant_Ozone, etc.) that already exist in canonical Google3 schema (//depot/google3/third_party/datacommons/schema/stat_vars/manual_epa_stat_vars.mcf), and lines 147–158 declare 2 UnitOfMeasure nodes (MicrogramsPerCubicMeter_lc, MicrogramsPerCubicMeter_25C) that already exist in //depot/google3/third_party/datacommons/schema/core/unit.mcf. Because air_quality.py is a non-StatVarProcessor import and manifest.json includes "node_mcf": "EPA_AirQuality.mcf", genmcf ingests all 18 canonical StatVars and 2 units directly into input0/genmcf/instance_mcf_nodes.mcf (verified at lines 97460 and 114284 in the Sep 14 staging run). Note that genmcf's built-in schema cache already resolves these canonical StatVars and units locally without node_mcf (as shown by the Sep 10 run reporting 0 missing reference errors on variableMeasured or unit).

Impact: Causes double-ingestion of 18 canonical StatVars and 2 units as instance nodes in the Knowledge Graph.

Recommendation: Remove lines 1–158 (the 18 StatisticalVariable nodes and 2 UnitOfMeasure nodes) from EPA_AirQuality.mcf, retaining only the 21 MeasurementMethodEnum nodes (Ozone_8hour_2015, NO2_1hour_2010, etc., lines 159–244) which do not exist in Google3 schema.

"rule_id": "check_missing_refs_count",
"validator": "MISSING_REFS_COUNT",
"params": {
"threshold": 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Fragile static snapshot of 8,404 AirQualitySite nodes in EPA_AirQuality.mcf paired with threshold: 0 validation

Finding: Checking 8,404 hardcoded AirQualitySite instance nodes (lines 246–50671 of EPA_AirQuality.mcf) into Git pre-populates genmcf's local symbol table (local_nodes), which resolved the observationAbout lint errors and location warnings seen when node_mcf was omitted (because genmcf does not index streamed table_mcf entities into local_nodes prior to reference checking). However, hardcoding a static station list in Git paired with threshold: 0 on check_missing_refs_count and check_lint_error_count is operationally brittle: whenever EPA commissions even 1 new monitoring station in future scheduled cron runs (cron_schedule: "15 22 10 1,7 *"), that station will be absent from EPA_AirQuality.mcf, causing genmcf to query the remote DC API (where the new station does not yet exist) and emit missing reference errors/warnings that immediately fail validation against threshold: 0.

Impact: Future automated cron refreshes will break whenever upstream EPA adds a new air quality monitoring station, requiring manual code updates to a 50,000-line file, and repository history is bloated by 1.37 MB of instance data.

Recommendation:

  1. Update air_quality.py (write_csv) to dynamically write unique AirQualitySite nodes encountered during CSV processing to a generated file EPA_AirQuality_sites.mcf at runtime.
  2. Remove the 8,404 hardcoded station nodes (lines 246–50671) from EPA_AirQuality.mcf.
  3. Update manifest.json to use a wildcard pattern "node_mcf": "EPA_AirQuality*.mcf" so genmcf loads both EPA_AirQuality.mcf (the 21 measurement methods) and the dynamically generated EPA_AirQuality_sites.mcf on every run.

"validator": "SQL_VALIDATOR",
"params": {
"query": "SELECT MAX(MaxDate) AS latest_date FROM stats",
"condition": "latest_date >= '2024'"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Hardcoded static year cutoff (>= '2024') in check_date_freshness

Finding: check_date_freshness uses a static string literal "condition": "latest_date >= '2024'". Because air_quality.py dynamically processes data up to datetime.now().year - 1 (MaxDate is 2025-12-31 in 2026), a hardcoded '2024' threshold will never advance in future years (2027, 2028, etc.), allowing multi-year upstream data stalls to pass validation undetected.

Impact: Future scheduled refreshes that fail to ingest new annual files will pass freshness validation silently.

Recommendation: Use a dynamic DuckDB date expression verifying that MaxDate is within the expected 1-year publication lag:

"condition": "CAST(SUBSTR(latest_date, 1, 4) AS INT) >= YEAR(CURRENT_DATE) - 1"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants