diff --git a/common/polars.py b/common/polars.py index ec0e45af3..3f6748920 100644 --- a/common/polars.py +++ b/common/polars.py @@ -1,5 +1,6 @@ from __future__ import annotations +import collections.abc import re import typing from dataclasses import dataclass @@ -181,6 +182,18 @@ def drop( *columns: ColumnNameOrSelector | Iterable[ColumnNameOrSelector], strict: bool = True, ) -> PathsDataFrame: + if strict: + requested: list[str] = [] + for col in columns: + if isinstance(col, str): + requested.append(col) + elif isinstance(col, collections.abc.Iterable): + requested.extend(c for c in col if isinstance(c, str)) + missing = [col for col in requested if col not in self.columns] + if missing: + raise Exception( + 'Cannot drop non-existent column(s) %s; available columns: %s' % (', '.join(missing), ', '.join(self.columns)) + ) meta = self.get_meta() df = super().drop(*columns, strict=strict) for col in list(meta.units.keys()): diff --git a/configs/longmont-dev.yaml b/configs/longmont-dev.yaml index 4504aad9a..2b076da3a 100644 --- a/configs/longmont-dev.yaml +++ b/configs/longmont-dev.yaml @@ -14,7 +14,7 @@ default_language: en supported_languages: [es-US] dataset_repo: url: https://github.com/kausaltech/dvctest.git - commit: ad50da5a21200b37695fc462081d7872840f6991 + commit: a7eacebcebdc8b131bc25a69fed8d8e38e2cedd7 dvc_remote: kausal-s3 name: 2025 Draft Longmont Greenhouse Gas Inventory name_es-US: 2025 Inventario de gases de efecto invernadero de Longmont @@ -55,9 +55,7 @@ result_excels: - aviation_fuel_usage - building_class_mix - building_electricity_eui_by_action - - building_electricity_use_historical - building_natural_gas_eui_by_action - - building_natural_gas_use_historical - building_square_footage_new - building_square_footage_old - commercial_air_conditioned_square_footage @@ -1123,16 +1121,12 @@ nodes: quantity: floor_area unit: ft^2 output_dimensions: [emission_sectors] - input_datasets: - - id: longmont/building_square_footage - tags: [all] - forecast_from: 2025 - column: floor_area input_nodes: - id: building_square_footage_old tags: [old] + - id: population params: - formula: all + arithmetic_inverse(old) + formula: old * (ratio_to_max_hist_year(population) - 1) - id: building_square_footage_by_building_class name: Building square footage by building class @@ -1159,17 +1153,122 @@ nodes: params: formula: (old + new) * action_mix +- id: updated_building_code_stock + name: Updated building code (accumulated stock share) + description: Cumulative share of existing building stock brought under the + updated building code so far, built from the action's own yearly incremental + share (see updated_building_code) so that each year's floor area is weighted + by the code vintage in effect when it was brought under the code, not by the + current year's improvement figure. + type: formula.FormulaNode + quantity: fraction + unit: '%' + input_dimensions: [building_energy_class, emission_sectors] + output_dimensions: [building_energy_class, emission_sectors] + input_nodes: + - id: updated_building_code + tags: [flow] + params: + formula: cumulative(flow) + +- id: develop_beyond_building_code_stock + name: Develop beyond building code (accumulated stock share) + description: Cumulative share of new building stock developed beyond code so + far, built from the action's own yearly incremental share (see + develop_beyond_building_code) so that each year's floor area is weighted by + the vintage in effect when it was built, not by the current year's + improvement figure. + type: formula.FormulaNode + quantity: fraction + unit: '%' + input_dimensions: [building_energy_class, emission_sectors] + output_dimensions: [building_energy_class, emission_sectors] + input_nodes: + - id: develop_beyond_building_code + tags: [flow] + params: + formula: cumulative(flow) + +- id: building_class_mix_none + name: Building class mix (no action taken, residual) + description: The complement of whatever share has been claimed by + updated_building_code and develop_beyond_building_code, so the two action + shares plus this one always sum to 100% for every (building_energy_class, + emission_sectors) cell. benchmark_energy_use has no floor-area share here -- + it's now a direct GWh/a subtraction on building_electricity_use / + building_natural_gas_use, per request to keep it out of the EUI + path (see benchmark_energy_use's own node and README item 5). + type: formula.FormulaNode + quantity: fraction + unit: '%' + input_dimensions: [building_energy_class, emission_sectors] + output_dimensions: [building_energy_class, emission_sectors] + input_nodes: + - id: updated_building_code_stock + tags: [ubc] + - id: develop_beyond_building_code_stock + tags: [dbbc] + params: + formula: complement(ubc + dbbc) + - id: building_class_mix name: Building class mix - type: generic.GenericNode + description: "Floor area by building class\nBuilding class mix — the share of floor area classified under + each action, per (building energy class, emission sector) cell: where a ∈ {updated_building_code, develop_beyond_building_code}. + benchmark_energy_use is not one of these -- it has no floor-area share; it's a + direct GWh/a subtraction on building_electricity_use / building_natural_gas_use + instead (see benchmark_energy_use's own node).\nVintage-weighted EUI improvement + — per action, weighted by when the floor area entered, not by the current year's + rate: \n\ + EUI by action: \nTotal building + energy use: " + + type: formula.FormulaNode quantity: fraction unit: '%' input_dimensions: [action, building_energy_class, emission_sectors] output_dimensions: [action, building_energy_class, emission_sectors] - input_datasets: - - id: longmont/building_class_mix + input_nodes: + - id: updated_building_code_stock + tags: [ubc] + to_dimensions: + - id: building_energy_class + - id: emission_sectors + - id: action + categories: [updated_building_code] + - id: develop_beyond_building_code_stock + tags: [dbbc] + to_dimensions: + - id: building_energy_class + - id: emission_sectors + - id: action + categories: [updated_building_code] + - id: building_class_mix_none + tags: [none_share] + to_dimensions: + - id: building_energy_class + - id: emission_sectors + - id: action + categories: [none] params: - operations: get_single_dataset,add_to_existing_dims + formula: ubc + dbbc + none_share - id: electricity_use_on_streetlights_and_internal_operations name: Electricity use on streetlights and internal operations @@ -1192,116 +1291,18 @@ nodes: drop_col: false output_nodes: - id: electricity_use + tags: [streetlights] from_dimensions: - id: site flatten: true -# TODO MAYBE REMOVE ALL THESE NODES --v - -- id: building_electricity_use_historical - name: Building electricity use (historical) - name_es-US: Consumo de electricidad en edificios (histórico) - type: generic.GenericNode - quantity: energy - unit: GWh/a - input_dimensions: [emission_sectors, site] - output_dimensions: [emission_sectors, site] - input_datasets: - - id: longmont/stationary_energy_consumption - column: electricity - filters: - - column: energy_carrier - value: electricity - -- id: building_electricity_use_per_resident - name: Building electricity use per resident - name_es-US: Consumo de electricidad en edificios por residente - type: formula.FormulaNode - quantity: per_capita - unit: GWh/a/cap - input_nodes: - - id: building_electricity_use_historical - from_dimensions: - - id: site - flatten: true - - id: population - to_dimensions: [] - input_dimensions: [emission_sectors] - output_dimensions: [emission_sectors] - params: - formula: extend_values(inventory_only(building_electricity_use_historical / - population)) - -- id: building_electricity_use_bau - name: Building electricity use (baseline) - name_es-US: Consumo de electricidad en edificios (referencia) - type: generic.GenericNode - quantity: energy - unit: GWh/a - input_nodes: - - id: building_electricity_use_per_resident - - id: population - to_dimensions: [] - input_dimensions: [emission_sectors] - output_dimensions: [emission_sectors] - -- id: building_natural_gas_use_historical - name: Building natural gas use (historical) - name_es-US: Consumo de gas natural en edificios (histórico) - type: buildings.HistoricalNode # FIXME - quantity: energy - unit: Mtherm/a - input_dimensions: [emission_sectors, site] - output_dimensions: [emission_sectors, site] - input_datasets: - - id: longmont/stationary_energy_consumption - column: natural_gas - filters: - - column: energy_carrier - value: natural_gas - input_dataset_processors: [LinearInterpolation] - -- id: building_natural_gas_use_per_resident - name: Building natural gas use per resident - name_es-US: Consumo de gas natural en edificios por residente - type: formula.FormulaNode - quantity: per_capita - unit: Mtherm/a/cap - input_nodes: - - id: building_natural_gas_use_historical - from_dimensions: - - id: site - flatten: true - - id: population - to_dimensions: [] - input_dimensions: [emission_sectors] - output_dimensions: [emission_sectors] - params: - formula: extend_values(inventory_only(building_natural_gas_use_historical / - population)) - -- id: building_natural_gas_use_bau - name: Building natural gas use (baseline) - name_es-US: Consumo de gas natural en edificios (referencia) - type: generic.GenericNode - quantity: energy - unit: Mtherm/a - input_nodes: - - id: building_natural_gas_use_per_resident - - id: population - to_dimensions: [] - input_dimensions: [emission_sectors] - output_dimensions: [emission_sectors] - -## TODO MAYBE REMOVE ALL THESE NODES ABOVE --^ - - id: building_natural_gas_eui_by_action name: Energy use intensity EUI baseline for natural gas by energy class type: formula.FormulaNode quantity: consumption_factor unit: therm/ft**2/a - input_dimensions: [emission_sectors, action] - output_dimensions: [emission_sectors, action] + input_dimensions: [building_energy_class, emission_sectors, action] + output_dimensions: [building_energy_class, emission_sectors, action] input_datasets: - id: longmont/building_energy_use_intensity_eui column: natural_gas @@ -1309,10 +1310,9 @@ nodes: tags: [eui, extend_both_ways] output_dimensions: [emission_sectors] input_nodes: - - id: energy_use_intensity_trend - metrics: [natural_gas] + - id: energy_use_intensity_trend_natural_gas tags: [trend, extend_both_ways] - - id: energy_use_intensity_improvement + - id: energy_use_intensity_improvement_vintage tags: [improvement] params: formula: (eui + trend) * complement(improvement) @@ -1320,7 +1320,10 @@ nodes: - id: building_natural_gas_use name: Building natural gas use name_es-US: Consumo de gas natural en edificios - description: Unit: Mthm/yr. = million therms per year. + description: Unit: Mthm/yr. = million therms per year. Includes a + direct Mthm/a-scale subtraction of benchmark_energy_use's cumulative savings, + assigned to the existing building stock (see that action's own node for the + rationale). description_es-US: Unidad: Mthm/año = millones de termias al año. type: formula.FormulaNode quantity: energy @@ -1337,18 +1340,20 @@ nodes: - id: building_natural_gas_eui_by_action tags: [eui] to_dimensions: + - id: building_energy_class - id: emission_sectors - id: action params: - formula: sum_dim(square_footage * eui, action) * efficiency_works_program + formula: sum_dim(square_footage * eui, action) * efficiency_works_program - + benchmark_natural_gas - id: building_electricity_eui_by_action name: Energy use intensity EUI for electricity by action type: formula.FormulaNode quantity: consumption_factor unit: kWh/ft**2/a - input_dimensions: [emission_sectors, action] - output_dimensions: [emission_sectors, action] + input_dimensions: [building_energy_class, emission_sectors, action] + output_dimensions: [building_energy_class, emission_sectors, action] input_datasets: - id: longmont/building_energy_use_intensity_eui column: electricity @@ -1356,10 +1361,9 @@ nodes: forecast_from: 2025 output_dimensions: [emission_sectors] input_nodes: - - id: energy_use_intensity_improvement + - id: energy_use_intensity_improvement_vintage tags: [improvement] - - id: energy_use_intensity_trend - metrics: [electricity] + - id: energy_use_intensity_trend_electricity tags: [trend] params: formula: (eui + trend) * complement(improvement) @@ -1368,7 +1372,9 @@ nodes: name: Building electricity use name_es-US: Consumo de electricidad en edificios description: Unit: GWh/yr. = gigawatt-hours per year = million - kilowatt-hours per year. + kilowatt-hours per year. Includes a direct GWh/a-scale subtraction of + benchmark_energy_use's cumulative savings, assigned to the existing building + stock (see that action's own node for the rationale). description_es-US: Unidad: GWh/año = gigavatios hora al año = millones de kilovatios hora al año. type: formula.FormulaNode @@ -1386,10 +1392,12 @@ nodes: - id: building_electricity_eui_by_action tags: [eui] to_dimensions: + - id: building_energy_class - id: emission_sectors - id: action params: - formula: sum_dim(square_footage * eui, action) * efficiency_works_program + formula: sum_dim(square_footage * eui, action) * efficiency_works_program - + benchmark_electricity - id: electricity_use name: Electricity use @@ -1398,15 +1406,16 @@ nodes: kilowatt-hours per year. description_es-US: Unidad: GWh/año = gigavatios hora al año = millones de kilovatios hora al año. - type: generic.ActionWithHistoryNode + type: formula.FormulaNode quantity: energy unit: GWh/a input_dimensions: [emission_sectors] output_dimensions: [emission_sectors] params: - operations: add,action_with_history + formula: (buildings + streetlights) * (1 - reduce_losses) input_nodes: - id: building_electricity_use + tags: [buildings] from_dimensions: - id: building_energy_class flatten: true @@ -1670,57 +1679,64 @@ nodes: - id: stationary_fuel_consumption_emissions name: Stationary fuel consumption emissions name_es-US: Emisiones del consumo de combustible estacionario - description:

