Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
29 changes: 8 additions & 21 deletions cea/analysis/lca/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@
This script is used to calculate the LCA
"""





import datetime
import os

from cea.analysis.lca.embodied import lca_embodied
Expand All @@ -28,28 +25,18 @@


def emissions_simplified(locator, config):

# Force the simplified embodied and operation to be true
# embodied = config.emissions.embodied
# operation = config.emissions.operational
embodied = True
operation = True

# embodied emissions
if embodied:
year_to_calculate = config.emissions.year_to_calculate
if year_to_calculate is None:
year_to_calculate = 2025
lca_embodied(year_to_calculate, locator)
# get current year
year_to_calculate = datetime.datetime.now().year
print(f'Running embodied emissions for year {year_to_calculate}')
lca_embodied(year_to_calculate, locator)

# operation emissions
if operation:
lca_operation(locator)
lca_operation(locator)

def emissions_detailed(config):
if config.emissions.include_emission_timeline:
operational_hourly(config)
total_yearly(config)
operational_hourly(config)
total_yearly(config)
Comment on lines +38 to +39
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

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:

-    operational_hourly(config)
-    total_yearly(config)
+    logger.info("Running hourly operational emissions (per-building and district)")
+    operational_hourly(config)
+    logger.info("Running total emissions timeline")
+    total_yearly(config)

To verify buildings default semantics:


🏁 Script executed:

#!/bin/bash
# Find where emissions.buildings is defined/used to confirm defaults
rg -n -C3 -P '\bemissions\.buildings\b' --type=py

Length of output: 1230


🏁 Script executed:

#!/bin/bash
# Display imports and logger setup in main.py
sed -n '1,100p' cea/analysis/lca/main.py

Length of output: 1502


🏁 Script executed:

#!/bin/bash
# Show Configuration class and emissions section in cea/config.py
awk '/class Configuration/,/^$/ {print}' cea/config.py
grep -R -n "emissions" -n cea/config.py

Length of output: 110


🏁 Script executed:

#!/bin/bash
# Search cea/config.py for configparser usage and dynamic attribute access
grep -R -n -E "ConfigParser|parser|__getattr__|__getattribute__" cea/config.py

Length of output: 3362


Add logger setup, logging calls and verify default buildings

  • In cea/analysis/lca/main.py, import logging and add at top:
    logger = logging.getLogger(__name__)
  • Around lines 38–39, wrap calls with logging:
    -    operational_hourly(config)
    -    total_yearly(config)
    +    logger.info("Running hourly operational emissions (per-building and district)")
    +    operational_hourly(config)
    +    logger.info("Running total emissions timeline")
    +    total_yearly(config)
  • Manually confirm that config.emissions.buildings has a sensible default in your config file or handle the unset case in code.
🤖 Prompt for AI Agents
In cea/analysis/lca/main.py around lines 38–39, add logging setup at the top by
importing logging and creating logger = logging.getLogger(__name__); before
calling operational_hourly(config) and total_yearly(config) insert
logger.debug/info calls indicating each step start and logger.info on successful
completion (and logger.exception on errors if you wrap calls in try/except);
finally, ensure config.emissions.buildings is validated before use—either check
for presence and set a sensible default or emit a clear logger.warning/error and
return/raise so the code does not proceed with an unset value.



def main(config):
Expand Down
13 changes: 0 additions & 13 deletions cea/default.config
Original file line number Diff line number Diff line change
Expand Up @@ -364,19 +364,6 @@ operational.type = BooleanParameter
operational.help = Estimate Operational costs (variable and fixed) due to supply systems operation (allocated according to the supply-systems input database)

[emissions]
year-to-calculate =
year-to-calculate.type = IntegerParameter
year-to-calculate.nullable = True
year-to-calculate.help = Defines the year to calculate the aggregated emissions for each building. Leave blank to use the current year. Different from the emission timeline calculation, for simplified calculations, if a building is greater than 60 years old, its embodied emissions are not considered.
year-to-calculate.category = CEA-LCA Simplified Settings

include-emission-timeline = false
include-emission-timeline.type = BooleanParameter
include-emission-timeline.help = True to record (8760) hourly operational emissions for heating, cooling, hot water and electricity use, and embodied emissions for each selected building,
and all activities every year until year-end, assuming all components are renovated or replaced until the building is demolished.
"Year-start" is the oldest of the (construction_)year of all selected buildings.
"Year-end" is customisable below.

year-end =
year-end.type = IntegerParameter
year-end.nullable = True
Expand Down
1 change: 1 addition & 0 deletions cea/demand/building_properties/building_hvac.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ def verify_overlap_season(building_name, has_heating_season, has_cooling_season,
return False


# TODO: Remove building_name from function signature, not useful in function
def verify_has_season(building_name, start, end):
def invalid_date(date):
if len(date) != 5 or "|" not in date:
Expand Down
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 48 to 53
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 +12 to +16
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",
),
]

@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
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
51 changes: 51 additions & 0 deletions cea/schemas.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19658,6 +19658,57 @@ get_thermal_network_velocity_edges_file:
values: '{0.0...n}'
min: 0.0
used_by: []
get_total_yearly_operational_building:
created_by:
- emission_time_dependent
file_path: outputs/data/emissions/timeline/Total_yearly_operational_building.csv
file_type: csv
schema:
columns:
hour:
description: Hour of the year.
type: int
unit: '[h]'
values: '{0...8759}'
min: 0
max: 8759
heating_tonCO2:
description: Total operational emissions from heating systems.
type: float
unit: '[ton CO2-eq]'
values: '{0.0...n}'
min: 0.0
hot_water_tonCO2:
description: Total operational emissions from hot water systems.
type: float
unit: '[ton CO2-eq]'
values: '{0.0...n}'
min: 0.0
cooling_tonCO2:
description: Total operational emissions from cooling systems.
type: float
unit: '[ton CO2-eq]'
values: '{0.0...n}'
min: 0.0
electricity_tonCO2:
description: Total operational emissions from electricity consumption.
type: float
unit: '[ton CO2-eq]'
values: '{0.0...n}'
min: 0.0
Qcs_sys_NONE_tonCO2:
description: Total operational emissions from cooling systems (NONE category).
type: float
unit: '[ton CO2-eq]'
values: '{0.0...n}'
min: 0.0
E_sys_GRID_tonCO2:
description: Total operational emissions from grid electricity systems.
type: float
unit: '[ton CO2-eq]'
values: '{0.0...n}'
min: 0.0
used_by: []
get_total_demand:
created_by:
- demand
Expand Down
8 changes: 4 additions & 4 deletions cea/visualisation/a_data_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,17 @@
'pv': solar_metrics,
'pvt': solar_metrics,
'sc': solar_metrics,
'emissions-embodied': embodied_emission_metrics,
'emissions-operational': operational_emission_metrics
'embodied-emissions': embodied_emission_metrics,
'operational-emissions': operational_emission_metrics
}

dict_plot_analytics_cea_feature = {
'demand': demand_analytics,
'pv': solar_analytics,
'pvt': [],
'sc': [],
'emissions-embodied': [],
'emissions-operational': []
'embodied-emissions': [],
'operational-emissions': []
}

# Trigger the summary feature and point to the csv results file
Expand Down
Loading
Loading