Merge pull request #10 from bjoernsteinhagen/bjorn/gro-116-write-ec-as-properties-on-objects
feat: write ec as properties on objects
This commit is contained in:
@@ -131,23 +131,24 @@ class RevitCarbonAnalyzer:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
steel_database: str,
|
||||
timber_database: str,
|
||||
concrete_database: str,
|
||||
country: str,
|
||||
reinforcement_rates: Dict[str, float],
|
||||
material_processor: MaterialProcessor,
|
||||
element_processor: ElementProcessor,
|
||||
carbon_calculator: CarbonCalculator,
|
||||
logger: Logging,
|
||||
):
|
||||
self.material_processor = MaterialProcessor()
|
||||
self.element_processor = ElementProcessor(
|
||||
material_processor=self.material_processor, logger=Logging()
|
||||
)
|
||||
self.carbon_calculator = CarbonCalculator(
|
||||
steel_database=steel_database,
|
||||
timber_database=timber_database,
|
||||
concrete_database=concrete_database,
|
||||
country=country,
|
||||
custom_reinforcement_rates=reinforcement_rates,
|
||||
)
|
||||
"""
|
||||
Initialize with injected dependencies.
|
||||
|
||||
Args:
|
||||
material_processor: Service for processing raw materials
|
||||
element_processor: Service for processing Revit elements
|
||||
carbon_calculator: Service for calculating carbon emissions
|
||||
logger: Logging service
|
||||
"""
|
||||
self.material_processor = material_processor
|
||||
self.element_processor = element_processor
|
||||
self.carbon_calculator = carbon_calculator
|
||||
self.logger = logger
|
||||
|
||||
def analyze_model(self, model_root) -> dict:
|
||||
"""Analyze a Revit model for carbon emissions."""
|
||||
@@ -223,6 +224,119 @@ class RevitCarbonAnalyzer:
|
||||
# Calculate carbon
|
||||
try:
|
||||
carbon_results = self.carbon_calculator.calculate_carbon(processed_element)
|
||||
|
||||
# Initialize Embodied Carbon Calculation dictionary
|
||||
embodied_carbon_data = {}
|
||||
|
||||
for material_name, result in carbon_results.items():
|
||||
# Create a dictionary for each material instead of an array
|
||||
material_data = {}
|
||||
|
||||
if result.category == "Wood":
|
||||
# For timber - use name/value/units format as dictionary entries
|
||||
material_data = {
|
||||
"volume": {
|
||||
"name": "volume",
|
||||
"value": result.quantity,
|
||||
"units": "m³",
|
||||
},
|
||||
"database": {
|
||||
"name": "database",
|
||||
"value": result.database,
|
||||
"units": None,
|
||||
},
|
||||
"ecf": {
|
||||
"name": "ecf",
|
||||
"value": result.factor,
|
||||
"units": "kgCO₂e/m³",
|
||||
},
|
||||
"embodied carbon": {
|
||||
"name": "embodied carbon",
|
||||
"value": result.total_carbon,
|
||||
"units": "kgCO₂e",
|
||||
},
|
||||
}
|
||||
elif result.category == "Concrete":
|
||||
# For concrete (include both concrete and reinforcement)
|
||||
material_data = {
|
||||
"volume": {
|
||||
"name": "volume",
|
||||
"value": result.concrete_volume,
|
||||
"units": "m³",
|
||||
},
|
||||
"database": {
|
||||
"name": "database",
|
||||
"value": result.database,
|
||||
"units": None,
|
||||
},
|
||||
"ecf": {
|
||||
"name": "ecf",
|
||||
"value": result.factor,
|
||||
"units": "kgCO₂e/m³",
|
||||
},
|
||||
"concrete carbon": {
|
||||
"name": "concrete carbon",
|
||||
"value": result.concrete_carbon,
|
||||
"units": "kgCO₂e",
|
||||
},
|
||||
"reinforcement mass": {
|
||||
"name": "reinforcement mass",
|
||||
"value": result.reinforcement_mass,
|
||||
"units": "kg",
|
||||
},
|
||||
"reinforcement rate": {
|
||||
"name": "reinforcement rate",
|
||||
"value": result.reinforcement_rate,
|
||||
"units": "kg/m³",
|
||||
},
|
||||
"reinforcement ecf": {
|
||||
"name": "reinforcement ecf",
|
||||
"value": result.reinforcement_factor,
|
||||
"units": "kgCO₂e/kg",
|
||||
},
|
||||
"reinforcement carbon": {
|
||||
"name": "reinforcement carbon",
|
||||
"value": result.reinforcement_carbon,
|
||||
"units": "kgCO₂e",
|
||||
},
|
||||
"embodied carbon": {
|
||||
"name": "embodied carbon",
|
||||
"value": result.total_carbon,
|
||||
"units": "kgCO₂e",
|
||||
},
|
||||
}
|
||||
elif result.category == "Metal":
|
||||
# For metal
|
||||
material_data = {
|
||||
"mass": {
|
||||
"name": "mass",
|
||||
"value": result.quantity,
|
||||
"units": "kg",
|
||||
},
|
||||
"database": {
|
||||
"name": "database",
|
||||
"value": result.database,
|
||||
"units": None,
|
||||
},
|
||||
"ecf": {
|
||||
"name": "ecf",
|
||||
"value": result.factor,
|
||||
"units": "kgCO₂e/kg",
|
||||
},
|
||||
"embodied carbon": {
|
||||
"name": "embodied carbon",
|
||||
"value": result.total_carbon,
|
||||
"units": "kgCO₂e",
|
||||
},
|
||||
}
|
||||
|
||||
# Add this material's data to the main dictionary
|
||||
embodied_carbon_data[material_name] = material_data
|
||||
|
||||
# Attach the data to the original element
|
||||
if hasattr(element, "properties"):
|
||||
element.properties["Embodied Carbon Calculation"] = embodied_carbon_data
|
||||
|
||||
return {
|
||||
"id": element_id,
|
||||
"status": "processed",
|
||||
@@ -233,7 +347,6 @@ class RevitCarbonAnalyzer:
|
||||
"name": m.properties.name,
|
||||
"type": m.type.value,
|
||||
"volume": m.properties.volume,
|
||||
# Add other material properties as needed
|
||||
}
|
||||
for m in processed_element.materials
|
||||
],
|
||||
@@ -292,13 +405,26 @@ def automate_function(
|
||||
"Topping Slabs": function_inputs.reinforcement_topping_slab,
|
||||
}
|
||||
|
||||
# Initialize analyzer
|
||||
analyzer = RevitCarbonAnalyzer(
|
||||
# Create dependencies with proper DI
|
||||
logger = Logging()
|
||||
material_processor = MaterialProcessor()
|
||||
element_processor = ElementProcessor(
|
||||
material_processor=material_processor, logger=logger
|
||||
)
|
||||
carbon_calculator = CarbonCalculator(
|
||||
steel_database=steel_db,
|
||||
timber_database=timber_db,
|
||||
concrete_database=concrete_db,
|
||||
country=country,
|
||||
reinforcement_rates=custom_reinforcement_rates,
|
||||
custom_reinforcement_rates=custom_reinforcement_rates,
|
||||
)
|
||||
|
||||
# Initialize analyzer with injected dependencies
|
||||
analyzer = RevitCarbonAnalyzer(
|
||||
material_processor=material_processor,
|
||||
element_processor=element_processor,
|
||||
carbon_calculator=carbon_calculator,
|
||||
logger=logger,
|
||||
)
|
||||
|
||||
# Get commit root
|
||||
@@ -357,6 +483,11 @@ def automate_function(
|
||||
"\nNOTE: All materials successfully matched with emission factors."
|
||||
)
|
||||
|
||||
# Upload mutated model
|
||||
automate_context.create_new_version_in_project(
|
||||
model_root, f"{commit_root.branchName}_embodied_carbon"
|
||||
)
|
||||
|
||||
# Mark success with detailed message
|
||||
automate_context.mark_run_success(success_message)
|
||||
|
||||
|
||||
@@ -12,9 +12,9 @@ class ConcreteEmissionDatabase(EmissionFactorDatabase):
|
||||
super().__init__()
|
||||
self._database_name = database_name
|
||||
self._factors = {}
|
||||
self._init_factors()
|
||||
self._load_emission_factors_from_database()
|
||||
|
||||
def _init_factors(self):
|
||||
def _load_emission_factors_from_database(self):
|
||||
"""Initialize factors based on the specific database."""
|
||||
# Define mappings from database type to strength values
|
||||
database_strength_values = {
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
from typing import Dict, Type
|
||||
|
||||
from src.domain.carbon.databases.base import EmissionFactorDatabase
|
||||
from src.domain.carbon.databases.enums import (
|
||||
TimberDatabase,
|
||||
SteelDatabase,
|
||||
ConcreteDatabase,
|
||||
)
|
||||
|
||||
# Import timber databases
|
||||
from src.domain.carbon.databases.timber.athena import Athena
|
||||
from src.domain.carbon.databases.timber.structurlam import Structurlam
|
||||
from src.domain.carbon.databases.timber.awc_cwc import AwcCwc
|
||||
from src.domain.carbon.databases.timber.katerra import Katerra
|
||||
from src.domain.carbon.databases.timber.nordic_structures import NordicStructures
|
||||
from src.domain.carbon.databases.timber.binderholz import Binderholz
|
||||
from src.domain.carbon.databases.timber.structuralam_abbotsford import (
|
||||
StructuralamAbbotsford,
|
||||
)
|
||||
from src.domain.carbon.databases.timber.clf_baseline_document import CLFBaselineDocument
|
||||
from src.domain.carbon.databases.timber.industry_average import IndustryAverage
|
||||
|
||||
# Import steel databases
|
||||
from src.domain.carbon.databases.steel.steel_350_mpa import Steel350MPa
|
||||
|
||||
# Import concrete databases
|
||||
from src.domain.carbon.databases.concrete.metric import ConcreteEmissionDatabase
|
||||
|
||||
|
||||
class DatabaseFactory:
|
||||
"""Factory for creating emission factor database instances."""
|
||||
|
||||
_timber_database_classes: Dict[str, Type[EmissionFactorDatabase]] = {
|
||||
TimberDatabase.Athena2021.value: Athena,
|
||||
TimberDatabase.Structurlam2020.value: Structurlam,
|
||||
TimberDatabase.AwcCwc2018.value: AwcCwc,
|
||||
TimberDatabase.Katerra2020.value: Katerra,
|
||||
TimberDatabase.NordicStructures2018.value: NordicStructures,
|
||||
TimberDatabase.Binderholz2019.value: Binderholz,
|
||||
TimberDatabase.StructuralamAbbotsford.value: StructuralamAbbotsford,
|
||||
TimberDatabase.CLFBaselineDocument.value: CLFBaselineDocument,
|
||||
TimberDatabase.IndustryAverage.value: IndustryAverage,
|
||||
}
|
||||
|
||||
_steel_database_classes: Dict[str, Type[EmissionFactorDatabase]] = {
|
||||
SteelDatabase.Type350MPa.value: Steel350MPa,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def create_timber_database(cls, database_name: str) -> EmissionFactorDatabase:
|
||||
"""Create a timber database instance by name."""
|
||||
if database_name not in cls._timber_database_classes:
|
||||
raise ValueError(
|
||||
f"Unknown timber database: '{database_name}'. "
|
||||
f"Available databases: {', '.join(cls._timber_database_classes.keys())}"
|
||||
)
|
||||
return cls._timber_database_classes[database_name]()
|
||||
|
||||
@classmethod
|
||||
def create_steel_database(cls, database_name: str) -> EmissionFactorDatabase:
|
||||
"""Create a steel database instance by name."""
|
||||
if database_name not in cls._steel_database_classes:
|
||||
raise ValueError(
|
||||
f"Unknown steel database: '{database_name}'. "
|
||||
f"Available databases: {', '.join(cls._steel_database_classes.keys())}"
|
||||
)
|
||||
return cls._steel_database_classes[database_name]()
|
||||
|
||||
@classmethod
|
||||
def create_concrete_database(cls, database_name: str) -> EmissionFactorDatabase:
|
||||
"""Create a concrete database instance by name."""
|
||||
# For concrete, we create a new instance with the database name
|
||||
try:
|
||||
return ConcreteEmissionDatabase(database_name)
|
||||
except ValueError as e:
|
||||
# Re-raise with more context
|
||||
available_databases = [db.value for db in ConcreteDatabase]
|
||||
raise ValueError(
|
||||
f"Error creating concrete database: {str(e)}. "
|
||||
f"Available databases: {', '.join(available_databases)}"
|
||||
)
|
||||
@@ -1,169 +1,62 @@
|
||||
from typing import Optional, Dict
|
||||
from functools import lru_cache
|
||||
from typing import Optional, Dict, List, cast
|
||||
|
||||
from src.domain.carbon.databases.base import EmissionFactorDatabase
|
||||
from src.domain.carbon.databases.concrete.metric import ConcreteEmissionDatabase
|
||||
from src.domain.carbon.schema import EmissionFactor
|
||||
from src.domain.carbon.databases.enums import (
|
||||
TimberDatabase,
|
||||
SteelDatabase,
|
||||
ConcreteDatabase,
|
||||
)
|
||||
|
||||
# Import timber databases
|
||||
from src.domain.carbon.databases.timber.athena import Athena
|
||||
from src.domain.carbon.databases.timber.structurlam import Structurlam
|
||||
from src.domain.carbon.databases.timber.awc_cwc import AwcCwc
|
||||
from src.domain.carbon.databases.timber.katerra import Katerra
|
||||
from src.domain.carbon.databases.timber.nordic_structures import NordicStructures
|
||||
from src.domain.carbon.databases.timber.binderholz import Binderholz
|
||||
from src.domain.carbon.databases.timber.structuralam_abbotsford import (
|
||||
StructuralamAbbotsford,
|
||||
)
|
||||
from src.domain.carbon.databases.timber.clf_baseline_document import CLFBaselineDocument
|
||||
from src.domain.carbon.databases.timber.industry_average import IndustryAverage
|
||||
|
||||
# Import steel databases
|
||||
from src.domain.carbon.databases.steel.steel_350_mpa import Steel350MPa
|
||||
from src.domain.carbon.schema import EmissionFactor
|
||||
from src.domain.carbon.material_alias_service import MaterialAliasService
|
||||
from src.domain.carbon.databases.database_factory import DatabaseFactory
|
||||
|
||||
|
||||
class EmissionFactorRegistry:
|
||||
"""Registry of available emission factor databases"""
|
||||
"""Registry of available emission factor databases with lazy loading."""
|
||||
|
||||
def __init__(self):
|
||||
self._timber_databases = {}
|
||||
self._steel_databases = {}
|
||||
self._concrete_databases = {}
|
||||
self._timber_databases: Dict[str, EmissionFactorDatabase] = {}
|
||||
self._steel_databases: Dict[str, EmissionFactorDatabase] = {}
|
||||
self._concrete_databases: Dict[str, ConcreteEmissionDatabase] = {}
|
||||
|
||||
# Material aliases for normalization
|
||||
# NOTE: Purely demonstrative → add aliases as needed.
|
||||
self._timber_aliases = {
|
||||
"clt": ["cross laminated timber", "cross-laminated timber"],
|
||||
"glulam": [
|
||||
"glue laminated timber",
|
||||
"glued laminated timber",
|
||||
"glulam beam",
|
||||
],
|
||||
"lvl": ["laminated veneer lumber"],
|
||||
"softwood lumber": ["dimensional lumber", "sawn lumber", "softwood"],
|
||||
"softwood plywood": ["plywood", "softwood ply"],
|
||||
"oriented strand board": ["osb", "osb board"],
|
||||
"glt/nlt/dlt": [
|
||||
"glt",
|
||||
"nlt",
|
||||
"dlt",
|
||||
"nail laminated timber",
|
||||
"dowel laminated timber",
|
||||
],
|
||||
}
|
||||
# Create the alias service
|
||||
self._alias_service = MaterialAliasService()
|
||||
|
||||
self._steel_aliases = {
|
||||
"hot rolled": [
|
||||
"hot-rolled",
|
||||
"hot_rolled",
|
||||
"hotrolled",
|
||||
"345 MPa", # NOTE: Needed!
|
||||
"350W", # NOTE: Needed!
|
||||
"350W(1)", # NOTE: Needed!
|
||||
],
|
||||
"hss": ["hollow structural section", "hollow section", "tube"],
|
||||
"plate": ["flat plate"],
|
||||
"rebar": ["reinforcing bar", "reinforcement"],
|
||||
"owsj": ["open web steel joist", "steel joist"],
|
||||
"fasteners": ["bolts", "screws", "nails", "rivets"],
|
||||
"metal deck": ["deck", "decking"],
|
||||
}
|
||||
def _get_timber_database(self, database_name: str) -> EmissionFactorDatabase:
|
||||
"""Get or create a timber database instance."""
|
||||
if database_name not in self._timber_databases:
|
||||
self._timber_databases[
|
||||
database_name
|
||||
] = DatabaseFactory.create_timber_database(database_name)
|
||||
return self._timber_databases[database_name]
|
||||
|
||||
self._concrete_aliases = {
|
||||
# To be added when concrete implementation is needed
|
||||
}
|
||||
def _get_steel_database(self, database_name: str) -> EmissionFactorDatabase:
|
||||
"""Get or create a steel database instance."""
|
||||
if database_name not in self._steel_databases:
|
||||
self._steel_databases[
|
||||
database_name
|
||||
] = DatabaseFactory.create_steel_database(database_name)
|
||||
return self._steel_databases[database_name]
|
||||
|
||||
# Initialize all database instances
|
||||
self._init_timber_databases()
|
||||
self._init_steel_databases()
|
||||
self._init_concrete_databases()
|
||||
|
||||
def _init_timber_databases(self) -> None:
|
||||
"""Initialize timber database implementations"""
|
||||
self._timber_databases = {
|
||||
TimberDatabase.Athena2021.value: Athena(),
|
||||
TimberDatabase.Structurlam2020.value: Structurlam(),
|
||||
TimberDatabase.AwcCwc2018.value: AwcCwc(),
|
||||
TimberDatabase.Katerra2020.value: Katerra(),
|
||||
TimberDatabase.NordicStructures2018.value: NordicStructures(),
|
||||
TimberDatabase.Binderholz2019.value: Binderholz(),
|
||||
TimberDatabase.StructuralamAbbotsford.value: StructuralamAbbotsford(),
|
||||
TimberDatabase.CLFBaselineDocument.value: CLFBaselineDocument(),
|
||||
TimberDatabase.IndustryAverage.value: IndustryAverage(),
|
||||
}
|
||||
|
||||
def _init_steel_databases(self) -> None:
|
||||
"""Initialize steel database implementations"""
|
||||
self._steel_databases = {
|
||||
SteelDatabase.Type350MPa.value: Steel350MPa(),
|
||||
}
|
||||
|
||||
def _init_concrete_databases(self) -> None:
|
||||
"""Initialize concrete database implementations"""
|
||||
self._concrete_databases = {
|
||||
ConcreteDatabase.GulLowAir.value: ConcreteEmissionDatabase(
|
||||
ConcreteDatabase.GulLowAir.value
|
||||
),
|
||||
ConcreteDatabase.GulHighAir.value: ConcreteEmissionDatabase(
|
||||
ConcreteDatabase.GulHighAir.value
|
||||
),
|
||||
ConcreteDatabase.GuLowAir.value: ConcreteEmissionDatabase(
|
||||
ConcreteDatabase.GuLowAir.value
|
||||
),
|
||||
ConcreteDatabase.GuHighAir.value: ConcreteEmissionDatabase(
|
||||
ConcreteDatabase.GuHighAir.value
|
||||
),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _normalize_material_name(name: str, aliases: Dict[str, list]) -> str:
|
||||
"""Normalize material name using centralized aliases with enhanced matching.
|
||||
|
||||
This improved version handles:
|
||||
- Case insensitivity
|
||||
- Direct matches (exact)
|
||||
- Substring matches (contains)
|
||||
- Special known cases
|
||||
"""
|
||||
|
||||
# Convert to lowercase for case-insensitive comparison
|
||||
name = name.lower().strip()
|
||||
|
||||
# Special case handling
|
||||
if any(
|
||||
steel_name in name
|
||||
for steel_name in ["345 mpa", "350w", "steel 345", "default_steel"]
|
||||
):
|
||||
return "Hot Rolled" # Map all these variants to Hot Rolled steel
|
||||
|
||||
# Check for direct match with standard names
|
||||
for standard_name in aliases.keys():
|
||||
if standard_name.lower() == name:
|
||||
return standard_name
|
||||
|
||||
# Check for standard name appearing as substring
|
||||
for standard_name in aliases.keys():
|
||||
if standard_name.lower() in name:
|
||||
return standard_name
|
||||
|
||||
# Check aliases
|
||||
for standard_name, variations in aliases.items():
|
||||
for variation in variations:
|
||||
if variation.lower() == name or variation.lower() in name:
|
||||
return standard_name
|
||||
|
||||
return name
|
||||
def _get_concrete_database(self, database_name: str) -> ConcreteEmissionDatabase:
|
||||
"""Get or create a concrete database instance."""
|
||||
if database_name not in self._concrete_databases:
|
||||
# We need to cast here because the factory returns the base type
|
||||
concrete_db = cast(
|
||||
ConcreteEmissionDatabase,
|
||||
DatabaseFactory.create_concrete_database(database_name),
|
||||
)
|
||||
self._concrete_databases[database_name] = concrete_db
|
||||
return self._concrete_databases[database_name]
|
||||
|
||||
@lru_cache(maxsize=128)
|
||||
def get_timber_factor(
|
||||
self, material_name: str, database: str
|
||||
) -> Optional[EmissionFactor]:
|
||||
"""Get emission factor for timber from specified database with name normalization"""
|
||||
db = self._timber_databases.get(database)
|
||||
if not db:
|
||||
raise ValueError(f"Unknown timber database: {database}")
|
||||
"""Get emission factor for timber from specified database with name normalization."""
|
||||
db = self._get_timber_database(database)
|
||||
|
||||
# Try direct lookup first
|
||||
factor = db.get_factor(material_name)
|
||||
@@ -171,18 +64,15 @@ class EmissionFactorRegistry:
|
||||
return factor
|
||||
|
||||
# If not found, try normalized name
|
||||
normalized_name = self._normalize_material_name(
|
||||
material_name, self._timber_aliases
|
||||
)
|
||||
normalized_name = self._alias_service.normalize_timber_name(material_name)
|
||||
return db.get_factor(normalized_name)
|
||||
|
||||
@lru_cache(maxsize=128)
|
||||
def get_steel_factor(
|
||||
self, material_name: str, database: str
|
||||
) -> Optional[EmissionFactor]:
|
||||
"""Get emission factor for steel from specified database with name normalization"""
|
||||
db = self._steel_databases.get(database)
|
||||
if not db:
|
||||
raise ValueError(f"Unknown steel database: {database}")
|
||||
"""Get emission factor for steel from specified database with name normalization."""
|
||||
db = self._get_steel_database(database)
|
||||
|
||||
# Try direct lookup first
|
||||
factor = db.get_factor(material_name)
|
||||
@@ -190,30 +80,30 @@ class EmissionFactorRegistry:
|
||||
return factor
|
||||
|
||||
# If not found, try normalized name
|
||||
normalized_name = self._normalize_material_name(
|
||||
material_name, self._steel_aliases
|
||||
)
|
||||
normalized_name = self._alias_service.normalize_steel_name(material_name)
|
||||
return db.get_factor(normalized_name)
|
||||
|
||||
@lru_cache(maxsize=128)
|
||||
def get_concrete_factor(
|
||||
self, strength: str, element_type: str, database: str
|
||||
) -> Optional[EmissionFactor]:
|
||||
"""Get emission factor for concrete from specified database based on strength and element type."""
|
||||
db = self._concrete_databases.get(database)
|
||||
if not db:
|
||||
raise ValueError(f"Unknown concrete database: {database}")
|
||||
db = self._get_concrete_database(database)
|
||||
|
||||
# Concrete database requires both strength and element type
|
||||
# Now we can safely call this method since we've ensured the correct type
|
||||
return db.get_factor_by_strength_and_element(strength, element_type)
|
||||
|
||||
def list_timber_databases(self) -> list[str]:
|
||||
"""List all registered timber databases"""
|
||||
return list(self._timber_databases.keys())
|
||||
@staticmethod
|
||||
def list_timber_databases() -> List[str]:
|
||||
"""List all available timber databases."""
|
||||
return [db.value for db in TimberDatabase]
|
||||
|
||||
def list_steel_databases(self) -> list[str]:
|
||||
"""List all registered steel databases"""
|
||||
return list(self._steel_databases.keys())
|
||||
@staticmethod
|
||||
def list_steel_databases() -> List[str]:
|
||||
"""List all available steel databases."""
|
||||
return [db.value for db in SteelDatabase]
|
||||
|
||||
def list_concrete_databases(self) -> list[str]:
|
||||
"""List all registered concrete databases"""
|
||||
return list(self._concrete_databases.keys())
|
||||
@staticmethod
|
||||
def list_concrete_databases() -> List[str]:
|
||||
"""List all available concrete databases."""
|
||||
return [db.value for db in ConcreteDatabase]
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
|
||||
class MaterialAliasService:
|
||||
"""Service for managing and resolving material name aliases"""
|
||||
|
||||
def __init__(self):
|
||||
# Material aliases used for normalization
|
||||
self._timber_aliases = {
|
||||
"clt": ["cross laminated timber", "cross-laminated timber"],
|
||||
"glulam": [
|
||||
"glue laminated timber",
|
||||
"glued laminated timber",
|
||||
"glulam beam",
|
||||
],
|
||||
"lvl": ["laminated veneer lumber"],
|
||||
"softwood lumber": ["dimensional lumber", "sawn lumber", "softwood"],
|
||||
"softwood plywood": ["plywood", "softwood ply"],
|
||||
"oriented strand board": ["osb", "osb board"],
|
||||
"glt/nlt/dlt": [
|
||||
"glt",
|
||||
"nlt",
|
||||
"dlt",
|
||||
"nail laminated timber",
|
||||
"dowel laminated timber",
|
||||
],
|
||||
}
|
||||
|
||||
self._steel_aliases = {
|
||||
"hot rolled": [
|
||||
"hot-rolled",
|
||||
"hot_rolled",
|
||||
"hotrolled",
|
||||
"345 MPa",
|
||||
"350W",
|
||||
"350W(1)",
|
||||
],
|
||||
"hss": ["hollow structural section", "hollow section", "tube"],
|
||||
"plate": ["flat plate"],
|
||||
"rebar": ["reinforcing bar", "reinforcement"],
|
||||
"owsj": ["open web steel joist", "steel joist"],
|
||||
"fasteners": ["bolts", "screws", "nails", "rivets"],
|
||||
"metal deck": ["deck", "decking"],
|
||||
}
|
||||
|
||||
self._concrete_aliases = {
|
||||
# To be added when concrete implementation is needed
|
||||
}
|
||||
|
||||
def normalize_timber_name(self, name: str) -> str:
|
||||
return self._normalize_material_name(name, self._timber_aliases)
|
||||
|
||||
def normalize_steel_name(self, name: str) -> str:
|
||||
return self._normalize_material_name(name, self._steel_aliases)
|
||||
|
||||
def normalize_concrete_name(self, name: str) -> str:
|
||||
return self._normalize_material_name(name, self._concrete_aliases)
|
||||
|
||||
@staticmethod
|
||||
def _normalize_material_name(name: str, aliases: Dict[str, List[str]]) -> str:
|
||||
"""Normalize material name using centralized aliases with enhanced matching."""
|
||||
|
||||
# Convert to lowercase for case-insensitive comparison
|
||||
name = name.lower().strip()
|
||||
|
||||
# Special case handling
|
||||
if any(
|
||||
steel_name in name
|
||||
for steel_name in ["345 mpa", "350w", "steel 345", "default_steel"]
|
||||
):
|
||||
return "Hot Rolled" # Map all these variants to Hot Rolled steel
|
||||
|
||||
# Check for direct match with standard names
|
||||
for standard_name in aliases.keys():
|
||||
if standard_name.lower() == name:
|
||||
return standard_name
|
||||
|
||||
# Check for standard name appearing as substring
|
||||
for standard_name in aliases.keys():
|
||||
if standard_name.lower() in name:
|
||||
return standard_name
|
||||
|
||||
# Check aliases
|
||||
for standard_name, variations in aliases.items():
|
||||
for variation in variations:
|
||||
if variation.lower() == name or variation.lower() in name:
|
||||
return standard_name
|
||||
|
||||
return name
|
||||
@@ -14,14 +14,3 @@ class EmissionFactor:
|
||||
valid_until: Optional[str] = None
|
||||
manufacturer: Optional[str] = None
|
||||
plant_location: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class CarbonResult:
|
||||
"""Result of a carbon calculation"""
|
||||
|
||||
material_name: str
|
||||
emission_factor: EmissionFactor
|
||||
quantity: float
|
||||
total_carbon: float
|
||||
category: str
|
||||
|
||||
+14
-1
@@ -37,8 +37,21 @@ class BuildingElement:
|
||||
materials: List[Material]
|
||||
carbon_data: Optional[Dict] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class CarbonResult:
|
||||
factor: float # kgCO2e/kg for metals, kgCO2e/m3 for wood
|
||||
total_carbon: float # kgCO2e
|
||||
category: str
|
||||
category: str
|
||||
|
||||
# Additional fields for detailed output
|
||||
quantity: float = None # volume (m³) for concrete/wood, mass (kg) for metal
|
||||
database: str = None # database source
|
||||
|
||||
# Concrete-specific fields
|
||||
concrete_volume: float = None # m³
|
||||
concrete_carbon: float = None # kgCO2e
|
||||
reinforcement_mass: float = None # kg
|
||||
reinforcement_rate: float = None # kg/m³
|
||||
reinforcement_factor: float = None # kgCO2e/kg
|
||||
reinforcement_carbon: float = None # kgCO2e
|
||||
@@ -121,6 +121,8 @@ class CarbonCalculator:
|
||||
factor=factor.value,
|
||||
total_carbon=material.mass * factor.value,
|
||||
category="Metal",
|
||||
quantity=material.mass,
|
||||
database=self._steel_database,
|
||||
)
|
||||
|
||||
def _calculate_wood_carbon(self, material: Material) -> CarbonResult:
|
||||
@@ -148,6 +150,8 @@ class CarbonCalculator:
|
||||
factor=factor.value,
|
||||
total_carbon=material.properties.volume * factor.value,
|
||||
category="Wood",
|
||||
quantity=material.properties.volume,
|
||||
database=self._timber_database,
|
||||
)
|
||||
|
||||
def _calculate_concrete_carbon(
|
||||
@@ -220,19 +224,20 @@ class CarbonCalculator:
|
||||
total_carbon = concrete_carbon + reinforcement_carbon
|
||||
|
||||
# Create result with additional metadata
|
||||
result = CarbonResult(
|
||||
return CarbonResult(
|
||||
factor=concrete_factor.value,
|
||||
total_carbon=total_carbon,
|
||||
category="Concrete",
|
||||
quantity=concrete_volume,
|
||||
database=self._concrete_database,
|
||||
concrete_volume=concrete_volume,
|
||||
concrete_carbon=concrete_carbon,
|
||||
reinforcement_mass=reinforcement_mass,
|
||||
reinforcement_rate=reinforcement_rate,
|
||||
reinforcement_factor=rebar_factor.value,
|
||||
reinforcement_carbon=reinforcement_carbon,
|
||||
)
|
||||
|
||||
# TODO: Add extra metadata (not in original CarbonResult class, would need extension)
|
||||
# TODO: result.concrete_carbon = concrete_carbon
|
||||
# TODO: result.reinforcement_carbon = reinforcement_carbon
|
||||
# TODO: result.reinforcement_rate = reinforcement_rate
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _map_element_category_to_concrete_type(
|
||||
element_category: ElementCategory,
|
||||
|
||||
Reference in New Issue
Block a user