1) Stationary Diesel Use data was provided by CDPHE. Spreadsheet - on file.

2) Xcel Energy emission factor, electricity and natural gas usage - data, and renewable energy data is from Xcel's 2021 Community Energy Report for - Longmont, on file.

3) Longmont Power & Communications data provided. PDF - on file. The commercial buildings in Longmont's Weld County portion consumed 31,000,000 - kWh in 2021.

4) T&D losses are calculated for Xcel and Longmont based on - the EIA 861 (https://www.eia.gov/electricity/data/eia861/) reported data. Specifically - the Utility Operational Data_2021. Spreadsheet on file.

5) Propane data - provided by FerrellGas. Spreadsheet on file.

6) Houses heated with wood - were pulled from the US Census Bureau, data on file. It was assumed that all of - the houses heated by wood in Longmont fell within the Boulder County portion of - Longmont.

7) Longmont Power and Communications data was provided by Longmont - Power & Communications. PDF on file.

8) Longmont Power & Communications - and Lyons Municipal rooftop solar data was provided in kW of capacity rather than - kWh generated. NREL's PVWatts was used to estimate kWh generated by rooftop solar - systems. PDF of results on file. See https://pvwatts.nrel.gov/

10) Due - to Xcel's 15/15 rule, with less than 15 customers in the commercial and residential - data sets they cannot provide usage for those customers to avoid violating privacy - concerns. Therefore Lotus applied the average residential and commercial kWh use - from LPC to the 7 residential and commercial customers (respectively) in Xcel - territory to estimate use.

