APM

>Agent Skill

@datadrivenconstruction/bim-validation-report

skilldevelopment

Generate comprehensive BIM model validation reports. Check data quality, completeness, and compliance with standards.

apm::install
$apm install @datadrivenconstruction/bim-validation-report
apm::skill.md
---
name: "bim-validation-report"
description: "Generate comprehensive BIM model validation reports. Check data quality, completeness, and compliance with standards."
homepage: "https://datadrivenconstruction.io"
metadata: {"openclaw": {"emoji": "🔍", "os": ["darwin", "linux", "win32"], "homepage": "https://datadrivenconstruction.io", "requires": {"bins": ["python3"]}}}
---
# BIM Validation Report Generator

## Business Case

### Problem Statement
BIM models often have quality issues:
- Missing required properties
- Invalid or inconsistent data
- Non-compliant with project standards
- Incomplete model information

### Solution
Automated BIM validation system that checks models against configurable rules and generates detailed compliance reports.

### Business Value
- **Quality assurance** - Catch issues early
- **Standards compliance** - Meet project requirements
- **Automation** - Reduce manual QC effort
- **Transparency** - Clear validation results

## Technical Implementation

```python
import pandas as pd
from datetime import datetime
from typing import Dict, Any, List, Optional, Callable
from dataclasses import dataclass, field
from enum import Enum


class ValidationSeverity(Enum):
    """Validation issue severity."""
    ERROR = "error"
    WARNING = "warning"
    INFO = "info"


class ValidationStatus(Enum):
    """Overall validation status."""
    PASSED = "passed"
    PASSED_WITH_WARNINGS = "passed_with_warnings"
    FAILED = "failed"


class RuleCategory(Enum):
    """Validation rule categories."""
    REQUIRED_PROPERTIES = "required_properties"
    DATA_FORMAT = "data_format"
    NAMING_CONVENTION = "naming_convention"
    GEOMETRIC = "geometric"
    CLASSIFICATION = "classification"
    RELATIONSHIPS = "relationships"


@dataclass
class ValidationRule:
    """Single validation rule."""
    rule_id: str
    name: str
    category: RuleCategory
    description: str
    severity: ValidationSeverity
    check_function: Callable
    applicable_categories: List[str] = field(default_factory=list)
    enabled: bool = True


@dataclass
class ValidationIssue:
    """Single validation issue."""
    issue_id: str
    rule_id: str
    rule_name: str
    element_id: str
    element_name: str
    element_category: str
    severity: ValidationSeverity
    message: str
    details: Dict[str, Any] = field(default_factory=dict)

    def to_dict(self) -> Dict[str, Any]:
        return {
            'issue_id': self.issue_id,
            'rule_id': self.rule_id,
            'rule_name': self.rule_name,
            'element_id': self.element_id,
            'element_name': self.element_name,
            'element_category': self.element_category,
            'severity': self.severity.value,
            'message': self.message
        }


@dataclass
class ValidationReport:
    """Complete validation report."""
    project_name: str
    model_name: str
    validated_at: datetime
    status: ValidationStatus
    total_elements: int
    elements_with_issues: int
    issues: List[ValidationIssue]
    rules_checked: int
    summary_by_severity: Dict[str, int]
    summary_by_category: Dict[str, int]


class BIMValidationEngine:
    """BIM model validation engine."""

    def __init__(self, project_name: str, model_name: str):
        self.project_name = project_name
        self.model_name = model_name
        self.rules: List[ValidationRule] = []
        self.issues: List[ValidationIssue] = []
        self._issue_counter = 0

        # Load default rules
        self._load_default_rules()

    def _load_default_rules(self):
        """Load standard validation rules."""

        # Required properties rules
        self.add_rule(ValidationRule(
            rule_id="REQ-001",
            name="Element Name Required",
            category=RuleCategory.REQUIRED_PROPERTIES,
            description="All elements must have a name",
            severity=ValidationSeverity.ERROR,
            check_function=lambda e: bool(e.get('name'))
        ))

        self.add_rule(ValidationRule(
            rule_id="REQ-002",
            name="Level Assignment Required",
            category=RuleCategory.REQUIRED_PROPERTIES,
            description="Elements must be assigned to a level",
            severity=ValidationSeverity.WARNING,
            check_function=lambda e: bool(e.get('level')),
            applicable_categories=["Walls", "Floors", "Doors", "Windows"]
        ))

        self.add_rule(ValidationRule(
            rule_id="REQ-003",
            name="Material Required",
            category=RuleCategory.REQUIRED_PROPERTIES,
            description="Structural elements must have material defined",
            severity=ValidationSeverity.ERROR,
            check_function=lambda e: bool(e.get('material')),
            applicable_categories=["Structural Columns", "Structural Framing", "Floors"]
        ))

        # Naming convention rules
        self.add_rule(ValidationRule(
            rule_id="NAM-001",
            name="No Special Characters",
            category=RuleCategory.NAMING_CONVENTION,
            description="Names should not contain special characters",
            severity=ValidationSeverity.WARNING,
            check_function=self._check_no_special_chars
        ))

        self.add_rule(ValidationRule(
            rule_id="NAM-002",
            name="Name Length Check",
            category=RuleCategory.NAMING_CONVENTION,
            description="Names should be between 3 and 100 characters",
            severity=ValidationSeverity.INFO,
            check_function=lambda e: 3 <= len(e.get('name', '')) <= 100
        ))

        # Classification rules
        self.add_rule(ValidationRule(
            rule_id="CLS-001",
            name="Classification Code Present",
            category=RuleCategory.CLASSIFICATION,
            description="Elements should have classification code",
            severity=ValidationSeverity.WARNING,
            check_function=lambda e: bool(e.get('classification_code') or e.get('uniformat'))
        ))

        # Geometric rules
        self.add_rule(ValidationRule(
            rule_id="GEO-001",
            name="Non-Zero Volume",
            category=RuleCategory.GEOMETRIC,
            description="3D elements must have non-zero volume",
            severity=ValidationSeverity.ERROR,
            check_function=lambda e: float(e.get('volume', 0)) > 0,
            applicable_categories=["Walls", "Floors", "Structural Columns", "Structural Framing"]
        ))

        self.add_rule(ValidationRule(
            rule_id="GEO-002",
            name="Valid Bounding Box",
            category=RuleCategory.GEOMETRIC,
            description="Elements must have valid bounding box",
            severity=ValidationSeverity.ERROR,
            check_function=self._check_valid_bbox
        ))

    def _check_no_special_chars(self, element: Dict[str, Any]) -> bool:
        """Check name for special characters."""
        import re
        name = element.get('name', '')
        return bool(re.match(r'^[\w\s\-\.]+$', name))

    def _check_valid_bbox(self, element: Dict[str, Any]) -> bool:
        """Check for valid bounding box."""
        try:
            min_x = float(element.get('min_x', 0))
            max_x = float(element.get('max_x', 0))
            min_y = float(element.get('min_y', 0))
            max_y = float(element.get('max_y', 0))
            min_z = float(element.get('min_z', 0))
            max_z = float(element.get('max_z', 0))
            return max_x > min_x and max_y > min_y and max_z > min_z
        except (ValueError, TypeError):
            return False

    def add_rule(self, rule: ValidationRule):
        """Add validation rule."""
        self.rules.append(rule)

    def add_custom_rule(self, rule_id: str, name: str, category: RuleCategory,
                       check_function: Callable, severity: ValidationSeverity = ValidationSeverity.WARNING,
                       description: str = "", categories: List[str] = None):
        """Add custom validation rule."""
        rule = ValidationRule(
            rule_id=rule_id,
            name=name,
            category=category,
            description=description,
            severity=severity,
            check_function=check_function,
            applicable_categories=categories or []
        )
        self.add_rule(rule)

    def validate_element(self, element: Dict[str, Any]) -> List[ValidationIssue]:
        """Validate single element against all rules."""
        issues = []
        element_category = element.get('category', '')

        for rule in self.rules:
            if not rule.enabled:
                continue

            # Check if rule applies to this category
            if rule.applicable_categories and element_category not in rule.applicable_categories:
                continue

            try:
                passed = rule.check_function(element)
                if not passed:
                    self._issue_counter += 1
                    issue = ValidationIssue(
                        issue_id=f"ISS-{self._issue_counter:05d}",
                        rule_id=rule.rule_id,
                        rule_name=rule.name,
                        element_id=str(element.get('element_id', '')),
                        element_name=str(element.get('name', '')),
                        element_category=element_category,
                        severity=rule.severity,
                        message=rule.description
                    )
                    issues.append(issue)
            except Exception as e:
                # Rule check failed
                self._issue_counter += 1
                issue = ValidationIssue(
                    issue_id=f"ISS-{self._issue_counter:05d}",
                    rule_id=rule.rule_id,
                    rule_name=rule.name,
                    element_id=str(element.get('element_id', '')),
                    element_name=str(element.get('name', '')),
                    element_category=element_category,
                    severity=ValidationSeverity.ERROR,
                    message=f"Rule check error: {str(e)}"
                )
                issues.append(issue)

        return issues

    def validate_model(self, elements_df: pd.DataFrame) -> ValidationReport:
        """Validate entire BIM model."""
        self.issues = []
        elements_with_issues = set()

        for _, row in elements_df.iterrows():
            element = row.to_dict()
            element_issues = self.validate_element(element)

            if element_issues:
                elements_with_issues.add(element.get('element_id'))
                self.issues.extend(element_issues)

        # Calculate summaries
        summary_by_severity = {
            'error': sum(1 for i in self.issues if i.severity == ValidationSeverity.ERROR),
            'warning': sum(1 for i in self.issues if i.severity == ValidationSeverity.WARNING),
            'info': sum(1 for i in self.issues if i.severity == ValidationSeverity.INFO)
        }

        summary_by_category = {}
        for issue in self.issues:
            cat = issue.element_category
            summary_by_category[cat] = summary_by_category.get(cat, 0) + 1

        # Determine overall status
        if summary_by_severity['error'] > 0:
            status = ValidationStatus.FAILED
        elif summary_by_severity['warning'] > 0:
            status = ValidationStatus.PASSED_WITH_WARNINGS
        else:
            status = ValidationStatus.PASSED

        return ValidationReport(
            project_name=self.project_name,
            model_name=self.model_name,
            validated_at=datetime.now(),
            status=status,
            total_elements=len(elements_df),
            elements_with_issues=len(elements_with_issues),
            issues=self.issues,
            rules_checked=len([r for r in self.rules if r.enabled]),
            summary_by_severity=summary_by_severity,
            summary_by_category=summary_by_category
        )

    def export_report(self, report: ValidationReport, output_path: str):
        """Export validation report to Excel."""
        with pd.ExcelWriter(output_path, engine='openpyxl') as writer:
            # Summary sheet
            summary_data = {
                'Metric': ['Project', 'Model', 'Validated At', 'Status',
                          'Total Elements', 'Elements with Issues', 'Rules Checked',
                          'Errors', 'Warnings', 'Info'],
                'Value': [report.project_name, report.model_name,
                         report.validated_at.isoformat(), report.status.value,
                         report.total_elements, report.elements_with_issues,
                         report.rules_checked, report.summary_by_severity['error'],
                         report.summary_by_severity['warning'], report.summary_by_severity['info']]
            }
            pd.DataFrame(summary_data).to_excel(writer, sheet_name='Summary', index=False)

            # Issues sheet
            issues_df = pd.DataFrame([i.to_dict() for i in report.issues])
            if not issues_df.empty:
                issues_df.to_excel(writer, sheet_name='Issues', index=False)

            # By Category sheet
            cat_df = pd.DataFrame([
                {'Category': k, 'Issue Count': v}
                for k, v in report.summary_by_category.items()
            ])
            if not cat_df.empty:
                cat_df.to_excel(writer, sheet_name='By Category', index=False)

        return output_path


def generate_validation_report(elements_df: pd.DataFrame,
                               project_name: str,
                               model_name: str,
                               output_path: str = None) -> ValidationReport:
    """Quick function to generate validation report."""
    engine = BIMValidationEngine(project_name, model_name)
    report = engine.validate_model(elements_df)

    if output_path:
        engine.export_report(report, output_path)

    return report
```

