Map construction data to standard ontologies. Create semantic mappings between different data schemas
npx skills add https://github.com/datadrivenconstruction/DDC_Skills_for_AI_Agents_in_Construction --skill ontology-mapper
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"
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
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}")
# 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}")
# 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"
)
| 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 |
Integration with protocols.io API for managing scientific protocols. This skill should be used when working with protocols.io to search, create, update, or publish protocols; manage protocol steps and materials; handle discussions and comments; organize workspaces; upload and manage files; or integrate protocols.io functionality into workflows. Applicable for protocol discovery, collaborative protocol development, experiment tracking, lab protocol management, and scientific documentation.
Analyzes job descriptions and generates tailored resumes that highlight relevant experience, skills, and achievements to maximize interview chances
Generate Excalidraw diagrams from natural language descriptions. Use when asked to "create a diagram", "make a flowchart", "visualize a process", "draw a system architecture", "create a mind map", or "generate an Excalidraw file". Supports flowcharts, relationship diagrams, mind maps, and system architecture diagrams. Outputs .excalidraw JSON files that can be opened directly in Excalidraw.
Build and distribute Expo development clients locally or via TestFlight
Use when you have a written implementation plan to execute in a separate session with review checkpoints
Data structure for annotated matrices in single-cell analysis. Use when working with .h5ad files or integrating with the scverse ecosystem. This is the data format skill—for analysis workflows use scanpy; for probabilistic models use scvi-tools; for population-scale queries use cellxgene-census.
Benchling R&D platform integration. Access registry (DNA, proteins), inventory, ELN entries, workflows via API, build Benchling Apps, query Data Warehouse, for lab data management automation.
Comprehensive molecular biology toolkit. Use for sequence manipulation, file parsing (FASTA/GenBank/PDB), phylogenetics, and programmatic NCBI/PubMed access (Bio.Entrez). Best for batch processing, custom bioinformatics pipelines, BLAST automation. For quick lookups use gget; for multi-service integration use bioservices.
Take datadrivenconstruction/ontology-mapper from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.