Files
gh-greyhaven-ai-claude-code…/skills/ontological-documentation/scripts/generate_ontology_diagram.py
2025-11-29 18:29:15 +08:00

289 lines
11 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Ontology Diagram Generator
This script generates visual representations of ontologies in various formats
including Mermaid, PlantUML, GraphViz DOT, and JSON-LD for semantic web applications.
"""
import json
import sys
import argparse
import re
from pathlib import Path
from typing import Dict, List, Any, Optional
class OntologyDiagramGenerator:
"""Generates diagrams for ontological documentation."""
def __init__(self):
self.relationship_symbols = {
'is_a': '--|>', # Inheritance
'part_of': '--*', # Composition
'depends_on': '-.->', # Dependency
'associates_with': '---', # Association
'instance_of': '..>' # Instantiation
}
def generate_mermaid(self, ontology: Dict[str, Any]) -> str:
"""Generate Mermaid diagram from ontology."""
lines = ["graph TD"]
lines.append(" %% Ontology Diagram")
lines.append(" %% Generated from ontological documentation")
# Add styling
lines.extend([
" classDef concept fill:#e1f5fe,stroke:#01579b,stroke-width:2px",
" classDef class fill:#f3e5f5,stroke:#4a148c,stroke-width:2px",
" classDef interface fill:#e8f5e8,stroke:#1b5e20,stroke-width:2px",
" classDef function fill:#fff3e0,stroke:#e65100,stroke-width:2px"
])
# Add concepts as nodes
concept_classes = {}
for concept_name, concept_data in ontology['concepts'].items():
concept_type = concept_data.get('type', 'concept')
if concept_type == 'class':
lines.append(f" {self._safe_name(concept_name)}[({concept_name})]")
concept_classes[concept_name] = 'class'
elif concept_type == 'interface':
lines.append(f" {self._safe_name(concept_name)}[{{interface}}{concept_name}]")
concept_classes[concept_name] = 'interface'
elif concept_type == 'function':
lines.append(f" {self._safe_name(concept_name)}[{concept_name}()]")
concept_classes[concept_name] = 'function'
else:
lines.append(f" {self._safe_name(concept_name)}[{concept_name}]")
concept_classes[concept_name] = 'concept'
# Add relationships
for rel_type, relationships in ontology['relationships'].items():
symbol = self.relationship_symbols.get(rel_type, '---')
for rel in relationships:
subject = self._safe_name(rel['subject'])
obj = self._safe_name(rel['object'])
label = rel.get('label', rel_type.replace('_', ' ').title())
lines.append(f" {subject} {symbol} {obj}")
# Apply classes
for concept_name, css_class in concept_classes.items():
lines.append(f" class {self._safe_name(concept_name)} {css_class}")
return "\n".join(lines)
def generate_plantuml(self, ontology: Dict[str, Any]) -> str:
"""Generate PlantUML diagram from ontology."""
lines = ["@startuml Ontology"]
lines.append("!theme plain")
lines.append("skinparam classAttributeIconSize 0")
# Add concepts as classes
for concept_name, concept_data in ontology['concepts'].items():
concept_type = concept_data.get('type', 'concept')
if concept_type == 'class':
lines.append(f"class {concept_name} {{}}")
elif concept_type == 'interface':
lines.append(f"interface {concept_name} {{}}")
elif concept_type == 'function':
lines.append(f"object {concept_name} {{}}")
else:
lines.append(f"abstract {concept_name} {{}}")
lines.append("") # Empty line for separation
# Add relationships
for rel_type, relationships in ontology['relationships'].items():
for rel in relationships:
subject = rel['subject']
obj = rel['object']
if rel_type == 'is_a':
lines.append(f"{subject} <|-- {obj}")
elif rel_type == 'part_of':
lines.append(f"{subject} *-- {obj}")
elif rel_type == 'depends_on':
lines.append(f"{subject} ..> {obj}")
else:
lines.append(f"{subject} -- {obj}")
lines.append("@enduml")
return "\n".join(lines)
def generate_dot(self, ontology: Dict[str, Any]) -> str:
"""Generate GraphViz DOT diagram from ontology."""
lines = ["digraph Ontology {"]
lines.append(' rankdir=TB;')
lines.append(' node [shape=box, style=filled];')
lines.append(' edge [fontsize=10];')
# Add concepts as nodes
for concept_name, concept_data in ontology['concepts'].items():
concept_type = concept_data.get('type', 'concept')
# Set colors based on type
if concept_type == 'class':
color = "lightpurple"
elif concept_type == 'interface':
color = "lightgreen"
elif concept_type == 'function':
color = "lightorange"
else:
color = "lightblue"
lines.append(f' "{concept_name}" [label="{concept_name}", fillcolor="{color}"];')
lines.append("") # Empty line for separation
# Add relationships
for rel_type, relationships in ontology['relationships'].items():
for rel in relationships:
subject = rel['subject']
obj = rel['object']
label = rel.get('label', rel_type.replace('_', ' ').title())
# Set arrow styles based on relationship type
if rel_type == 'is_a':
arrow = 'empty'
elif rel_type == 'part_of':
arrow = 'diamond'
elif rel_type == 'depends_on':
arrow = 'dashed'
else:
arrow = 'normal'
lines.append(f' "{subject}" -> "{obj}" [label="{label}", arrowhead={arrow}];')
lines.append("}")
return "\n".join(lines)
def generate_json_ld(self, ontology: Dict[str, Any]) -> Dict[str, Any]:
"""Generate JSON-LD representation of ontology."""
context = {
"@context": {
"rdfs": "http://www.w3.org/2000/01/rdf-schema#",
"owl": "http://www.w3.org/2002/07/owl#",
"Concept": "rdfs:Class",
"subClassOf": {
"@id": "rdfs:subClassOf",
"@type": "@id"
},
"partOf": {
"@id": "http://example.org/ontology#partOf",
"@type": "@id"
},
"dependsOn": {
"@id": "http://example.org/ontology#dependsOn",
"@type": "@id"
}
},
"@graph": []
}
# Add concepts
for concept_name, concept_data in ontology['concepts'].items():
concept_entry = {
"@id": f"http://example.org/ontology#{concept_name}",
"@type": ["Concept"]
}
concept_type = concept_data.get('type', 'concept')
if concept_type == 'class':
concept_entry["@type"].append("owl:Class")
elif concept_type == 'interface':
concept_entry["@type"].append("owl:Class")
context["@graph"].append(concept_entry)
# Add relationships
for rel_type, relationships in ontology['relationships'].items():
for rel in relationships:
subject = rel['subject']
obj = rel['object']
if rel_type == 'is_a':
context["@graph"].append({
"@id": f"http://example.org/ontology#{subject}",
"subClassOf": f"http://example.org/ontology#{obj}"
})
elif rel_type == 'part_of':
context["@graph"].append({
"@id": f"http://example.org/ontology#{subject}",
"partOf": f"http://example.org/ontology#{obj}"
})
elif rel_type == 'depends_on':
context["@graph"].append({
"@id": f"http://example.org/ontology#{subject}",
"dependsOn": f"http://example.org/ontology#{obj}"
})
return context
def _safe_name(self, name: str) -> str:
"""Convert name to safe identifier for diagram formats."""
# Replace special characters and spaces with underscores
return re.sub(r'[^a-zA-Z0-9_]', '_', name)
def load_ontology(file_path: Path) -> Dict[str, Any]:
"""Load ontology from JSON file."""
with open(file_path, 'r', encoding='utf-8') as f:
return json.load(f)
def main():
"""Main function to generate diagrams."""
parser = argparse.ArgumentParser(description='Generate ontology diagrams')
parser.add_argument('ontology_file', help='Path to ontology JSON file')
parser.add_argument('--format', choices=['mermaid', 'plantuml', 'dot', 'json-ld', 'all'],
default='all', help='Diagram format to generate')
parser.add_argument('--output', help='Output directory (default: current directory)')
args = parser.parse_args()
ontology_path = Path(args.ontology_file)
if not ontology_path.exists():
print(f"Error: Ontology file '{ontology_path}' not found")
sys.exit(1)
ontology = load_ontology(ontology_path)
generator = OntologyDiagramGenerator()
output_dir = Path(args.output) if args.output else Path('.')
output_dir.mkdir(exist_ok=True)
formats_to_generate = ['mermaid', 'plantuml', 'dot', 'json-ld'] if args.format == 'all' else [args.format]
for format_type in formats_to_generate:
if format_type == 'mermaid':
diagram = generator.generate_mermaid(ontology)
output_file = output_dir / f"{ontology_path.stem}_mermaid.md"
with open(output_file, 'w', encoding='utf-8') as f:
f.write(f"# {ontology_path.stem} Ontology Diagram\n\n")
f.write("```mermaid\n")
f.write(diagram)
f.write("\n```")
print(f"Generated Mermaid diagram: {output_file}")
elif format_type == 'plantuml':
diagram = generator.generate_plantuml(ontology)
output_file = output_dir / f"{ontology_path.stem}_plantuml.puml"
with open(output_file, 'w', encoding='utf-8') as f:
f.write(diagram)
print(f"Generated PlantUML diagram: {output_file}")
elif format_type == 'dot':
diagram = generator.generate_dot(ontology)
output_file = output_dir / f"{ontology_path.stem}.dot"
with open(output_file, 'w', encoding='utf-8') as f:
f.write(diagram)
print(f"Generated GraphViz DOT diagram: {output_file}")
elif format_type == 'json-ld':
json_ld = generator.generate_json_ld(ontology)
output_file = output_dir / f"{ontology_path.stem}_jsonld.json"
with open(output_file, 'w', encoding='utf-8') as f:
json.dump(json_ld, f, indent=2)
print(f"Generated JSON-LD: {output_file}")
if __name__ == "__main__":
main()