Skip to content

Conversation

@ShiZhongming
Copy link
Collaborator

@ShiZhongming ShiZhongming commented Nov 30, 2025

Summary by CodeRabbit

  • New Features

    • Baseline cost engine for standalone and district (DH/DC) scenarios, solar-panel costing, anthropogenic heat‑rejection analysis, and a heat‑rejection map layer.
  • Configuration

    • New system-costs and anthropogenic-heat sections, DC/DH network selectors, multi-select district supply, dynamic solar-panel choice, and metrics flags for costs and heat rejection.
  • Visualization

    • Cost‑breakdown chart and heat‑rejection plotting/colouring support.
  • Improvements

    • More robust selection/fallback logic, clearer warnings, safer file handling, and resilient dashboard data flows.
  • Tests

    • New tests for baseline costs and anthropogenic‑heat workflows.

✏️ Tip: You can customize this high-level summary in your review settings.

ShiZhongming and others added 30 commits November 10, 2025 09:57
Added a check to warn and set base demand to zero for buildings connected to the network without a demand profile. This prevents errors when such buildings are present and informs the user to review the network configuration.
@github-actions
Copy link

github-actions bot commented Dec 4, 2025

⚠️ Type stub update required

Your changes to config.py, default.config, or config_type_generator.py require regenerating the type stubs.

Please run:

python -m cea.utilities.config_type_generator

And commit the updated config.pyi file.

@ShiZhongming ShiZhongming marked this pull request as ready for review December 4, 2025 23:35
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 4, 2025

Walkthrough

Adds end-to-end baseline-costs and anthropogenic-heat analyses: new orchestration, supply‑component selection with multi‑level fallbacks, solar costing, heat‑rejection computations (standalone and networked), expanded config/schema/locator surfaces, visualization support, and tests; legacy system_costs marked deprecated.

Changes

Cohort / File(s) Summary
Baseline Costs Core Modules
\cea/analysis/costs/main.py`, `cea/analysis/costs/supply_costs.py`, `cea/analysis/costs/solar_costs.py`, `cea/analysis/costs/format_simplified.py``
New orchestration and helpers implementing baseline_costs_main, component selection (assemblies + 3‑level fallbacks), standalone vs district flows, solar-panel cost calculations, and simplified result formatting (summary + detailed outputs).
Heat Rejection Analysis
\cea/analysis/heat/main.py`, `cea/analysis/heat/heat_rejection.py`, `cea/analysis/heat/init.py`, `cea/analysis/heat/AGENTS.md``
New anthropogenic-heat module: main entry, standalone/network heat‑rejection, DHW handling, supply‑system reuse, plant splitting, merging, and CSV outputs for buildings, components, and hourly profiles.
Cost Documentation & Deprecation
\cea/analysis/costs/AGENTS.md`, `cea/analysis/costs/system_costs.py``
Expanded documentation of selection & fallback logic; legacy system_costs now emits deprecation warnings and points to new main.
Configuration Framework
\cea/config.py`, `cea/config.pyi``
New parameter types (DCNetworkLayoutChoiceParameter, DHNetworkLayoutChoiceParameter, DistrictSupplyTypeParameter, SolarPanelChoiceParameter), PlotContextParameter encode/decode, renamed/expanded sections (costs → system_costs), and new anthropogenic_heat / plots sections.
Default Configuration
\cea/default.config``
Replaced [costs] with [system-costs], added [anthropogenic-heat], new metrics flags (metrics-costs, metrics-heat-rejection) and plotting blocks (plots-heat-rejection, plots-cost-breakdown).
InputLocator & Schemas
\cea/inputlocator.py`, `cea/schemas.yml``
New path helpers for baseline and heat outputs (get_baseline_costs*, get_heat_rejection_*), updated thermal‑network path helpers, canonical solar potentials folder, and new/detailed baseline‑costs & heat‑rejection schemas.
Import/Export Result Summary
\cea/import_export/result_summary.py``
Integrated heat_rejection into aggregation/plotting pipelines and added copy_costs_to_summary to include baseline cost CSVs in results summary.
Visualization Support
\cea/visualisation/a_data_loader.py`, `cea/visualisation/b_data_processor.py`, `cea/visualisation/c_plotter.py`, `cea/visualisation/format/plot_colours.py`, `cea/visualisation/special/cost_breakdown.py`, `cea/visualisation/special/emission_timeline.py`, `cea/visualisation/plot_main.py``
Added heat‑rejection plotting; extended data_processor signatures to accept scenario; added plant‑area calculations; cost‑breakdown visualization module; color mapping and call‑site updates.
Scripts & Tests
\cea/scripts.yml`, `cea/scripts.py`, `cea/tests/test_baseline_costs.py`, `cea/tests/test_anthropogenic_heat.py``
scripts.yml updated to register new features and plots; YAML opens use UTF‑8; new tests for baseline‑costs and anthropogenic‑heat (skipped by default).
Domain & Optimization
\cea/optimization_new/building.py`, `cea/optimization_new/containerclasses/energyCarrier.py`, `cea/optimization_new/containerclasses/supplySystemStructure.py`, `cea/optimization_new/domain.py``
Dynamic supply DB reloading by network type, tie‑aware energy‑carrier selection, relaxed cooling temperature bound, DHW/demand‑carrier fallbacks, and selective skipping for network‑type mismatches.
Dashboard & Map Layers
\cea/interfaces/dashboard/api/utils.py`, `cea/interfaces/dashboard/map_layers/demand/layers.py`, `cea/interfaces/dashboard/map_layers/life_cycle_analysis/layers.py`, `cea/interfaces/dashboard/map_layers/renewable_energy_potentials/layers.py``
Added safe_filter_buildings_with_geometry helper across layers, made several per‑building file requirements optional, added AnthropogenicHeatMapLayer, and robust error handling/early‑exit when geometry or files are missing.
Small I/O / Encoding Fixes
\cea/plugin/base.py`, `cea/schemas.py`, `cea/scripts.py``
Open YAML files with encoding="utf-8" for Unicode safety.
Documentation Updates
\cea/demand/AGENTS.md`, `cea/analysis/costs/AGENTS.md`, `cea/analysis/heat/AGENTS.md``
Updated AGENTS.md content: demand end‑use/final‑energy taxonomy for system sizing and docs for cost & heat modules.

Sequence Diagram(s)

sequenceDiagram
    participant Config as Configuration
    participant Locator as InputLocator
    participant Main as baseline_costs_main
    participant Supply as supply_costs
    participant Solar as solar_costs
    participant Format as format_output_simplified
    participant FS as Files (CSV)

    Config->>Main: run baseline_costs_main(config)
    Main->>Locator: create InputLocator(config.scenario)
    alt No network selected
        Main->>Supply: calculate_all_buildings_as_standalone(locator, config)
        Supply-->>FS: write baseline & detailed costs
    else Network mode
        Main->>Main: validate_network_results_exist(...) per network_type
        Main->>Supply: calculate_all_buildings_as_standalone(locator, config)
        loop per valid network type
            Main->>Supply: calculate_costs_for_network_type(..., standalone_results)
            Supply->>Supply: select components (assemblies or category fallbacks)
            Supply-->>FS: write network-specific baseline & detailed costs
        end
        Main->>Supply: merge_network_type_costs(all_results)
    end
    Main->>Solar: calculate_building_solar_costs(config, locator, merged_results)
    Solar-->>FS: append solar costs
    Main->>Format: format_output_simplified(merged_results, locator)
    Format-->>FS: final summary + detailed CSVs
Loading
sequenceDiagram
    participant Config as Configuration
    participant Locator as InputLocator
    participant Heat as anthropogenic_heat_main
    participant Supply as supply_costs
    participant Network as SupplySystem
    participant FS as Files (CSV)

    Config->>Heat: run anthropogenic_heat_main(config)
    Heat->>Locator: create InputLocator(config.scenario)
    alt Standalone mode
        Heat->>Supply: calculate_standalone_heat_rejection(locator, config, network_types)
        Supply->>Network: create SupplySystem per building
        Network-->>Heat: hourly heat_rejection per building
    else Network mode
        loop per network_type
            Heat->>Heat: validate layout & get connected buildings
            Heat->>Heat: get_user_component_selection(config, network_type)
            Heat->>Supply: create_network_supply_system(locator, config, network_type, network_name, connected_buildings, standalone_results)
            Supply->>Network: build/evaluate district SupplySystem
            Network-->>Heat: plant & building heat_rejection contributions
        end
    end
    Heat->>Heat: merge_heat_rejection_results(...)
    Heat-->>FS: write heat_rejection_buildings.csv, heat_rejection_components.csv, per-building hourly CSVs
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Focus review on:
    • cea/analysis/costs/supply_costs.py — dense selection/fallback logic, capacity filtering, scale handling.
    • cea/analysis/costs/main.py — orchestration, validation, merging, error paths.
    • cea/analysis/heat/heat_rejection.py — DHW logic, multi‑plant splitting, supply‑system reuse.
    • cea/config.py / cea/config.pyi — new parameter classes and encode/decode constraints.
    • cea/import_export/result_summary.py & cea/inputlocator.py — new metric and path helpers; ensure backward compatibility.
    • cea/visualisation/special/cost_breakdown.py — grouping/normalisation and unit conversions.

Possibly related PRs

Suggested labels

feature-new, feature-data-management, feature-visualisation

Suggested reviewers

  • reyery
  • MatNif
  • martin-mosteiro

Poem

🐰 I hopped through configs, schemas, and code,
I picked components where fallbacks showed,
Solar and heat, plants big and small,
Networks and buildings — I checked them all,
A carrot cheer for outputs neatly sowed!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.26% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Anthropogenic heat feature' is concise and directly reflects the main change—adding a comprehensive anthropogenic heat assessment module with heat rejection calculations.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch anthropogenic-heat-feature

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 12

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
cea/visualisation/b_data_processor.py (1)

192-347: Update docstring for generate_dataframe_for_plotly to list all supported features

The docstring lists only 'demand', 'pv', 'pvt', 'sc' as valid plot_cea_feature values, but the function now also supports 'operational-emissions', 'lifecycle-emissions', and 'heat-rejection'. Update the docstring to reflect all supported features.

Additionally, _calculate_plant_floor_area redundantly imports pandas at line 205 even though it is already imported at the top of the file (line 7); remove the local import.

All call sites of generate_dataframe_for_plotly and calc_x_y_metric in plot_main.py, emission_timeline.py, and b_data_processor.py correctly pass the scenario argument.

cea/schemas.yml (1)

15782-15833: Duplicate schema definitions for get_new_optimization_optimal_network_layout_file with conflicting fields

The file defines this schema twice with different field names/cases ('building' vs 'Building', 'pipe_DN' vs 'tipe_DN'). This will confuse consumers and schema validators.

Please keep a single definition and reconcile field names. Suggested approach: retain the first (lines 15782-15833) and remove/merge the second, fixing 'tipe_DN' typo to 'pipe_DN'.

Also applies to: 16096-16151

cea/import_export/result_summary.py (1)

1381-1419: network_name is undefined inside exec_aggregate_building (Ruff F821) and needs to be passed in

Within exec_aggregate_building, the new heat_rejection handling branches call:

heat_buildings_df = pd.read_csv(locator.get_heat_rejection_buildings(network_name=network_name))

but network_name is not defined in this function’s scope, which is exactly what Ruff flags (F821). The only place network_name is available is in process_building_summary.

You can fix this by threading network_name through the function signature and call site. For example:

-def exec_aggregate_building(locator, hour_start, hour_end, summary_folder, list_metrics, bool_use_acronym, list_list_useful_cea_results, list_buildings, list_appendix, list_selected_time_period, date_column='date', plot=False):
+def exec_aggregate_building(
+    locator,
+    hour_start,
+    hour_end,
+    summary_folder,
+    list_metrics,
+    bool_use_acronym,
+    list_list_useful_cea_results,
+    list_buildings,
+    list_appendix,
+    list_selected_time_period,
+    date_column='date',
+    plot=False,
+    network_name='',
+):
@@
-                if appendix == 'heat_rejection':
-                    # Get entity names from heat_rejection_buildings.csv
-                    heat_buildings_df = pd.read_csv(locator.get_heat_rejection_buildings(network_name=network_name))
+                if appendix == 'heat_rejection':
+                    # Get entity names from heat_rejection_buildings.csv
+                    heat_buildings_df = pd.read_csv(
+                        locator.get_heat_rejection_buildings(network_name=network_name)
+                    )
@@
-                if appendix == 'heat_rejection':
-                    # Get entity names from heat_rejection_buildings.csv
-                    heat_buildings_df = pd.read_csv(locator.get_heat_rejection_buildings(network_name=network_name))
+                if appendix == 'heat_rejection':
+                    # Get entity names from heat_rejection_buildings.csv
+                    heat_buildings_df = pd.read_csv(
+                        locator.get_heat_rejection_buildings(network_name=network_name)
+                    )
@@
-                if appendix == 'heat_rejection':
-                    # Get entity names from heat_rejection_buildings.csv
-                    heat_buildings_df = pd.read_csv(locator.get_heat_rejection_buildings(network_name=network_name))
+                if appendix == 'heat_rejection':
+                    # Get entity names from heat_rejection_buildings.csv
+                    heat_buildings_df = pd.read_csv(
+                        locator.get_heat_rejection_buildings(network_name=network_name)
+                    )

and in process_building_summary Step 8:

-                    exec_aggregate_building(locator, hour_start, hour_end, summary_folder, list_metrics, bool_use_acronym, list_list_useful_cea_results, list_buildings, list_appendix, list_selected_time_period, plot=plot)
+                    exec_aggregate_building(
+                        locator,
+                        hour_start,
+                        hour_end,
+                        summary_folder,
+                        list_metrics,
+                        bool_use_acronym,
+                        list_list_useful_cea_results,
+                        list_buildings,
+                        list_appendix,
+                        list_selected_time_period,
+                        plot=plot,
+                        network_name=network_name,
+                    )

This both fixes the lint error and ensures the heat-rejection building/entity mapping uses the same network scope as the rest of the summary.

Also applies to: 1453-1464

cea/inputlocator.py (1)

1053-1065: Thermal-network helper refactor: unnamed networks produce malformed filenames

The _get_thermal_network_results_file_path helper centralizes path logic, but has an edge case with unnamed networks:

When network_name is empty or None, the helper falls back to get_thermal_network_folder() but still interpolates network_name into the filename, producing malformed names like DH__metadata_edges.csv (with double underscore) instead of a proper pattern.

Call sites confirm this is a real issue:

  • cea/optimization/optimization_main.py:146 explicitly calls get_thermal_network_edge_list_file(network_type, '') with an empty string in a validation check
  • cea/optimization/master/summarize_network.py:57 calls get_thermal_network_edge_list_file(network_type) with a missing network_name argument entirely (runtime error)

Either:

  • Enforce non-empty network_name for all thermal network paths (simplest approach), or
  • Add an explicit branch in _get_thermal_network_results_file_path to handle unnamed networks with a different naming pattern
🟡 Minor comments (10)
cea/analysis/costs/supply_costs.py-636-636 (1)

636-636: Remove unused import to fix pipeline failure.

The Ruff linter reports F401: Unused import 'os'. This import is redundant as os is already imported at module level (line 9).

 def calculate_standalone_building_costs(locator, config, network_name):
     ...
-    import os
-
     print("  Loading buildings and demands...")
cea/analysis/heat/main.py-7-7 (1)

7-7: Remove unused import to fix pipeline failure.

