--- name: "ontology-mapper" description: "Map construction data to standard ontologies. Create semantic mappings between different data schemas" homepage: "https://datadrivenconstruction.io" metadata: {"openclaw": {"emoji": "🌐", "os": ["darwin", "linux", "win32"], "homepage": "https://datadrivenconstruction.io", "requires": {"bins": ["python3"]}}} --- # Ontology Mapper ## Overview Based on DDC methodology (Chapter 2.2), this skill maps construction data to standard ontologies like IFC, COBie, Uniclass, and OmniClass, enabling semantic interoperability between systems. **Book Reference:** "Доминирование открытых данных" / "Open Data Dominance" ## Quick Start ```python from dataclasses import dataclass, field from enum import Enum from typing import List, Dict, Optional, Set, Tuple from datetime import datetime import json import re class OntologyType(Enum): """Standard construction ontologies""" IFC = "ifc" # Industry Foundation Classes COBIE = "cobie" # Construction Operations Building Information Exchange UNICLASS = "uniclass" # UK classification OMNICLASS = "omniclass" # North American classification MASTERFORMAT = "masterformat" # CSI MasterFormat UNIFORMAT = "uniformat" # CSI UniFormat CUSTOM = "custom" # Custom ontology class MappingConfidence(Enum): """Confidence level of mapping""" EXACT = "exact" # 100% match HIGH = "high" # 90%+ match MEDIUM = "medium" # 70-90% match LOW = "low" # 50-70% match UNCERTAIN = "uncertain" # <50% match class RelationType(Enum): """Types of relationships between concepts""" EQUIVALENT = "equivalent" # Same concept BROADER = "broader" # Source is more specific NARROWER = "narrower" # Source is more general RELATED = "related" # Related but not equivalent PART_OF = "part_of" # Component relationship HAS_PART = "has_part" # Contains components @dataclass class OntologyConcept: """Concept in an ontology""" id: str name: str ontology: OntologyType definition: Optional[str] = None parent_id: Optional[str] = None synonyms: List[str] = field(default_factory=list) properties: Dict[str, str] = field(default_factory=dict) @dataclass class SemanticMapping: """Mapping between two concepts""" source_concept: str source_ontology: OntologyType target_concept: str target_ontology: OntologyType relation: RelationType confidence: MappingConfidence notes: Optional[str] = None created_by: str = "auto" created_at: datetime = field(default_factory=datetime.now) @dataclass class MappingResult: """Result of ontology mapping operation""" source_field: str source_value: str mappings: List[SemanticMapping] best_match: Optional[SemanticMapping] = None unmapped: bool = False @dataclass class OntologyMappingReport: """Complete mapping report""" total_fields: int mapped_fields: int unmapped_fields: int mappings: List[MappingResult] coverage: float confidence_distribution: Dict[str, int] recommendations: List[str] class OntologyMapper: """ Map construction data to standard ontologies. Based on DDC methodology Chapter 2.2. """ def __init__(self): self.ontologies = self._load_ontologies() self.mapping_rules = self._load_mapping_rules() self.synonym_map = self._build_synonym_map() def _load_ontologies(self) -> Dict[OntologyType, Dict[str, OntologyConcept]]: """Load standard construction ontologies""" ontologies = {} # IFC Schema (simplified) ontologies[OntologyType.IFC] = { "IfcWall": OntologyConcept("IfcWall", "Wall", OntologyType.IFC, "A vertical construction that bounds or subdivides spaces"), "IfcSlab": OntologyConcept("IfcSlab", "Slab", OntologyType.IFC, "A horizontal planar building element"), "IfcBeam": OntologyConcept("IfcBeam", "Beam", OntologyType.IFC, "A horizontal structural member"), "IfcColumn": OntologyConcept("IfcColumn", "Column", OntologyType.IFC, "A vertical structural member"), "IfcDoor": OntologyConcept("IfcDoor", "Door", OntologyType.IFC, "A building element for access"), "IfcWindow": OntologyConcept("IfcWindow", "Window", OntologyType.IFC, "A building element for light and ventilation"), "IfcRoof": OntologyConcept("IfcRoof", "Roof", OntologyType.IFC, "A building element covering a building"), "IfcStair": OntologyConcept("IfcStair", "Stair", OntologyType.IFC, "A vertical circulation element"), "IfcSpace": OntologyConcept("IfcSpace", "Space", OntologyType.IFC, "A defined volume of air"), "IfcBuildingStorey": OntologyConcept("IfcBuildingStorey", "Building Storey", OntologyType.IFC, "A horizontal aggregation of spaces"), } # COBie (simplified) ontologies[OntologyType.COBIE] = { "Floor": OntologyConcept("Floor", "Floor", OntologyType.COBIE, "A floor or level in a building"), "Space": OntologyConcept("Space", "Space", OntologyType.COBIE, "A spatial region"), "Type": OntologyConcept("Type", "Type", OntologyType.COBIE, "A product type or specification"), "Component": OntologyConcept("Component", "Component", OntologyType.COBIE, "An individual product instance"), "Zone": OntologyConcept("Zone", "Zone", OntologyType.COBIE, "A spatial grouping of spaces"), "System": OntologyConcept("System", "System", OntologyType.COBIE, "A building system or network"), } # Uniclass (simplified) ontologies[OntologyType.UNICLASS] = { "Ss_25": OntologyConcept("Ss_25", "Wall Systems", OntologyType.UNICLASS), "Ss_30": OntologyConcept("Ss_30", "Roof Systems", OntologyType.UNICLASS), "Ss_32": OntologyConcept("Ss_32", "Floor Systems", OntologyType.UNICLASS), "Ss_35": OntologyConcept("Ss_35", "Stair Systems", OntologyType.UNICLASS), "Pr_20": OntologyConcept("Pr_20", "Structural Products", OntologyType.UNICLASS), "Pr_30": OntologyConcept("Pr_30", "Wall Products", OntologyType.UNICLASS), "Pr_35": OntologyConcept("Pr_35", "Door Products", OntologyType.UNICLASS), "Pr_40": OntologyConcept("Pr_40", "Window Products", OntologyType.UNICLASS), } # MasterFormat (simplified) ontologies[OntologyType.MASTERFORMAT] = { "03": OntologyConcept("03", "Concrete", OntologyType.MASTERFORMAT), "04": OntologyConcept("04", "Masonry", OntologyType.MASTERFORMAT), "05": OntologyConcept("05", "Metals", OntologyType.MASTERFORMAT), "06": OntologyConcept("06", "Wood and Plastics", OntologyType.MASTERFORMAT), "07": OntologyConcept("07", "Thermal and Moisture Protection", OntologyType.MASTERFORMAT), "08": OntologyConcept("08", "Doors and Windows", OntologyType.MASTERFORMAT), "09": OntologyConcept("09", "Finishes", OntologyType.MASTERFORMAT), "22": OntologyConcept("22", "Plumbing", OntologyType.MASTERFORMAT), "23": OntologyConcept("23", "HVAC", OntologyType.MASTERFORMAT), "26": OntologyConcept("26", "Electrical", OntologyType.MASTERFORMAT), } return ontologies def _load_mapping_rules(self) -> List[SemanticMapping]: """Load predefined mapping rules between ontologies""" rules = [ # IFC to COBie SemanticMapping("IfcBuildingStorey", OntologyType.IFC, "Floor", OntologyType.COBIE, RelationType.EQUIVALENT, MappingConfidence.EXACT), SemanticMapping("IfcSpace", OntologyType.IFC, "Space", OntologyType.COBIE, RelationType.EQUIVALENT, MappingConfidence.EXACT), # IFC to Uniclass SemanticMapping("IfcWall", OntologyType.IFC, "Ss_25", OntologyType.UNICLASS, RelationType.RELATED, MappingConfidence.HIGH), SemanticMapping("IfcRoof", OntologyType.IFC, "Ss_30", OntologyType.UNICLASS, RelationType.RELATED, MappingConfidence.HIGH), SemanticMapping("IfcSlab", OntologyType.IFC, "Ss_32", OntologyType.UNICLASS, RelationType.RELATED, MappingConfidence.HIGH), SemanticMapping("IfcDoor", OntologyType.IFC, "Pr_35", OntologyType.UNICLASS, RelationType.RELATED, MappingConfidence.HIGH), SemanticMapping("IfcWindow", OntologyType.IFC, "Pr_40", OntologyType.UNICLASS, RelationType.RELATED, MappingConfidence.HIGH), # IFC to MasterFormat SemanticMapping("IfcDoor", OntologyType.IFC, "08", OntologyType.MASTERFORMAT, RelationType.BROADER, MappingConfidence.MEDIUM), SemanticMapping("IfcWindow", OntologyType.IFC, "08", OntologyType.MASTERFORMAT, RelationType.BROADER, MappingConfidence.MEDIUM), ] return rules def _build_synonym_map(self) -> Dict[str, List[str]]: """Build synonym mappings for fuzzy matching""" return { "wall": ["partition", "barrier", "divider"], "door": ["entrance", "portal", "opening"], "window": ["glazing", "fenestration", "opening"], "floor": ["slab", "deck", "storey", "level"], "roof": ["roofing", "covering", "canopy"], "beam": ["girder", "joist", "lintel"], "column": ["pillar", "post", "pier"], "stair": ["stairway", "staircase", "steps"], "space": ["room", "area", "zone"], "concrete": ["cement", "reinforced"], "steel": ["metal", "iron"], } def map_field( self, field_name: str, field_value: str, source_ontology: Optional[OntologyType] = None, target_ontology: OntologyType = OntologyType.IFC ) -> MappingResult: """ Map a single field to target ontology. Args: field_name: Name of the field field_value: Value to map source_ontology: Source ontology if known target_ontology: Target ontology to map to Returns: Mapping result with possible matches """ mappings = [] # Normalize the value normalized = self._normalize_value(field_value) # Check direct matches in existing rules for rule in self.mapping_rules: if rule.target_ontology == target_ontology: if self._matches(normalized, rule.source_concept): mappings.append(rule) # Check target ontology directly target_concepts = self.ontologies.get(target_ontology, {}) for concept_id, concept in target_concepts.items(): similarity = self._calculate_similarity(normalized, concept) if similarity > 0.5: confidence = self._similarity_to_confidence(similarity) mappings.append(SemanticMapping( source_concept=field_value, source_ontology=source_ontology or OntologyType.CUSTOM, target_concept=concept_id, target_ontology=target_ontology, relation=RelationType.EQUIVALENT if similarity > 0.9 else RelationType.RELATED, confidence=confidence )) # Sort by confidence confidence_order = [ MappingConfidence.EXACT, MappingConfidence.HIGH, MappingConfidence.MEDIUM, MappingConfidence.LOW, MappingConfidence.UNCERTAIN ] mappings.sort(key=lambda m: confidence_order.index(m.confidence)) return MappingResult( source_field=field_name, source_value=field_value, mappings=mappings, best_match=mappings[0] if mappings else None, unmapped=len(mappings) == 0 ) def _normalize_value(self, value: str) -> str: """Normalize a value for matching""" # Remove common prefixes prefixes = ["ifc", "cobie", "type", "element"] normalized = value.lower().strip() for prefix in prefixes: if normalized.startswith(prefix): normalized = normalized[len(prefix):] return normalized.strip("_- ") def _matches(self, value: str, concept: str) -> bool: """Check if value matches concept""" normalized_value = self._normalize_value(value) normalized_concept = self._normalize_value(concept) return normalized_value == normalized_concept def _calculate_similarity( self, value: str, concept: OntologyConcept ) -> float: """Calculate similarity between value and concept""" value_lower = value.lower() concept_name_lower = concept.name.lower() concept_id_lower = concept.id.lower() # Exact match if value_lower == concept_name_lower or value_lower == concept_id_lower: return 1.0 # Partial match in name if value_lower in concept_name_lower or concept_name_lower in value_lower: return 0.8 # Check synonyms for key, synonyms in self.synonym_map.items(): if key in value_lower: if key in concept_name_lower: return 0.9 for syn in synonyms: if syn in concept_name_lower: return 0.7 # Definition match if concept.definition: if value_lower in concept.definition.lower(): return 0.6 return 0.0 def _similarity_to_confidence(self, similarity: float) -> MappingConfidence: """Convert similarity score to confidence level""" if similarity >= 0.95: return MappingConfidence.EXACT elif similarity >= 0.8: return MappingConfidence.HIGH elif similarity >= 0.6: return MappingConfidence.MEDIUM elif similarity >= 0.4: return MappingConfidence.LOW else: return MappingConfidence.UNCERTAIN def map_schema( self, schema: Dict[str, List[str]], target_ontology: OntologyType = OntologyType.IFC ) -> OntologyMappingReport: """ Map entire schema to target ontology. Args: schema: Dictionary of field names to sample values target_ontology: Target ontology Returns: Complete mapping report """ all_mappings = [] confidence_dist = {c.value: 0 for c in MappingConfidence} for field_name, sample_values in schema.items(): # Use first sample value value = sample_values[0] if sample_values else field_name result = self.map_field(field_name, value, target_ontology=target_ontology) all_mappings.append(result) if result.best_match: confidence_dist[result.best_match.confidence.value] += 1 mapped = sum(1 for m in all_mappings if not m.unmapped) unmapped = len(all_mappings) - mapped coverage = mapped / len(all_mappings) if all_mappings else 0 recommendations = self._generate_recommendations(all_mappings, coverage) return OntologyMappingReport( total_fields=len(all_mappings), mapped_fields=mapped, unmapped_fields=unmapped, mappings=all_mappings, coverage=coverage, confidence_distribution=confidence_dist, recommendations=recommendations ) def _generate_recommendations( self, mappings: List[MappingResult], coverage: float ) -> List[str]: """Generate recommendations for improving mappings""" recommendations = [] if coverage < 0.7: recommendations.append( f"Low mapping coverage ({coverage:.0%}). Consider adding custom mappings." ) low_confidence = [m for m in mappings if m.best_match and m.best_match.confidence in [MappingConfidence.LOW, MappingConfidence.UNCERTAIN]] if low_confidence: recommendations.append( f"{len(low_confidence)} mappings have low confidence. Review manually." ) unmapped = [m for m in mappings if m.unmapped] if unmapped: fields = [m.source_field for m in unmapped[:5]] recommendations.append( f"Unmapped fields: {', '.join(fields)}. Add custom mappings." ) return recommendations def create_mapping( self, source: str, source_ontology: OntologyType, target: str, target_ontology: OntologyType, relation: RelationType = RelationType.EQUIVALENT, notes: Optional[str] = None ) -> SemanticMapping: """Create a new manual mapping""" mapping = SemanticMapping( source_concept=source, source_ontology=source_ontology, target_concept=target, target_ontology=target_ontology, relation=relation, confidence=MappingConfidence.EXACT, notes=notes, created_by="manual" ) self.mapping_rules.append(mapping) return mapping def export_mappings(self, format: str = "json") -> str: """Export all mappings""" if format == "json": mappings_data = [] for rule in self.mapping_rules: mappings_data.append({ "source": rule.source_concept, "source_ontology": rule.source_ontology.value, "target": rule.target_concept, "target_ontology": rule.target_ontology.value, "relation": rule.relation.value, "confidence": rule.confidence.value }) return json.dumps(mappings_data, indent=2) else: raise ValueError(f"Unsupported format: {format}") def generate_report(self, report: OntologyMappingReport) -> str: """Generate mapping report""" output = f""" # Ontology Mapping Report ## Summary - **Total Fields:** {report.total_fields} - **Mapped Fields:** {report.mapped_fields} - **Unmapped Fields:** {report.unmapped_fields} - **Coverage:** {report.coverage:.0%} ## Confidence Distribution """ for conf, count in report.confidence_distribution.items(): if count > 0: output += f"- **{conf.title()}:** {count}\n" output += "\n## Recommendations\n" for rec in report.recommendations: output += f"- {rec}\n" output += "\n## Mappings\n" for mapping in report.mappings[:20]: status = "✓" if not mapping.unmapped else "✗" target = mapping.best_match.target_concept if mapping.best_match else "unmapped" conf = mapping.best_match.confidence.value if mapping.best_match else "-" output += f"- {status} {mapping.source_field}: {mapping.source_value} → {target} ({conf})\n" return output ``` ## Common Use Cases ### Map Field to IFC ```python mapper = OntologyMapper() # Map a single field result = mapper.map_field( field_name="element_type", field_value="Wall", target_ontology=OntologyType.IFC ) if result.best_match: print(f"Mapped to: {result.best_match.target_concept}") print(f"Confidence: {result.best_match.confidence.value}") ``` ### Map Entire Schema ```python # Define schema with sample values schema = { "element_type": ["Wall", "Door", "Window"], "level": ["Level 1", "Level 2"], "material": ["Concrete", "Steel"], "room_type": ["Office", "Corridor"] } report = mapper.map_schema(schema, target_ontology=OntologyType.IFC) print(f"Coverage: {report.coverage:.0%}") print(f"Mapped: {report.mapped_fields}/{report.total_fields}") ``` ### Create Custom Mappings ```python # Add custom mapping mapper.create_mapping( source="CustomWallType", source_ontology=OntologyType.CUSTOM, target="IfcWall", target_ontology=OntologyType.IFC, relation=RelationType.EQUIVALENT, notes="Custom wall type from legacy system" ) ``` ## Quick Reference | Component | Purpose | |-----------|---------| | `OntologyMapper` | Main mapping engine | | `OntologyType` | Standard ontologies (IFC, COBie, etc.) | | `SemanticMapping` | Mapping between concepts | | `MappingResult` | Result of mapping operation | | `RelationType` | Relationship types | | `MappingConfidence` | Confidence levels | ## Resources - **Book**: "Data-Driven Construction" by Artem Boiko, Chapter 2.2 - **Website**: https://datadrivenconstruction.io ## Next Steps - Use [open-data-integrator](../open-data-integrator/SKILL.md) for open data - Use [data-model-designer](../../Chapter-2.5/data-model-designer/SKILL.md) for schema design - Use [bim-validation-pipeline](../../Chapter-4.3/bim-validation-pipeline/SKILL.md) for validation