Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
f5692d5
Add logging for missing file requirement parameters
reyery Sep 24, 2025
ce3e934
Split emissions map layer into embodied and operational
reyery Sep 25, 2025
4e42ee2
Add schema for get_total_yearly_operational_building
reyery Sep 25, 2025
83933bd
Remove redundant slicing by building
reyery Sep 25, 2025
77a0903
Refactor comfort chart to pass building_name explicitly
reyery Sep 25, 2025
e8852de
Fix building HVAC data access in comfort_chart.py
reyery Sep 25, 2025
f69569d
Update emissions keys in plot metrics and analytics
reyery Sep 26, 2025
4b00f27
Add error handling for invalid plot_cea_feature values
reyery Sep 26, 2025
3dccbd9
Add embodied emissions support to bar_plot
reyery Sep 26, 2025
25260e0
Add support for operational emissions plots
reyery Sep 26, 2025
84e9577
Refactor LCA emissions calculation logic
reyery Sep 26, 2025
6ee383a
Update schema creator for yearly operational building
reyery Sep 26, 2025
203cc1c
Change range type in ParameterDefinition
reyery Sep 26, 2025
905afd2
Add period selection to operational emissions layer
reyery Sep 26, 2025
238eaff
Add dynamic range generation for parameter sliders
reyery Sep 28, 2025
22aa587
Add endpoint to get parameter range for map layers
reyery Sep 28, 2025
079ec7f
Add period selection to embodied emissions map layer
reyery Sep 28, 2025
2fcd82b
Change to use timeline as parameter name
reyery Sep 29, 2025
382177c
Return None for missing data in period range
reyery Sep 29, 2025
8d3745c
Improve error handling in FileRequirement.get_required_files
reyery Sep 29, 2025
25ecb08
Rename color_array to colour_array in ColourGradient
reyery Sep 29, 2025
63ae267
Add y-axis labels for emissions per conditioned floor area
reyery Sep 29, 2025
12206d7
Improve error handling for missing HVAC config
reyery Sep 29, 2025
ba964d1
Improve error handling for missing parameters in map layers
reyery Sep 29, 2025
00aee77
Handle empty building list in LCA map layers
reyery Sep 29, 2025
6769526
Remove print statement
reyery Sep 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions cea/interfaces/dashboard/map_layers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
from cea import MissingInputDataException
from cea.config import Configuration, DEFAULT_CONFIG
from cea.inputlocator import InputLocator
from cea.interfaces.dashboard.lib.logs import getCEAServerLogger


logger = getCEAServerLogger("cea-server-map-layers")