## Quick Start

```python
# Load BIM elements
elements = pd.read_excel("bim_elements.xlsx")

# Run validation
report = generate_validation_report(
    elements,
    project_name="Office Tower",
    model_name="Architectural Model v3.2",
    output_path="validation_report.xlsx"
)

print(f"Status: {report.status.value}")
print(f"Errors: {report.summary_by_severity['error']}")
print(f"Warnings: {report.summary_by_severity['warning']}")
```

## Common Use Cases

### 1. Custom Validation Rules
```python
engine = BIMValidationEngine("Project", "Model")

# Add custom rule
engine.add_custom_rule(
    rule_id="CUSTOM-001",
    name="Fire Rating Required",
    category=RuleCategory.REQUIRED_PROPERTIES,
    check_function=lambda e: bool(e.get('fire_rating')),
    severity=ValidationSeverity.ERROR,
    categories=["Walls", "Doors"]
)
```

### 2. Filter Issues
```python
# Get only errors
errors = [i for i in report.issues if i.severity == ValidationSeverity.ERROR]

# Get issues for specific category
wall_issues = [i for i in report.issues if i.element_category == "Walls"]
```

### 3. Automated QC Pipeline
```python
report = engine.validate_model(elements)
if report.status == ValidationStatus.FAILED:
    send_notification("BIM validation failed", report.summary_by_severity)
```

## Resources
- **DDC Book**: Chapter 4.3 - BIM Validation
- **Reference**: ISO 19650, buildingSMART IDS