Merge pull request #5 from bjoernsteinhagen/bjorn/web-2672-implement-tiered-material-to-ec-factor-mapping-with-fallback

feat: mutate object with enough info for ecf lookup
This commit is contained in:
Chuck Driesler
2025-02-13 18:39:18 +00:00
committed by GitHub
18 changed files with 529 additions and 224 deletions
+36 -20
View File
@@ -5,13 +5,13 @@ from speckle_automate import (
execute_automate_function,
)
from src.applications.revit.revit_material import RevitMaterial
from src.applications.revit.revit_material_processor import RevitMaterialProcessor
from src.applications.revit.revit_compliance import RevitCompliance
from src.applications.revit.revit_model import RevitModel
from src.applications.revit.revit_source_validator import RevitSourceValidator
from src.carbon.aggregator import MassAggregator
from src.applications.revit.revit_logger import RevitLogger
from src.core.base import Model, Logger
from src.core.base import Model
# TODO: Function inputs
@@ -66,26 +66,40 @@ def automate_function(
processor.process_elements(model_root)
# Logger information - successes
logger_successes, logger_warnings = processor.get_processing_results()
if logger_successes:
(
logger_successes,
logger_infos,
logger_warnings,
logger_failures,
) = processor.get_processing_results()
for category, object_ids in logger_successes.items():
automate_context.attach_success_to_objects(
category="Successfully Processed",
object_ids=logger_successes,
message="Carbon calculations completed successfully for this element.",
category=category,
object_ids=object_ids,
message="Carbon calculations completed successfully for these elements.",
)
# Logger information - warnings
if logger_warnings:
for missing_property, elements in logger_warnings.items():
automate_context.attach_warning_to_objects(
category="Missing Required Revit Properties",
object_ids=elements,
message=(
f"Property '{missing_property}' is missing, which prevents carbon "
f"calculations. If this element is critical to your analysis, please "
f"update its Revit properties."
),
)
for category, object_ids in logger_infos.items():
automate_context.attach_info_to_objects(
category=category,
object_ids=object_ids,
message="Elements deemed not applicable and skipped.",
)
for category, object_ids in logger_warnings.items():
automate_context.attach_warning_to_objects(
category=category,
object_ids=object_ids,
message="Elements requiring careful review.",
)
for category, object_ids in logger_failures.items():
automate_context.attach_error_to_objects(
category=category,
object_ids=object_ids,
message="Failure processing the following elements.",
)
# TODO: Create new version
# automate_context.create_new_version_in_project(model_root, "dev", "")
@@ -116,7 +130,9 @@ def configure_components() -> Model:
# TODO: results_aggregator = ResultAggregator and get rid of mass_aggregator
# Create processors
material_processor = RevitMaterial(mass_aggregator) # Material handler to "inject"
material_processor = RevitMaterialProcessor(
mass_aggregator, logger
) # Material handler to "inject"
compliance_checker = RevitCompliance(logger) # Compliance checker to "inject"
# Create and return the main processor with dependencies "injected"
+38 -50
View File
@@ -1,4 +1,4 @@
from typing import Any, List
from typing import Any
from src.core.base import Compliance, Logger
from src.applications.revit.utils.constants import (
@@ -9,9 +9,10 @@ from src.applications.revit.utils.constants import (
CIRCLE,
PROPERTIES,
MATERIAL_QUANTITIES,
STRUCTURAL_ASSET,
VOLUME,
DENSITY,
MATERIAL_CATEGORY,
MATERIAL_CLASS,
MATERIAL_NAME,
)
@@ -23,81 +24,68 @@ class RevitCompliance(Compliance):
def __init__(self, logger: Logger):
self._logger = logger
def check_compliance(
self, element: Any, required_properties: List[str]
) -> Compliance.ValidationResult:
def check_compliance(self, element: Any) -> bool:
"""
Validates element and returns validation result with material data if valid.
Args:
element: Element to validate
required_properties: List of required properties (unused but kept for interface)
Returns:
ValidationResult containing validation status and material data if valid
"""
validation = self._validate_element(element)
if not validation.is_valid:
self._logger.log_warning(
validation.error_message,
object_id=getattr(element, ID, "unknown"),
missing_property=validation.error_property,
)
# Check ID
object_id = getattr(element, ID, None)
if not object_id:
raise ValueError("Should have an id.")
return validation
def _validate_element(self, element: Any) -> Compliance.ValidationResult:
"""Internal validation logic for a single element.
Args:
element: Element to validate
Returns:
ValidationResult with validation status and error details or material data
"""
# Skip geometry elements
speckle_type = getattr(element, SPECKLE_TYPE, None)
if speckle_type in [LINE, ARC, CIRCLE]:
return self.ValidationResult(
is_valid=False, error_message="Geometry element - skipping"
)
# Check ID
element_id = getattr(element, ID, None)
if not element_id:
return self.ValidationResult(
is_valid=False, error_property=ID, error_message="Missing element ID"
self._logger.log_info(
object_id, "Skipped Geometry", "Skipped based on 'speckle_type'."
)
return False
# Check Properties
properties = getattr(element, PROPERTIES, None)
if not properties:
return self.ValidationResult(
is_valid=False,
error_property=PROPERTIES,
error_message="Missing Properties",
self._logger.log_error(
object_id,
"Missing 'properties'",
"Valid object without a 'properties' " "attribute shouldn't happen.",
)
return False
# Check Material Quantities
material_quantities = properties.get(MATERIAL_QUANTITIES, None)
if not material_quantities:
return self.ValidationResult(
is_valid=False,
error_property=MATERIAL_QUANTITIES,
error_message="Missing Material Quantities",
self._logger.log_warning(
object_id,
"Missing 'Material Quantities'",
"Absense of 'Material Quantities' " "indicates a non model-object.",
)
return False
# Validate material properties
# After discussions 11.02.2025, we're being forgiving on missing "Physical" (aka
# StructuralAsset)
for material_name, material_data in material_quantities.items():
for required_prop in [VOLUME, STRUCTURAL_ASSET, DENSITY]:
for required_prop in [
VOLUME,
MATERIAL_CATEGORY,
MATERIAL_CLASS,
MATERIAL_NAME,
]:
if required_prop not in material_data:
return self.ValidationResult(
is_valid=False,
error_property=required_prop,
error_message=f"Missing {required_prop}",
self._logger.log_error(
object_id,
f"Missing {required_prop}.",
"Indicates changes to the Revit "
"connector. Inspect commit and "
"update accordingly.",
)
return False
return self.ValidationResult(
is_valid=True, material_quantities=material_quantities
)
return True
+67 -30
View File
@@ -1,48 +1,85 @@
import structlog
from typing import Dict, DefaultDict
from typing import Dict, Set, Optional
from collections import defaultdict
from src.core.base.logger import Logger # Import the interface
# NOTE: Only provide docstring if not covered by base class
class RevitLogger(Logger):
"""Implements Logger interface"""
"""Implements Logger interface with category-based logging"""
def __init__(self):
self.missing_properties: DefaultDict[str, set] = defaultdict(set)
self.successful_elements: set = set()
self._structlog = structlog.get_logger()
self._errors: Dict[str, Set[str]] = defaultdict(set)
self._warnings: Dict[str, Set[str]] = defaultdict(set)
self._successes: Dict[str, Set[str]] = defaultdict(set)
self._info: Dict[str, Set[str]] = defaultdict(set)
def log_error(self, message: str, **kwargs) -> None:
self._structlog.error(message, **kwargs)
def log_error(
self, object_id: str, category: str, message: Optional[str] = None
) -> None:
"""Log an error for a specific object under a category"""
self._errors[category].add(object_id)
if message:
self._structlog.error(message, object_id=object_id, category=category)
else:
self._structlog.error(
"Error logged", object_id=object_id, category=category
)
def log_warning(self, message: str, **kwargs) -> None:
"""Log a warning message.
Categorises and caches missing properties if 'missing_property' and 'object_id' specified in the kwargs.
def log_warning(
self, object_id: str, category: str, message: Optional[str] = None
) -> None:
"""Log a warning for a specific object under a category"""
self._warnings[category].add(object_id)
if message:
self._structlog.warning(message, object_id=object_id, category=category)
else:
self._structlog.warning(
"Warning logged", object_id=object_id, category=category
)
Args:
message: Warning message to log
**kwargs: Additional context. If 'object_id' and 'missing_property' are
provided, they will be tracked for compliance reporting.
"""
self._structlog.warn(message, **kwargs)
def log_success(
self, object_id: str, category: str, message: Optional[str] = None
) -> None:
"""Log a success for a specific object under a category"""
self._successes[category].add(object_id)
if message:
self._structlog.info(message, object_id=object_id, category=category)
else:
self._structlog.info(
"Success logged", object_id=object_id, category=category
)
# Track missing properties if relevant kwargs are provided
object_id = kwargs.get("object_id")
missing_property = kwargs.get("missing_property")
if object_id and missing_property:
self.missing_properties[missing_property].add(object_id)
def log_info(
self, object_id: str, category: str, message: Optional[str] = None
) -> None:
"""Log information for a specific object under a category"""
self._info[category].add(object_id)
if message:
self._structlog.info(message, object_id=object_id, category=category)
else:
self._structlog.info(
"Information logged", object_id=object_id, category=category
)
def log_success(self, object_id: str, **kwargs) -> None:
self._structlog.info(f"Successfully processed element", object_id=object_id)
self.successful_elements.add(object_id)
@staticmethod
def _convert_sets_to_lists(data: Dict[str, Set[str]]) -> Dict[str, list]:
"""Convert set values to lists in dictionary"""
return {category: list(objects) for category, objects in data.items()}
def get_warnings_summary(self) -> Dict[str, list]:
return {
prop: list(elements) for prop, elements in self.missing_properties.items()
}
"""Get all warnings grouped by category"""
return self._convert_sets_to_lists(self._warnings)
def get_successful_summary(self) -> list:
return list(self.successful_elements)
def get_errors_summary(self) -> Dict[str, list]:
"""Get all errors grouped by category"""
return self._convert_sets_to_lists(self._errors)
def get_success_summary(self) -> Dict[str, list]:
"""Get all successes grouped by category"""
return self._convert_sets_to_lists(self._successes)
def get_info_summary(self) -> Dict[str, list]:
"""Get all info logs grouped by category"""
return self._convert_sets_to_lists(self._info)
-35
View File
@@ -1,35 +0,0 @@
from typing import Dict, Any
from src.core.base import Material
from src.core.types.material import (
MaterialData,
)
from src.applications.revit.utils.constants import (
VOLUME,
VALUE,
DENSITY,
STRUCTURAL_ASSET,
)
# NOTE: Only provide docstring if not covered by base class
class RevitMaterial(Material):
"""Implementation of the MaterialProcessor for the Revit context."""
def __init__(self, mass_aggregator: "MassAggregator"):
self._mass_aggregator = mass_aggregator
def process_material(
self, material_data: Dict[str, Any], level: str, type_name: str
) -> MaterialData:
volume = material_data[VOLUME][VALUE]
density = material_data[DENSITY][VALUE]
mass = volume * density
structural_asset = material_data[STRUCTURAL_ASSET]
mat_data = MaterialData(
volume=volume, density=density, structural_asset=structural_asset, mass=mass
)
self._mass_aggregator.add_mass(mass, level, type_name, structural_asset)
return mat_data
@@ -0,0 +1,43 @@
from typing import Dict, Any
from src.core.base.logger import Logger
from src.applications.revit.utils.material_quality_strategy import (
HighQualityStrategy,
LowQualityStrategy,
)
from src.core.base import MaterialProcessor
from src.core.types.material_data import (
MaterialData,
)
from src.applications.revit.utils.constants import (
VOLUME,
VALUE,
STRUCTURAL_ASSET,
)
class RevitMaterialProcessor(MaterialProcessor):
"""Implementation of the MaterialProcessor for the Revit context."""
def __init__(self, mass_aggregator: "MassAggregator", logger: Logger):
self._mass_aggregator = mass_aggregator
self._logger = logger
self._high_quality_strategy = HighQualityStrategy()
self._low_quality_strategy = LowQualityStrategy()
def process(
self, object_id: str, material_data: Dict[str, Any], level: str, type_name: str
) -> MaterialData:
# Volume has already been checked for
volume = material_data[VOLUME][VALUE]
try:
if STRUCTURAL_ASSET in material_data:
return self._high_quality_strategy.process(
object_id, material_data, volume, self._logger
)
else:
return self._low_quality_strategy.process(
object_id, material_data, volume, self._logger
)
except Exception as e:
raise ValueError(str(e))
+70 -18
View File
@@ -1,12 +1,14 @@
from typing import Any
from typing import Any, Tuple, Dict, List
from src.core.base import Model
from src.core.base import Material
from src.core.base import MaterialProcessor
from src.core.base import Compliance
from src.core.base.logger import Logger
from src.applications.revit.utils.constants import (
ELEMENTS,
NAME,
ID,
MATERIAL_QUANTITIES,
PROPERTIES,
)
# NOTE: Only provide docstring if not covered by base class
@@ -17,7 +19,7 @@ class RevitModel(Model):
def __init__(
self,
material_processor: Material,
material_processor: MaterialProcessor,
compliance_checker: Compliance,
logger: Logger,
):
@@ -67,32 +69,62 @@ class RevitModel(Model):
model_object (Any): speckle object containing properties for processing
"""
# First check compliance - this also handles logging any validation warnings
validation = self._compliance_checker.check_compliance(model_object, [])
if not validation.is_valid:
valid = self._compliance_checker.check_compliance(model_object)
if not valid:
return
# These are now safe to do after compliance checking
material_quantities = model_object[PROPERTIES][MATERIAL_QUANTITIES]
object_id = getattr(model_object, ID)
try:
# Process each material if element passed validation
# TODO: Project 2427 is an interesting one with compound materials
# TODO: Checkout object fefcc95c2f0ecd28a49ecdd7764e2d79. Worth skipping if volume = 0?
for material_name, material_data in validation.material_quantities.items():
self._material_processor.process_material(
material_data, level, type_name
for material_name, material_data in material_quantities.items():
processed_material = self._material_processor.process(
object_id, material_data, level, type_name
)
# Log success only after all material processing complete
self._logger.log_success(getattr(model_object, ID))
if processed_material: # If processing was successful
self._logger.log_success(
object_id=object_id,
category="Successfully Processed",
message="Carbon calculations completed successfully for this element.",
)
model_object[PROPERTIES]["Embodied Carbon Data"] = vars(
processed_material
)
if getattr(processed_material, "type") == "Concrete":
model_object[PROPERTIES]["Embodied Carbon Data"][
"element"
] = self._categorize(type_name)
except Exception as e:
# Log any processing errors that occur
self._logger.log_error(
f"Failed to process element {getattr(model_object, ID)}", error=str(e)
)
self._logger.log_error(object_id, "Material Processing Error", str(e))
# TODO: This is gross.
def get_processing_results(self) -> tuple[list, dict]:
def get_processing_results(
self,
) -> Tuple[
Dict[str, List[str]],
Dict[str, List[str]],
Dict[str, List[str]],
Dict[str, List[str]],
]:
"""Get processing results in the format expected by main.py.
Returns:
Tuple containing:
- Dict mapping success categories to lists of successfully processed object IDs
- Dict mapping information categories to lists of affected object IDs
- Dict mapping warning categories to lists of affected object IDs
- Dict mapping error categories to lists of affected object IDs
"""
return (
self._logger.get_successful_summary(),
self._logger.get_success_summary(),
self._logger.get_info_summary(),
self._logger.get_warnings_summary(),
self._logger.get_errors_summary(),
)
@staticmethod
@@ -118,3 +150,23 @@ class RevitModel(Model):
def _get_name(node: Any) -> str:
"""Safely get name from node, with fallback"""
return getattr(node, NAME, "Unknown")
@staticmethod
def _categorize(type_name: str) -> str:
searchable_string = type_name.lower()
if (
"floor" in searchable_string
or "stair" in searchable_string
or "slab edges" in searchable_string
):
return "Slabs"
elif "wall" in searchable_string:
return "Walls"
elif "column" in searchable_string:
return "Columns"
elif "framing" in searchable_string or "beam" in searchable_string:
return "Beam"
elif "foundation" in searchable_string:
return "Foundations"
else:
raise ValueError(f"{type_name} not accounted for.")
@@ -6,12 +6,17 @@ REQUIRED_PROPERTIES = ["volume", "density", "structuralAsset"]
APPLICATION_ID = "applicationId"
ARC = "Objects.Geometry.Arc"
CIRCLE = "Objects.Geometry.Circle"
COMPRESSIVE_STRENGTH = "compressiveStrength"
DENSITY = "density"
ELEMENTS = "elements"
ID = "id"
LINE = "Objects.Geometry.Line"
MASS = "mass"
MATERIAL_CATEGORY = "materialCategory"
MATERIAL_CLASS = "materialClass"
MATERIAL_NAME = "materialName"
MATERIAL_QUANTITIES = "Material Quantities"
MATERIAL_TYPE = "materialType"
NAME = "name"
PROPERTIES = "properties"
SOURCE_APPLICATION = "sourceApplication"
@@ -0,0 +1,113 @@
from typing import Dict, Any, Protocol
from src.core.base import Logger
from src.core.types.material_data import MaterialData
from src.applications.revit.utils.material_type_handler import (
MaterialType,
ConcreteHandler,
MetalHandler,
WoodHandler,
)
from src.applications.revit.utils.constants import (
MATERIAL_TYPE,
MATERIAL_NAME,
)
class MaterialQualityStrategy(Protocol):
"""Protocol defining how to process materials of different quality levels"""
def process(
self,
object_id: str,
material_data: Dict[str, Any],
volume: float,
logger: Logger,
) -> MaterialData:
...
class HighQualityStrategy(MaterialQualityStrategy):
"""Strategy for processing high-quality materials (with structural asset)"""
def __init__(self):
self._handlers = {
MaterialType.CONCRETE.value: ConcreteHandler(),
MaterialType.METAL.value: MetalHandler(),
MaterialType.WOOD.value: WoodHandler(),
}
def process(
self,
object_id: str,
material_data: Dict[str, Any],
volume: float,
logger: Logger,
) -> MaterialData:
if MATERIAL_TYPE not in material_data:
raise ValueError("Missing material type") # Rather safe than sorry
material_type = material_data[MATERIAL_TYPE]
handler = self._handlers.get(material_type)
if not handler:
raise ValueError(f"Unsupported material type: {material_type}")
try:
result = handler.create_material_data(material_data, volume)
logger.log_success(
object_id,
"High-Quality Material Definitions",
"Contains all expected attributes.",
)
return result
except Exception as e:
raise Exception(f"Failed to process material: {str(e)}")
class LowQualityStrategy(MaterialQualityStrategy):
"""Strategy for processing low-quality materials (without structural asset)"""
DEFAULT_CONCRETE_GRADE = "35"
DEFAULT_STEEL_DENSITY = 7851.81483993
def process(
self,
object_id: str,
material_data: Dict[str, Any],
volume: float,
logger: Logger,
) -> MaterialData:
material_name = material_data[MATERIAL_NAME].lower()
if "clt" in material_name:
logger.log_warning(
object_id,
"Low-Quality Wood Material Definitions",
"Wood has no structural asset and found base on string search",
)
return MaterialData(MaterialType.WOOD.value, volume)
elif "concrete" in material_name:
logger.log_warning(
object_id,
"Low-Quality Concrete Material Definitions",
"Concrete has no structural asset and found based on string search",
)
return MaterialData(
MaterialType.CONCRETE.value, volume, grade=self.DEFAULT_CONCRETE_GRADE
)
elif "steel" in material_name:
logger.log_warning(
object_id,
"Low-Quality Steel Material Definitions",
"Steel has no structural asset and found based on string search",
)
return MaterialData(
MaterialType.METAL.value,
volume,
density=self.DEFAULT_STEEL_DENSITY,
mass=volume * self.DEFAULT_STEEL_DENSITY,
)
else:
raise ValueError(
f"Unable to determine material type from name: {material_name}"
)
@@ -0,0 +1,87 @@
from typing import Dict, Any
from enum import Enum
from abc import ABC, abstractmethod
from src.core.types.material_data import MaterialData
from src.applications.revit.utils.constants import (
VALUE,
DENSITY,
STRUCTURAL_ASSET,
UNITS,
MATERIAL_NAME,
COMPRESSIVE_STRENGTH,
)
class MaterialType(Enum):
CONCRETE = "Concrete"
METAL = "Metal"
WOOD = "Wood"
class MaterialTypeHandler(ABC):
"""Abstract base class for handling different material types"""
@abstractmethod
def create_material_data(
self, material_data: Dict[str, Any], volume: float
) -> MaterialData:
pass
class ConcreteHandler(MaterialTypeHandler):
def create_material_data(
self, material_data: Dict[str, Any], volume: float
) -> MaterialData:
if DENSITY not in material_data:
raise ValueError("Missing density in concrete")
density_dict = material_data[DENSITY]
if COMPRESSIVE_STRENGTH not in material_data:
raise ValueError("Missing compression strength in concrete")
compressive_strength_dict = material_data[COMPRESSIVE_STRENGTH]
density = density_dict[VALUE]
comp_strength_units = compressive_strength_dict[UNITS]
comp_strength_value = compressive_strength_dict[VALUE]
if comp_strength_units != "Kilonewtons per square meter":
raise ValueError(f"Unsupported units: {comp_strength_units}")
grade = comp_strength_value * 0.001 # Convert to MPa
return MaterialData(
type=MaterialType.CONCRETE.value,
volume=volume,
structural_asset=material_data[STRUCTURAL_ASSET],
density=density,
mass=volume * density,
grade=str(grade),
)
class MetalHandler(MaterialTypeHandler):
def create_material_data(
self, material_data: Dict[str, Any], volume: float
) -> MaterialData:
if "steel" not in material_data[MATERIAL_NAME].lower():
raise ValueError(
f"Material name '{material_data[MATERIAL_NAME]}' does not contain 'steel'"
)
density = material_data[DENSITY][VALUE]
return MaterialData(
type=MaterialType.METAL.value,
volume=volume,
structural_asset=material_data[STRUCTURAL_ASSET],
density=density,
mass=density * volume,
grade=material_data[STRUCTURAL_ASSET],
)
class WoodHandler(MaterialTypeHandler):
def create_material_data(
self, material_data: Dict[str, Any], volume: float
) -> MaterialData:
return MaterialData(type=MaterialType.WOOD.value, volume=volume)
+2 -2
View File
@@ -1,13 +1,13 @@
from .logger import Logger
from .model import Model
from .source_validator import SourceApplicationValidator
from .material import Material
from .material_processor import MaterialProcessor
from .compliance import Compliance
__all__ = [
"Logger",
"Model",
"SourceApplicationValidator",
"Material",
"MaterialProcessor",
"Compliance",
]
+2 -15
View File
@@ -1,7 +1,5 @@
from abc import ABC, abstractmethod
from typing import Any, List
from dataclasses import dataclass
from typing import Optional
from typing import Any
class Compliance(ABC):
@@ -9,18 +7,7 @@ class Compliance(ABC):
Compliance are intended to be called for every object where an attribute is assumed.
"""
@dataclass
class ValidationResult:
"""Results of element validation including material data if valid"""
is_valid: bool
material_quantities: Optional[dict] = None
error_property: Optional[str] = None
error_message: Optional[str] = None
@abstractmethod
def check_compliance(
self, element: Any, required_properties: List[str]
) -> ValidationResult:
def check_compliance(self, element: Any) -> bool:
"""Check if element contains attribute(s)"""
pass
+35 -33
View File
@@ -1,52 +1,54 @@
from typing import Dict, Optional
from abc import ABC, abstractmethod
from typing import Dict, List
# TODO: Implementations use **kwargs which is silly. Formalize.
class Logger(ABC):
"""Interface for logging."""
"""Abstract base class for logging functionality"""
@abstractmethod
def log_error(self, message: str, **kwargs) -> None:
"""Log an error.
Args:
message (str): error message
"""
def log_error(
self, object_id: str, category: str, message: Optional[str] = None
) -> None:
"""Log an error for a specific object"""
pass
@abstractmethod
def log_warning(self, message: str, **kwargs) -> None:
"""Log a warning.
Args:
message (str): warning message
"""
def log_warning(
self, object_id: str, category: str, message: Optional[str] = None
) -> None:
"""Log a warning for a specific object"""
pass
@abstractmethod
def log_success(self, object_id: str, **kwargs) -> None:
"""Log a success.
Args:
object_id (str): success message
"""
def log_success(
self, object_id: str, category: str, message: Optional[str] = None
) -> None:
"""Log a success for a specific object"""
pass
@abstractmethod
def get_warnings_summary(self) -> Dict:
"""Returns a dictionary of warning messages. The dictionary groups the warning types.
Returns:
Dict: {warning_type : [object_ids], ...}
"""
def log_info(
self, object_id: str, category: str, message: Optional[str] = None
) -> None:
"""Log information for a specific object"""
pass
@abstractmethod
def get_successful_summary(self) -> List:
"""Returns a list of all successfully processed elements.
Returns:
List: [object_ids]
"""
def get_warnings_summary(self) -> Dict[str, list]:
"""Get summary of all warnings grouped by category"""
pass
@abstractmethod
def get_errors_summary(self) -> Dict[str, list]:
"""Get summary of all errors grouped by category"""
pass
@abstractmethod
def get_success_summary(self) -> Dict[str, list]:
"""Get summary of all successes grouped by category"""
pass
@abstractmethod
def get_info_summary(self) -> Dict[str, list]:
"""Get summary of all info logs grouped by category"""
pass
@@ -2,16 +2,17 @@ from abc import ABC, abstractmethod
from typing import Dict, Any
class Material(ABC):
class MaterialProcessor(ABC):
"""Interface for processing a material."""
@abstractmethod
def process_material(
self, material_data: Dict[str, Any], level: str, type_name: str
def process(
self, object_id: str, material_data: Dict[str, Any], level: str, type_name: str
) -> None:
"""Process material data and compute required properties
Args:
object_id (str): Object ID
material_data (Dict[str, Any]): material data
level (str): associated level of object
type_name (str): object type
+9 -2
View File
@@ -1,5 +1,5 @@
from abc import ABC, abstractmethod
from typing import Any
from typing import Any, Dict, Tuple, List
class Model(ABC):
@@ -28,6 +28,13 @@ class Model(ABC):
# TODO: This is gross
@abstractmethod
def get_processing_results(self) -> tuple[list, dict]:
def get_processing_results(
self,
) -> Tuple[
Dict[str, List[str]],
Dict[str, List[str]],
Dict[str, List[str]],
Dict[str, List[str]],
]:
"""Expose logging results."""
pass
+1 -1
View File
@@ -1,3 +1,3 @@
from .material import MaterialData
from .material_data import MaterialData
__all__ = ["MaterialData"]
-12
View File
@@ -1,12 +0,0 @@
from dataclasses import dataclass
from typing import Optional
@dataclass
class MaterialData:
"""Data class for material properties"""
volume: float
density: float
structural_asset: str
mass: Optional[float] = None
+14
View File
@@ -0,0 +1,14 @@
from dataclasses import dataclass
from typing import Optional
@dataclass
class MaterialData:
"""Data class for material properties"""
type: str # Concrete / Steel / Wood
volume: float
structural_asset: Optional[str] = None # Not ideal, but we're being forgiving
density: Optional[float] = None # Only needed for steel
mass: Optional[float] = None # Only needed for steel
grade: Optional[str] = None # Needed for concrete
+3 -3
View File
@@ -1,6 +1,6 @@
# pytest: skip-file
from src.applications.revit.revit_material import RevitMaterial
from src.applications.revit.revit_material_processor import RevitMaterialProcessor
from src.applications.revit.revit_compliance import RevitCompliance
from src.applications.revit.revit_model import RevitModel
from src.applications.revit.revit_source_validator import RevitSourceValidator
@@ -16,7 +16,7 @@ from specklepy.transports.server import ServerTransport
HOST = "https://app.speckle.systems/"
AUTHENTICATION_TOKEN = "840e5a18cda38ccc2a9ed8b52e9316530505c14181"
STREAM_ID = "99bdf924fb"
BRANCH_NAME = "2391"
BRANCH_NAME = "2843"
# Setting up SpeckleClient and authenticating
client = SpeckleClient(host=HOST)
@@ -42,7 +42,7 @@ def create_processor_chain() -> tuple[RevitModel, RevitLogger]:
mass_aggregator = MassAggregator()
# Create processors
material_processor = RevitMaterial(mass_aggregator)
material_processor = RevitMaterialProcessor(mass_aggregator, logger)
compliance_checker = RevitCompliance(logger)
# Create and return the main processor with logger