The os import is unused and causes the Ruff linter to fail (F401). This blocks CI.

 """
 import os
+# Remove the unused import above
 import cea.config

Apply this fix:

-import os
 import cea.config
cea/analysis/costs/solar_costs.py-451-512 (1)

451-512: Potential KeyError if expected columns don't exist in final_results.

When updating existing rows (lines 481-491), the code assumes columns like Capex_total_USD, TAC_USD, etc. exist. If final_results is missing any of these columns, this will raise a KeyError.

Consider adding a guard or ensuring upstream code always provides these columns:

# Ensure required columns exist before updating
required_cols = ['Capex_total_USD', 'Capex_a_USD', 'Opex_fixed_a_USD', 
                 'Opex_var_a_USD', 'Opex_a_USD', 'TAC_USD']
missing = [c for c in required_cols if c not in result.columns]
if missing:
    raise ValueError(f"Missing required columns in final_results: {missing}")
cea/tests/test_anthropogenic_heat.py-106-107 (1)

106-107: Trivial assertion: len(results) >= 0 is always true.

     # Should have results (buildings may have cooling systems even with DH only)
-    assert len(results) >= 0, "Results should be present"
+    # DH-only may still produce results if buildings have standalone cooling
+    # This is a weak assertion - consider checking for specific expected behavior
cea/tests/test_baseline_costs.py-115-118 (1)

115-118: Manual test runner should also create the locator.

If the function signature requires locator, the manual test runner needs updating too:

 if __name__ == '__main__':
     # For manual testing
     config = cea.config.Configuration()
-    test_baseline_costs_reference_case(config)
+    # Note: test_baseline_costs_reference_case expects a config fixture
+    # For manual testing, you may need to run via pytest

Committable suggestion skipped: line range outside the PR's diff.

cea/tests/test_anthropogenic_heat.py-73-74 (1)

73-74: Trivial assertion: len(detailed) >= 0 is always true.

This assertion provides no value as a list length can never be negative. Consider asserting a minimum expected count or removing the assertion.

     # Check detailed output
     detailed = pd.read_csv(locator.get_heat_rejection_components())
-    assert len(detailed) >= 0, "Components output missing"
+    # Components output can be empty if no heat-rejection components exist
+    assert 'name' in detailed.columns, "Missing 'name' column in components output"
cea/config.py-1838-1941 (1)

1838-1941: SolarPanelChoiceParameter: docstring vs behaviour and decode I/O

The SolarPanelChoiceParameter implementation is generally consistent with the config guidelines (lenient decode, strict encode, dynamic _choices via the potentials/solar folder), but there are two nits:

  • The decode docstring says “Returns empty string if value is None or empty”, while the implementation returns 'No solar technology installed' in that case. The docstring should be updated to match the actual behaviour.
  • decode calls _get_available_solar_technologies(), which touches the filesystem via InputLocator and glob. That is fine in practice, but if these parameters are accessed frequently it may be worth keeping decode as cheap as possible and relying on encode for stricter validation, in line with the config guidelines.

Suggested docstring tweak:

-        """
-        Decode value - lenient parsing for config loading.
-        Returns empty string if value is None or empty.
-        """
+        """
+        Decode value – lenient parsing for config loading.
+        Returns 'No solar technology installed' if value is None, empty,
+        or not present in the available solar technologies.
+        """
cea/config.py-1025-1091 (1)

1025-1091: DC/DH network layout subclasses do not actually filter by network type and unnecessarily mutate config state

The DCNetworkLayoutChoiceParameter and DHNetworkLayoutChoiceParameter implementations temporarily override config.thermal_network.network_type and config.optimization_new.network_type, but this has no effect. The parent NetworkLayoutChoiceParameter._get_available_networks() lists folders in the thermal-network directory that are not named 'DH' or 'DC' (see line 908: if os.path.isdir(...) and name not in self._network_types). It does not inspect the config parameters being overridden.

Consequently:

  • The docstrings claiming "Hardcoded to filter for DC/DH networks" are misleading; no actual filtering occurs.
  • Mutating config state in a parameter helper is surprising and violates the principle that config helpers should not have side effects.

Remove the temporary config overrides and either delegate directly to the parent implementation or implement true filtering via InputLocator if DC/DH-specific results need to be distinguished (e.g. checking for folders at {network_name}/DC/ or {network_name}/DH/).

cea/visualisation/special/cost_breakdown.py-336-440 (1)

336-440: Refine y-axis range handling clarity and plumb network_name through baseline costs loading

The y-axis range code at lines ~425–426 is functionally correct—Plotly supports None in range parameters, treating it as autorange for that bound. However, the current logic is redundant: checking (y_min is not None or y_max is not None) then unconditionally passing both values will silently use autorange for whichever is None. For clarity, simplify to:

y_min = plot_config_general.y_min
y_max = plot_config_general.y_max
if y_min is not None or y_max is not None:
    fig.update_xaxes(range=[y_min, y_max])

More importantly, load_baseline_costs_data() (line 76) and main() (line 462) call get_baseline_costs_detailed() without passing network_name, always loading from the no-network folder. Since baseline costs are written per-network (as shown in cea/analysis/costs/main.py), plumb the network name through the config or via a dedicated parameter—similar to how get_network_buildings_and_area() already handles it in this file—so costs can be loaded for the specified network context.

Finally, the module docstring uses American spelling ("visualization"); align it to British spelling ("visualisation") for consistency with project guidelines and the filename itself.

cea/analysis/heat/heat_rejection.py-356-452 (1)

356-452: Network plant splitting vs. component breakdown may double-count per-plant components

calculate_network_heat_rejection correctly:

  • Builds a single network-scale SupplySystem (plant_supply_system) for the DH/DC network.
  • Computes total annual and peak heat rejection from plant_supply_system.heat_rejection.
  • Splits the summary metrics (annual, hourly profile, peak) equally among all plant nodes when there are multiple plants.

However, in save_heat_rejection_outputs:

  • Each plant entry carries the same plant_supply_system object via the supply_system field.
  • extract_component_heat_rejection is called for each plant, and always uses supply_system.heat_rejection and installed_components as-is.

This means:

  • The heat_rejection_buildings.csv summary correctly reports per-plant values (split by num_plants), but
  • heat_rejection_components.csv will contain duplicate component rows for each plant, each representing the full network’s components and heat rejection. Aggregating that file naively will over-count district-scale heat rejection by a factor of num_plants.

If the intent is for the components file to be per-network (not per-plant), you could:

  • Only emit component rows once per network (e.g. for the first plant) or
  • Add a field like network_id and document that the rows are network-level, not per-plant.

If, instead, you want true per-plant component accounting, you’ll need to either:

  • Divide heat_series and any relevant capacity indicators by num_plants before passing them into extract_component_heat_rejection, or
  • Construct one SupplySystem per plant with appropriately scaled demand.

Right now the split logic only applies to summaries, not to component breakdowns.

Also applies to: 820-922

🧹 Nitpick comments (34)
cea/optimization_new/domain.py (1)

120-128: Consider logging the skipped building for transparency.

The defensive error handling is appropriate and prevents crashes when loading buildings with incompatible network types. However, silently skipping buildings may confuse users about why certain buildings aren't included in the optimization.

Consider adding a log message:

                 if has_demand and not is_district:
                     try:
                         building.check_demand_energy_carrier()
                     except ValueError as e:
                         # Skip buildings with incompatible network types (e.g., DH network with cooling-only building)
                         # This can happen when loading all buildings with a specific network_type filter
                         if "Network type mismatch" in str(e):
+                            print(f"  Skipping {building_code}: Incompatible with {network_type} network (network type mismatch)")
                             continue
                         else:
                             raise

This would help users understand why the building count might differ from expectations.

cea/analysis/heat/AGENTS.md (1)

11-11: Consider adding language specifiers to fenced code blocks.

For improved syntax highlighting and markdown rendering, consider specifying languages for the fenced code blocks:

-```
+```text
 ✅ **DO**: Treat plants as "buildings" with serviced GFA for normalisation
 ❌ **DON'T**: Use plant footprint area - meaningless for district systems

This is a minor formatting improvement flagged by markdownlint.




Also applies to: 243-243

</blockquote></details>
<details>
<summary>cea/interfaces/dashboard/map_layers/life_cycle_analysis/layers.py (3)</summary><blockquote>

`26-27`: **Remove redundant imports inside the function.**

`geopandas` and `CRS` are already imported at module level (lines 5-6). These local imports are unnecessary.

```diff
 def safe_filter_buildings_with_geometry(locator, buildings: list) -> tuple:
     """
     Filter buildings to only include those that exist in zone geometry.
     Returns tuple of (filtered_buildings, geometry_df, centroids).
     Gracefully handles missing buildings by excluding them.
     """
-    import geopandas as gpd
-    from pyproj import CRS
-
     if not buildings:
         return [], None, []

568-592: Move the empty entity check earlier to avoid unnecessary processing.

The empty check at line 586 happens after creating the GeoDataFrame. Move it earlier to avoid unnecessary work when there are no entities.

     # Set buildings file as index
     buildings_df = buildings_df.set_index('name')

+    # Handle case where there are no entities
+    if not entity_names:
+        output['data'] = []
+        output['properties']['range'] = {
+            'total': {'label': 'Total Range', 'min': 0.0, 'max': 0.0},
+            'period': {'label': 'Period Range', 'min': 0.0, 'max': 0.0}
+        }
+        return output
+
     # Get zone geometry for CRS
     zone_gdf = gpd.read_file(self.locator.get_zone_geometry())

     # Create GeoDataFrame with entity coordinates
     entity_gdf = gpd.GeoDataFrame(
         buildings_df.loc[entity_names],
         geometry=gpd.points_from_xy(
             buildings_df.loc[entity_names]['x_coord'],
             buildings_df.loc[entity_names]['y_coord']
         ),
         crs=zone_gdf.crs
     )
     entity_centroids = entity_gdf.geometry.to_crs(CRS.from_epsg(4326))

-    # Handle case where there are no entities or all files are missing
-    if not entity_names:
-        output['data'] = []
-        output['properties']['range'] = {
-            'total': {'label': 'Total Range', 'min': 0.0, 'max': 0.0},
-            'period': {'label': 'Period Range', 'min': 0.0, 'max': 0.0}
-        }
-        return output
-
     values = (get_data(entity_name, centroid)

531-533: Add error handling for reading the buildings file.

Other layers in this file wrap CSV reading in try/except blocks (e.g., lines 176-198). Consider adding similar protection here to handle missing or malformed files gracefully.

+    try:
         # Read buildings file to get entity list and coordinates
         buildings_df = pd.read_csv(self.locator.get_heat_rejection_buildings())
         entity_names = buildings_df['name'].tolist()
+    except (FileNotFoundError, pd.errors.EmptyDataError, KeyError) as e:
+        print(f"Warning: Error reading heat rejection buildings: {e}")
+        output['properties']['range'] = {
+            'total': {'label': 'Total Range', 'min': 0.0, 'max': 0.0},
+            'period': {'label': 'Period Range', 'min': 0.0, 'max': 0.0}
+        }
+        return output
cea/interfaces/dashboard/map_layers/demand/layers.py (1)

12-36: Consider extracting shared utility function to reduce duplication.

This safe_filter_buildings_with_geometry function is duplicated across three files:

  • cea/interfaces/dashboard/map_layers/demand/layers.py
  • cea/interfaces/dashboard/map_layers/life_cycle_analysis/layers.py
  • cea/interfaces/dashboard/map_layers/renewable_energy_potentials/layers.py

Consider extracting it to a shared module (e.g., cea/interfaces/dashboard/map_layers/utils.py) to maintain a single source of truth.

cea/analysis/costs/supply_costs.py (3)

152-152: Remove redundant local imports.

Several functions have local imports of modules already imported at module level (line 9-13). Consider removing these redundant imports:

  • Line 152: import pandas as pd
  • Line 195: import pandas as pd
  • Lines 256-262: Multiple imports already at module level
  • Line 457: import pandas as pd
  • Lines 738-739: import pandas as pd and import os

Also applies to: 195-195, 256-262, 457-457, 738-739


816-824: Consider using logging levels instead of stdout suppression.

The pattern of redirecting stdout to suppress messages is fragile. Consider using Python's logging module with appropriate log levels, allowing callers to control verbosity via configuration.

This is a minor improvement suggestion for future refactoring.


546-547: Avoid silently swallowing exceptions.

The bare except Exception: pass hides all errors, making debugging difficult. At minimum, log the exception.

     except Exception:
-        pass
+        pass  # Network layout file may not exist yet - this is expected

Or better:

-    except Exception:
-        pass
+    except Exception as e:
+        # Network layout may not exist - return empty set
+        print(f"Warning: Could not read network layout: {e}")
cea/analysis/costs/system_costs.py (1)

246-246: Consider terminal compatibility for emoji.

The emoji ⚠️ may not render correctly in all terminal environments (e.g., Windows cmd.exe, some CI logs). Consider using a text-based alternative.

-    print('\n⚠️  WARNING: This script is deprecated. Please use "cea baseline-costs" instead.\n')
+    print('\nWARNING: This script is deprecated. Please use "cea baseline-costs" instead.\n')
cea/analysis/costs/format_simplified.py (1)

100-109: Consider defensive access for service_costs keys.

The code directly accesses keys like service_costs['scale'], service_costs['capex_total_USD'], etc. without checking if they exist. If the upstream data structure is malformed, this will raise KeyError.

Consider using .get() with defaults for non-critical fields, or add validation at the function entry to ensure the expected structure:

# Example defensive access
scale = service_costs.get('scale', 'BUILDING')
capex_total = service_costs.get('capex_total_USD', 0.0)
cea/optimization_new/building.py (1)

257-268: The self-assignment on line 268 is a no-op.

Line 268 assigns self.demand_flow.energy_carrier to itself, which has no effect. If the intent is to preserve the existing carrier, simply remove this line or add a clarifying comment.

             # Otherwise: has demand but no space heating components -> DHW-only building
             # Set demand flow energy carrier to match what was loaded (T30W for DH, T10W for DC)
             # DHW fallback logic in supply_costs.py will handle creating the actual DHW system
-            self.demand_flow.energy_carrier = self.demand_flow.energy_carrier  # Keep existing carrier from load_demand_profile
+            # Keep existing energy carrier from load_demand_profile (T30W for DH, T10W for DC)
+            pass
cea/analysis/costs/solar_costs.py (1)

416-419: Semantic mismatch: storing area in capacity_kW column.

The column name capacity_kW implies power capacity, but solar panel costs are area-based (m²). This could confuse downstream consumers. Consider renaming to capacity_m2 or adding a separate column.

             detail_rows.append({
                 'name': building_name,
                 'network_type': '',
                 'service': 'SOLAR',
                 'code': tech_code,
-                'capacity_kW': costs['area_m2'],  # Store area in the capacity column
+                'capacity_m2': costs['area_m2'],  # Solar uses area, not power capacity
                 'placement': facade_suffix.replace('_m2', ''),  # e.g., 'roofs_top'

If schema constraints require capacity_kW, add a comment explaining this is actually area for solar systems.

cea/analysis/costs/main.py (5)

31-31: Import os inside function.

Consider moving to module-level imports for consistency, unless lazy loading is intentional.

 import pandas as pd
 from cea.inputlocator import InputLocator
 import cea.config
+import os

Then remove the import os from inside validate_network_results_exist.


121-149: Significant code duplication between standalone and network modes.

The solar cost calculation and result merging logic (lines 121-149 for standalone, lines 263-297 for network mode) is nearly identical. Extract to a helper function.

def _calculate_and_merge_solar_costs(config, locator, final_results, detailed_results):
    """Calculate solar costs and merge into results."""
    from cea.analysis.costs.solar_costs import calculate_building_solar_costs, merge_solar_costs_to_buildings
    import geopandas as gpd

    zone_gdf = gpd.read_file(locator.get_zone_geometry())
    all_building_names = zone_gdf['name'].tolist()

    if config.system_costs.buildings:
        all_building_names = [b for b in all_building_names if b in config.system_costs.buildings]

    solar_details, solar_summary = calculate_building_solar_costs(config, locator, all_building_names)

    if not solar_details.empty:
        detailed_results = pd.concat([detailed_results, solar_details], ignore_index=True)
        print(f"  Added {len(solar_details)} solar panel component rows")

    if not solar_summary.empty:
        final_results = merge_solar_costs_to_buildings(final_results, solar_summary)
        print(f"  Updated costs for {len(solar_summary)} buildings with solar panels")

    return final_results, detailed_results

Also applies to: 263-297


325-327: Fragile check: assumes network names start with 'N'.

name.startswith('N') to identify networks could match building names starting with 'N'. Consider a more robust check.

     # Check if any networks were found
-    has_networks = any(name.startswith('N') for name in final_results['name'])
+    # Networks have 'type' == 'network' or 'plant' in the results
+    has_networks = 'type' in final_results.columns and final_results['type'].eq('plant').any()

Or use a dedicated column/flag to identify network entries.


227-243: Error handling catches ValueError but may hide unexpected issues.

The check for specific error messages (lines 230-239) is fragile. If error messages change, this logic breaks silently.

Consider using custom exception types instead of string matching:

class NoValidSupplySystemError(ValueError):
    """Raised when no valid supply systems are found."""
    pass

Then catch this specific exception type.


82-89: Docstring return type mismatch.

The docstring says :return: DataFrame with cost results but the function returns None in standalone mode (line 164) and final_results in network mode. Update for accuracy.

     """
     Calculate baseline costs for heating and/or cooling systems.

     :param locator: InputLocator instance
     :param config: Configuration instance