# locator_func = Callable[..., str]
Expand Down Expand Up @@ -44,6 +48,7 @@ def get_required_files(self, layer: "MapLayer", current_params: Dict[str, Any] =
if self.depends_on is not None:
# Check if the current parameters meet the requirements
if not all(value in current_params for value in self.depends_on):
logger.error(f"Missing required parameters for file requirement: {self.depends_on}")
raise ValueError("Missing required parameters")

Comment on lines 49 to 55
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

Guard against None parameters and log more context

Membership test on current_params can raise if None. Also, include locator name and provided params for easier debugging.

Apply:

-        if self.depends_on is not None:
-            # Check if the current parameters meet the requirements
-            if not all(value in current_params for value in self.depends_on):
-                logger.error(f"Missing required parameters for file requirement: {self.depends_on}")
-                raise ValueError("Missing required parameters")
+        if self.depends_on is not None:
+            # Check if the current parameters meet the requirements
+            missing = [k for k in self.depends_on if not current_params or k not in current_params]
+            if missing:
+                logger.error(
+                    f"Missing required parameters for file requirement {self.file_locator}: "
+                    f"missing={missing}, provided_keys={list((current_params or {}).keys())}"
+                )
+                raise ValueError(f"Missing required parameters: {missing}")
📝 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 self.depends_on is not None:
# Check if the current parameters meet the requirements
if not all(value in current_params for value in self.depends_on):
logger.error(f"Missing required parameters for file requirement: {self.depends_on}")
raise ValueError("Missing required parameters")
if self.depends_on is not None:
# Check if the current parameters meet the requirements
missing = [k for k in self.depends_on if not current_params or k not in current_params]
if missing:
logger.error(
f"Missing required parameters for file requirement {self.file_locator}: "
f"missing={missing}, provided_keys={list((current_params or {}).keys())}"
)
raise ValueError(f"Missing required parameters: {missing}")
🤖 Prompt for AI Agents
In cea/interfaces/dashboard/map_layers/base.py around lines 48 to 53, guard
against current_params being None before performing membership tests and improve
the error log: first check that current_params is not None (or convert it to an
empty container) before using all(value in current_params for value in
self.depends_on); if the check fails, include the locator name (self.locator or
appropriate attribute) and the actual provided current_params in the
logger.error message and raise a ValueError with a more descriptive message
mentioning the missing parameters and locator context.

# Parse string locator
Expand Down
148 changes: 112 additions & 36 deletions cea/interfaces/dashboard/map_layers/life_cycle_analysis/layers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,14 @@
from cea.plots.colors import color_to_hex


class EmissionsMapLayer(MapLayer):
class EmbodiedEmissionsMapLayer(MapLayer):
category = LifeCycleAnalysisCategory
name = "life-cycle-analysis"
label = "GHG Emissions"
description = "GHG Emissions"
name = "embodied-emissions"
label = "Embodied Emissions"
description = ""
Comment on lines +13 to +17
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

❓ Verification inconclusive

Public rename may break imports—add a back‑compat alias

Renaming EmissionsMapLayer to EmbodiedEmissionsMapLayer is a breaking change. Provide a temporary alias to avoid downstream breakage.

Run this to find existing references:

Add after class definitions:

# Backward compatibility alias (remove in a future major release)
EmissionsMapLayer = EmbodiedEmissionsMapLayer

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -nP -C2 '\bEmissionsMapLayer\b'

Length of output: 0


Add backward-compat alias for EmissionsMapLayer
No internal references to EmissionsMapLayer were found, but the public rename is breaking for downstream users. After the EmbodiedEmissionsMapLayer class (e.g. after line 16), add:

# Backward compatibility alias (remove in a future major release)
EmissionsMapLayer = EmbodiedEmissionsMapLayer
🤖 Prompt for AI Agents
In cea/interfaces/dashboard/map_layers/life_cycle_analysis/layers.py around
lines 12 to 16, create a backward-compatibility alias for the renamed class by
adding an alias assignment after the EmbodiedEmissionsMapLayer class definition
that binds the old public name EmissionsMapLayer to the new
EmbodiedEmissionsMapLayer, and include a short comment indicating it is for
backward compatibility and slated for removal in a future major release.


def _get_emissions_types(self):
return ["embodied", "operational"]

def _get_data_columns(self, parameters: dict) -> Optional[list]:
results_path = self._get_results_files(parameters)
def _get_data_columns(self) -> Optional[list]:
results_path = self.locator.get_lca_embodied()

try:
emissions_df = pd.read_csv(results_path)
Expand All @@ -29,34 +26,121 @@ def _get_data_columns(self, parameters: dict) -> Optional[list]:

return sorted(list(columns - {"name", "GFA_m2"}))

def _get_results_files(self, parameters: dict) -> str:
emissions_type = parameters['emissions-type']
if emissions_type == "embodied":
return self.locator.get_lca_embodied()
elif emissions_type == "operational":
return self.locator.get_lca_operation()
else:
raise ValueError(f"Invalid emissions type: {emissions_type}")

@classmethod
def expected_parameters(cls):
return {
'emissions-type':
'data-column':
ParameterDefinition(
"Emissions Type",
"Data Column",
"string",
description="Type of emissions",
options_generator="_get_emissions_types",
description="Data column to use",
options_generator="_get_data_columns",
selector="choice",
),
'radius':
ParameterDefinition(
"Radius",
"number",
default=5,
description="Radius of hexagon bin in meters",
selector="input",
range=[0, 100],
filter="radius",
),
'scale':
ParameterDefinition(
"Scale",
"number",
default=1,
description="Scale of hexagon bin height",
selector="input",
range=[0.1, 10],
filter="scale",
),
}

@classmethod
def file_requirements(cls):
return [
FileRequirement(
"Zone Buildings Geometry",
file_locator="locator:get_zone_geometry",
),
FileRequirement(
"Embodied Emissions Results",
file_locator="locator:get_lca_embodied",
),
]

Comment on lines +85 to +97
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Align file_requirements with actual per‑building files used.
Current locators ("get_lca_embodied", "get_lca_operation") likely won’t validate or cache the same files you read (per‑building timelines). Use the existing _get_results_files helpers to enumerate those files so missing‑file checks and caching are accurate.

 EmbodiedEmissionsMapLayer.file_requirements():
-            FileRequirement(
-                "Embodied Emissions Results",
-                file_locator="locator:get_lca_embodied",
-            ),
+            FileRequirement(
+                "Embodied Emissions Timeline Files",
+                file_locator="layer:_get_results_files",
+            ),
 
 OperationalEmissionsMapLayer.file_requirements():
-            FileRequirement(
-                "Operational Emissions Results",
-                file_locator="locator:get_lca_operation",
-            ),
+            FileRequirement(
+                "Operational Emissions Hourly Files",
+                file_locator="layer:_get_results_files",
+            ),

Add the missing embodied helper:

def _get_results_files(self, _):
    buildings = self.locator.get_zone_building_names()
    return [self.locator.get_lca_timeline_building(b) for b in buildings]

To confirm the old locators exist, run:

Also applies to: 225-236


🏁 Script executed:

#!/bin/bash
rg -nP 'def\s+get_lca_(embodied|operation)\b' cea/inputlocator.py

Length of output: 74


🏁 Script executed:

#!/bin/bash
rg -nP 'def\s+get_lca_timeline_building' cea/inputlocator.py
rg -nP 'def\s+get_zone_building_names' cea/inputlocator.py

Length of output: 103


Use per-building timeline files via _get_results_files
Replace the generic locators in both layers’ file_requirements with a helper that enumerates each building’s timeline file:

 EmbodiedEmissionsMapLayer.file_requirements():
-    FileRequirement("Embodied Emissions Results", file_locator="locator:get_lca_embodied")
+    FileRequirement("Embodied Emissions Timeline Files", file_locator="layer:_get_results_files")

 OperationalEmissionsMapLayer.file_requirements():
-    FileRequirement("Operational Emissions Results", file_locator="locator:get_lca_operation")
+    FileRequirement("Operational Emissions Hourly Files", file_locator="layer:_get_results_files")

Then add this method to each map-layer class:

def _get_results_files(self, _):
    buildings = self.locator.get_zone_building_names()
    return [self.locator.get_lca_timeline_building(b) for b in buildings]

This ensures missing-file checks and caching cover every per-building output.

🤖 Prompt for AI Agents
In cea/interfaces/dashboard/map_layers/life_cycle_analysis/layers.py around
lines 85 to 97, the FileRequirement entries currently reference generic locators
for embodied emissions results; replace those generic locators with a helper
that returns per-building timeline files by implementing a
_get_results_files(self, _) method that calls
self.locator.get_zone_building_names() and maps each name to
self.locator.get_lca_timeline_building(name), then use that helper for the file
requirement so missing-file checks and caching operate on each building timeline
file.

@cache_output
def generate_data(self, parameters):
"""Generates the output for this layer"""

# FIXME: Hardcoded to zone buildings for now
buildings = self.locator.get_zone_building_names()

data_column = parameters['data-column']

output = {
"data": [],
"properties": {
"name": self.name,
"label": "Embodied Emissions",
"description": self.description,
"colours": {
"colour_array": [color_to_hex("grey_lighter"), color_to_hex("black")],
"points": 12
}
Comment on lines +115 to +118
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

Verify colours payload shape: 'colour_array' vs 'color_array'

ColourGradient.to_dict() uses 'color_array' (US). Here it's 'colour_array' (UK). Confirm which the frontend expects to avoid broken colour gradients.


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "Occurrences of colour/color array keys:"
rg -nP -C1 '\b(colo[u]?r_array)\b'
echo "Occurrences of the 'colours' property structure:"
rg -nP -C2 '\bcolou?rs\b'

Length of output: 28889


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "---- ColourGradient definition ----"
rg -nP 'class ColourGradient' -C10 cea/interfaces/dashboard/map_layers/base.py
echo "---- ColourGradient.to_dict ----"
rg -nP 'def to_dict' -C5 cea/interfaces/dashboard/map_layers/base.py
echo "---- MapLayer.colours usage in to_dict ----"
rg -nP 'colours' -C5 cea/interfaces/dashboard/map_layers/base.py

Length of output: 1541


Replace ‘colour_array’ with ‘color_array’ in cea/interfaces/dashboard/map_layers/life_cycle_analysis/layers.py:90-93; ColourGradient.to_dict() emits “color_array”, so this must match to avoid broken gradients.

🤖 Prompt for AI Agents
In cea/interfaces/dashboard/map_layers/life_cycle_analysis/layers.py around
lines 90 to 93, the dictionary key "colour_array" must be renamed to
"color_array" to match ColourGradient.to_dict() output; update the key name in
that dict (and any nearby occurrences) so it uses "color_array":
[color_to_hex("grey_lighter"), color_to_hex("black")], leaving values and
structure unchanged.

}
}

df = gpd.read_file(self.locator.get_zone_geometry()).set_index("name").loc[buildings]
building_centroids = df.geometry.centroid.to_crs(CRS.from_epsg(4326))

results_path = self.locator.get_lca_embodied()
emissions_df = pd.read_csv(results_path, usecols=["name", data_column], index_col="name")[data_column].loc[
buildings]

output['data'] = [{"position": [centroid.x, centroid.y], "value": emissions} for centroid, emissions in
zip(building_centroids, emissions_df)]
output['properties']['range'] = {
'total': {
'label': 'Total Range',
'min': float(min(emissions_df)),
'max': float(max(emissions_df))
},
}

return output


class OperationalEmissionsMapLayer(MapLayer):
category = LifeCycleAnalysisCategory
name = "operational-emissions"
label = "Operational Emissions"
description = ""

def _get_data_columns(self) -> Optional[list]:
results_path = self.locator.get_lca_operation()

try:
emissions_df = pd.read_csv(results_path)
columns = set(emissions_df.columns)
except (pd.errors.EmptyDataError, FileNotFoundError):
return

return sorted(list(columns - {"name", "GFA_m2"}))

@classmethod
def expected_parameters(cls):
return {
'data-column':
ParameterDefinition(
"Data Column",
"string",
description="Data column to use",
options_generator="_get_data_columns",
selector="choice",
depends_on=["emissions-type"],
),
'radius':
ParameterDefinition(
Expand Down Expand Up @@ -88,9 +172,8 @@ def file_requirements(cls):
file_locator="locator:get_zone_geometry",
),
FileRequirement(
"Demand Results",
file_locator="layer:_get_results_files",
depends_on=["emissions-type"],
"Operational Emissions Results",
file_locator="locator:get_lca_operation",
),
]

Expand All @@ -101,14 +184,13 @@ def generate_data(self, parameters):
# FIXME: Hardcoded to zone buildings for now
buildings = self.locator.get_zone_building_names()

emissions_type = parameters['emissions-type']
data_column = parameters['data-column']

output = {
"data": [],
"properties": {
"name": self.name,
"label": f"GHG Emissions: {emissions_type}",
"label": "Operational Emissions",
"description": self.description,
"colours": {
"colour_array": [color_to_hex("grey_lighter"), color_to_hex("black")],
Expand All @@ -119,14 +201,8 @@ def generate_data(self, parameters):

df = gpd.read_file(self.locator.get_zone_geometry()).set_index("name").loc[buildings]
building_centroids = df.geometry.centroid.to_crs(CRS.from_epsg(4326))

if emissions_type == "embodied":
results_path = self.locator.get_lca_embodied()
elif emissions_type == "operational":
results_path = self.locator.get_lca_operation()
else:
raise ValueError(f"Invalid emissions type: {emissions_type}")


results_path = self.locator.get_lca_operation()
emissions_df = pd.read_csv(results_path, usecols=["name", data_column], index_col="name")[data_column].loc[
buildings]

Expand Down