11) PRPA emission factor and transmission and - distribution loss rate for 2021 was provided. Email on file. Emissions factor - value represents Longmont's load share emissions rate without unbundled RECs.

- description_es-US:

1) Los datos sobre el consumo de diésel estacionario fueron - brindados por el CDPHE. Hoja de cálculo en el archivo.

2) El factor de - emisión de Xcel Energy, los datos de consumo de electricidad y gas natural y los - datos de energía renovable provienen del Informe de energía comunitaria 2021 de - Xcel para Longmont (en el archivo).

3) Datos facilitados por Longmont Power - & Communications. PDF en el archivo. Los edificios comerciales en la parte del - condado de Weld de Longmont consumieron 31.000.000 kWh en el año 2021.

4) - Las pérdidas de transmisión y distribución se calculan para Xcel y Longmont según - los datos de la EIA 861 (https://www.eia.gov/electricity/data/eia861/). Específicamente - los Datos operativos de servicios públicos 2021. Hoja de cálculo en el archivo.

-

5) Datos de propano facilitados por FerrellGas. Hoja de cálculo en el archivo.

-

6) Las casas con calefacción de madera fueron retiradas de la Oficina del Censo - de los Estados Unidos (datos en el archivo). Se supuso que todas las casas con - calefacción de madera se encontraban en la parte del condado de Boulder de Longmont.

-

7) Los datos de energía y comunicaciones de Longmont fueron facilitados por - Longmont Power & Communications. PDF en el archivo.

8) Los datos de sistemas - solares de techo de Longmont Power & Communications y Lyons Municipal se ofrecieron - en kW de capacidad en lugar de kWh generados. Los PVWatts del NREL se utilizaron - para estimar los kWh generados por los sistemas solares de techo. PDF de los resultados - en el archivo. Visite https://pvwatts.nrel.gov/

10) Debido a la regla 15/15 - de Xcel, con menos de 15 clientes en los conjuntos de datos comerciales y residenciales, - no pueden brindar el consumo de esos clientes para evitar problemas de privacidad. - Por lo tanto, Lotus aplicó el consumo promedio de kWh residencial y comercial - de LPC a los siete clientes residenciales y comerciales (respectivamente) en el - territorio de Xcel para estimar el consumo.

11) Se proporcionó la tasa - de pérdida de transmisión y distribución y el factor de emisión de la PRPA de - 2021. Correo electrónico en el archivo. El valor del factor de emisiones representa - la tasa de emisiones de carga compartida de Longmont sin REC desagregados.

+ description:

1) Stationary Diesel Use data was provided by CDPHE. + Spreadsheet on file.

2) Xcel Energy emission factor, electricity and + natural gas usage data, and renewable energy data is from Xcel's 2021 + Community Energy Report for Longmont, on file.

3) Longmont Power & + Communications data provided. PDF on file. The commercial buildings in + Longmont's Weld County portion consumed 31,000,000 kWh in 2021.

4) + T&D losses are calculated for Xcel and Longmont based on the EIA 861 + (https://www.eia.gov/electricity/data/eia861/) reported data. Specifically + the Utility Operational Data_2021. Spreadsheet on file.

5) Propane + data provided by FerrellGas. Spreadsheet on file.

6) Houses heated + with wood were pulled from the US Census Bureau, data on file. It was + assumed that all of the houses heated by wood in Longmont fell within the + Boulder County portion of Longmont.

7) Longmont Power and + Communications data was provided by Longmont Power & Communications. PDF on + file.

8) Longmont Power & Communications and Lyons Municipal rooftop + solar data was provided in kW of capacity rather than kWh generated. NREL's + PVWatts was used to estimate kWh generated by rooftop solar systems. PDF of + results on file. See https://pvwatts.nrel.gov/

10) Due to Xcel's + 15/15 rule, with less than 15 customers in the commercial and residential + data sets they cannot provide usage for those customers to avoid violating + privacy concerns. Therefore Lotus applied the average residential and + commercial kWh use from LPC to the 7 residential and commercial customers + (respectively) in Xcel territory to estimate use.

11) PRPA emission + factor and transmission and distribution loss rate for 2021 was provided. + Email on file. Emissions factor value represents Longmont's load share + emissions rate without unbundled RECs.

+ description_es-US:

1) Los datos sobre el consumo de diésel estacionario + fueron brindados por el CDPHE. Hoja de cálculo en el archivo.

2) El + factor de emisión de Xcel Energy, los datos de consumo de electricidad y gas + natural y los datos de energía renovable provienen del Informe de energía + comunitaria 2021 de Xcel para Longmont (en el archivo).

3) Datos + facilitados por Longmont Power & Communications. PDF en el archivo. Los + edificios comerciales en la parte del condado de Weld de Longmont + consumieron 31.000.000 kWh en el año 2021.

4) Las pérdidas de + transmisión y distribución se calculan para Xcel y Longmont según los datos + de la EIA 861 (https://www.eia.gov/electricity/data/eia861/). + Específicamente los Datos operativos de servicios públicos 2021. Hoja de + cálculo en el archivo.

5) Datos de propano facilitados por + FerrellGas. Hoja de cálculo en el archivo.

6) Las casas con + calefacción de madera fueron retiradas de la Oficina del Censo de los + Estados Unidos (datos en el archivo). Se supuso que todas las casas con + calefacción de madera se encontraban en la parte del condado de Boulder de + Longmont.