-    :return: DataFrame with cost results
+    :return: DataFrame with cost results (network mode) or None (standalone mode)
     """
cea/config.py (1)

1269-1519: DistrictSupplyTypeParameter logic looks solid; watch decode/choices I/O cost

The DistrictSupplyTypeParameter implementation correctly:

  • Builds dynamic choices from ASSEMBLIES/SUPPLY CSVs with explicit BUILDING vs DISTRICT scale.
  • Prioritises options currently in use in supply.csv.
  • Enforces at most one building‑scale and one district‑scale selection in encode, with clear error messages.
  • Returns labelled values from get() while storing unlabelled codes in the config, and auto‑defaults from supply.csv when the config is empty.

Two minor points to consider:

  • _get_all_supply_options, _get_supply_options_in_use, and _get_auto_default all hit the filesystem / pandas, and are invoked from _choices, decode, and get(). Depending on how often these parameters are accessed, this could become noticeable. If it does, you could cache results per‑scenario or per‑parameter instance with simple invalidation (e.g. on set).
  • The string "Custom (use component settings below)" appears in several places; extracting it into a small constant on the class would reduce duplication and avoid future typos.

Functionally this looks correct and matches the documented 1‑building + 1‑district selection rule.

cea/analysis/costs/AGENTS.md (1)

7-8: Keep API signature and examples for get_component_codes_from_categories in sync; tighten wording

In this AGENTS doc, the top‑level API section documents:

get_component_codes_from_categories(locator, component_categories, network_type) → list[str]

but the DC/DH examples later call:

cooling_codes = get_component_codes_from_categories(locator, cooling_cats, network_type, peak_demand_kw)
heat_rejection_codes = get_component_codes_from_categories(locator, heat_rejection_cats, network_type, peak_demand_kw)

If the implementation now accepts peak_demand_kw (as implied by the example and “Filter by peak demand” principle), the main signature line should be updated to include that parameter and its meaning; otherwise, the example should be adjusted to match the actual function. Keeping the doc consistent makes it much easier to use from other modules.

Also, in the “Key behaviour” / fallback sections, there is a repeated adverb (“only ... only applies”) that could be simplified for clarity (e.g. “Checks services the building provides standalone – applies only if Building Properties/Supply has a DISTRICT‑scale code”).

Also applies to: 82-117, 260-305

cea/visualisation/special/cost_breakdown.py (1)

29-67: Guard against zero total area during normalisation and avoid per-row recomputation

process_data_by_grouping normalises costs per area when y_normalised_by != 'no_normalisation' by computing:

if y_normalised_by == 'gross_floor_area':
    total_area = architecture_df['GFA_m2'].sum()
else:
    total_area = architecture_df['Af_m2'].sum()
...
for col in selected_cost_cols:
    df_agg[col] = df_agg[col] / total_area

and similarly uses per‑group normaliser values for the by_building_and_network case, with a few fallbacks to 1 to avoid divide‑by‑zero.

Two improvements would make this more robust:

  • If total_area is 0 (e.g. malformed Total_demand.csv or all‑zero areas), the aggregated views will divide by zero and produce infinities. Consider explicitly checking for total_area <= 0 and either:
    • Skipping normalisation (and perhaps emitting a warning), or
    • Returning an empty frame or raising a clear error, rather than silently producing invalid numbers.
  • Inside the by_building_and_network branch, get_network_buildings_and_area is called once per network row; that’s fine for small networks but scales linearly. If this becomes hot, you could memoise results per (network_name, network_type, area_col) to avoid re-reading the same shapefile repeatedly.

The high‑level logic looks correct; this is mainly about defensive handling of degenerate area data and minor performance tuning.

Also applies to: 140-267

cea/schemas.yml (4)

1859-1935: Unify currency units and mark primary key; fill used_by for traceability

  • Currency unit uses '[$USD(2015)]' here but most tables use '[USD 2015]'. Pick one and apply consistently to avoid downstream unit parsing issues.
  • Consider marking name as primary for easier validation and joins.
  • used_by is empty; add downstream consumers (e.g., multi_criteria_analysis) for pipeline traceability.

Apply minimally:

@@
-      Capex_total_USD:
-        description: Total capital expenditure for all supply systems and components
-        type: float
-        unit: '[$USD(2015)]'
+      Capex_total_USD:
+        description: Total capital expenditure for all supply systems and components
+        type: float
+        unit: '[USD 2015]'
@@
-      Capex_total_building_scale_USD:
+      Capex_total_building_scale_USD:
         description: Total capital expenditure for building-scale systems
         type: float
-        unit: '[$USD(2015)]'
+        unit: '[USD 2015]'
@@
-      Capex_total_district_scale_USD:
+      Capex_total_district_scale_USD:
         description: Total capital expenditure for district-scale systems (networks + piping)
         type: float
-        unit: '[$USD(2015)]'
+        unit: '[USD 2015]'
@@
-      name:
-        description: Building or network name
-        type: string
-        unit: NA
+      name:
+        description: Building or network name
+        type: string
+        primary: true
+        unit: NA
@@
-  used_by: []
+  used_by:
+  - multi_criteria_analysis

Based on learnings, Opex_var_a_USD should reflect FEEDSTOCKS costs joined to assemblies; this schema matches that split.


1937-1994: Constrain enums and add lookups for stronger validation

  • Add choices for network_type and scale; constrain placement values to the documented set; make service values explicit to avoid drift.
  • Consider lookups for component codes and energy carriers to existing databases.
@@
-      network_type:
-        description: Network type (DC for district cooling, DH for district heating) or empty for standalone buildings
-        type: string
-        unit: NA
+      network_type:
+        description: Network type (empty for standalone buildings)
+        type: string
+        unit: NA
+        choice:
+          values: ["", "DC", "DH"]
@@
-      service:
-        description: Energy service (e.g., GRID_cs for grid cooling, DC_network for district cooling network, NONE for buildings with no systems)
-        type: string
-        unit: NA
+      service:
+        description: Energy service label
+        type: string
+        unit: NA
+        choice:
+          values: ["GRID_cs","GRID_hs","GRID_ww","DC_network","DC_plant","DH_network","DH_plant","NONE"]
@@
-      code:
-        description: Component code (e.g., CH1 for chiller type 1, CT1 for cooling tower type 1, PIPES for distribution piping) or energy carrier code (e.g., E230AC for electricity, NATURALGAS for natural gas)
-        type: string
-        unit: NA
+      code:
+        description: Component code (CONVERSION) or energy carrier code (ENERGY_CARRIERS)
+        type: string
+        unit: NA
+        choice:
+          lookup:
+            path: get_database_components_feedstocks_energy_carriers
+            column: code
@@
-      placement:
+      placement:
         description: Component placement in system (primary for heat/cooling generation, secondary for intermediate conversion, tertiary for heat rejection, distribution for piping, energy_carrier for fuel/electricity costs)
-        type: string
-        unit: NA
+        type: string
+        unit: NA
+        choice:
+          values: ["primary","secondary","tertiary","distribution","energy_carrier"]
@@
-      scale:
-        description: System scale (BUILDING for individual building systems, DISTRICT for district network systems)
-        type: string
-        unit: NA
+      scale:
+        description: System scale
+        type: string
+        unit: NA
+        choice:
+          values: ["BUILDING","DISTRICT"]

Also align currency units to '[USD 2015]' like in the previous comment.


2043-2085: Add enums/lookups; align units and scale

  • Constrain network_type/scale, and add lookups for energy_carrier (ENERGY_CARRIERS) and component_code (CONVERSION tables).
  • Align units for currency if present (future-proofing).
@@
-      network_type:
+      network_type:
         description: Network type (DC for district cooling, DH for district heating) or empty for standalone buildings
-        type: string
-        unit: NA
+        type: string
+        unit: NA
+        choice:
+          values: ["", "DC", "DH"]
@@
-      component_code:
-        description: Component code (e.g., CH1 for chiller type 1, CT1 for cooling tower type 1)
-        type: string
-        unit: NA
+      component_code:
+        description: Component code (see CONVERSION databases)
+        type: string
+        unit: NA
+        choice:
+          lookup:
+            path: get_database_components_conversion_vapor_compression_chillers
+            column: code
@@
-      energy_carrier:
-        description: Energy carrier code for heat rejection medium (e.g., T_AIR_20C for air at 20C, T_AIR_ODB for outdoor air)
-        type: string
-        unit: NA
+      energy_carrier:
+        description: Energy carrier code for heat rejection medium
+        type: string
+        unit: NA
+        choice:
+          lookup:
+            path: get_database_components_feedstocks_energy_carriers
+            column: code
@@
-      scale:
+      scale:
         description: System scale (BUILDING for individual building systems, DISTRICT for district network plants)
-        type: string
-        unit: NA
+        type: string
+        unit: NA
+        choice:
+          values: ["BUILDING","DISTRICT"]

2086-2103: Use date type for timestamp; align column naming with repo convention

  • DATE is typed as string here but other time-series use type: date with '[datetime]'.
  • Consider 'Date' (capitalization used widely) for consistency.
@@
-      DATE:
-        description: Timestamp for this hour (8760 hours total, no leap day)
-        type: string
-        unit: NA
+      Date:
+        description: Timestamp for this hour (8760 hours total, no leap day)
+        type: date
+        unit: '[datetime]'
+        values: '{yyyy-mm-dd hh:mm:ss-Z}'
cea/inputlocator.py (1)

1217-1220: Solar potentials folder consolidation is good; docstrings slightly stale

Switching PV/SC/PVT totals to use get_potentials_solar_folder() standardises all solar potentials under scenario/outputs/data/potentials/solar, which matches the new API surface and simplifies downstream consumers. Implementation is straightforward and correct.

Minor nit: several existing docstrings around PV_totals, SC_totals, etc. still refer to older, more specific filenames (and even contain typos like a duplicated .csv). Consider updating those docstrings in a follow-up to avoid confusion, but no functional issue here.

Also applies to: 1243-1248, 1260-1265, 1274-1278

cea/import_export/result_summary.py (4)

266-275: Heat rejection results path: be explicit about error handling for missing inputs

The new cea_feature == 'heat_rejection' branch reads heat_rejection_buildings.csv to discover entities and then builds per-entity hourly paths via get_heat_rejection_hourly_building.

This is structurally correct and matches the new heat-rejection output layout. However, unlike most other features, any missing or misnamed heat_rejection_buildings.csv will raise immediately via pd.read_csv, instead of being handled more gracefully like load_cea_results_from_csv_files does for missing files.

Consider wrapping the read in an existence check / try–except to emit a clear, user-facing message such as “heat rejection results not found – run anthropogenic-heat first”, rather than a bare FileNotFoundError.


681-683: Heat rejection column mapping vs. writer: ensure units and naming are consistent

You’ve introduced:

  • A metrics→column mapping: "heat_rejection[kWh]" -> ["heat_rejection_kWh"]
  • A loader-side rename: heat_rejection_kW -> heat_rejection_kWh for CSVs that currently store heat_rejection_kW.

This keeps the internal mapping coherent and allows map_metrics_and_cea_columns to find the heat-rejection series, but there are two points to double‑check:

  1. save_heat_rejection_outputs still writes the hourly files with a heat_rejection_kW column name. After the rename, downstream users may see the same series under two different unit labels (kW in the raw file, kWh in summaries).
  2. If the underlying data truly represents power (kW) rather than hourly energy (kWh), treating it as energy in aggregations (sum over time) is conceptually inconsistent.

I’d recommend either:

  • Switching the writer to emit heat_rejection_kWh (and ensuring the series is hourly energy, not instantaneous power), or
  • Keeping it as heat_rejection_kW and mapping a dedicated “power” metric (e.g. heat_rejection[kW]) rather than renaming it to _kWh.

Please verify the intended physical quantity of SupplySystem.heat_rejection and align names/metrics accordingly.

Also applies to: 862-865


1405-1419: Re-reading heat_rejection_buildings.csv three times is redundant

In the annually, monthly, and seasonally branches you re-read heat_rejection_buildings.csv each time to derive entity_names. This is correct but a bit wasteful.

Once network_name is passed into exec_aggregate_building, consider loading the heat_rejection_buildings table once per appendix == 'heat_rejection' and reusing entity_names across the three blocks, e.g. by:

  • Computing entity_names and num_entities_per_period once before the if annually_rows / if monthly_rows / if seasonally_rows blocks, and
  • Sharing the computed list instead of re-reading the CSV.

This keeps behaviour identical but avoids unnecessary disk I/O in large scenarios.

Also applies to: 1453-1464


2740-2793: copy_costs_to_summary logic is sound; minor cleanups possible

The new helper does the right things:

  • Resolves source cost files via get_baseline_costs / get_baseline_costs_detailed, respecting network_name and the "no-network" fallback.
  • Optionally filters both buildings and components by name ∈ list_buildings.
  • Writes filtered copies into scenario/export/results/{folder_name}/costs/… via the new InputLocator helpers, creating the subfolder as needed.
  • Returns (success, error_message) for structured error handling in Step 8.5.

Two small polish points:

  • The module already imports pandas as pd at the top; you can drop the inner import pandas as pd here.
  • The user-facing error messages (“Please run 'system-costs' first.”) are clear and in British English, aligned with the guidelines.

Overall this is a good encapsulation of the cost-export step.

cea/analysis/heat/heat_rejection.py (3)

229-354: DHW heat-rejection system creation is powerful but quite complex; consider guarding against subtle failures

create_dhw_heat_rejection_system reimplements a good chunk of the supply-system construction pipeline to build a DHW-only SupplySystem for heat-rejection extraction. It:

  • Looks up the building’s DHW assembly code from supply.csv.
  • Resolves the assembly to a feedstock in SUPPLY_HOTWATER.csv.
  • Uses get_dhw_component_fallback to pick an appropriate component code.
  • Builds an hourly DHW demand series from Qww_sys_kWh in the building’s demand file.
  • Constructs EnergyFlow, SupplySystemStructure, and SupplySystem objects, initialising Component globals via a temporary Domain.

This is a reasonable approach, but several failure paths already return None (missing supply entry, missing assemblies file, feedstock == 'NONE', zero demand, etc.). Combined with the broad except Exception at the end, it can be hard to diagnose why DHW heat rejection is missing for a given building.

Two suggestions to make this easier to maintain and debug:

  • Narrow the try/except scope to just the SupplySystemStructure/SupplySystem build/evaluate block, and let earlier validation failures log a short, explicit reason (e.g. “no DHW assembly for building X”).
  • Log which of the early return conditions was hit (e.g. feedstock is NONE, missing Qww_sys_kWh, zero DHW demand) so users can tell those apart from genuine internal errors.

The existing functional behaviour is fine; this is about observability and future debugging.


748-771: Coordinate extraction should handle both Name and name columns in zone geometry

get_building_coordinates_from_geometry currently does:

zone_geom = gpd.read_file(zone_geom_path).set_index('name')

and falls back to (0.0, 0.0) if anything goes wrong. In other parts of the codebase (e.g. exec_aggregate_building for architecture), you already handle the Name vs name naming inconsistency explicitly.

For robustness across older scenarios and reference cases, consider mirroring that logic here:

-        zone_geom = gpd.read_file(zone_geom_path).set_index('name')
-
-        if building_name in zone_geom.index:
-            centroid = zone_geom.loc[building_name].geometry.centroid
+        zone_geom_raw = gpd.read_file(zone_geom_path)
+        if 'name' in zone_geom_raw.columns:
+            zone_geom = zone_geom_raw.set_index('name')
+        elif 'Name' in zone_geom_raw.columns:
+            zone_geom = zone_geom_raw.set_index('Name')
+        else:
+            print(f"      Warning: Zone geometry missing 'name'/'Name' column, using (0.0, 0.0)")
+            return 0.0, 0.0
+
+        if building_name in zone_geom.index:
+            centroid = zone_geom.loc[building_name].geometry.centroid
             return centroid.x, centroid.y

This avoids spurious (0.0, 0.0) coordinates when only the capitalised Name column is present.


820-877: Hourly writer assumes 8760+ values; consider guarding against short series

save_heat_rejection_outputs assumes each hourly_profile has at least 8760 points, with an optional leap-day removal when len(hourly_profile) > 8760. If, for any reason, a shorter series is passed (e.g. partial-year simulations or trimming upstream), pd.DataFrame({'date': timestamps, 'heat_rejection_kW': hourly_profile[:8760]}) will raise due to mismatched lengths.

It may be worth asserting the expected length before writing, for clearer failures:

-        # Ensure exactly 8760 hours (remove leap day if present)
-        if len(hourly_profile) > 8760:
+        # Ensure exactly 8760 hours (remove leap day if present)
+        if len(hourly_profile) > 8760:
             # Remove Feb 29 (day 60, hours 1416-1439 in leap year)
             hourly_profile = remove_leap_day(hourly_profile)
+        if len(hourly_profile) < 8760:
+            raise ValueError(
+                f"Expected at least 8760 hourly values for '{building_name}', "
+                f"got {len(hourly_profile)}."
+            )

or, if partial years are expected, explicitly documenting and handling that case (e.g. by reindexing with NaNs) instead of relying on implicit errors.

cea/default.config (1)

529-575: Semantic duplication between [system-costs] and [anthropogenic-heat] sections.

Both sections redundantly declare identical parameter structures (network-name, network-type, supply-type-cs/hs/dhw, cooling/heating/heat-rejection-components). While functionally acceptable if they serve distinct workflows, consider whether a shared base configuration or DRY principle could eliminate this duplication.

Comment on lines +50 to +56
buildings_in_network = network_data.get('DH', {}).get('buildings') or \
network_data.get('DC', {}).get('buildings', [])
total_gfa = 0.0
for building in buildings_in_network:
building_demand = demand[demand['name'] == building.identifier]
if not building_demand.empty:
total_gfa += building_demand.iloc[0]['GFA_m2']
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Find how buildings are stored in network data
rg -n "buildings.*=" cea/analysis/costs/format_simplified.py -A 3 -B 3

# Search for building class definitions and identifier attribute
rg -n "class.*[Bb]uilding" cea/ --type py | head -20

# Look for how identifier is used in building contexts
rg -n "\.identifier" cea/analysis/costs/ --type py | head -20

# Search for network data structure definition
rg -n "network_data.*get.*buildings" cea/ --type py | head -20

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 4918


🏁 Script executed:

# Check the demand DataFrame and how it uses 'name'
rg -n "demand\['name'\]" cea/ --type py | head -20

# Look for building object definitions in the codebase
fd "building" cea/ --type py | xargs grep -l "identifier" | head -10

# Search for how network.buildings is populated
rg -n "\.buildings\s*=" cea/ --type py | head -20

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 1605


🏁 Script executed:

# Check if buildings_in_network is ever populated with strings vs objects
rg -n "buildings_in_network" cea/ --type py -B 5 -A 5

# Look for building data structures in network-related code
rg -n "class.*Network" cea/ --type py | head -10

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 6494


🏁 Script executed:

# Find the full context of format_simplified.py to see where network_data comes from
cat -n cea/analysis/costs/format_simplified.py | head -100

# Search for where network_data is passed to this function
rg -n "format_simplified" cea/ --type py -B 5 -A 5

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 7496


🏁 Script executed:

# Look for Network class definition to see if it has a buildings attribute
cat -n cea/optimization_new/network.py | head -150

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 8320


🏁 Script executed:

# Search for where network_data dict is constructed with 'buildings' key
rg -n "network_data\[.*\]\[.*buildings" cea/ --type py -B 3 -A 3
rg -n "'buildings':" cea/ --type py -B 5 -A 5

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 36731


🏁 Script executed:

# Find where network_buildings is created in supply_costs.py
rg -n "network_buildings\s*=" cea/analysis/costs/supply_costs.py -B 5 -A 2 | head -50

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 986


🏁 Script executed:

# Find how network_buildings is stored in results dict for networks
rg -n "results\[network_id\]\s*=" cea/analysis/costs/supply_costs.py -A 10 | grep -A 10 "buildings"

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 899


🏁 Script executed:

# Also check the standalone building case to see what 'buildings' structure should be
rg -n "results\[building_id\]\s*=" cea/analysis/costs/supply_costs.py -B 2 -A 15

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 1145


🏁 Script executed:

# Find the context around line 1422 where buildings is defined
sed -n '1350,1430p' cea/analysis/costs/supply_costs.py | cat -n

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 4334


🏁 Script executed:

# Find where merge_network_type_costs is called and trace the results
rg -n "merge_network_type_costs" cea/analysis/costs/ -A 5 -B 5

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 2488


🏁 Script executed:

# Find the merge_network_type_costs function definition
rg -n "def merge_network_type_costs" cea/analysis/costs/ -A 30

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 1988


🏁 Script executed:

# Get full context around line 1315 and 1422 to understand the code flow
sed -n '1280,1330p' cea/analysis/costs/supply_costs.py

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 2312


🏁 Script executed:

# Get context around line 1422
sed -n '1380,1430p' cea/analysis/costs/supply_costs.py

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 2384


🏁 Script executed:

# Find which function is actually called - trace from baseline_costs_main
sed -n '82,300p' cea/analysis/costs/main.py | cat -n | grep -A 50 "baseline_costs_main"

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 2724


🏁 Script executed:

# Search for calls to calculate_network_costs or the function with line 1315
rg -n "def calculate" cea/analysis/costs/supply_costs.py | head -20

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 824


🏁 Script executed:

# Find calculate_costs_for_network_type and see what it returns
sed -n '1433,1550p' cea/analysis/costs/supply_costs.py

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 6257


🏁 Script executed:

# Check if there's any object conversion happening between calculate_costs_for_network_type and format_simplified
sed -n '1480,1700p' cea/analysis/costs/supply_costs.py | head -100

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 5804


Line 54 will raise AttributeError: buildings in network_data are strings, not objects.

The buildings_in_network list contains building identifier strings (from nodes_df['building'].unique().tolist() in supply_costs.py), not Building objects. Accessing building.identifier on a string will fail. Use building directly in the comparison since it's already the identifier:

building_demand = demand[demand['name'] == building]
🤖 Prompt for AI Agents
In cea/analysis/costs/format_simplified.py around lines 50 to 56, the loop
assumes each item in buildings_in_network is a Building object and uses
building.identifier, but buildings_in_network actually contains identifier
strings; change the comparison to use the string directly (e.g.,
demand[demand['name'] == building]) and remove the .identifier access so the
code filters by the building id string and avoids AttributeError.

Comment on lines +1303 to +1305
# Annualize (default network lifetime: 20 years)
network_lifetime_yrs = 20.0
piping_cost_annual = piping_cost_total / network_lifetime_yrs
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Piping cost annualization should use the proper formula.

Per coding guidelines, annualized CAPEX should use: CAPEX × IR% / (1 - (1 + IR%)^-LT).

The current simple division (piping_cost_total / network_lifetime_yrs) doesn't account for the time value of money and will underestimate annualized costs.

     # Annualize (default network lifetime: 20 years)
     network_lifetime_yrs = 20.0
-    piping_cost_annual = piping_cost_total / network_lifetime_yrs
+    # Use default interest rate (typical for infrastructure)
+    ir_pct = 0.05  # 5% - consider making this configurable
+    if ir_pct > 0:
+        piping_cost_annual = piping_cost_total * ir_pct / (1 - (1 + ir_pct) ** -network_lifetime_yrs)
+    else:
+        piping_cost_annual = piping_cost_total / network_lifetime_yrs

Based on coding guidelines: "Calculate Annualized CAPEX using the formula: CAPEX × IR% / (1 - (1 + IR%)^-LT)".

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In cea/analysis/costs/supply_costs.py around lines 1303-1305, the piping cost is
currently annualized by simple division which ignores discounting; replace it
with the standard annualized CAPEX formula using the model's real discount rate
(IR) and the network_lifetime_yrs: compute piping_cost_annual =
piping_cost_total * ir / (1 - (1 + ir) ** (-network_lifetime_yrs)), ensuring ir
is a decimal (e.g., 0.03 for 3%); also add a safe fallback that if ir == 0 uses
piping_cost_total / network_lifetime_yrs to avoid division by zero.

Comment on lines +49 to +83
def anthropogenic_heat_main(locator, config):
"""
Main function to calculate anthropogenic heat rejection.
This follows the same 6-case building connectivity logic as system-costs:
- Case 1: Standalone mode (no networks)
- Cases 2-6: Network + standalone combinations
:param locator: InputLocator instance
:param config: Configuration instance with anthropogenic-heat parameters
"""
print("\n" + "=" * 70)
print("ANTHROPOGENIC HEAT ASSESSMENT")
print("=" * 70)

# Get network configuration
network_name = config.anthropogenic_heat.network_name
network_types = config.anthropogenic_heat.network_type

# Validate network types
if not network_types or len(network_types) == 0:
raise ValueError("No network types selected. Please select at least one from: DH, DC")

# Check for standalone-only mode
is_standalone_only = (not network_name or network_name.strip() == "" or
network_name.strip() == "(none)")

if is_standalone_only:
print("Mode: STANDALONE ONLY (all buildings assessed as standalone systems)")
print("District-scale supply systems will be treated as building-scale for heat calculations.")
print(f"Network types (for supply system selection): {', '.join(network_types)}\n")
else:
print(f"Mode: NETWORK + STANDALONE")
print(f"Network layout: {network_name}")
print(f"Network types: {', '.join(network_types)}\n")
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix Ruff errors: unused imports, unused variable, and f-strings without placeholders

Ruff is correctly flagging several issues that will block CI:

  • Line 18: from cea.optimization_new.domain import Domain at module level is unused (Domain is only needed inside create_dhw_heat_rejection_system).
  • Line 81 & 101: print(f"Mode: NETWORK + STANDALONE") and print(f"\n-" * 70) are f-strings without interpolation (F541).
  • Line 205: from cea.analysis.costs.supply_costs import apply_dhw_component_fallback in calculate_standalone_heat_rejection is unused.
  • Line 246: dhw_system = apply_dhw_component_fallback(...) in create_dhw_heat_rejection_system is assigned but never used (and the subsequent code builds its own DHW system anyway).
  • Line 471: import cea.config in create_network_supply_system is unused.

You can address all of these with a small, safe refactor:

@@
-from cea.optimization_new.domain import Domain
@@
-    if is_standalone_only:
-        print("Mode: STANDALONE ONLY (all buildings assessed as standalone systems)")
-        print("District-scale supply systems will be treated as building-scale for heat calculations.")
-        print(f"Network types (for supply system selection): {', '.join(network_types)}\n")
-    else:
-        print(f"Mode: NETWORK + STANDALONE")
-        print(f"Network layout: {network_name}")
-        print(f"Network types: {', '.join(network_types)}\n")
+    if is_standalone_only:
+        print("Mode: STANDALONE ONLY (all buildings assessed as standalone systems)")
+        print("District-scale supply systems will be treated as building-scale for heat calculations.")
+        print(f"Network types (for supply system selection): {', '.join(network_types)}\n")
+    else:
+        print("Mode: NETWORK + STANDALONE")
+        print(f"Network layout: {network_name}")
+        print(f"Network types: {', '.join(network_types)}\n")
@@
-        for network_type in network_types:
-            print(f"\n-" * 70)
+        for network_type in network_types:
+            print("\n" + "-" * 70)
@@ def calculate_standalone_heat_rejection(locator, config, network_types):
-    from cea.analysis.costs.supply_costs import filter_supply_code_by_scale
+    from cea.analysis.costs.supply_costs import filter_supply_code_by_scale
@@
-                from cea.analysis.costs.supply_costs import apply_dhw_component_fallback
                 dhw_system = create_dhw_heat_rejection_system(locator, building_obj, supply_system)
@@ def create_dhw_heat_rejection_system(locator, building, main_supply_system):
-    from cea.analysis.costs.supply_costs import apply_dhw_component_fallback
@@
-        dhw_system = apply_dhw_component_fallback(locator, building, main_supply_system)
-
-        # apply_dhw_component_fallback returns costs dict, not the supply system
-        # We need to recreate the DHW supply system to get heat rejection
-        # Actually, we need a different approach - let's directly call the DHW system creation
-        #
-        # Import the necessary modules
+        # Apply DHW fallback logic directly by constructing a DHW-only SupplySystem
@@ def create_network_supply_system(locator, config, network_type, network_name,
-    from cea.optimization_new.containerclasses.energyFlow import EnergyFlow
-    import cea.config
+    from cea.optimization_new.containerclasses.energyFlow import EnergyFlow

This preserves behaviour while making the file lint-clean.

Also applies to: 205-207, 246-247, 471-471

🧰 Tools
🪛 GitHub Actions: Ruff

[error] 81-81: F541: f-string without any placeholders. Remove extraneous 'f' prefix.

🤖 Prompt for AI Agents
In cea/analysis/heat/heat_rejection.py around lines 49-83 (and additionally
address the related occurrences at 205-207, 246-247, and 471), fix Ruff F541 and
unused-import/unused-variable issues: remove or relocate unused imports (drop
module-level "from cea.optimization_new.domain import Domain" and instead import
Domain inside create_dhw_heat_rejection_system where it is needed; remove "from
cea.analysis.costs.supply_costs import apply_dhw_component_fallback" at line
~205 and the unused "import cea.config" at ~471), remove the unused assignment
dhw_system = apply_dhw_component_fallback(...) in
create_dhw_heat_rejection_system, and convert f-strings that have no
placeholders to regular string literals (e.g., change print(f"Mode: NETWORK +
STANDALONE") to print("Mode: NETWORK + STANDALONE") and print(f"\n" + "-" * 70)
or print("\n" + "-" * 70) as appropriate). Ensure behavior is unchanged and run
linter to confirm warnings are resolved.

Comment on lines +380 to 456
[system-costs]
network-name =
network-name.type = NetworkLayoutChoiceParameter
network-name.nullable = true
network-name.help = Select an existing network layout for the cost calculation, or select "(none)" to assess all buildings as standalone systems (ignoring district-scale supply settings). Networks are created by the 'thermal-network' script (part 1 and part 2 must be completed).

network-type = DH, DC
network-type.type = MultiChoiceParameter
network-type.choices = DH, DC
network-type.help = Type(s) of district network to calculate costs for: "DH" (district heating), "DC" (district cooling), or both. Select multiple by separating with commas.

supply-type-cs =
supply-type-cs.type = DistrictSupplyTypeParameter
supply-type-cs.supply-category = SUPPLY_COOLING
supply-type-cs.help = Cooling supply system type(s) for baseline cost calculations. Select up to 2 assemblies: one building-scale (for standalone buildings) and one district-scale (for district-connected buildings). CEA automatically uses the appropriate scale based on each building's connectivity. If nothing is selected, CEA will fall back to the supply systems specified in Building Properties/Supply. Select "Custom (use component settings below)" to use the component parameters instead of a predefined assembly.
supply-type-cs.category = Energy Supply Configuration

supply-type-hs =
supply-type-hs.type = DistrictSupplyTypeParameter
supply-type-hs.supply-category = SUPPLY_HEATING
supply-type-hs.help = Heating supply system type(s) for baseline cost calculations. Select up to 2 assemblies: one building-scale (for standalone buildings) and one district-scale (for district-connected buildings). CEA automatically uses the appropriate scale based on each building's connectivity. If nothing is selected, CEA will fall back to the supply systems specified in Building Properties/Supply. Select "Custom (use component settings below)" to use the component parameters instead of a predefined assembly.
supply-type-hs.category = Energy Supply Configuration

supply-type-dhw =
supply-type-dhw.type = DistrictSupplyTypeParameter
supply-type-dhw.supply-category = SUPPLY_HOTWATER
supply-type-dhw.help = Domestic hot water supply system type(s) for baseline cost calculations. Select up to 2 assemblies: one building-scale (for standalone buildings) and one district-scale (for district-connected buildings). CEA automatically uses the appropriate scale based on each building's connectivity. If nothing is selected, CEA will fall back to the supply systems specified in Building Properties/Supply. Select "Custom (use component settings below)" to use the component parameters instead of a predefined assembly.
supply-type-dhw.category = Energy Supply Configuration

cooling-components = VAPOR_COMPRESSION_CHILLERS, ABSORPTION_CHILLERS
cooling-components.type = MultiChoiceParameter
cooling-components.choices = VAPOR_COMPRESSION_CHILLERS, ABSORPTION_CHILLERS
cooling-components.help = Technologies to consider for district cooling plants. Order indicates priority for deployment. Used both as fallback when supply-type-cs is not set AND to determine component activation order when SUPPLY assemblies are used.
cooling-components.category = Energy Supply Configuration

operational = true
operational.type = BooleanParameter
operational.help = Estimate Operational costs (variable and fixed) due to supply systems operation (allocated according to the supply-systems input database)
heating-components = BOILERS, HEAT_PUMPS
heating-components.type = MultiChoiceParameter
heating-components.choices = BOILERS, HEAT_PUMPS, COGENERATION_PLANTS
heating-components.help = Technologies to consider for district heating plants. Order indicates priority for deployment. Used both as fallback when supply-type-hs is not set AND to determine component activation order when SUPPLY assemblies are used.
heating-components.category = Energy Supply Configuration

heat-rejection-components = COOLING_TOWERS
heat-rejection-components.type = MultiChoiceParameter
heat-rejection-components.choices = COOLING_TOWERS
heat-rejection-components.help = Technologies used for heat rejection. Used both as fallback when supply assemblies don't specify heat rejection components AND to determine component activation order when SUPPLY assemblies are used.
heat-rejection-components.category = Energy Supply Configuration

buildings =
buildings.type = BuildingsParameter
buildings.help = Buildings to include in solar technology installation.
buildings.category = Solar Technology Configuration

panels-on-roof =
panels-on-roof.type = SolarPanelChoiceParameter
panels-on-roof.help = Type(s) of solar panels to consider for rooftop installation cost calculations.
panels-on-roof.category = Solar Technology Configuration

panels-on-wall-north =
panels-on-wall-north.type = SolarPanelChoiceParameter
panels-on-wall-north.help = Type(s) of solar panels to consider for north-facing facade installation cost calculations.
panels-on-wall-north.category = Solar Technology Configuration

panels-on-wall-south =
panels-on-wall-south.type = SolarPanelChoiceParameter
panels-on-wall-south.help = Type(s) of solar panels to consider for south-facing facade installation cost calculations.
panels-on-wall-south.category = Solar Technology Configuration

panels-on-wall-east =
panels-on-wall-east.type = SolarPanelChoiceParameter
panels-on-wall-east.help = Type(s) of solar panels to consider for east-facing facade installation cost calculations.
panels-on-wall-east.category = Solar Technology Configuration

panels-on-wall-west =
panels-on-wall-west.type = SolarPanelChoiceParameter
panels-on-wall-west.help = Type(s) of solar panels to consider for west-facing facade installation cost calculations.
panels-on-wall-west.category = Solar Technology Configuration

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add default values or declare .nullable = true for empty configuration parameters.

Per the coding guidelines, all parameters in default.config must maintain default values. Several parameters in the new [system-costs] section lack explicit defaults and are missing .nullable = true declarations:

  • network-name (line 381): empty, has .nullable = true
  • supply-type-cs/hs/dhw (lines 391, 397, 403): empty, no .nullable declared
  • buildings (line 427): empty, no .nullable declared
  • panels-on-* (lines 432–455): empty, no .nullable declared

Either assign meaningful defaults or explicitly mark parameters as nullable for clarity.

  supply-type-cs =
  supply-type-cs.type = DistrictSupplyTypeParameter
  supply-type-cs.supply-category = SUPPLY_COOLING
+ supply-type-cs.nullable = true
  supply-type-cs.help = ...
  
  buildings =
  buildings.type = BuildingsParameter
+ buildings.nullable = true
  buildings.help = Buildings to include in solar technology installation.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In cea/default.config around lines 380 to 456, several parameters in the
[system-costs] section (supply-type-cs at ~391, supply-type-hs ~397,
supply-type-dhw ~403, buildings ~427, panels-on-roof and panels-on-wall-*
~432–455) are empty and missing .nullable = true or a default; update each empty
parameter to either provide a sensible default value or explicitly add a
.nullable = true declaration (matching nearby parameter style), ensuring you add
the .type and .help entries remain unchanged and keep categories consistent.

Comment on lines +3049 to +3058
if config.result_summary.metrics_heat_rejection:
try:
calc_ubem_analytics_normalised(locator, hour_start, hour_end, "heat-rejection", summary_folder,
list_selected_time_period, bool_aggregate_by_building, bool_use_acronym,
bool_use_conditioned_floor_area_for_normalisation, plot=plot)
except Exception as e:
error_msg = f"Step 9 (Advanced Analytics - heat-rejection): {str(e)}"
errors_encountered.append(error_msg)
print(f"Warning: {error_msg}")
print(" Continuing with remaining analytics...")
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Advanced analytics for heat rejection uses wrong feature key (heat-rejection vs heat_rejection)

In Step 9, you’ve added:

if config.result_summary.metrics_heat_rejection:
    calc_ubem_analytics_normalised(locator, hour_start, hour_end, "heat-rejection", summary_folder, ...)

However, throughout the rest of the module:

  • The feature key is "heat_rejection" (underscore), e.g. in dict_plot_metrics_cea_feature and map_metrics_cea_features.
  • "heat-rejection" (hyphen) is used only as the plot folder slug via that mapping.

Passing "heat-rejection" into calc_ubem_analytics_normalised causes:

  • map_metrics_cea_features(..., direction="features_to_metrics") to return no metrics, since "heat-rejection" is not a key in the mapping dict.
  • results_writer_time_period to later receive an empty list_metrics, which can cascade into incorrect folder resolution or a TypeError when computing target_path.

Use the canonical feature key with underscore here:

-            if config.result_summary.metrics_heat_rejection:
-                try:
-                    calc_ubem_analytics_normalised(locator, hour_start, hour_end, "heat-rejection", summary_folder,
+            if config.result_summary.metrics_heat_rejection:
+                try:
+                    calc_ubem_analytics_normalised(locator, hour_start, hour_end, "heat_rejection", summary_folder,
                                                    list_selected_time_period, bool_aggregate_by_building, bool_use_acronym,
                                                    bool_use_conditioned_floor_area_for_normalisation, plot=plot)

This keeps it consistent with the rest of the mapping and allows dict_plot_metrics_cea_feature to handle the hyphenated plot folder name when plot=True.

🤖 Prompt for AI Agents
In cea/import_export/result_summary.py around lines 3049 to 3058, the call to
calc_ubem_analytics_normalised passes the hyphenated feature key
"heat-rejection" which is not present in the module's feature-to-metric
mappings; change the fourth argument to the canonical underscored feature key
"heat_rejection" so map_metrics_cea_features and dict_plot_metrics_cea_feature
resolve metrics correctly (leave plot and other flags unchanged so the plotting
logic can still derive the hyphenated folder slug when needed).

Comment on lines +23 to +24
@pytest.mark.skip(reason="Requires reference case to be extracted and demand to be run")
def test_anthropogenic_heat_reference_case(config):
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing config fixture definition (same issue as test_baseline_costs.py).

The test accepts a config parameter but no fixture is defined.

Add a fixture or ensure it exists in conftest.py.

🤖 Prompt for AI Agents
In cea/tests/test_anthropogenic_heat.py around lines 23-24, the test function
declares a config parameter but no config fixture is defined; add a pytest
fixture named config (either in this test module or centrally in conftest.py)
that yields the needed configuration object or import the existing fixture, and
update the test (or conftest) so pytest can resolve the config parameter before
running the test.

Comment on lines +23 to +24
@pytest.mark.skip(reason="Requires reference case to be extracted and demand to be run")
def test_baseline_costs_reference_case(config):
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

rg -n "def config" cea/tests/conftest.py

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 152


🏁 Script executed:

fd -name "conftest.py" .

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 321


🏁 Script executed:

cat -n cea/tests/test_baseline_costs.py | head -50

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 2080


🏁 Script executed:

fd "test_.*\.py" cea/tests/ --type f

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 1042


🏁 Script executed:

rg -l "def test_" cea/tests/ | head -5 | xargs -I {} sh -c 'echo "=== {} ===" && head -40 {}'

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 7516


🏁 Script executed:

rg "@pytest.fixture" cea/tests/ -A 2

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 576


Missing config fixture definition.

The test function accepts a config parameter but no fixture is defined in this file or in cea/tests/conftest.py. Define a config fixture at module level:

@pytest.fixture
def config():
    return cea.config.Configuration()

Alternatively, create cea/tests/conftest.py if it doesn't exist and add this fixture there to share it across multiple test files (test_baseline_costs.py and test_anthropogenic_heat.py both use this pattern).

🤖 Prompt for AI Agents
In cea/tests/test_baseline_costs.py around lines 23-24, the test function uses
an undefined pytest fixture named `config`; add a module-level pytest fixture
that returns a Configuration instance (or create cea/tests/conftest.py to
provide it globally) so tests receive a valid config object; implement a fixture
named `config` that returns cea.config.Configuration() and ensure pytest imports
pytest and cea.config as needed.

Comment on lines +36 to +37
# Run baseline-costs
baseline_costs_main(config)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Incorrect function signature: missing locator argument.

baseline_costs_main expects (locator, config) but only config is passed. This will cause a TypeError when the test is unskipped.

     # Run baseline-costs
-    baseline_costs_main(config)
+    locator = cea.inputlocator.InputLocator(config.scenario)
+    baseline_costs_main(locator, config)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Run baseline-costs
baseline_costs_main(config)
# Run baseline-costs
locator = cea.inputlocator.InputLocator(config.scenario)
baseline_costs_main(locator, config)
🤖 Prompt for AI Agents
In cea/tests/test_baseline_costs.py around lines 36-37, the call to
baseline_costs_main is missing the required locator argument; change the call to
baseline_costs_main(locator, config) and ensure a valid locator is available in
the test (either add a locator fixture parameter to the test function or
create/import a Locator instance before the call).

Comment on lines +74 to +75

baseline_costs_main(config)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Same signature issue: missing locator argument.

-    baseline_costs_main(config)
+    locator = cea.inputlocator.InputLocator(config.scenario)
+    baseline_costs_main(locator, config)
🤖 Prompt for AI Agents
In cea/tests/test_baseline_costs.py around lines 74-75, the call
baseline_costs_main(config) is missing the required locator argument; update the
test to call baseline_costs_main(config, locator) (providing the appropriate
locator fixture or constructing one) or modify the test setup to supply the
locator variable expected by the function signature so the function is invoked
with both parameters.

Comment on lines +91 to +92

baseline_costs_main(config)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Same signature issue: missing locator argument.

-    baseline_costs_main(config)
+    locator = cea.inputlocator.InputLocator(config.scenario)
+    baseline_costs_main(locator, config)
🤖 Prompt for AI Agents
In cea/tests/test_baseline_costs.py around lines 91-92, the call
baseline_costs_main(config) is missing the required locator argument matching
the function signature; update the test to call baseline_costs_main(config,
locator) where locator is the appropriate project locator used in other tests
(import or construct it in the test setup if not already available) so the call
matches the function signature and provides a valid locator instance.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (3)
cea/analysis/costs/supply_costs.py (2)

769-819: Still mutating supply.csv in-place when applying config fallbacks

apply_config_fallbacks_for_service_needs edits supply_df and then writes it back to the original supply.csv:

This permanently mutates input data during a calculation run, which is risky:

  • Breaks the inputs/outputs separation (scenario configuration changes without explicit user action).
  • Can cause hard‑to‑trace differences between runs and issues in multi‑script workflows.
  • Creates potential race conditions if scenarios are processed concurrently.

Prefer keeping these fallbacks in memory only: e.g. compute a per‑building fallback map and pass it into the domain/sizing logic, or write a temporary copy under /outputs/ and have all downstream readers use that path instead of overwriting the original.


1385-1388: Annualise piping CAPEX using the standard formula, not simple division

Piping costs are currently annualised as:

Per the cost guidelines, annualised CAPEX should use:

CAPEX × IR% / (1 - (1 + IR%)^-LT)

Using straight division ignores discounting and underestimates annual costs for non‑zero interest rates.

Consider switching to the standard formula with a configurable interest rate (falling back to simple division when IR = 0), e.g.:

-    network_lifetime_yrs = 20.0
-    piping_cost_annual = piping_cost_total / network_lifetime_yrs
+    network_lifetime_yrs = 20.0
+    ir = 0.05  # example default; ideally read from config or database
+    if ir > 0:
+        piping_cost_annual = piping_cost_total * ir / (1 - (1 + ir) ** -network_lifetime_yrs)
+    else:
+        piping_cost_annual = piping_cost_total / network_lifetime_yrs
cea/import_export/result_summary.py (1)

3060-3069: Advanced analytics for heat rejection still uses wrong feature key (heat-rejection vs heat_rejection)

This block still calls:

calc_ubem_analytics_normalised(..., "heat-rejection", ...)

but everywhere else in this module the feature key is "heat_rejection" (underscore), and "heat-rejection" is only the plot slug in dict_plot_metrics_cea_feature. Passing the hyphenated string leads to:

  • map_metrics_cea_features(..., direction="features_to_metrics") returning an empty metric list,
  • results_writer_time_period later seeing cea_feature=None and constructing an invalid folder path (TypeError).

Use the canonical feature key here.

-            if config.result_summary.metrics_heat_rejection:
-                try:
-                    calc_ubem_analytics_normalised(locator, hour_start, hour_end, "heat-rejection", summary_folder,
+            if config.result_summary.metrics_heat_rejection:
+                try:
+                    calc_ubem_analytics_normalised(locator, hour_start, hour_end, "heat_rejection", summary_folder,
                                                    list_selected_time_period, bool_aggregate_by_building, bool_use_acronym,
                                                    bool_use_conditioned_floor_area_for_normalisation, plot=plot)
🧹 Nitpick comments (8)
cea/config.py (3)

1025-1056: Avoid mutating config inside DC/DH network layout parameters

DCNetworkLayoutChoiceParameter._get_available_networks (and DH equivalent) temporarily overwrite config.thermal_network.network_type and config.optimization_new.network_type. This couples a pure “choices” helper to global configuration state and can surprise callers, especially if configuration is reused concurrently or in long‑lived UIs.

Given that InputLocator is instantiated only with scenario inside _get_available_networks, these temporary assignments likely have no effect on the locator path resolution and just add risk.

Prefer deriving DC/DH‑specific layouts directly from locator methods (e.g. using network‑type‑specific folders or file presence) without mutating config, or at least centralise this logic in a small, well‑documented helper with no external side‑effects.


1269-1563: DistrictSupplyTypeParameter logic looks solid; consider small clarity tweaks

The multi‑select + scale‑aware behaviour (auto‑defaults from supply.csv, bidirectional auto‑add of missing scales, and strict validation in encode) is consistent with the documented 6‑case logic and cost workflows. The use of labels in get() while storing bare codes is also clean.

Two minor clarity improvements you may want to consider:

  • In _get_auto_default / get, explicitly document in the docstring that internal storage is unlabeled but get() always returns labels (this is already implied, but being explicit will help future maintainers).
  • In decode, you silently drop values not present in all_options; if that is expected only for legacy/bad configs, a short comment noting that this is intentional backward‑compatibility behaviour would avoid confusion.

Functionally this class looks good to ship.


1883-1985: Align SolarPanelChoiceParameter.decode with guidelines and its docstring

Two points here:

  1. The docstring says “Returns empty string if value is None or empty”, but decode actually returns 'No solar technology installed'. Either the behaviour or the docstring should be updated so they match (current behaviour seems sensible given _choices).

  2. decode calls _get_available_solar_technologies(), which hits the filesystem via InputLocator and glob. Per the config guidelines, decode should be lenient and avoid expensive I/O and business‑rule validation; that work is better done in encode, using _choices as the authoritative allowed set. You can typically implement decode as:

    • map empty/None → 'No solar technology installed'
    • otherwise, return the raw string (possibly stripped), deferring existence checks to encode.

This would also avoid duplicating logic already present in _choices.

cea/analysis/costs/AGENTS.md (2)

7-7: Document full get_component_codes_from_categories signature

The API line currently omits the optional peak_demand_kw parameter, but the implementation in supply_costs.py and the examples below use it.

Consider updating the signature here to:

get_component_codes_from_categories(locator, component_categories, network_type, peak_demand_kw=None) → list[str]

so docs match the public API.


243-260: Add languages to fenced code blocks to satisfy markdownlint

Several newly added code blocks use bare triple backticks without a language (e.g. decision trees and ASCII diagrams around lines 243, 408, 424, 548, 599, 654). markdownlint is flagging these with MD040.

To fix, add appropriate languages, for example:

  • Use ```python for Python snippets.
  • Use text (or none) for decision trees / ASCII diagrams.
  • Use ```csv where showing CSV samples.

This keeps the file linter‑clean and improves syntax highlighting without changing semantics.

Also applies to: 408-414, 424-432, 548-554, 599-608, 654-675

cea/analysis/costs/main.py (1)

82-165: Clarify baseline_costs_main return type (standalone vs network mode)

In standalone mode (network-name empty/"(none)"), baseline_costs_main writes CSVs and returns None; in network mode it returns final_results after saving.

Since main() ignores the return value, this is harmless, but the docstring advertises “DataFrame with cost results” unconditionally. Either update the docstring to note the None return in standalone mode, or standardise to always return final_results (even if the caller currently discards it).

cea/analysis/costs/supply_costs.py (1)

1069-1276: District network component selection and fallbacks are consistent with AGENTS spec

The calculate_district_network_costs implementation correctly mirrors the documented behaviour:

  • Uses filter_supply_code_by_scale to respect BUILDING vs DISTRICT scale for SUPPLY assemblies.
  • When assemblies are missing or mismatched, falls back to cooling_components / heating_components / heat_rejection_components, filtered by peak_demand_kw via get_component_codes_from_categories.
  • For baseline, limits to the first viable component per category to avoid over‑complex mixes, and excludes absorption chillers by default in DC mode.

One minor nit: the comment at line 1281 (# ... None for fallback, dict for SUPPLY assembly) is now reversed: user_component_selection is a dict in both modes. Updating that comment would avoid confusion; otherwise the logic looks good.

cea/import_export/result_summary.py (1)

2751-2804: copy_costs_to_summary is correct overall; minor cleanups and behaviour checks

The helper correctly:

  • uses InputLocator for all paths (good per guidelines),
  • filters both costs_buildings and costs_components by list_buildings,
  • and writes into a dedicated summary costs folder.

Two small points:

  • The inner import pandas as pd is redundant since pd is already imported at module level; you can drop it.
  • Filtering df_costs_components by name will also drop any network/plant-level rows whose name does not match selected buildings; if you intend to keep plant costs even when filtering buildings, you may want a different criterion (e.g. keep non-building rows unfiltered).

Would you like to retain non-building components in costs_components.csv when list_buildings is provided, or should costs be strictly limited to the selected buildings?

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9a215b3 and da3b210.

📒 Files selected for processing (5)
  • cea/analysis/costs/AGENTS.md (2 hunks)
  • cea/analysis/costs/main.py (1 hunks)
  • cea/analysis/costs/supply_costs.py (1 hunks)
  • cea/config.py (4 hunks)
  • cea/import_export/result_summary.py (16 hunks)
🧰 Additional context used
📓 Path-based instructions (7)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Extract meaningful patterns into helper functions only when logic must be done in a specific way to avoid bugs, complexity warrants abstraction, algorithm requires deep understanding, or pattern encapsulates important business rules
Don't extract trivial wrappers: avoid extracting 1-2 line wrappers around existing functions, standard library usage, abstractions that obscure rather than clarify intent, or code that would be clearer inline
Always use InputLocator for file path resolution instead of hardcoding paths
Use British English spelling in all user-facing print statements, docstrings for public APIs, and error messages shown to users
Don't modify config directly in worker processes; use kwargs or create new instance instead
Check config.multiprocessing before using Pool for parallelization
Respect scenario structure conventions: use /inputs/ and /outputs/ directories for file organization
Use f-strings only when the string contains variables (e.g., f"Value: {x}"); use regular strings otherwise (e.g., "No variables") to avoid linter warnings

Files:

  • cea/analysis/costs/main.py
  • cea/config.py
  • cea/import_export/result_summary.py
  • cea/analysis/costs/supply_costs.py
cea/analysis/costs/**/*.py

📄 CodeRabbit inference engine (cea/analysis/costs/AGENTS.md)

cea/analysis/costs/**/*.py: Calculate Total CAPEX as: peak_kW × efficiency × CAPEX_USD2015kW
Calculate Fixed OPEX (O&M) as: CAPEX × O&M_% / 100
Calculate Annualized CAPEX using the formula: CAPEX × IR% / (1 - (1 + IR%)^-LT)
Calculate Total Annualized Cost (TAC) as: Fixed OPEX + Annualized CAPEX + Variable OPEX

Files:

  • cea/analysis/costs/main.py
  • cea/analysis/costs/supply_costs.py
cea/config.py

📄 CodeRabbit inference engine (AGENTS.md)

After modifying config.py, regenerate config.pyi by running pixi run python cea/utilities/config_type_generator.py

Files:

  • cea/config.py
cea/**/config.py

📄 CodeRabbit inference engine (cea/AGENTS.md)

cea/**/config.py: Separate parsing from validation in Parameter classes: decode() should only perform security checks (path traversal, injection) and be lenient, while encode() should validate both security concerns and business rules
Do not validate business rules in decode() - only validate security concerns in decode() to ensure fast and lenient config file loading
Use @Property decorator for dynamic choices in ChoiceParameter subclasses to scan resources on each access rather than caching statically
Declare dependencies in ChoiceParameter.initialize() using the depends_on list when dynamic choices depend on other parameters
Extract shared validation logic into separate helper methods: use _validate_security() for checks in both decode() and encode(), and _validate_business() for checks in encode() only
Avoid validating in decode() as it causes fragile config loading - expensive I/O operations and breaking old configs when resources are deleted
Avoid caching dynamic choices without invalidation mechanisms - use @Property to ensure choices are recalculated on each access
Regenerate config.pyi type stubs after modifying parameter classes by running: pixi run python cea/utilities/config_type_generator.py
Implement Parameter.decode(value: str) → Any to parse values from config files with only security checks and no business rule validation
Implement Parameter.encode(value: Any) → str to validate values before saving with both security checks and business rule validation

Files:

  • cea/config.py
cea/**/AGENTS.md

📄 CodeRabbit inference engine (cea/CLAUDE.md)

cea/**/AGENTS.md: Document agent implementations with clear descriptions of purpose, inputs, outputs, and configuration in AGENTS.md
Maintain AGENTS.md as the single source of truth for agent specifications and configurations

Files:

  • cea/analysis/costs/AGENTS.md
cea/analysis/costs/**/AGENTS.md

📄 CodeRabbit inference engine (cea/analysis/costs/CLAUDE.md)

Document agent implementations and configurations in AGENTS.md

Files:

  • cea/analysis/costs/AGENTS.md
**/AGENTS.md

📄 CodeRabbit inference engine (AGENTS.md)

**/AGENTS.md: Always create context-specific documentation as AGENTS.md (not CLAUDE.md)
Write AGENTS.md with concise, actionable content (aim for <150 lines), lead with API signatures, use code examples over prose, focus on patterns not details, use scannable structure, reference related files instead of duplicating, and never add emoticons to code files or comments
Prune verbose sections in AGENTS.md - if >200 lines, look for opportunities to condense and move detailed explanations to separate *_GUIDE.md files for human readers

Files:

  • cea/analysis/costs/AGENTS.md
🧠 Learnings (26)
📓 Common learnings
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/analysis/costs/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:11.074Z
Learning: Applies to cea/analysis/costs/cea/analysis/{costs,lca}/**/*.py : Merge CAPEX data from SUPPLY assemblies with variable OPEX data from FEEDSTOCKS for a complete cost picture
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/demand/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:30.367Z
Learning: Maintain distinction between HVAC components (affect demand via losses only, no costs) and SUPPLY components (contain all costs and emissions)
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/demand/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:30.367Z
Learning: Applies to cea/demand/**/*.py : Use final energy columns (`GRID_*`, `NG_*`, `DH_*`, `DC_*`) for energy cost and emissions calculations
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/analysis/costs/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:11.074Z
Learning: Applies to cea/analysis/costs/cea/analysis/**/*.py : Use the following database parameters from SUPPLY assemblies: CAPEX_USD2015kW (cost per kW), LT_yr (lifetime in years), O&M_% (annual O&M percentage), IR_% (interest rate), and efficiency (system efficiency)
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/demand/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:30.367Z
Learning: Applies to cea/demand/**/SUPPLY_*.csv : Classify SUPPLY components (chiller, boiler, etc.) as generation systems with CAPEX and OPEX costs in database
📚 Learning: 2025-11-26T23:07:11.074Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/analysis/costs/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:11.074Z
Learning: Applies to cea/analysis/costs/cea/analysis/{costs,lca}/**/*.py : Merge CAPEX data from SUPPLY assemblies with variable OPEX data from FEEDSTOCKS for a complete cost picture

Applied to files:

  • cea/analysis/costs/main.py
  • cea/analysis/costs/AGENTS.md
  • cea/analysis/costs/supply_costs.py
📚 Learning: 2025-11-26T23:06:48.123Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-26T23:06:48.123Z
Learning: Applies to **/cea/**/script.py : Create `main(config: Configuration)` as the entry point for new scripts in `cea/<module>/script.py`, register in `scripts.yml`, and the script auto-registers via `cea.api`

Applied to files:

  • cea/analysis/costs/main.py
📚 Learning: 2025-11-26T23:07:19.249Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/databases/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:19.249Z
Learning: Applies to cea/databases/**/*.py : Merge SUPPLY_ELECTRICITY.csv (infrastructure CAPEX) with COMPONENTS/FEEDSTOCKS/FEEDSTOCKS_LIBRARY/GRID.csv (hourly operational costs) using a left join on feedstock code to combine infrastructure and variable costs

Applied to files:

  • cea/analysis/costs/main.py
  • cea/analysis/costs/AGENTS.md
  • cea/import_export/result_summary.py
  • cea/analysis/costs/supply_costs.py
📚 Learning: 2025-11-26T23:07:19.249Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/databases/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:19.249Z
Learning: Applies to cea/databases/**/*.py : Use COMPONENTS/CONVERSION/*.csv files for district energy optimization with detailed equipment specifications including efficiency and capacity-dependent cost curves

Applied to files:

  • cea/config.py
  • cea/analysis/costs/AGENTS.md
  • cea/import_export/result_summary.py
  • cea/analysis/costs/supply_costs.py
📚 Learning: 2025-11-26T23:07:00.021Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:00.021Z
Learning: Applies to cea/**/config.py : Use property decorator for dynamic choices in ChoiceParameter subclasses to scan resources on each access rather than caching statically

Applied to files:

  • cea/config.py
📚 Learning: 2025-11-26T23:07:00.021Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:00.021Z
Learning: Applies to cea/**/config.py : Declare dependencies in ChoiceParameter.initialize() using the depends_on list when dynamic choices depend on other parameters

Applied to files:

  • cea/config.py
📚 Learning: 2025-11-26T23:06:18.144Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/analysis/costs/CLAUDE.md:0-0
Timestamp: 2025-11-26T23:06:18.144Z
Learning: Applies to cea/analysis/costs/**/AGENTS.md : Document agent implementations and configurations in AGENTS.md

Applied to files:

  • cea/analysis/costs/AGENTS.md
📚 Learning: 2025-11-26T23:07:30.367Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/demand/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:30.367Z
Learning: Applies to cea/demand/**/HVAC_*.csv : Classify HVAC components (AHU, radiators, etc.) as distribution/emission systems with no associated costs in database

Applied to files:

  • cea/analysis/costs/AGENTS.md
  • cea/import_export/result_summary.py
  • cea/analysis/costs/supply_costs.py
📚 Learning: 2025-11-26T23:07:11.074Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/analysis/costs/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:11.074Z
Learning: Applies to cea/analysis/costs/cea/analysis/**/*.py : Use the `calc_capex_annualized(capex_total, IR_%, LT_yr)` function to convert total CAPEX to annual cost

Applied to files:

  • cea/analysis/costs/AGENTS.md
  • cea/analysis/costs/supply_costs.py
📚 Learning: 2025-11-26T23:07:11.074Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/analysis/costs/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:11.074Z
Learning: Applies to cea/analysis/costs/**/*.py : Calculate Annualized CAPEX using the formula: CAPEX × IR% / (1 - (1 + IR%)^-LT)

Applied to files:

  • cea/analysis/costs/AGENTS.md
  • cea/analysis/costs/supply_costs.py
📚 Learning: 2025-11-26T23:07:11.074Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/analysis/costs/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:11.074Z
Learning: Applies to cea/analysis/costs/**/*.py : Calculate Total Annualized Cost (TAC) as: Fixed OPEX + Annualized CAPEX + Variable OPEX

Applied to files:

  • cea/analysis/costs/AGENTS.md
  • cea/analysis/costs/supply_costs.py
📚 Learning: 2025-11-26T23:07:11.074Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/analysis/costs/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:11.074Z
Learning: Applies to cea/analysis/costs/**/*.py : Calculate Total CAPEX as: peak_kW × efficiency × CAPEX_USD2015kW

Applied to files:

  • cea/analysis/costs/AGENTS.md
📚 Learning: 2025-11-26T23:07:19.249Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/databases/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:19.249Z
Learning: Applies to cea/databases/**/*.py : Use ASSEMBLIES/SUPPLY/*.csv files (not COMPONENTS) for building-level demand calculations with conservative efficiency values and single baseline costs

Applied to files:

  • cea/analysis/costs/AGENTS.md
  • cea/analysis/costs/supply_costs.py
📚 Learning: 2025-11-26T23:07:30.367Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/demand/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:30.367Z
Learning: Applies to cea/demand/**/SUPPLY_*.csv : Classify SUPPLY components (chiller, boiler, etc.) as generation systems with CAPEX and OPEX costs in database

Applied to files:

  • cea/analysis/costs/AGENTS.md
  • cea/analysis/costs/supply_costs.py
📚 Learning: 2025-11-26T23:07:11.074Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/analysis/costs/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:11.074Z
Learning: Applies to cea/analysis/costs/cea/analysis/**/*.py : Read CAPEX parameters from SUPPLY assemblies using files matching pattern `databases/{region}/ASSEMBLIES/SUPPLY/SUPPLY_*.csv`

Applied to files:

  • cea/analysis/costs/AGENTS.md
  • cea/analysis/costs/supply_costs.py
📚 Learning: 2025-11-26T23:07:11.074Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/analysis/costs/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:11.074Z
Learning: Applies to cea/analysis/costs/cea/analysis/**/*.py : Use the following database parameters from SUPPLY assemblies: CAPEX_USD2015kW (cost per kW), LT_yr (lifetime in years), O&M_% (annual O&M percentage), IR_% (interest rate), and efficiency (system efficiency)

Applied to files:

  • cea/analysis/costs/AGENTS.md
  • cea/import_export/result_summary.py
  • cea/analysis/costs/supply_costs.py
📚 Learning: 2025-11-26T23:07:11.074Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/analysis/costs/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:11.074Z
Learning: Applies to cea/analysis/costs/cea/analysis/**/*.py : Read variable OPEX (energy purchase costs) from feedstocks using files matching pattern `databases/{region}/COMPONENTS/FEEDSTOCKS/FEEDSTOCKS_LIBRARY/*.csv`

Applied to files:

  • cea/analysis/costs/AGENTS.md
📚 Learning: 2025-11-26T23:07:30.367Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/demand/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:30.367Z
Learning: Applies to cea/demand/**/*.py : Use final energy columns (`GRID_*`, `NG_*`, `DH_*`, `DC_*`) for energy cost and emissions calculations

Applied to files:

  • cea/analysis/costs/AGENTS.md
  • cea/import_export/result_summary.py
📚 Learning: 2025-11-26T23:07:30.367Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/demand/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:30.367Z
Learning: Applies to cea/demand/**/*.py : Use end-use columns (`Q*_sys_kWh`) for HVAC system sizing and capacity calculations

Applied to files:

  • cea/import_export/result_summary.py
📚 Learning: 2025-11-26T23:07:30.367Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/demand/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:30.367Z
Learning: Applies to cea/demand/**/thermal_loads.py : Calculate final energy from end-use energy by dividing by efficiency: `final_energy = Qhs_sys / efficiency_average_year` in thermal load calculations

Applied to files:

  • cea/import_export/result_summary.py
📚 Learning: 2025-11-26T23:07:30.367Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/demand/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:30.367Z
Learning: Document the distinction between end-use energy (includes HVAC losses) and final energy (includes efficiency losses) in output datasets

Applied to files:

  • cea/import_export/result_summary.py
📚 Learning: 2025-11-26T23:08:17.967Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/technologies/network_layout/AGENTS.md:0-0
Timestamp: 2025-11-26T23:08:17.967Z
Learning: Applies to cea/technologies/network_layout/**/*.py : Build thermal network connectivity by calling `calc_connectivity_network_with_geometry(streets_gdf, buildings_gdf, connection_candidates=k)` to produce NetworkX graph with preserved geometries, normalized node coordinates, and building_terminals metadata

Applied to files:

  • cea/import_export/result_summary.py
📚 Learning: 2025-11-26T23:07:30.367Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/demand/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:30.367Z
Learning: Maintain distinction between HVAC components (affect demand via losses only, no costs) and SUPPLY components (contain all costs and emissions)

Applied to files:

  • cea/analysis/costs/supply_costs.py
📚 Learning: 2025-11-26T23:07:11.074Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/analysis/costs/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:11.074Z
Learning: Ensure CAPEX is annualized before comparing with annual OPEX figures

Applied to files:

  • cea/analysis/costs/supply_costs.py
📚 Learning: 2025-11-26T23:07:11.074Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/analysis/costs/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:11.074Z
Learning: Applies to cea/analysis/costs/**/*.py : Calculate Fixed OPEX (O&M) as: CAPEX × O&M_% / 100

Applied to files:

  • cea/analysis/costs/supply_costs.py
🧬 Code graph analysis (3)
cea/analysis/costs/main.py (4)
cea/inputlocator.py (3)
  • InputLocator (28-1456)
  • get_baseline_costs (1370-1379)
  • get_baseline_costs_detailed (1381-1390)
cea/analysis/costs/supply_costs.py (2)
  • calculate_all_buildings_as_standalone (459-481)
  • calculate_standalone_building_costs (668-766)
cea/analysis/costs/format_simplified.py (1)
  • format_output_simplified (19-202)
cea/analysis/costs/solar_costs.py (2)
  • calculate_building_solar_costs (252-448)
  • merge_solar_costs_to_buildings (451-513)
cea/config.py (2)
cea/inputlocator.py (2)
  • get_building_supply (917-919)
  • get_potentials_solar_folder (1217-1219)
cea/utilities/__init__.py (1)
  • parse_string_to_list (90-96)
cea/import_export/result_summary.py (1)
cea/inputlocator.py (4)
  • get_heat_rejection_buildings (1397-1406)
  • get_heat_rejection_hourly_building (1419-1429)
  • get_baseline_costs (1370-1379)
  • get_baseline_costs_detailed (1381-1390)
🪛 GitHub Actions: Ruff
cea/import_export/result_summary.py

[error] 1384-1384: F821 Undefined name network_name


[error] 1407-1407: F821 Undefined name network_name


[error] 1456-1456: F821 Undefined name network_name

cea/analysis/costs/supply_costs.py

[error] 678-679: F401 [*] os imported but unused

🪛 LanguageTool
cea/analysis/costs/AGENTS.md

[style] ~357-~357: Consider using an alternative to strengthen your wording.
Context: ...designed for standalone buildings, user wants to explore district networks --- ####...

(WANT_KEEN)


[style] ~481-~481: This adverb was used twice in the sentence. Consider removing one of them or replacing them with a synonym.
Context: ...ices the building provides standalone - Only applies if Building Properties/Supply h...

(ADVERB_REPETITION_PREMIUM)

🪛 markdownlint-cli2 (0.18.1)
cea/analysis/costs/AGENTS.md

243-243: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


408-408: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


424-424: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


548-548: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


599-599: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


654-654: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: tests (macos-latest)
  • GitHub Check: tests (windows-latest)
  • GitHub Check: tests (ubuntu-latest)
🔇 Additional comments (6)
cea/analysis/costs/main.py (1)

170-259: Error handling for per‑network failures looks robust

The validation/calculation flow for network mode is sensible:

  • Per‑type validation with validate_network_results_exist and clear messages;
  • Per‑type calculation with selective skipping for “no demand/no valid supply system” cases;
  • Aggregation of validation_errors and calculation_errors into a single RuntimeError before saving, ensuring partial network results are not written.

This behaviour matches the multi‑network design described in AGENTS.md and keeps outputs consistent.

Also applies to: 300-329

cea/analysis/costs/supply_costs.py (1)

225-427: DHW fallback logic is well thought‑through

The DHW fallback path (get_dhw_component_fallback + apply_dhw_component_fallback) cleanly:

  • Maps feedstocks to boiler fuel codes and components using the BOILERS CSV;
  • Sizes the fallback system from hourly Qww_sys_kWh;
  • Builds a standalone SupplySystem with correct T60W carrier and re‑labels services to _ww.

This is a solid safety net for DHW‑only buildings and aligns with the documented Level‑3B fallback behaviour.

cea/import_export/result_summary.py (4)

69-70: Heat-rejection plot slug wiring looks consistent

Mapping 'heat_rejection''heat-rejection' here aligns with the feature key used elsewhere and the hyphenated folder slug convention; no issues spotted.


134-135: Heat-rejection metric is consistently wired through mappings and selectors

The additions for "heat_rejection[kWh]" in:

  • normalisation_name_mapping,
  • map_metrics_cea_features,
  • map_metrics_and_cea_columns,
  • and all the list_metrics_* helpers (with/without date, building, and plot variants),

are internally consistent and should allow the new metric to flow correctly through selection, loading, aggregation, and normalisation.

Also applies to: 397-399, 681-683, 2619-2620, 2654-2655, 2682-2683, 2721-2723, 2745-2746


266-275: Heat-rejection paths ignore list_buildings filter — confirm intended behaviour

This branch derives entity names solely from heat_rejection_buildings.csv and completely ignores the list_buildings filter passed into get_results_path. That means heat-rejection summaries will always include all entities in the network file, even if the user filtered buildings earlier.

Would you like heat-rejection results to honour the same building filters (intersection of entity_names and list_buildings), or is the “all entities in network” behaviour intentional for this feature?


2976-2991: Step 8.5 integration of copy_costs_to_summary looks good

The new Step 8.5 hooks metrics_costs into the pipeline cleanly: it only runs for non-plot summaries, respects network_name, and follows the same error-handling pattern as earlier steps (collecting and logging warnings instead of aborting).

Comment on lines +678 to +687
import os

print(" Loading buildings and demands...")

# 0. Determine STANDALONE mode or NETWORK mode
if network_name is None or network_name == "(none)":
all_buildings_standalone = True
else:
all_buildings_standalone = False

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove unused inner import os (Ruff F401)

Inside calculate_standalone_building_costs you re‑import os:

os is already imported at module level and not used in this function, which Ruff flags as F401.

You can safely delete the inner import os line to satisfy the linter.

🧰 Tools
🪛 GitHub Actions: Ruff

[error] 678-679: F401 [*] os imported but unused

🤖 Prompt for AI Agents
In cea/analysis/costs/supply_costs.py around lines 678 to 687 there is an
unnecessary inner "import os" inside calculate_standalone_building_costs which
is redundant because os is imported at module level and not used in the
function; simply remove that inner import line to satisfy Ruff F401 and keep the
function using the module-level os if needed.

Comment on lines +862 to +865
# Rename heat_rejection_kW to heat_rejection_kWh for consistency with energy units
if 'heat_rejection_kW' in df.columns:
df = df.rename(columns={'heat_rejection_kW': 'heat_rejection_kWh'})

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, examine the specific file and context around the problematic lines
cd /dev/shm/repo && sed -n '850,880p' cea/import_export/result_summary.py | cat -n

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 154


🏁 Script executed:

# Search for 'heat_rejection_kW' across the entire codebase
rg 'heat_rejection_kW' -n

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 2640


🏁 Script executed:

# Search for where heat_rejection columns are created or sourced
rg 'heat_rejection' -n -A 2 -B 2

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 50405


🏁 Script executed:

# Check for any transformations or calculations related to this column
ast-grep --pattern 'heat_rejection'

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 2981


🏁 Script executed:

sed -n '820,880p' cea/import_export/result_summary.py | cat -n

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 3179


🏁 Script executed:

# Check what data is being processed/imported at this point
sed -n '1,100p' cea/import_export/result_summary.py | cat -n

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 4449


🏁 Script executed:

# Look for any documentation or comments about heat_rejection handling
rg -B 5 -A 5 'heat_rejection_kW.*heat_rejection_kWh' cea/import_export/result_summary.py

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 925


🏁 Script executed:

# Check the file structure and method names
rg -n 'def ' cea/import_export/result_summary.py | head -30

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 2766


🏁 Script executed:

# Check what happens downstream with the renamed 'heat_rejection_kWh' column
rg 'heat_rejection_kWh' cea/import_export/result_summary.py

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 331


🏁 Script executed:

# Check if values are converted or aggregated after the rename in this function
sed -n '844,950p' cea/import_export/result_summary.py | cat -n

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 6501


🏁 Script executed:

# Verify the schema definition once more to confirm units
grep -A 5 'heat_rejection_kW:' cea/schemas.yml

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 794


🏁 Script executed:

# Final check: Verify if any conversion is applied to the hourly data to transform it to energy
rg -B 3 -A 3 'heat_rejection.*sum\|sum.*heat_rejection' cea/import_export/result_summary.py | head -40

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 73


🏁 Script executed:

# Check if aggregation or summation converts hourly power to energy
sed -n '937,1050p' cea/import_export/result_summary.py | cat -n

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 6102


The heat_rejection_kW to heat_rejection_kWh rename is a semantic error that misrepresents power as energy.

The source column heat_rejection_kW contains hourly power rates (documented in cea/schemas.yml as "Heat rejection rate at this hour" with unit [kW]). Renaming to heat_rejection_kWh without converting the values is incorrect—this mislabels power as energy. Downstream aggregation functions then sum these power values thinking they represent energy, which violates the expected energy unit convention used throughout the codebase (e.g., Q*_sys_kWh columns).

To fix: either multiply hourly power by 1 hour to produce actual energy values before renaming, or retain the kW unit and adjust result mappings accordingly.

Comment on lines +1381 to +1392
# For heat_rejection, use entity names from the data files instead of zone buildings
if appendix == 'heat_rejection':
# Get entity names from heat_rejection_buildings.csv
heat_buildings_df = pd.read_csv(locator.get_heat_rejection_buildings(network_name=network_name))
entity_names = heat_buildings_df['name'].tolist()
num_entities_per_period = len(entity_names)
else:
entity_names = list_buildings
num_entities_per_period = len(list_buildings)

if len(hourly_annually_df) / len(hourly_annually_df['period'].unique().tolist()) == num_entities_per_period:
hourly_annually_df.insert(0, 'name', entity_names)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's check if the file exists and its size
wc -l cea/import_export/result_summary.py

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 131


🏁 Script executed:

# Read the function signature and surrounding context
sed -n '1370,1395p' cea/import_export/result_summary.py | cat -n

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 1901


🏁 Script executed:

# Read the other two locations mentioned
sed -n '1395,1420p' cea/import_export/result_summary.py | cat -n

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 2157


🏁 Script executed:

# Read lines 1453-1464
sed -n '1450,1470p' cea/import_export/result_summary.py | cat -n

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 1701


🏁 Script executed:

# Find the function definition of exec_aggregate_building
rg -n "def exec_aggregate_building" cea/import_export/result_summary.py -A 1

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 592


🏁 Script executed:

# Find all calls to exec_aggregate_building
rg -n "exec_aggregate_building(" cea/import_export/result_summary.py

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 189


🏁 Script executed:

# Use simpler grep search for function calls
grep -n "exec_aggregate_building(" cea/import_export/result_summary.py

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 550


🏁 Script executed:

# Get more context around those call sites
grep -n "exec_aggregate_building(" cea/import_export/result_summary.py -B 5 -A 2

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 1493


🏁 Script executed:

# Check if network_name is available in the caller's context (look for the function containing the call)
sed -n '1700,1750p' cea/import_export/result_summary.py | cat -n

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 3296


🏁 Script executed:

# Let's find where this is called - search backwards from line 1700+
sed -n '1600,1700p' cea/import_export/result_summary.py | tail -50 | cat -n

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 2979


Add missing network_name parameter to exec_aggregate_building function

The function uses network_name at lines 1385, 1413, and 1457 when calling locator.get_heat_rejection_buildings(network_name=network_name), but network_name is not defined as a function parameter. This causes a runtime NameError (F821).

Add network_name to the function signature and pass it from the call site at line 2968 where network_name is available:

-def exec_aggregate_building(locator, hour_start, hour_end, summary_folder, list_metrics, bool_use_acronym, list_list_useful_cea_results, list_buildings, list_appendix, list_selected_time_period, date_column='date', plot=False):
+def exec_aggregate_building(locator, hour_start, hour_end, summary_folder, list_metrics, bool_use_acronym, list_list_useful_cea_results, list_buildings, list_appendix, list_selected_time_period, date_column='date', plot=False, network_name=None):

And at the call site (line 2968):

-                    exec_aggregate_building(locator, hour_start, hour_end, summary_folder, list_metrics, bool_use_acronym, list_list_useful_cea_results, list_buildings, list_appendix, list_selected_time_period, plot=plot)
+                    exec_aggregate_building(locator, hour_start, hour_end, summary_folder, list_metrics, bool_use_acronym, list_list_useful_cea_results, list_buildings, list_appendix, list_selected_time_period, plot=plot, network_name=network_name)

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 GitHub Actions: Ruff

[error] 1384-1384: F821 Undefined name network_name

🤖 Prompt for AI Agents
In cea/import_export/result_summary.py around lines 1381 to 1392, the function
exec_aggregate_building uses network_name when calling
locator.get_heat_rejection_buildings at lines ~1385, ~1413 and ~1457 but
network_name is not a parameter, causing a NameError; add network_name to the
exec_aggregate_building function signature, update any internal references to
use that parameter, and then update the call site at line 2968 to pass the
available network_name variable into exec_aggregate_building.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (3)
cea/analysis/costs/supply_costs.py (3)

821-824: Verify if input modification issue is fully resolved.

Past review flagged writing to supply.csv as risky (race conditions, inconsistent state). While marked as addressed, the code still modifies supply.csv at line 824. Ensure this approach is intentional or consider in-memory-only fallbacks.

#!/bin/bash
# Verify if there are any safeguards or documentation about this write operation

# Check for comments or safeguards around the supply.csv write
rg -n -B5 -A5 "supply_df.to_csv\(supply_csv_path" cea/analysis/costs/supply_costs.py

# Check if there's any locking mechanism or transaction handling
rg -n "lock|transaction|backup" cea/analysis/costs/supply_costs.py

1391-1393: Use proper annualization formula for piping costs.

Per coding guidelines, annualized CAPEX should use: CAPEX × IR% / (1 - (1 + IR%)^-LT). Simple division doesn't account for time value of money and underestimates costs.

Apply this diff:

     # Annualize (default network lifetime: 20 years)
     network_lifetime_yrs = 20.0
-    piping_cost_annual = piping_cost_total / network_lifetime_yrs
+    # Use standard annualization with interest rate (default 5% for infrastructure)
+    ir = 0.05  # 5% interest rate - consider making configurable
+    if ir > 0:
+        piping_cost_annual = piping_cost_total * ir / (1 - (1 + ir) ** -network_lifetime_yrs)
+    else:
+        piping_cost_annual = piping_cost_total / network_lifetime_yrs

Based on coding guidelines: "Calculate Annualized CAPEX using the formula: CAPEX × IR% / (1 - (1 + IR%)^-LT)".


684-684: Remove unused import.

os is already imported at module level (line 9) and not used in this function.

Apply this diff:

-    import os
-
     print("  Loading buildings and demands...")
🧹 Nitpick comments (1)
cea/optimization_new/containerclasses/energyCarrier.py (1)

338-367: Validate T60W/T100W threshold assumptions and add tests for new tie-breaking

The new 90 °C split for water (forcing all ≤90 °C to 60 and >90 °C to 100) and the “prefer lower temperature on ties” logic both look consistent and deterministic, but they bake in domain assumptions that are not enforced by the data:

  • The special-case will always choose 60 or 100 whenever both exist, even if other water temperatures are later added and would be a closer physical match.
  • The tie-breaker behaviour (choosing the lower mean_qual) changes previous implicit semantics; any downstream logic relying on the old arbitrary choice should be re-checked.

I recommend:

  • Confirming that the water subtype is guaranteed to only have 60 °C and 100 °C entries (now and in foreseeable DB versions), or explicitly documenting that intermediate values will be ignored by design.
  • Adding unit tests for:
    • water at 80–82 °C and just below/above 90 °C to confirm the T60W/T100W mapping;
    • a subtype with symmetric options around a target (e.g. 40/60 around 50) to lock in the “lower temperature wins” behaviour.

This will make the new selection rules explicit and guard against future DB changes.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between da3b210 and e4555f1.

📒 Files selected for processing (2)
  • cea/analysis/costs/supply_costs.py (1 hunks)
  • cea/optimization_new/containerclasses/energyCarrier.py (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Extract meaningful patterns into helper functions only when logic must be done in a specific way to avoid bugs, complexity warrants abstraction, algorithm requires deep understanding, or pattern encapsulates important business rules
Don't extract trivial wrappers: avoid extracting 1-2 line wrappers around existing functions, standard library usage, abstractions that obscure rather than clarify intent, or code that would be clearer inline
Always use InputLocator for file path resolution instead of hardcoding paths
Use British English spelling in all user-facing print statements, docstrings for public APIs, and error messages shown to users
Don't modify config directly in worker processes; use kwargs or create new instance instead
Check config.multiprocessing before using Pool for parallelization
Respect scenario structure conventions: use /inputs/ and /outputs/ directories for file organization
Use f-strings only when the string contains variables (e.g., f"Value: {x}"); use regular strings otherwise (e.g., "No variables") to avoid linter warnings

Files:

  • cea/optimization_new/containerclasses/energyCarrier.py
  • cea/analysis/costs/supply_costs.py
cea/analysis/costs/**/*.py

📄 CodeRabbit inference engine (cea/analysis/costs/AGENTS.md)

cea/analysis/costs/**/*.py: Calculate Total CAPEX as: peak_kW × efficiency × CAPEX_USD2015kW
Calculate Fixed OPEX (O&M) as: CAPEX × O&M_% / 100
Calculate Annualized CAPEX using the formula: CAPEX × IR% / (1 - (1 + IR%)^-LT)
Calculate Total Annualized Cost (TAC) as: Fixed OPEX + Annualized CAPEX + Variable OPEX

Files:

  • cea/analysis/costs/supply_costs.py
🧠 Learnings (14)
📓 Common learnings
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/analysis/costs/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:11.074Z
Learning: Applies to cea/analysis/costs/cea/analysis/{costs,lca}/**/*.py : Merge CAPEX data from SUPPLY assemblies with variable OPEX data from FEEDSTOCKS for a complete cost picture
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/demand/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:30.367Z
Learning: Applies to cea/demand/**/SUPPLY_*.csv : Classify SUPPLY components (chiller, boiler, etc.) as generation systems with CAPEX and OPEX costs in database
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/demand/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:30.367Z
Learning: Maintain distinction between HVAC components (affect demand via losses only, no costs) and SUPPLY components (contain all costs and emissions)
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/analysis/costs/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:11.074Z
Learning: Applies to cea/analysis/costs/cea/analysis/**/*.py : Use the following database parameters from SUPPLY assemblies: CAPEX_USD2015kW (cost per kW), LT_yr (lifetime in years), O&M_% (annual O&M percentage), IR_% (interest rate), and efficiency (system efficiency)
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/demand/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:30.367Z
Learning: Applies to cea/demand/**/HVAC_*.csv : Classify HVAC components (AHU, radiators, etc.) as distribution/emission systems with no associated costs in database
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/databases/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:19.249Z
Learning: Applies to cea/databases/**/*.py : Use ASSEMBLIES/SUPPLY/*.csv files (not COMPONENTS) for building-level demand calculations with conservative efficiency values and single baseline costs
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/demand/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:30.367Z
Learning: Applies to cea/demand/**/*.py : Use final energy columns (`GRID_*`, `NG_*`, `DH_*`, `DC_*`) for energy cost and emissions calculations
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/databases/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:19.249Z
Learning: Applies to cea/databases/**/*.py : Use COMPONENTS/CONVERSION/*.csv files for district energy optimization with detailed equipment specifications including efficiency and capacity-dependent cost curves
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/databases/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:19.249Z
Learning: Applies to cea/databases/**/*.py : Merge SUPPLY_ELECTRICITY.csv (infrastructure CAPEX) with COMPONENTS/FEEDSTOCKS/FEEDSTOCKS_LIBRARY/GRID.csv (hourly operational costs) using a left join on feedstock code to combine infrastructure and variable costs
📚 Learning: 2025-11-26T23:07:11.074Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/analysis/costs/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:11.074Z
Learning: Applies to cea/analysis/costs/cea/analysis/{costs,lca}/**/*.py : Merge CAPEX data from SUPPLY assemblies with variable OPEX data from FEEDSTOCKS for a complete cost picture

Applied to files:

  • cea/analysis/costs/supply_costs.py
📚 Learning: 2025-11-26T23:07:19.249Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/databases/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:19.249Z
Learning: Applies to cea/databases/**/*.py : Use ASSEMBLIES/SUPPLY/*.csv files (not COMPONENTS) for building-level demand calculations with conservative efficiency values and single baseline costs

Applied to files:

  • cea/analysis/costs/supply_costs.py
📚 Learning: 2025-11-26T23:07:30.367Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/demand/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:30.367Z
Learning: Applies to cea/demand/**/SUPPLY_*.csv : Classify SUPPLY components (chiller, boiler, etc.) as generation systems with CAPEX and OPEX costs in database

Applied to files:

  • cea/analysis/costs/supply_costs.py
📚 Learning: 2025-11-26T23:07:11.074Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/analysis/costs/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:11.074Z
Learning: Applies to cea/analysis/costs/cea/analysis/**/*.py : Use the following database parameters from SUPPLY assemblies: CAPEX_USD2015kW (cost per kW), LT_yr (lifetime in years), O&M_% (annual O&M percentage), IR_% (interest rate), and efficiency (system efficiency)

Applied to files:

  • cea/analysis/costs/supply_costs.py
📚 Learning: 2025-11-26T23:07:19.249Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/databases/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:19.249Z
Learning: Applies to cea/databases/**/*.py : Merge SUPPLY_ELECTRICITY.csv (infrastructure CAPEX) with COMPONENTS/FEEDSTOCKS/FEEDSTOCKS_LIBRARY/GRID.csv (hourly operational costs) using a left join on feedstock code to combine infrastructure and variable costs

Applied to files:

  • cea/analysis/costs/supply_costs.py
📚 Learning: 2025-11-26T23:07:19.249Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/databases/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:19.249Z
Learning: Applies to cea/databases/**/*.py : Use COMPONENTS/CONVERSION/*.csv files for district energy optimization with detailed equipment specifications including efficiency and capacity-dependent cost curves

Applied to files:

  • cea/analysis/costs/supply_costs.py
📚 Learning: 2025-11-26T23:07:30.367Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/demand/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:30.367Z
Learning: Maintain distinction between HVAC components (affect demand via losses only, no costs) and SUPPLY components (contain all costs and emissions)

Applied to files:

  • cea/analysis/costs/supply_costs.py
📚 Learning: 2025-11-26T23:07:11.074Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/analysis/costs/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:11.074Z
Learning: Applies to cea/analysis/costs/cea/analysis/**/*.py : Read CAPEX parameters from SUPPLY assemblies using files matching pattern `databases/{region}/ASSEMBLIES/SUPPLY/SUPPLY_*.csv`

Applied to files:

  • cea/analysis/costs/supply_costs.py
📚 Learning: 2025-11-26T23:07:11.074Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/analysis/costs/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:11.074Z
Learning: Applies to cea/analysis/costs/cea/analysis/**/*.py : Use the `calc_capex_annualized(capex_total, IR_%, LT_yr)` function to convert total CAPEX to annual cost

Applied to files:

  • cea/analysis/costs/supply_costs.py
📚 Learning: 2025-11-26T23:07:11.074Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/analysis/costs/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:11.074Z
Learning: Applies to cea/analysis/costs/**/*.py : Calculate Annualized CAPEX using the formula: CAPEX × IR% / (1 - (1 + IR%)^-LT)

Applied to files:

  • cea/analysis/costs/supply_costs.py
📚 Learning: 2025-11-26T23:07:11.074Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/analysis/costs/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:11.074Z
Learning: Ensure CAPEX is annualized before comparing with annual OPEX figures

Applied to files:

  • cea/analysis/costs/supply_costs.py
📚 Learning: 2025-11-26T23:07:11.074Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/analysis/costs/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:11.074Z
Learning: Applies to cea/analysis/costs/**/*.py : Calculate Fixed OPEX (O&M) as: CAPEX × O&M_% / 100

Applied to files:

  • cea/analysis/costs/supply_costs.py
📚 Learning: 2025-11-26T23:07:11.074Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/analysis/costs/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:11.074Z
Learning: Applies to cea/analysis/costs/**/*.py : Calculate Total Annualized Cost (TAC) as: Fixed OPEX + Annualized CAPEX + Variable OPEX

Applied to files:

  • cea/analysis/costs/supply_costs.py
🪛 GitHub Actions: Ruff
cea/analysis/costs/supply_costs.py

[error] 375-375: Ruff: f-string without placeholders. Remove extraneous f prefix.


[error] 684-684: Ruff: Unused import 'os'.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: tests (windows-latest)
  • GitHub Check: tests (ubuntu-latest)
  • GitHub Check: tests (macos-latest)
🔇 Additional comments (16)
cea/analysis/costs/supply_costs.py (16)

25-70: LGTM: Component extraction logic is sound.

The function correctly extracts components from SUPPLY assembly definitions with appropriate fallbacks for missing data.


73-182: LGTM: Scale filtering logic is correct.

The functions properly handle multi-select supply codes and filter by scale (BUILDING vs DISTRICT) with appropriate fallbacks.


184-222: LGTM: Component code mapping with capacity filtering.

The function correctly maps component categories to codes and optionally filters by peak demand capacity.


225-279: LGTM: DHW component fallback logic is sound.

The function provides a reasonable fallback mechanism for DHW systems when SUPPLY assemblies don't specify components.


438-487: LGTM: Service filtering and standalone calculation.

Both functions are correctly implemented with clear logic.


490-567: LGTM: Config fallback logic for scale mismatch.

The function correctly applies config fallbacks when supply.csv has district-scale codes for standalone services.


570-671: LGTM: Network connectivity and service needs determination.

All three functions are correctly implemented with proper 4-case logic for determining building service needs.


827-922: LGTM: Heating system calculation with proper network awareness.

The function correctly calculates heating systems with appropriate scale determination based on network connectivity.


925-1019: LGTM: Cooling system calculation mirrors heating logic.

The function correctly calculates cooling systems with consistent scale determination logic.


1022-1072: LGTM: System merging logic is sound.

The function correctly merges heating and cooling systems with proper component and cost aggregation.


1413-1518: LGTM: Network cost calculation with proper aggregation.

The function correctly calculates network costs with demand aggregation and potential summation.


1521-1743: LGTM: Comprehensive network-type cost calculation.

The function correctly orchestrates cost calculations for DH/DC networks with proper four-case logic and service filtering.


1746-1874: LGTM: Cost extraction with proper TAC calculation.

The function correctly extracts costs from supply systems and calculates Total Annualized Cost (TAC) as per guidelines.


1877-1896: LGTM: Scale determination based on actual connectivity.

The function correctly determines scale based on actual network connectivity rather than assembly configuration.


1899-1984: LGTM: Component-to-service mapping with smart inference.

The function correctly maps components to services with proper fallback logic and temperature-based service inference.


1987-2054: LGTM: Energy carrier mapping with comprehensive coverage.

The function correctly maps energy carriers to services with appropriate handling of ambiguous cases.

energy_flow_profile=dhw_demand_profile
)

print(f" DHW fallback: EnergyFlow created with T60W carrier")
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove extraneous f prefix.

This string has no placeholders, so the f prefix is unnecessary and triggers a linter warning.

Apply this diff:

-    print(f"    DHW fallback: EnergyFlow created with T60W carrier")
+    print("    DHW fallback: EnergyFlow created with T60W carrier")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
print(f" DHW fallback: EnergyFlow created with T60W carrier")
print(" DHW fallback: EnergyFlow created with T60W carrier")
🧰 Tools
🪛 GitHub Actions: Ruff

[error] 375-375: Ruff: f-string without placeholders. Remove extraneous f prefix.

🤖 Prompt for AI Agents
In cea/analysis/costs/supply_costs.py around line 375, remove the unnecessary
f-string prefix on the print literal; replace the f-prefixed print call with a
normal string print so it reads print("    DHW fallback: EnergyFlow created with
T60W carrier") to eliminate the linter warning.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
cea/analysis/costs/main.py (3)

22-56: Prefer InputLocator accessors for thermal-network shapefiles instead of hardcoded filenames

The logic is fine, but you are partly hardcoding paths for nodes.shp / edges.shp. If InputLocator exposes dedicated helpers for these layout shapefiles, it would be more robust to use them end‑to‑end instead of assembling filenames manually, so future path/name changes are centralised in one place.

As per coding guidelines, prefer using InputLocator for file path resolution rather than hardcoding filenames.


176-204: Clarify or relax behaviour when some requested network types fail (validation / “no supply system” / “no demand”)

The current flow treats several conditions as non-fatal in the moment but still aborts the whole run later:

  • Network types that fail validate_network_results_exist are logged under “VALIDATION WARNINGS” and you continue with valid_network_types, but they are then included in all_errors and cause a final RuntimeError.
  • Network types skipped due to “None of the components chosen … / T30W / T10W” (with logs explicitly saying this is “Expected for scenarios with no {network_type} demand”) are also accumulated in calculation_errors and likewise make the overall run fail at the all_errors check, even if other types succeeded.

That means you never actually persist partial results when any requested type is missing or has zero demand, which is stricter than what the log messages suggest.

Consider one of:

  • Treating “expected” cases (e.g. zero demand / no valid systems) as non-fatal and excluding them from all_errors, so that successful network types still produce saved outputs; or
  • Keeping the fail‑fast behaviour but updating the earlier log messages to make it clear that these are hard errors for the overall request, not mere warnings.

This would make the UX more predictable when users select multiple network types but only some are meaningful for a given scenario.

Also applies to: 213-259, 299-329


345-351: Heuristic name.startswith('N') for detecting networks may misclassify buildings

Using has_networks = any(name.startswith('N') for name in final_results['name']) assumes that all network rows start with 'N' and that no ordinary building does. In larger projects, regular buildings can easily start with 'N' (e.g. “NORTH01”), which would trigger the network note even when there are no actual network plants.

If final_results carries some explicit flag/column for network assets (e.g. a type / category / “is_network” field), or if you know the exact naming convention (e.g. N followed by digits only), it would be more robust to rely on that instead of a single‑letter prefix check.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e4555f1 and 330452f.

📒 Files selected for processing (1)
  • cea/analysis/costs/main.py (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Extract meaningful patterns into helper functions only when logic must be done in a specific way to avoid bugs, complexity warrants abstraction, algorithm requires deep understanding, or pattern encapsulates important business rules
Don't extract trivial wrappers: avoid extracting 1-2 line wrappers around existing functions, standard library usage, abstractions that obscure rather than clarify intent, or code that would be clearer inline
Always use InputLocator for file path resolution instead of hardcoding paths
Use British English spelling in all user-facing print statements, docstrings for public APIs, and error messages shown to users
Don't modify config directly in worker processes; use kwargs or create new instance instead
Check config.multiprocessing before using Pool for parallelization
Respect scenario structure conventions: use /inputs/ and /outputs/ directories for file organization
Use f-strings only when the string contains variables (e.g., f"Value: {x}"); use regular strings otherwise (e.g., "No variables") to avoid linter warnings

Files:

  • cea/analysis/costs/main.py
cea/analysis/costs/**/*.py

📄 CodeRabbit inference engine (cea/analysis/costs/AGENTS.md)

cea/analysis/costs/**/*.py: Calculate Total CAPEX as: peak_kW × efficiency × CAPEX_USD2015kW
Calculate Fixed OPEX (O&M) as: CAPEX × O&M_% / 100
Calculate Annualized CAPEX using the formula: CAPEX × IR% / (1 - (1 + IR%)^-LT)
Calculate Total Annualized Cost (TAC) as: Fixed OPEX + Annualized CAPEX + Variable OPEX

Files:

  • cea/analysis/costs/main.py
🧠 Learnings (4)
📓 Common learnings
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/analysis/costs/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:11.074Z
Learning: Applies to cea/analysis/costs/cea/analysis/{costs,lca}/**/*.py : Merge CAPEX data from SUPPLY assemblies with variable OPEX data from FEEDSTOCKS for a complete cost picture
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/demand/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:30.367Z
Learning: Applies to cea/demand/**/SUPPLY_*.csv : Classify SUPPLY components (chiller, boiler, etc.) as generation systems with CAPEX and OPEX costs in database
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/demand/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:30.367Z
Learning: Maintain distinction between HVAC components (affect demand via losses only, no costs) and SUPPLY components (contain all costs and emissions)
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/analysis/costs/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:11.074Z
Learning: Applies to cea/analysis/costs/cea/analysis/**/*.py : Use the following database parameters from SUPPLY assemblies: CAPEX_USD2015kW (cost per kW), LT_yr (lifetime in years), O&M_% (annual O&M percentage), IR_% (interest rate), and efficiency (system efficiency)
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/demand/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:30.367Z
Learning: Applies to cea/demand/**/HVAC_*.csv : Classify HVAC components (AHU, radiators, etc.) as distribution/emission systems with no associated costs in database
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/demand/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:30.367Z
Learning: Applies to cea/demand/**/*.py : Use final energy columns (`GRID_*`, `NG_*`, `DH_*`, `DC_*`) for energy cost and emissions calculations
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/databases/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:19.249Z
Learning: Applies to cea/databases/**/*.py : Use ASSEMBLIES/SUPPLY/*.csv files (not COMPONENTS) for building-level demand calculations with conservative efficiency values and single baseline costs
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/databases/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:19.249Z
Learning: Applies to cea/databases/**/*.py : Use COMPONENTS/CONVERSION/*.csv files for district energy optimization with detailed equipment specifications including efficiency and capacity-dependent cost curves
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/databases/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:19.249Z
Learning: Applies to cea/databases/**/*.py : Merge SUPPLY_ELECTRICITY.csv (infrastructure CAPEX) with COMPONENTS/FEEDSTOCKS/FEEDSTOCKS_LIBRARY/GRID.csv (hourly operational costs) using a left join on feedstock code to combine infrastructure and variable costs
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/demand/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:30.367Z
Learning: Applies to cea/demand/**/*.py : Use end-use columns (`Q*_sys_kWh`) for HVAC system sizing and capacity calculations
📚 Learning: 2025-11-26T23:07:11.074Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/analysis/costs/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:11.074Z
Learning: Applies to cea/analysis/costs/cea/analysis/{costs,lca}/**/*.py : Merge CAPEX data from SUPPLY assemblies with variable OPEX data from FEEDSTOCKS for a complete cost picture

Applied to files:

  • cea/analysis/costs/main.py
📚 Learning: 2025-11-26T23:06:48.123Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-26T23:06:48.123Z
Learning: Applies to **/cea/**/script.py : Create `main(config: Configuration)` as the entry point for new scripts in `cea/<module>/script.py`, register in `scripts.yml`, and the script auto-registers via `cea.api`

Applied to files:

  • cea/analysis/costs/main.py
📚 Learning: 2025-11-26T23:07:19.249Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/databases/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:19.249Z
Learning: Applies to cea/databases/**/*.py : Merge SUPPLY_ELECTRICITY.csv (infrastructure CAPEX) with COMPONENTS/FEEDSTOCKS/FEEDSTOCKS_LIBRARY/GRID.csv (hourly operational costs) using a left join on feedstock code to combine infrastructure and variable costs

Applied to files:

  • cea/analysis/costs/main.py
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: tests (ubuntu-latest)
  • GitHub Check: tests (macos-latest)
  • GitHub Check: tests (windows-latest)
🔇 Additional comments (1)
cea/analysis/costs/main.py (1)

90-96: Good orchestration: cost logic stays in dedicated modules, consistent with cost-data guidelines

The orchestration here cleanly delegates all actual cost calculations to supply_costs, solar_costs, and format_simplified, and only handles merging, error handling, and I/O. That keeps the CAPEX / OPEX business rules centralised instead of being re‑implemented in this script, which should help avoid divergence between baseline‑costs and other cost analyses.

Based on learnings, keeping cost formulas in the dedicated cost modules (using SUPPLY and FEEDSTOCKS data) rather than in orchestration code matches the recommended architecture for cea/analysis/costs/**/*.py.

Also applies to: 205-213, 260-283

Comment on lines +82 to +89
def baseline_costs_main(locator, config):
"""
Calculate baseline costs for heating and/or cooling systems.
:param locator: InputLocator instance
:param config: Configuration instance
:return: DataFrame with cost results
"""
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Align baseline_costs_main return value with its docstring across both modes

The docstring says baseline_costs_main “returns a DataFrame with cost results”, and the network mode does return final_results, but the standalone‑only branch ends with a bare return, so callers will receive None in that case.

I’d recommend returning final_results in the standalone branch as well to keep the function contract consistent (or, alternatively, adjust the docstring and stop returning in the network branch, but that weakens the API).

-        print("\nNote: Standalone-only mode provides simplified cost estimates")
-        return
+        print("\nNote: Standalone-only mode provides simplified cost estimates")
+        return final_results

Also applies to: 158-164, 352-352

🤖 Prompt for AI Agents
In cea/analysis/costs/main.py around lines 82-89 (also check similar patterns at
158-164 and line 352), the function baseline_costs_main's docstring promises
"returns a DataFrame with cost results" but the standalone-only branch ends with
a bare return (None), causing inconsistent return types; update the standalone
branch to return final_results (or the equivalent DataFrame variable used there)
so both network and standalone branches consistently return the DataFrame, and
make the same change for the other occurrences on lines 158-164 and 352 to
preserve the function contract.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (3)
cea/analysis/costs/main.py (1)

82-89: Inconsistent return value in baseline_costs_main.

The docstring promises "returns a DataFrame with cost results", but standalone mode (line 164) has a bare return yielding None, while network mode (line 352) returns final_results. This contract inconsistency can cause TypeError if callers chain operations on the result.

Apply this diff to fix the standalone branch:

         print("\nNote: Standalone-only mode provides simplified cost estimates")
-        return
+        return final_results

Also applies to: 158-164, 352-352

cea/analysis/costs/supply_costs.py (2)

1385-1387: Piping cost annualization should use proper formula.

Per coding guidelines, annualized CAPEX should use: CAPEX × IR% / (1 - (1 + IR%)^-LT). The current simple division doesn't account for the time value of money.

     # Annualize (default network lifetime: 20 years)
     network_lifetime_yrs = 20.0
-    piping_cost_annual = piping_cost_total / network_lifetime_yrs
+    # Use standard annualization formula with interest rate
+    ir_pct = 0.05  # 5% - consider making this configurable or reading from database
+    if ir_pct > 0:
+        piping_cost_annual = piping_cost_total * ir_pct / (1 - (1 + ir_pct) ** -network_lifetime_yrs)
+    else:
+        piping_cost_annual = piping_cost_total / network_lifetime_yrs

As per coding guidelines: "Calculate Annualized CAPEX using the formula: CAPEX × IR% / (1 - (1 + IR%)^-LT)".


678-678: Remove unused os import causing pipeline failure.

The os module is imported at line 678 but never used within calculate_standalone_building_costs. It's already available from the module-level import at line 9.

 def calculate_standalone_building_costs(locator, config, network_name):
-    import os
-
     print("  Loading buildings and demands...")
🧹 Nitpick comments (6)
cea/optimization_new/containerclasses/energyCarrier.py (1)

338-354: Tie‑break logic for equidistant temperatures looks correct; consider a small robustness guard

The new distance/tie handling correctly:

  • Picks all entries at the minimum absolute distance, and
  • Chooses the lower temperature when there is more than one candidate.

This achieves the “prefer lower temperature” requirement without changing behaviour in the unique‑closest case. Nice improvement.

One optional robustness tweak: if, for some reason, thermal_ec_mean_quals contained no finite values for a subtype (all NaN or filtered out), tied_indices would be empty and tied_indices[0] would raise an IndexError (same as the old implementation, but less obvious here). You could fail more explicitly:

-            tied_indices = distances[distances == min_distance].index
-
-            if len(tied_indices) > 1:
+            tied_indices = distances[distances == min_distance].index
+
+            if len(tied_indices) == 0:
+                raise ValueError(
+                    f"No valid mean_qual values found for thermal energy carrier subtype "
+                    f"'{energy_carrier_subtype}'."
+                )
+            elif len(tied_indices) > 1:
                 # Multiple carriers at same distance - choose the one with LOWER temperature
                 tied_temps = thermal_ec_mean_quals.loc[tied_indices]
                 index_closest_mean_temp = tied_temps.idxmin()
             else:
                 # Single closest match
                 index_closest_mean_temp = tied_indices[0]

This keeps the current semantics while turning a low‑level IndexError into a clearer data‑quality error.

cea/analysis/costs/main.py (2)

109-109: Simplify redundant falsy checks.

The condition not network_name or network_name == '' is redundant since an empty string is already falsy in Python.

-    if not network_name or network_name == '' or network_name == '(none)':
+    if not network_name or network_name == '(none)':

346-350: Network detection relies on fragile naming convention.

The check name.startswith('N') assumes all network identifiers begin with 'N'. If network naming conventions change, this will silently fail.

Consider using the is_network flag from the results structure instead:

-    has_networks = any(name.startswith('N') for name in final_results['name'])
+    # Check if any results represent network-level systems
+    has_networks = any(
+        merged_results.get(name, {}).get('is_network', False) 
+        for name in final_results['name'] if name in merged_results
+    )

Alternatively, store network identifiers explicitly during result merging.

cea/analysis/costs/supply_costs.py (3)

184-222: Remove redundant pandas import.

pandas is already imported at module level (line 10).

 def get_component_codes_from_categories(locator, component_categories, network_type, peak_demand_kw=None):
-    import pandas as pd
-
     component_codes = []

371-379: Heavy initialization just to populate class variables.

Creating a full Domain object solely to initialize Component.code_to_class_mapping is expensive. Consider extracting the component initialization logic or caching the initialized state.

     # Initialize Component class if not already done
     # This is needed to map component codes to their energy carriers
     if Component.code_to_class_mapping is None:
-        # Create a minimal domain just to initialize Component class
-        domain_config_dhw = cea.config.Configuration()
-        # Get scenario from locator (locator.scenario is the current scenario path)
-        domain_config_dhw.scenario = locator.scenario
-        temp_domain = Domain(domain_config_dhw, locator)
-        Component.initialize_class_variables(temp_domain)
+        # Initialize Component class directly using locator
+        # Note: Component.initialize_class_variables requires a domain with locator attribute
+        from cea.optimization_new.component import Component
+        Component._load_components(locator)  # If available, otherwise keep current approach

If direct initialization isn't possible, document why the full Domain is needed.


858-866: Stdout suppression is acceptable for clean output.

The use of sys.stdout redirection to suppress verbose optimization messages is reasonable for user experience. Consider using a context manager for cleaner resource handling.

from contextlib import redirect_stdout
import io

with redirect_stdout(io.StringIO()):
    domain.load_potentials()
    domain._initialize_energy_system_descriptor_classes()

Also applies to: 961-969

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 330452f and a974499.

📒 Files selected for processing (3)
  • cea/analysis/costs/main.py (1 hunks)
  • cea/analysis/costs/supply_costs.py (1 hunks)
  • cea/optimization_new/containerclasses/energyCarrier.py (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Extract meaningful patterns into helper functions only when logic must be done in a specific way to avoid bugs, complexity warrants abstraction, algorithm requires deep understanding, or pattern encapsulates important business rules
Don't extract trivial wrappers: avoid extracting 1-2 line wrappers around existing functions, standard library usage, abstractions that obscure rather than clarify intent, or code that would be clearer inline
Always use InputLocator for file path resolution instead of hardcoding paths
Use British English spelling in all user-facing print statements, docstrings for public APIs, and error messages shown to users
Don't modify config directly in worker processes; use kwargs or create new instance instead
Check config.multiprocessing before using Pool for parallelization
Respect scenario structure conventions: use /inputs/ and /outputs/ directories for file organization
Use f-strings only when the string contains variables (e.g., f"Value: {x}"); use regular strings otherwise (e.g., "No variables") to avoid linter warnings

Files:

  • cea/optimization_new/containerclasses/energyCarrier.py
  • cea/analysis/costs/main.py
  • cea/analysis/costs/supply_costs.py
cea/analysis/costs/**/*.py

📄 CodeRabbit inference engine (cea/analysis/costs/AGENTS.md)

cea/analysis/costs/**/*.py: Calculate Total CAPEX as: peak_kW × efficiency × CAPEX_USD2015kW
Calculate Fixed OPEX (O&M) as: CAPEX × O&M_% / 100
Calculate Annualized CAPEX using the formula: CAPEX × IR% / (1 - (1 + IR%)^-LT)
Calculate Total Annualized Cost (TAC) as: Fixed OPEX + Annualized CAPEX + Variable OPEX

Files:

  • cea/analysis/costs/main.py
  • cea/analysis/costs/supply_costs.py
🧠 Learnings (16)
📓 Common learnings
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/analysis/costs/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:11.074Z
Learning: Applies to cea/analysis/costs/cea/analysis/{costs,lca}/**/*.py : Merge CAPEX data from SUPPLY assemblies with variable OPEX data from FEEDSTOCKS for a complete cost picture
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/demand/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:30.367Z
Learning: Applies to cea/demand/**/SUPPLY_*.csv : Classify SUPPLY components (chiller, boiler, etc.) as generation systems with CAPEX and OPEX costs in database
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/demand/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:30.367Z
Learning: Maintain distinction between HVAC components (affect demand via losses only, no costs) and SUPPLY components (contain all costs and emissions)
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/analysis/costs/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:11.074Z
Learning: Applies to cea/analysis/costs/cea/analysis/**/*.py : Use the following database parameters from SUPPLY assemblies: CAPEX_USD2015kW (cost per kW), LT_yr (lifetime in years), O&M_% (annual O&M percentage), IR_% (interest rate), and efficiency (system efficiency)
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/demand/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:30.367Z
Learning: Applies to cea/demand/**/HVAC_*.csv : Classify HVAC components (AHU, radiators, etc.) as distribution/emission systems with no associated costs in database
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/demand/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:30.367Z
Learning: Applies to cea/demand/**/*.py : Use final energy columns (`GRID_*`, `NG_*`, `DH_*`, `DC_*`) for energy cost and emissions calculations
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/databases/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:19.249Z
Learning: Applies to cea/databases/**/*.py : Use ASSEMBLIES/SUPPLY/*.csv files (not COMPONENTS) for building-level demand calculations with conservative efficiency values and single baseline costs
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/databases/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:19.249Z
Learning: Applies to cea/databases/**/*.py : Use COMPONENTS/CONVERSION/*.csv files for district energy optimization with detailed equipment specifications including efficiency and capacity-dependent cost curves
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/databases/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:19.249Z
Learning: Applies to cea/databases/**/*.py : Merge SUPPLY_ELECTRICITY.csv (infrastructure CAPEX) with COMPONENTS/FEEDSTOCKS/FEEDSTOCKS_LIBRARY/GRID.csv (hourly operational costs) using a left join on feedstock code to combine infrastructure and variable costs
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/demand/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:30.367Z
Learning: Applies to cea/demand/**/*.py : Use end-use columns (`Q*_sys_kWh`) for HVAC system sizing and capacity calculations
📚 Learning: 2025-11-26T23:07:11.074Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/analysis/costs/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:11.074Z
Learning: Applies to cea/analysis/costs/cea/analysis/{costs,lca}/**/*.py : Merge CAPEX data from SUPPLY assemblies with variable OPEX data from FEEDSTOCKS for a complete cost picture

Applied to files:

  • cea/analysis/costs/main.py
  • cea/analysis/costs/supply_costs.py
📚 Learning: 2025-11-26T23:06:48.123Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-26T23:06:48.123Z
Learning: Applies to **/cea/**/script.py : Create `main(config: Configuration)` as the entry point for new scripts in `cea/<module>/script.py`, register in `scripts.yml`, and the script auto-registers via `cea.api`

Applied to files:

  • cea/analysis/costs/main.py
📚 Learning: 2025-11-26T23:07:19.249Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/databases/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:19.249Z
Learning: Applies to cea/databases/**/*.py : Use ASSEMBLIES/SUPPLY/*.csv files (not COMPONENTS) for building-level demand calculations with conservative efficiency values and single baseline costs

Applied to files:

  • cea/analysis/costs/supply_costs.py
📚 Learning: 2025-11-26T23:07:19.249Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/databases/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:19.249Z
Learning: Applies to cea/databases/**/*.py : Use COMPONENTS/CONVERSION/*.csv files for district energy optimization with detailed equipment specifications including efficiency and capacity-dependent cost curves

Applied to files:

  • cea/analysis/costs/supply_costs.py
📚 Learning: 2025-11-26T23:07:30.367Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/demand/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:30.367Z
Learning: Applies to cea/demand/**/SUPPLY_*.csv : Classify SUPPLY components (chiller, boiler, etc.) as generation systems with CAPEX and OPEX costs in database

Applied to files:

  • cea/analysis/costs/supply_costs.py
📚 Learning: 2025-11-26T23:07:11.074Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/analysis/costs/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:11.074Z
Learning: Applies to cea/analysis/costs/cea/analysis/**/*.py : Use the following database parameters from SUPPLY assemblies: CAPEX_USD2015kW (cost per kW), LT_yr (lifetime in years), O&M_% (annual O&M percentage), IR_% (interest rate), and efficiency (system efficiency)

Applied to files:

  • cea/analysis/costs/supply_costs.py
📚 Learning: 2025-11-26T23:07:19.249Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/databases/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:19.249Z
Learning: Applies to cea/databases/**/*.py : Merge SUPPLY_ELECTRICITY.csv (infrastructure CAPEX) with COMPONENTS/FEEDSTOCKS/FEEDSTOCKS_LIBRARY/GRID.csv (hourly operational costs) using a left join on feedstock code to combine infrastructure and variable costs

Applied to files:

  • cea/analysis/costs/supply_costs.py
📚 Learning: 2025-11-26T23:07:11.074Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/analysis/costs/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:11.074Z
Learning: Applies to cea/analysis/costs/cea/analysis/**/*.py : Read CAPEX parameters from SUPPLY assemblies using files matching pattern `databases/{region}/ASSEMBLIES/SUPPLY/SUPPLY_*.csv`

Applied to files:

  • cea/analysis/costs/supply_costs.py
📚 Learning: 2025-11-26T23:07:30.367Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/demand/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:30.367Z
Learning: Maintain distinction between HVAC components (affect demand via losses only, no costs) and SUPPLY components (contain all costs and emissions)

Applied to files:

  • cea/analysis/costs/supply_costs.py
📚 Learning: 2025-11-26T23:07:11.074Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/analysis/costs/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:11.074Z
Learning: Applies to cea/analysis/costs/cea/analysis/**/*.py : Use the `calc_capex_annualized(capex_total, IR_%, LT_yr)` function to convert total CAPEX to annual cost

Applied to files:

  • cea/analysis/costs/supply_costs.py
📚 Learning: 2025-11-26T23:07:11.074Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/analysis/costs/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:11.074Z
Learning: Applies to cea/analysis/costs/**/*.py : Calculate Annualized CAPEX using the formula: CAPEX × IR% / (1 - (1 + IR%)^-LT)

Applied to files:

  • cea/analysis/costs/supply_costs.py
📚 Learning: 2025-11-26T23:07:11.074Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/analysis/costs/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:11.074Z
Learning: Ensure CAPEX is annualized before comparing with annual OPEX figures

Applied to files:

  • cea/analysis/costs/supply_costs.py
📚 Learning: 2025-11-26T23:07:11.074Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/analysis/costs/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:11.074Z
Learning: Applies to cea/analysis/costs/**/*.py : Calculate Fixed OPEX (O&M) as: CAPEX × O&M_% / 100

Applied to files:

  • cea/analysis/costs/supply_costs.py
📚 Learning: 2025-11-26T23:07:11.074Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: cea/analysis/costs/AGENTS.md:0-0
Timestamp: 2025-11-26T23:07:11.074Z
Learning: Applies to cea/analysis/costs/**/*.py : Calculate Total Annualized Cost (TAC) as: Fixed OPEX + Annualized CAPEX + Variable OPEX

Applied to files:

  • cea/analysis/costs/supply_costs.py
📚 Learning: 2025-11-26T23:06:48.123Z
Learnt from: CR
Repo: architecture-building-systems/CityEnergyAnalyst PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-26T23:06:48.123Z
Learning: Applies to **/*.py : Use f-strings only when the string contains variables (e.g., `f"Value: {x}"`); use regular strings otherwise (e.g., `"No variables"`) to avoid linter warnings

Applied to files:

  • cea/analysis/costs/supply_costs.py
🧬 Code graph analysis (2)
cea/analysis/costs/main.py (4)
cea/inputlocator.py (3)
  • InputLocator (28-1456)
  • get_baseline_costs (1370-1379)
  • get_baseline_costs_detailed (1381-1390)
cea/analysis/costs/supply_costs.py (3)
  • calculate_all_buildings_as_standalone (459-481)
  • calculate_standalone_building_costs (668-766)
  • calculate_costs_for_network_type (1515-1737)
cea/analysis/costs/format_simplified.py (1)
  • format_output_simplified (19-202)
cea/analysis/costs/solar_costs.py (2)
  • calculate_building_solar_costs (252-448)
  • merge_solar_costs_to_buildings (451-513)
cea/analysis/costs/supply_costs.py (9)
cea/optimization_new/domain.py (3)
  • Domain (58-993)
  • load_potentials (316-350)
  • _initialize_energy_system_descriptor_classes (978-986)
cea/optimization_new/building.py (3)
  • Building (27-348)
  • distribute_building_potentials (312-348)
  • calculate_supply_system (274-309)
cea/inputlocator.py (8)
  • get_database_assemblies_supply_cooling (797-799)
  • get_database_assemblies_supply_heating (805-807)
  • get_database_assemblies_supply_hot_water (809-811)
  • get_db4_components_conversion_conversion_technology_csv (821-823)
  • get_building_supply (917-919)
  • get_total_demand (1289-1291)
  • get_demand_results_file (1297-1299)
  • get_output_thermal_network_type_folder (995-997)
cea/utilities/__init__.py (1)
  • unique (74-87)
cea/optimization_new/containerclasses/supplySystemStructure.py (8)
  • SupplySystemStructure (38-1526)
  • initialize_class_variables (1494-1526)
  • user_component_selection (224-225)
  • user_component_selection (228-242)
  • available_potentials (206-207)
  • available_potentials (210-217)
  • build (334-667)
  • activation_order (245-246)
cea/optimization_new/containerclasses/energyFlow.py (2)
  • EnergyFlow (27-221)
  • isolate_peak (214-221)
cea/optimization_new/containerclasses/energyCarrier.py (1)
  • initialize_class_variables (143-152)
cea/plots/thermal_networks/__init__.py (1)
  • nodes_df (286-302)
cea/optimization_new/helperclasses/multiprocessing/memoryPreserver.py (1)
  • update (56-58)
🪛 GitHub Actions: Ruff
cea/analysis/costs/supply_costs.py

[error] 678-678: F401 [*] os imported but unused

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: tests (windows-latest)
  • GitHub Check: tests (macos-latest)
  • GitHub Check: tests (ubuntu-latest)
🔇 Additional comments (10)
cea/analysis/costs/main.py (4)

1-23: Module structure and imports look good.

The module-level organization follows CEA conventions with proper imports using InputLocator for path resolution. The metadata block is complete.


22-56: Validation function is well-structured.

The validate_network_results_exist function correctly checks for required thermal-network output files and provides clear error messages guiding users to run the prerequisite scripts.


59-79: Merge function is clear and correct.

The merge_network_type_costs function properly transposes the data structure from {network_type: {building: results}} to {building: {network_type: results}}.


355-366: Entry point follows CEA conventions.

The main function correctly creates an InputLocator and delegates to baseline_costs_main. The if __name__ == '__main__' block enables standalone execution.

cea/analysis/costs/supply_costs.py (6)

1-23: Module structure and imports are appropriate.

Good organization with module-level imports for commonly used dependencies (os, pandas, Domain, Building, cea.config).


25-70: Component extraction from SUPPLY assembly is well-implemented.

The function correctly handles missing files, empty codes, and parses comma-separated component lists with proper '-' placeholder handling.


73-182: Scale filtering logic correctly implements multi-level fallback.

The filter_supply_code_by_scale and _verify_scale_match functions properly handle multi-select parameters and return None to trigger Level 3 fallback when scale mismatches occur. This aligns with the documented 3-level fallback approach.


225-279: DHW fallback component mapping is comprehensive.

Good coverage of feedstock-to-boiler mappings. The function correctly handles both direct fuel codes (e.g., 'Cgas') and feedstock names (e.g., 'NATURALGAS').


1740-1868: Cost extraction logic is comprehensive.

The extract_costs_from_supply_system function properly separates component costs (CAPEX + fixed OPEX) from energy carrier costs (variable OPEX) and calculates TAC correctly. The service mapping handles both network and standalone scenarios.


1981-2047: Energy carrier mapping is well-documented.

Good coverage of energy carrier codes with clear comments indicating the source database structure. The heating/cooling carrier classification is logical.

Comment on lines +584 to +591
if os.path.exists(nodes_file):
nodes_df = gpd.read_file(nodes_file)
network_buildings = nodes_df[nodes_df['type'] == 'CONSUMER']['building'].unique().tolist()
return set([b for b in network_buildings if b and b != 'NONE'])
except Exception:
pass

return set()
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Silent exception handling masks errors.

The bare except Exception: pass swallows all errors without logging, making debugging difficult when network file reading fails unexpectedly.

     try:
         network_folder = locator.get_output_thermal_network_type_folder(network_type, network_name)
         layout_folder = os.path.join(network_folder, 'layout')
         nodes_file = os.path.join(layout_folder, 'nodes.shp')
 
         if os.path.exists(nodes_file):
             nodes_df = gpd.read_file(nodes_file)
             network_buildings = nodes_df[nodes_df['type'] == 'CONSUMER']['building'].unique().tolist()
             return set([b for b in network_buildings if b and b != 'NONE'])
-    except Exception:
-        pass
+    except Exception as e:
+        # Log warning but don't fail - caller handles empty set
+        import warnings
+        warnings.warn(f"Failed to read network buildings for {network_type}/{network_name}: {e}")
 
     return set()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if os.path.exists(nodes_file):
nodes_df = gpd.read_file(nodes_file)
network_buildings = nodes_df[nodes_df['type'] == 'CONSUMER']['building'].unique().tolist()
return set([b for b in network_buildings if b and b != 'NONE'])
except Exception:
pass
return set()
if os.path.exists(nodes_file):
nodes_df = gpd.read_file(nodes_file)
network_buildings = nodes_df[nodes_df['type'] == 'CONSUMER']['building'].unique().tolist()
return set([b for b in network_buildings if b and b != 'NONE'])
except Exception as e:
# Log warning but don't fail - caller handles empty set
import warnings
warnings.warn(f"Failed to read network buildings for {network_type}/{network_name}: {e}")
return set()
🤖 Prompt for AI Agents
In cea/analysis/costs/supply_costs.py around lines 584 to 591, replace the bare
"except Exception: pass" that swallows all errors when reading nodes_file with
explicit error handling: catch the exception as e and log it (e.g.,
logger.exception or logging.exception) including the filename and context, and
then return the empty set as current behavior; optionally narrow the exception
to file/IO/geopandas-specific errors if appropriate. Ensure the log message
contains the nodes_file path and the exception details to aid debugging.

Comment on lines +815 to +818
# Write back modified supply.csv temporarily
if fallback_count > 0:
print(f" Applied config fallbacks for {fallback_count} building(s) with scale mismatch")
supply_df.to_csv(supply_csv_path, index=False)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n cea/analysis/costs/supply_costs.py | sed -n '800,835p'

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 1891


🏁 Script executed:

rg -n "supply_csv_path\s*=" cea/analysis/costs/supply_costs.py -A 2

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 349


🏁 Script executed:

rg -n "def get_building_supply" cea/analysis/costs/ -A 10

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 73


🏁 Script executed:

fd -type f -name "*.py" | xargs rg -l "class InputLocator" | head -5

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 298


🏁 Script executed:

rg -n "def get_building_supply" cea/ -A 5

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 518


🏁 Script executed:

rg -n "def get_building_properties_folder" cea/inputlocator.py -A 2

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 275


🏁 Script executed:

rg -n "apply_supply_code_fallback_for_standalone" cea/analysis/costs/supply_costs.py -B 5 -A 20 | head -60

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 2437


🏁 Script executed:

cat -n cea/analysis/costs/supply_costs.py | sed -n '770,820p'

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 2269


🏁 Script executed:

rg -n "apply_level_1_fallback_config" cea/ -B 2 -A 2

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 73


🏁 Script executed:

rg -n "supply.csv" cea/analysis/costs/supply_costs.py | head -20

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 983


🏁 Script executed:

git log --oneline cea/analysis/costs/supply_costs.py | head -10

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 168


🏁 Script executed:

cat -n cea/analysis/costs/supply_costs.py | sed -n '765,775p'

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 539


🏁 Script executed:

rg -n "def.*fallback" cea/analysis/costs/supply_costs.py

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 479


🏁 Script executed:

rg -n "supply_df\s*=" cea/analysis/costs/supply_costs.py -B 2 -A 5 | head -50

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 594


🏁 Script executed:

rg -n "apply_config_fallbacks_for_service_needs" cea/ -B 3 -A 3

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 1402


🏁 Script executed:

cat -n cea/analysis/costs/supply_costs.py | sed -n '821,860p'

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 2060


🏁 Script executed:

rg -n "get_building_supply\|supply.csv" cea/analysis/costs/supply_costs.py | grep -A 2 "calculate_heating_systems"

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 73


🏁 Script executed:

cat -n cea/analysis/costs/supply_costs.py | sed -n '821,850p'

Repository: architecture-building-systems/CityEnergyAnalyst

Length of output: 1725


Remove file modification; pass corrected supply data to downstream functions instead.

The code modifies scenario/inputs/building-properties/supply.csv at line 818, which violates the scenario structure convention that inputs should be read-only. While the fallbacks are applied intentionally so that calculate_heating_systems reads the corrected supply codes, persisting these changes to the input file creates reproducibility and concurrency issues.

Instead, collect the fallback corrections in a dict or DataFrame and pass them to calculate_heating_systems (or the Domain/Building classes it uses) as an optional parameter to override input values. Alternatively, write the corrected data to a temporary location in the outputs directory for this calculation only.

Comment on lines +1494 to +1496
# Network._calculate_piping_cost uses: annualised = total / network_lifetime_yrs
network_lifetime_yrs = network.configuration_defaults.get('network_lifetime_yrs', 20.0)
piping_cost_total = piping_cost_annual * network_lifetime_yrs
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Same annualization issue in calculate_network_costs.

The piping cost calculation here also uses simple division instead of the proper annualization formula.

             # Calculate total CAPEX from annualized cost
-            # Network._calculate_piping_cost uses: annualised = total / network_lifetime_yrs
-            network_lifetime_yrs = network.configuration_defaults.get('network_lifetime_yrs', 20.0)
-            piping_cost_total = piping_cost_annual * network_lifetime_yrs
+            # Reverse the annualization to get total CAPEX
+            # Note: If Network uses simple division, this calculation is consistent but both should
+            # ideally use proper annualization. For now, maintain consistency with Network class.
+            network_lifetime_yrs = network.configuration_defaults.get('network_lifetime_yrs', 20.0)
+            piping_cost_total = piping_cost_annual * network_lifetime_yrs

If the Network class uses simple division, consider updating both to use the proper formula for consistency with the cost calculation guidelines.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In cea/analysis/costs/supply_costs.py around lines 1494-1496 the code converts
piping annual cost to a total cost by multiplying by lifetime (piping_cost_total
= piping_cost_annual * network_lifetime_yrs), which uses simple
division/aggregation instead of the proper annuity annualisation; compute the
annuity factor from the network discount rate and lifetime (discount_rate =
network.configuration_defaults.get('discount_rate', <default>), n =
network.configuration_defaults.get('network_lifetime_yrs', 20.0)), then convert
annual to total with total = annual / annuity_factor where annuity_factor = r /
(1 - (1 + r) ** -n); replace the multiply with this calculation and ensure you
use the same keys/defaults as other cost functions for consistency.

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