-
Notifications
You must be signed in to change notification settings - Fork 75
Split emissions layer categories #3917
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
f5692d5
ce3e934
4e42ee2
83933bd
77a0903
e8852de
f69569d
4b00f27
3dccbd9
25260e0
84e9577
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -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] | ||||||||||||||||||||||||||||||
|
@@ -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
48
to
53
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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
Suggested change
🤖 Prompt for AI Agents
|
||||||||||||||||||||||||||||||
# Parse string locator | ||||||||||||||||||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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
+12
to
+16
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major ❓ Verification inconclusivePublic 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 # Backward compatibility alias (remove in a future major release)
EmissionsMapLayer = EmbodiedEmissionsMapLayer 🤖 Prompt for AI Agents
|
||
|
||
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) | ||
|
@@ -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", | ||
), | ||
] | ||
|
||
@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
+90
to
+93
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chainVerify 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
|
||
} | ||
} | ||
|
||
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( | ||
|
@@ -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", | ||
), | ||
] | ||
|
||
|
@@ -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")], | ||
|
@@ -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] | ||
|
||
|
There was a problem hiding this comment.
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
Hourly + timeline runs: confirm default building selection and add logging
Running both unconditionally can be heavy; ensure this matches CLI expectations and that config.emissions.buildings has sensible defaults when unset.
Apply this diff:
To verify buildings default semantics:
🏁 Script executed:
Length of output: 1230
🏁 Script executed:
Length of output: 1502
🏁 Script executed:
Length of output: 110
🏁 Script executed:
Length of output: 3362
Add logger setup, logging calls and verify default buildings
logging
and add at top:config.emissions.buildings
has a sensible default in your config file or handle the unset case in code.🤖 Prompt for AI Agents