7) Los datos de energía y comunicaciones de Longmont fueron + facilitados por Longmont Power & Communications. PDF en el archivo.

+

8) Los datos de sistemas solares de techo de Longmont Power & + Communications y Lyons Municipal se ofrecieron en kW de capacidad en lugar + de kWh generados. Los PVWatts del NREL se utilizaron para estimar los kWh + generados por los sistemas solares de techo. PDF de los resultados en el + archivo. Visite https://pvwatts.nrel.gov/

10) Debido a la regla 15/15 + de Xcel, con menos de 15 clientes en los conjuntos de datos comerciales y + residenciales, no pueden brindar el consumo de esos clientes para evitar + problemas de privacidad. Por lo tanto, Lotus aplicó el consumo promedio de + kWh residencial y comercial de LPC a los siete clientes residenciales y + comerciales (respectivamente) en el territorio de Xcel para estimar el + consumo.

11) Se proporcionó la tasa de pérdida de transmisión y + distribución y el factor de emisión de la PRPA de 2021. Correo electrónico + en el archivo. El valor del factor de emisiones representa la tasa de + emisiones de carga compartida de Longmont sin REC desagregados.

type: formula.FormulaNode quantity: emissions unit: t_co2e/a @@ -1743,7 +1759,7 @@ nodes: - id: ghg - id: emission_sectors - id: scope - categories: [scope1] + categories: [scope1] params: formula: use * ef @@ -2470,6 +2486,9 @@ nodes: input_datasets: - id: longmont/waste_data column: waste_landfilled + input_nodes: + - id: population + tags: [ratio_to_max_hist_year] - id: diversion_rate name: Diversion rate @@ -2519,7 +2538,8 @@ nodes: - id: gas_collection_rate to_dimensions: [] params: - formula: mass * complement(diversion_rate) * msw_characterization * ef * complement(gas_collection_rate) + formula: mass * complement(diversion_rate) * msw_characterization * ef * + complement(gas_collection_rate) output_nodes: - id: waste_emissions from_dimensions: @@ -2562,6 +2582,9 @@ nodes: input_datasets: - id: longmont/waste_data column: waste_composted + input_nodes: + - id: population + tags: [ratio_to_max_hist_year] - id: composted_waste_emissions name: Composted waste emissions @@ -2638,6 +2661,9 @@ nodes: input_datasets: - id: longmont/waste_data column: waste_recycled + input_nodes: + - id: population + tags: [ratio_to_max_hist_year] - id: recycling_characterization name: Recycling characterization @@ -2858,8 +2884,9 @@ nodes: - id: electric_vehicle_miles_traveled name: Electric vehicle miles traveled name_es-US: Millas recorridas por vehículos eléctricos - description_en: Amount of electric vehicle miles traveled replacing combustion engine miles. - Note! The energy carrier shows what energy carrier is replaced by electric vehicles. + description_en: Amount of electric vehicle miles traveled replacing combustion + engine miles. Note! The energy carrier shows what energy carrier is replaced + by electric vehicles. type: formula.FormulaNode quantity: mileage unit: VMT/a @@ -2877,11 +2904,13 @@ nodes: - id: vehicle_miles_traveled tags: [electric_vmt] params: - formula: vehicles * vmt_per_vehicle * extend_all(vmt / sum_dim(sum_dim(vmt, vehicle_type), energy_carrier)) + formula: vehicles * vmt_per_vehicle * extend_all(vmt / sum_dim(sum_dim(vmt, + vehicle_type), energy_carrier)) - id: vmt_change_due_to_public_transportation name: Change in vehicle miles traveled due to public transportation - name_es-US: Cambio en las millas recorridas por vehículos debido al transporte público + name_es-US: Cambio en las millas recorridas por vehículos debido al transporte + público type: formula.FormulaNode quantity: mileage unit: VMT/a @@ -2925,7 +2954,8 @@ nodes: - id: population tags: [pop] params: - formula: (extend_all(baseline) * ratio_to_max_hist_year(pop) - electric_vmt - transit) * complement(multimod) + formula: (extend_all(baseline) * ratio_to_max_hist_year(pop) - electric_vmt + - transit) * complement(multimod) - id: vehicle_fuel_use name: Vehicle fuel use @@ -3054,6 +3084,288 @@ nodes: params: operations: get_single_dataset,extend_to_history +- id: energy_use_intensity_improvement_updated_building_code + name: Energy use intensity improvement, updated_building_code + (vintage-weighted) + description: Vintage-weighted average of energy_use_intensity_improvement's + updated_building_code figure -- each year's newly-brought-under-code floor + area is weighted by that year's achievable improvement, so floor area + brought under code in an earlier year keeps that year's rate rather than + being retroactively upgraded to the current year's rate. + type: formula.FormulaNode + quantity: fraction + unit: '%' + output_dimensions: [emission_sectors] + input_nodes: + - id: updated_building_code + tags: [flow] + from_dimensions: + - id: building_energy_class + categories: [existing] + flatten: true + - id: updated_building_code_stock + tags: [stock] + from_dimensions: + - id: building_energy_class + categories: [existing] + flatten: true + - id: energy_use_intensity_improvement + tags: [improvement] + from_dimensions: + - id: action + categories: [updated_building_code] + flatten: true + params: + formula: cumulative(flow * improvement) / (stock + 0.0000001) + +- id: energy_use_intensity_improvement_develop_beyond_building_code + name: Energy use intensity improvement, develop_beyond_building_code + (vintage-weighted) + description: Vintage-weighted average of energy_use_intensity_improvement's + develop_beyond_building_code figure -- see + energy_use_intensity_improvement_updated_building_code for the mechanism. + type: formula.FormulaNode + quantity: fraction + unit: '%' + output_dimensions: [emission_sectors] + input_nodes: + - id: develop_beyond_building_code + tags: [flow] + from_dimensions: + - id: building_energy_class + categories: [new] + flatten: true + - id: develop_beyond_building_code_stock + tags: [stock] + from_dimensions: + - id: building_energy_class + categories: [new] + flatten: true + - id: energy_use_intensity_improvement + tags: [improvement] + from_dimensions: + - id: action + categories: [develop_beyond_building_code] + flatten: true + params: + formula: cumulative(flow * improvement) / (stock + 0.0000001) + +- id: energy_use_intensity_improvement_vintage_existing + name: Energy use intensity improvement (vintage-weighted, existing building + stock) + description: Per-action EUI improvement for existing building stock -- see + energy_use_intensity_improvement_updated_building_code for the vintage- + weighting mechanism. No develop_beyond_building_code term here -- it doesn't + apply to existing stock (see energy_use_intensity_improvement_vintage_new). + type: formula.FormulaNode + quantity: fraction + unit: '%' + input_dimensions: [emission_sectors, action] + output_dimensions: [emission_sectors, action] + input_nodes: + - id: energy_use_intensity_improvement + tags: [none_imp] + from_dimensions: + - id: action + categories: [none] + flatten: true + to_dimensions: + - id: emission_sectors + - id: action + categories: [none] + - id: energy_use_intensity_improvement_updated_building_code + tags: [ubc_imp] + to_dimensions: + - id: emission_sectors + - id: action + categories: [updated_building_code] + params: + formula: none_imp + ubc_imp + +- id: energy_use_intensity_improvement_vintage_new + name: Energy use intensity improvement (vintage-weighted, new building stock) + description: Per-action EUI improvement for new building stock. + develop_beyond_building_code has no category of its own here (matching + building_class_mix -- it applies to the same new-construction population as + updated_building_code, not a separate share, per the original consultant + spreadsheet's Building Energy Model rows 69-75 and 112-119 both using the + same affected-square-footage formula and compliance rate). Its + vintage-weighted improvement is instead added into the updated_building_code + slot here. + type: formula.FormulaNode + quantity: fraction + unit: '%' + input_dimensions: [emission_sectors, action] + output_dimensions: [emission_sectors, action] + input_nodes: + - id: energy_use_intensity_improvement + tags: [none_imp] + from_dimensions: + - id: action + categories: [none] + flatten: true + to_dimensions: + - id: emission_sectors + - id: action + categories: [none] + - id: energy_use_intensity_improvement_updated_building_code + tags: [ubc_imp] + to_dimensions: + - id: emission_sectors + - id: action + categories: [updated_building_code] + - id: energy_use_intensity_improvement_develop_beyond_building_code + tags: [dbbc_imp] + to_dimensions: + - id: emission_sectors + - id: action + categories: [updated_building_code] + params: + formula: none_imp + ubc_imp + dbbc_imp + +- id: energy_use_intensity_improvement_vintage + name: Energy use intensity improvement (vintage-weighted) + description: Per-action EUI improvement replacing + energy_use_intensity_improvement's raw current-year lookup with a + vintage-weighted average for updated_building_code and + develop_beyond_building_code -- combines + energy_use_intensity_improvement_vintage_existing and _new (see those for + the per-building-class breakdown; see + energy_use_intensity_improvement_updated_building_code for the + vintage-weighting mechanism itself). benchmark_energy_use is not part of + this mechanism -- it's a direct GWh/a subtraction on + building_electricity_use / building_natural_gas_use instead (see that + action's own node). + type: formula.FormulaNode + quantity: fraction + unit: '%' + input_dimensions: [building_energy_class, emission_sectors, action] + output_dimensions: [building_energy_class, emission_sectors, action] + input_nodes: + - id: energy_use_intensity_improvement_vintage_existing + tags: [existing_slice] + to_dimensions: + - id: emission_sectors + - id: action + - id: building_energy_class + categories: [existing] + - id: energy_use_intensity_improvement_vintage_new + tags: [new_slice] + to_dimensions: + - id: emission_sectors + - id: action + - id: building_energy_class + categories: [new] + params: + formula: existing_slice + new_slice + +- id: energy_use_intensity_trend_natural_gas + name: Energy use intensity trend (natural gas) + name_es-US: Tendencia de la intensidad del uso de la energía (gas natural) + description: This action describes the assumed improvement in the energy use + intensity for the baseline scenario, i.e. changes that will occur anyway + without any activity from the city. + description_es-US: Esta acción describe la mejora supuesta en la intensidad + del uso de la energía para el escenario de referencia, es decir, los cambios + que se producirán de todos modos sin intervención alguna por parte de la + ciudad. + type: generic.GenericNode + quantity: consumption_factor + unit: thm/ft**2/a + output_dimensions: [emission_sectors] + input_datasets: + - id: longmont/energy_use_intensity_improvements + column: natural_gas + tags: [cleaned, extend_both_ways] + filters: + - column: action + value: none + forecast_from: 2025 + +- id: energy_use_intensity_trend_electricity + name: Energy use intensity trend (electricity) + name_es-US: Tendencia de la intensidad del uso de la energía (electricidad) + description: This action describes the assumed improvement in the energy use + intensity for the baseline scenario, i.e. changes that will occur anyway + without any activity from the city. + description_es-US: Esta acción describe la mejora supuesta en la intensidad + del uso de la energía para el escenario de referencia, es decir, los cambios + que se producirán de todos modos sin intervención alguna por parte de la + ciudad. + type: generic.GenericNode + quantity: consumption_factor + unit: kWh/ft**2/a + output_dimensions: [emission_sectors] + input_datasets: + - id: longmont/energy_use_intensity_improvements + column: electricity + tags: [cleaned, extend_both_ways] + filters: + - column: action + value: none + forecast_from: 2025 + +# Trends beyond the city's own control. + +- id: mpg_trend + name: Miles per gallon increasing trend + name_es-US: Tendencia al aumento de las millas recorridas por galón + description: The model assumes that internal combustion vehicles that use + gasoline and diesel will become more efficient, increase miles traveled per + gallon, over time. + description_es-US: El modelo supone que, con el tiempo, los vehículos de + combustión interna que usan gasolina y diésel serán más eficientes y + aumentarán las millas recorridas por galón. + type: simple.AdditiveNode + quantity: fuel_consumption + unit: MPG + input_datasets: + - id: longmont/energy_vehicle_and_waste_actions + forecast_from: 2025 + column: mpg_trend + output_nodes: + - id: transportation_miles_per_gallon + to_dimensions: + - id: energy_carrier + categories: [petrol] + - id: vehicle_type + categories: [car] + +- id: vmt_per_vehicle_trend + name: Vehicle miles per vehicle trend + name_es-US: Tendencia de millas recorridas por vehículo + description:

The model assumes that overtime there will be a decrease in + the annual miles traveled per vehicle.

Unit: mi/(yr. v) = + miles per year per vehicle

+ description_es-US:

El modelo supone que con el tiempo se producirá una + disminución de las millas anuales recorridas por vehículo.

+

Unidad: mi/(a.v) = millas anuales por vehículo

+ type: simple.AdditiveNode + quantity: mileage + unit: VMT/vehicle/a + input_datasets: + - id: longmont/energy_vehicle_and_waste_actions + forecast_from: 2025 + column: vmt_per_vehicle_trend + output_nodes: + - vehicle_miles_per_vehicle + +- id: electric_vehicle_efficiency_trend + name: Electric vehicle efficiency trend + name_es-US: Tendencia de la eficiencia de los vehículos eléctricos + description: The model assumes that electric vehicles will become more + efficient, increase in miles traveled per kilowatt hour of energy, overtime. + description_es-US: El modelo supone que, con el tiempo, los vehículos + eléctricos serán más eficientes y aumentarán los kilómetros recorridos por + kilovatio hora de energía. + type: simple.AdditiveNode + quantity: fuel_consumption + unit: kWh/VMT + input_datasets: + - id: longmont/energy_vehicle_and_waste_actions + forecast_from: 2025 + column: electric_vehicle_efficiency_trend + actions: - id: updated_building_code @@ -3070,28 +3382,16 @@ actions: de energía (EUI, por sus siglas en inglés) causada por esta acción.

Seguimiento de la aplicación:

' - type: shift.ShiftAction + type: simple.GenericAction quantity: fraction - unit: '%' + unit: '%/a' group: buildings - output_nodes: - - id: building_class_mix - tags: [add_to_existing_dims] + output_dimensions: [building_energy_class, emission_sectors] + input_datasets: + - id: longmont/building_actions + column: updated_building_code params: - - id: shift - unit: '%' - is_customizable: false - value: - - source: - categories: - action: none - building_energy_class: existing - dests: - - categories: - action: updated_building_code - building_energy_class: existing - amounts: - - {year: 2025, source_amount: -1.0, dest_amounts: [100]} + operations: get_single_dataset - id: develop_beyond_building_code name: Develop beyond building code @@ -3110,29 +3410,16 @@ actions: del consumo de energía (EUI, por sus siglas en inglés) causada por esta acción.

Seguimiento de la aplicación:

' - type: shift.ShiftAction + type: simple.GenericAction quantity: fraction - unit: '%' + unit: '%/a' group: buildings - output_nodes: - - id: building_class_mix - tags: [add_to_existing_dims] + output_dimensions: [building_energy_class, emission_sectors] + input_datasets: + - id: longmont/building_actions + column: develop_beyond_building_code params: - - id: shift - unit: '%' - is_customizable: false - value: - - source: - categories: - action: none - building_energy_class: new - dests: - - categories: - action: develop_beyond_building_code - building_energy_class: new - amounts: - - {year: 2025, source_amount: -90.0, dest_amounts: [100]} - - {year: 2026, source_amount: -0.0, dest_amounts: [100]} + operations: get_single_dataset - id: benchmark_energy_use name: Report and benchmark energy use in commercial buildings @@ -3150,30 +3437,32 @@ actions: de la intensidad del consumo de energía (EUI, por sus siglas en inglés) causada por esta acción.

Seguimiento de la aplicación:

' - type: shift.ShiftAction - quantity: fraction - unit: '%' + type: simple.AdditiveAction group: buildings - output_dimensions: [emission_sectors, building_energy_class, energy_carrier] + output_dimensions: [emission_sectors, building_energy_class] + output_metrics: + - id: electricity + quantity: energy + unit: GWh/a + column_id: benchmark_energy_use_electricity + - id: natural_gas + quantity: energy + unit: Mthm/a + column_id: benchmark_energy_use_natural_gas + input_datasets: + - id: longmont/building_actions + tags: [extend_all] + forecast_from: 2025 + filters: + - column: updated_building_code + - column: develop_beyond_building_code output_nodes: - - id: building_class_mix - tags: [add_to_existing_dims] - params: - - id: shift - unit: '%' - is_customizable: false - value: - - source: - categories: - action: none - emission_sectors: commercial_industrial_energy - dests: - - categories: - action: benchmark_energy_use - emission_sectors: commercial_industrial_energy - amounts: - - {year: 2027, source_amount: -88.0, dest_amounts: [100]} - - {year: 2028, source_amount: -0.0, dest_amounts: [100]} + - id: building_electricity_use + metrics: [benchmark_energy_use_electricity] + tags: [benchmark_electricity] + - id: building_natural_gas_use + metrics: [benchmark_energy_use_natural_gas] + tags: [benchmark_natural_gas] - id: efficiency_works_program name: Expand the Efficiency Works Program @@ -3404,7 +3693,7 @@ actions: forecast_from: 2025 output_nodes: - id: electricity_use - tags: [extend_to_history, complement, action_with_history] + tags: [reduce_losses] - id: leak_detection name: Adopt and enforce leak detection and repair @@ -3462,28 +3751,6 @@ actions: output_nodes: - diversion_rate -- id: energy_use_intensity_trend - name: Energy use intensity trend - name_es-US: Tendencia de la intensidad del uso de la energía - description: This action describes the assumed improvement in the energy use - intensity for the baseline scenario, i.e. changes that will occur anyway - without any activity from the city. - description_es-US: Esta acción describe la mejora supuesta en la intensidad - del uso de la energía para el escenario de referencia, es decir, los cambios - que se producirán de todos modos sin intervención alguna por parte de la - ciudad. - type: energy_saving.EnergyAction - quantity: consumption_factor - group: trend - input_datasets: - - id: longmont/energy_use_intensity_improvements - tags: [cleaned, extend_both_ways] - filters: - - column: action - value: none - forecast_from: 2025 - output_dimensions: [emission_sectors] - - id: vehicle_incentives name: Increase electric vehicles through vehicle incentives and improved infrastructure @@ -3600,70 +3867,6 @@ actions: # output_nodes: # - id: total_transit_vmt -# Trend actions. TODO Do we need these? - -- id: mpg_trend - name: Miles per gallon increasing trend - name_es-US: Tendencia al aumento de las millas recorridas por galón - description: The model assumes that internal combustion vehicles that use - gasoline and diesel will become more efficient, increase miles traveled per - gallon, over time. - description_es-US: El modelo supone que, con el tiempo, los vehículos de - combustión interna que usan gasolina y diésel serán más eficientes y - aumentarán las millas recorridas por galón. - type: simple.AdditiveAction - quantity: fuel_consumption - unit: MPG - group: trend - input_datasets: - - id: longmont/energy_vehicle_and_waste_actions - forecast_from: 2025 - column: mpg_trend - output_nodes: - - id: transportation_miles_per_gallon - to_dimensions: - - id: energy_carrier - categories: [petrol] - - id: vehicle_type - categories: [car] - -- id: vmt_per_vehicle_trend - name: Vehicle miles per vehicle trend - name_es-US: Tendencia de millas recorridas por vehículo - description:

The model assumes that overtime there will be a decrease in - the annual miles traveled per vehicle.

Unit: mi/(yr. v) = - miles per year per vehicle

- description_es-US:

El modelo supone que con el tiempo se producirá una - disminución de las millas anuales recorridas por vehículo.

-

Unidad: mi/(a.v) = millas anuales por vehículo

- type: simple.AdditiveAction - quantity: mileage - unit: VMT/vehicle/a - group: trend - input_datasets: - - id: longmont/energy_vehicle_and_waste_actions - forecast_from: 2025 - column: vmt_per_vehicle_trend - output_nodes: - - vehicle_miles_per_vehicle - -- id: electric_vehicle_efficiency_trend - name: Electric vehicle efficiency trend - name_es-US: Tendencia de la eficiencia de los vehículos eléctricos - description: The model assumes that electric vehicles will become more - efficient, increase in miles traveled per kilowatt hour of energy, overtime. - description_es-US: El modelo supone que, con el tiempo, los vehículos - eléctricos serán más eficientes y aumentarán los kilómetros recorridos por - kilovatio hora de energía. - type: simple.AdditiveAction - quantity: fuel_consumption - unit: kWh/VMT - group: trend - input_datasets: - - id: longmont/energy_vehicle_and_waste_actions - forecast_from: 2025 - column: electric_vehicle_efficiency_trend - pages: - id: home name: Longmont Greenhouse Gas Inventory @@ -3675,23 +3878,18 @@ scenarios: - id: baseline name: Business as Usual name_es-US: Normalidad - params: - - id: energy_use_intensity_trend.enabled - value: true - id: default default: true name: Climate Action Initiatives name_es-US: Iniciativas de acción climática all_actions_enabled: true - params: - - id: energy_use_intensity_trend.enabled - value: false -- id: historical_actions - name: Historical actions - all_actions_enabled: false - is_selectable: false - params: - - id: reduce_transmission_distribution_losses.enabled - value: true +# This was an exploration of how previous actions should work in historical years. Not needed now. +# - id: historical_actions +# name: Historical actions +# all_actions_enabled: false +# is_selectable: false +# params: +# - id: reduce_transmission_distribution_losses.enabled +# value: true diff --git a/nodes/datasets.py b/nodes/datasets.py index d5c1c7210..156ad830d 100644 --- a/nodes/datasets.py +++ b/nodes/datasets.py @@ -243,6 +243,7 @@ def get_input_dataset_filter_adapter() -> TypeAdapter[InputDatasetFilterDef]: class FilterDatasetKwargs(DatasetKwargs): column: str | None + columns: list[str] | None filters: list[InputDatasetFilterDef] | None dropna: bool | None min_year: int | None @@ -254,6 +255,7 @@ class FilterDatasetKwargs(DatasetKwargs): @dataclass class DatasetWithFilters(Dataset, ABC): column: str | None = None + columns: list[str] | None = None filters: list[InputDatasetFilterDef] | None = None dropna: bool | None = None min_year: int | None = None @@ -268,6 +270,7 @@ def kwargs_from_def(cls, ds_def: InputDatasetDef) -> FilterDatasetKwargs: return FilterDatasetKwargs( **super().kwargs_from_def(ds_def), column=ds_def.column, + columns=ds_def.columns, filters=ds_def.filters, dropna=ds_def.dropna, min_year=ds_def.min_year, @@ -280,6 +283,8 @@ def __rich_repr__(self) -> RichReprResult: yield from super().__rich_repr__() if self.column is not None: yield 'column', self.column + if self.columns is not None: + yield 'columns', self.columns if self.filters is not None: yield 'filters', len(self.filters) @@ -438,6 +443,43 @@ def _operate_tags(self, df: ppl.PathsDataFrame) -> ppl.PathsDataFrame: df = df.paths.get_operation(tag)(df, context) return df + def _select_column(self, df: ppl.PathsDataFrame, cols: list[str]) -> tuple[ppl.PathsDataFrame, list[str]]: + assert self.column + if self.column not in cols: + available = ', '.join(cols) + raise DatasetError( + self, + "Column '%s' not found in dataset '%s'. Available columns: %s" % (self.column, self.id, available), + ) + df = df.with_columns(pl.col(self.column).alias(VALUE_COLUMN)) + df = df.filter(pl.col(VALUE_COLUMN).is_not_null()) + return df, [YEAR_COLUMN, VALUE_COLUMN, *df.dim_ids] + + def _select_columns(self, df: ppl.PathsDataFrame, cols: list[str]) -> tuple[ppl.PathsDataFrame, list[str]]: + assert self.columns + missing = [col for col in self.columns if col not in cols] + if missing: + available = ', '.join(cols) + raise DatasetError( + self, + "Column(s) %s not found in dataset '%s'. Available columns: %s" % (', '.join(missing), self.id, available), + ) + null_condition = pl.lit(True) # noqa: FBT003 + for col in self.columns: + null_condition = null_condition & pl.col(col).is_null() + df = df.filter(~null_condition) + all_null_dims = [dim_id for dim_id in df.dim_ids if df[dim_id].null_count() == len(df)] + if all_null_dims: + df = df.drop(all_null_dims) + return df, [YEAR_COLUMN, *self.columns, *df.dim_ids] + + def _select_dataset_columns(self, df: ppl.PathsDataFrame, cols: list[str]) -> tuple[ppl.PathsDataFrame, list[str]]: + if self.column: + return self._select_column(df, cols) + if self.columns: + return self._select_columns(df, cols) + return df, cols + @measure_dataset_call('dataset.filter', capture_df_result=True, capture_df_arg=True) def _filter_and_process_df(self, df: ppl.PathsDataFrame) -> ppl.PathsDataFrame: from nodes.defs.node_defs import RenameColumnDatasetFilterDef @@ -449,22 +491,7 @@ def _filter_and_process_df(self, df: ppl.PathsDataFrame) -> ppl.PathsDataFrame: df = self._rename_col_filter(df, filter_def) cols = list(df.columns) - - if self.column: - if self.column not in cols: - available = ', '.join(cols) - raise DatasetError( - self, - "Column '%s' not found in dataset '%s'. Available columns: %s" - % ( - self.column, - self.id, - available, - ), - ) - df = df.with_columns(pl.col(self.column).alias(VALUE_COLUMN)) - df = df.filter(pl.col(VALUE_COLUMN).is_not_null()) - cols = [YEAR_COLUMN, VALUE_COLUMN, *df.dim_ids] + df, cols = self._select_dataset_columns(df, cols) if YEAR_COLUMN in cols and YEAR_COLUMN not in df.primary_keys: df = df.add_to_index(YEAR_COLUMN) @@ -789,6 +816,7 @@ def hash_data(self) -> dict[str, Any]: extra_fields = [ 'input_dataset', 'column', + 'columns', 'filters', 'dropna', 'forecast_from', diff --git a/nodes/defs/node_defs.py b/nodes/defs/node_defs.py index 492d83556..b399f3546 100644 --- a/nodes/defs/node_defs.py +++ b/nodes/defs/node_defs.py @@ -76,6 +76,13 @@ class SelectColumnDatasetTransformOp(BaseModel): column: str +class SelectColumnsDatasetTransformOp(BaseModel): + model_config = ConfigDict(extra='forbid') + + kind: Literal['select_columns'] = 'select_columns' + columns: list[str] + + class FilterColumnDatasetTransformOp(BaseModel): model_config = ConfigDict(extra='forbid') @@ -140,6 +147,7 @@ class ForecastFromDatasetTransformOp(BaseModel): type DatasetTransformOp = ( SelectColumnDatasetTransformOp + | SelectColumnsDatasetTransformOp | FilterColumnDatasetTransformOp | FilterDimensionDatasetTransformOp | RenameItemDatasetTransformOp @@ -179,6 +187,18 @@ class InputDatasetDef(I18nBaseModel): input_dataset: str | None = None """DVC dataset identifier override (when different from ``id``).""" column: str | None = None + columns: list[str] | None = None + """ + Explicit whitelist of metric columns to keep, for nodes that consume + several (but not all) metric columns from a shared multi-metric dataset + (e.g. a multimetric ``simple.AdditiveAction``). Every other unlisted + metric column is dropped, rows where all listed columns are null are + dropped, and any dimension column left entirely null by that row-drop is + dropped too -- the same "empty dimensions are dropped automatically" + behavior ``column`` gets, generalized to more than one column. Mutually + exclusive with ``column``, which aliases a single column to the internal + value column instead of keeping it under its own name. + """ forecast_from: int | None = None filters: list[InputDatasetFilterDef] = Field(default_factory=list) dropna: bool | None = None @@ -187,10 +207,18 @@ class InputDatasetDef(I18nBaseModel): unit: Unit | None = None output_dimensions: list[DimensionRef] | None = None + @model_validator(mode='after') + def validate_column_and_columns(self) -> InputDatasetDef: + if self.column is not None and self.columns is not None: + raise ValueError("'column' and 'columns' are mutually exclusive") + return self + def to_transform_pipeline(self) -> DatasetTransformPipelineDef: operations: list[DatasetTransformOp] = [] if self.column is not None: operations.append(SelectColumnDatasetTransformOp(column=self.column)) + if self.columns is not None: + operations.append(SelectColumnsDatasetTransformOp(columns=self.columns)) operations.extend(input_dataset_filter_to_transform_op(filter_def) for filter_def in self.filters) if self.forecast_from is not None: operations.append(ForecastFromDatasetTransformOp(year=self.forecast_from)) @@ -220,6 +248,8 @@ class DatasetPortSpec(I18nBaseModel): wide DVC datasets use human-readable column labels with spaces (e.g. "Trucks and lorries"). """ + columns: list[str] | None = None + """Mirrors ``InputDatasetDef.columns`` -- see there. Mutually exclusive with ``column``.""" forecast_from: int | None = None filters: list[InputDatasetFilterDef] = Field(default_factory=list) dropna: bool | None = None @@ -234,6 +264,7 @@ def from_input_dataset(cls, ds_def: InputDatasetDef) -> DatasetPortSpec: tags=ds_def.tags, input_dataset=ds_def.input_dataset, column=ds_def.column, + columns=ds_def.columns, forecast_from=ds_def.forecast_from, filters=ds_def.filters, dropna=ds_def.dropna, @@ -249,6 +280,7 @@ def to_input_dataset(self, *, id: DatasetIdentifier) -> InputDatasetDef: tags=self.tags, input_dataset=self.input_dataset, column=self.column, + columns=self.columns, forecast_from=self.forecast_from, filters=self.filters, dropna=self.dropna, diff --git a/nodes/metric.py b/nodes/metric.py index 28dfcb86a..15f8113c5 100644 --- a/nodes/metric.py +++ b/nodes/metric.py @@ -583,7 +583,7 @@ def from_action_node(cls, node: ActionNode) -> None | DimensionalFlow: # noqa: flow_nodes: dict[str, FlowNode] = {} - def get_flow_node(row: dict[str, Any], is_source: bool) -> FlowNode: + def get_flow_node(row: dict[str, Any], is_source: bool) -> FlowNode: # noqa: C901 path_parts = [] label_parts = [] @@ -621,6 +621,14 @@ def get_flow_node(row: dict[str, Any], is_source: bool) -> FlowNode: val_col = node.get_default_output_metric().column_id if sdf_exprs: sdf = sdf.filter(functools.reduce(lambda a, b: a & b, sdf_exprs)) # pyright: ignore[reportUnknownLambdaType] + # The source node (e.g. a ShiftAction's output_nodes[0]) may carry dimensions the + # shift itself never mentions in its source/dest categories (`dims` above comes from + # the shift's own dims, via df.primary_keys). Sum those out here so one row per year + # remains; this only affects this flow-diagram value, not the source node's own output + # or any actual effect computation elsewhere. + extra_dims = [dim_id for dim_id in sdf.dim_ids if dim_id not in dim_cats] + if extra_dims: + sdf = sdf.paths.sum_over_dims(extra_dims) sdf = sdf.select([YEAR_COLUMN, val_col]) assert not sdf.paths.index_has_duplicates() assert flow_node_id not in source_values diff --git a/nodes/spec_export.py b/nodes/spec_export.py index a4af2ce09..ab396f7f6 100644 --- a/nodes/spec_export.py +++ b/nodes/spec_export.py @@ -354,12 +354,14 @@ def _dataset_binding_columns_for_node(node: Node, ds_instance: DatasetWithFilter """ Return dataset metric columns to expose as editor bindings. - Single-column dataset inputs carry ``column`` directly. Multi-metric - action datasets usually leave it unset and consume columns matching the - node's output metrics. + Single-column dataset inputs carry ``column`` directly; an explicit + ``columns`` whitelist is used as-is. Multi-metric action datasets that + leave both unset consume columns matching the node's output metrics. """ if ds_instance.column is not None: return [ds_instance.column] + if ds_instance.columns is not None: + return list(ds_instance.columns) columns: list[str] = [] seen: set[str] = set() for metric in node.output_metrics.values(): @@ -512,6 +514,7 @@ def _input_dataset_def_from_instance(ds: DatasetWithFilters) -> InputDatasetDef: tags=ds.tags or [], input_dataset=ds.input_dataset if isinstance(ds, DVCDataset) else None, column=ds.column, + columns=ds.columns, forecast_from=ds.forecast_from, filters=ds.filters or [], dropna=ds.dropna, @@ -706,7 +709,8 @@ def _resolve_dataset_ports( ports: list[DatasetPort] = [] spec = DatasetPortSpec.from_input_dataset(_input_dataset_def_from_instance(ds_instance)) metric_columns = _dataset_binding_columns_for_node(node, ds_instance) - if ds_instance.column is None: + explicit_columns = ds_instance.column is not None or ds_instance.columns is not None + if not explicit_columns: # Column-less bindings: the node consumes the full frame. Bind to every # metric the dataset actually exposes so ports stay accurate even when # the node renames columns post-load (e.g. HsyNode translating Finnish @@ -718,7 +722,7 @@ def _resolve_dataset_ports( for column in metric_columns: metric = metrics_by_schema_and_name.get((dataset_obj.schema.pk, column)) if metric is None: - if ds_instance.column is not None: + if explicit_columns: raise ValueError(f'No metric {column} in dataset {ds_instance.id} for node {node.id}') logger.debug('No metric %s in dataset %s for node %s; skipping dataset-port binding', column, ds_instance.id, node.id) continue