# IOM Implementationsplan — quiXzoom Fas 1 > Konkret plan för att bygga Infrastructure Object Model > Status: Aktiv | 2026-06-26 --- ## Vecka 1-2: Fas 1 Grund (Lager 1, 2, 5, 6) ### Dag 1-2: Taxonomi (Lager 1) **Uppgift:** Definiera domäner för quiXzoom Fas 1 ```bash # Skapa taxonomi-fil cat > iom_taxonomy_v1.json << 'EOF' { "version": "1.0.0", "domains": { "BYG": { "name": "Byggnad", "systems": { "FAC": { "name": "Fasad", "objects": { "WIN": {"name": "Fönster", "components": ["GLA", "FRM", "SIL"]}, "PAN": {"name": "Fasadpanel", "components": []}, "ENT": {"name": "Entré", "components": ["DOR", "TRP", "CAN"]} } }, "ROF": { "name": "Tak", "objects": { "SUR": {"name": "Takyta", "components": []}, "GUT": {"name": "Ränna", "components": []}, "CHI": {"name": "Skorsten", "components": []} } } } }, "BEL": { "name": "Belysning", "systems": { "STR": { "name": "Gatlykta", "objects": { "LED": {"name": "LED-armatur", "components": ["FND", "POL", "DRV"]}, "SON": {"name": "Natrium", "components": ["FND", "POL", "BAL"]} } } } }, "COM": { "name": "Kommersiellt", "systems": { "DIS": { "name": "Butiksfront", "objects": { "SGN": {"name": "Skylt", "components": []}, "WIN": {"name": "Skyltfönster", "components": ["GLA", "FRM"]} } } } } } } EOF ``` **Validering:** - 3 bokstäver per nivå - Unika inom förälder - Engelska förkortningar --- ### Dag 3-4: GOID (Lager 2) **Uppgift:** Bygg ID-generering ```python # goid_generator.py import hashlib import time from typing import Optional class GOIDGenerator: """Genererar globala objekt-ID:n för IOM""" def __init__(self, domain: str, system: str, subsystem: str, obj_type: str): self.prefix = f"{domain}-{system}-{subsystem}-{obj_type}" self.sequence = 0 def generate(self, location_hash: Optional[str] = None) -> str: """Generera unikt GOID""" self.sequence += 1 # Format: DOM-SYS-SUB-OBJ-SEQ # Exempel: BYG-FAC-WIN-GLA-0001 goid = f"{self.prefix}-{self.sequence:04d}" # Om plats-hash finns, lägg till för extra unikhet if location_hash: short_hash = hashlib.md5(location_hash.encode()).hexdigest()[:4] goid = f"{goid}-{short_hash}" return goid def validate(self, goid: str) -> bool: """Validera GOID-format""" parts = goid.split('-') if len(parts) < 5: return False # Validera att alla delar är 3 bokstäver förutom sekvens for part in parts[:-1]: if len(part) != 3 or not part.isalpha(): return False # Validera sekvens try: int(parts[-1]) except ValueError: return False return True # Exempel if __name__ == "__main__": gen = GOIDGenerator("BYG", "FAC", "WIN", "GLA") print(gen.generate()) # BYG-FAC-WIN-GLA-0001 print(gen.generate()) # BYG-FAC-WIN-GLA-0002 ``` --- ### Dag 5-7: Observationer (Lager 6) **Uppgift:** Bygg observation-API ```python # observation_api.py from datetime import datetime from typing import List, Dict, Optional from pydantic import BaseModel class Finding(BaseModel): type: str code: str # Felkod från Lager 7 description: str measurement: Optional[str] = None confidence: float # 0.0 - 1.0 class Media(BaseModel): type: str # image, video, depth_map url: str timestamp: datetime geotag: Optional[Dict] = None class Observation(BaseModel): id: str timestamp: datetime object_goid: str observer: str # zoomer:id eller sensor:id findings: List[Finding] media: List[Media] # AI-analys ai_model: Optional[str] = None overall_condition: Optional[int] = None # 1-5 recommended_action: Optional[str] = None next_observation_due: Optional[datetime] = None class Config: schema_extra = { "example": { "id": "OBS-2026-0012847", "timestamp": "2026-06-26T09:15:00Z", "object_goid": "BYG-FAC-WIN-GLA-2847", "observer": "zoomer:anna_k", "findings": [ { "type": "dirt_accumulation", "code": "2100", "description": "Smuts på fönster", "confidence": 0.94 } ], "media": [ { "type": "image", "url": "https://.../img_2847.jpg", "timestamp": "2026-06-26T09:15:03Z" } ], "ai_model": "infrastructure-v3.2", "overall_condition": 3, "recommended_action": "schedule_cleaning" } } class ObservationStore: """Lagra och hämta observationer""" def __init__(self, db_connection): self.db = db_connection def create(self, obs: Observation) -> str: """Spara observation""" # Generera ID om inte angivet if not obs.id: obs.id = f"OBS-{datetime.now().year}-{self._next_sequence():07d}" # Spara i databas self.db.observations.insert_one(obs.dict()) # Uppdatera objektets senaste tillstånd self._update_object_condition(obs.object_goid, obs.overall_condition) return obs.id def get_for_object(self, goid: str, limit: int = 100) -> List[Observation]: """Hämta alla observationer för ett objekt""" cursor = self.db.observations.find( {"object_goid": goid} ).sort("timestamp", -1).limit(limit) return [Observation(**doc) for doc in cursor] def get_trend(self, goid: str, months: int = 6) -> Dict: """Analysera trend för objekt""" observations = self.get_for_object(goid, limit=1000) # Gruppera per månad monthly = {} for obs in observations: month_key = obs.timestamp.strftime("%Y-%m") if month_key not in monthly: monthly[month_key] = [] monthly[month_key].append(obs.overall_condition) # Beräkna medel per månad trend = { month: sum(conditions) / len(conditions) for month, conditions in monthly.items() } return { "goid": goid, "trend": trend, "improving": trend[-1] < trend[0] if len(trend) > 1 else None, "observation_count": len(observations) } ``` --- ## Vecka 3-4: Fas 2 Struktur (Lager 3, 7, 4) ### Dag 8-10: Metadata (Lager 3) **Uppgift:** Bygg objekt-metadata ```python # object_metadata.py from pydantic import BaseModel from typing import List, Optional, Dict from datetime import date class Dimensions(BaseModel): length: Optional[float] = None width: Optional[float] = None height: Optional[float] = None diameter: Optional[float] = None unit: str = "m" class ObjectMetadata(BaseModel): goid: str object_type: str material: List[str] dimensions: Optional[Dimensions] = None manufacturer: Optional[str] = None manufacturing_year: Optional[int] = None installation_date: Optional[date] = None design_lifespan: Optional[int] = None # år owner: Optional[str] = None # org:id maintainer: Optional[str] = None # org:id # Standarder standard: Optional[str] = None certification: Optional[str] = None class Config: schema_extra = { "example": { "goid": "BYG-FAC-WIN-GLA-2847", "object_type": "window_glass", "material": ["glass", "aluminum"], "dimensions": { "width": 2.1, "height": 1.5, "unit": "m" }, "installation_date": "2020-03-15", "owner": "org:ikea" } } ``` --- ### Dag 11-12: Felkoder (Lager 7) **Uppgift:** Definiera felkoder för Fas 1 ```python # defect_codes.py from enum import Enum class DefectCode(str, Enum): """Felkoder för IOM — Fas 1 (kommersiellt fokus)""" # 2000 — Ytskada DIRT_ACCUMULATION = "2100" # Nedsmutsning COLOR_CHANGE = "2200" # Färgförändring SURFACE_DAMAGE = "2300" # Ytskada GRAFFITI = "2400" # Klotter # 3000 — Strukturell skada CRACK = "3100" # Spricka DEFORMATION = "3200" # Deformation MATERIAL_LOSS = "3300" # Materialförlust # 4000 — Saknad / Trasig komponent MISSING_PART = "4100" # Saknad del BROKEN_PART = "4200" # Trasig del LOOSE_PART = "4300" # Lossnad del # 5000 — Blockering PHYSICAL_BLOCK = "5100" # Fysisk blockering VISUAL_BLOCK = "5200" # Synlig blockering # 6000 — Miljö VEGETATION = "6100" # Vegetation WATER_DAMAGE = "6200" # Vattenskada ICE_DAMAGE = "6300" # Isskada class DefectRegistry: """Register över felkoder med beskrivningar""" CODES = { "2100": {"sv": "Nedsmutsning", "en": "Dirt accumulation", "category": "surface"}, "2200": {"sv": "Färgförändring", "en": "Color change", "category": "surface"}, "2300": {"sv": "Ytskada", "en": "Surface damage", "category": "surface"}, "2400": {"sv": "Klotter", "en": "Graffiti", "category": "surface"}, "3100": {"sv": "Spricka", "en": "Crack", "category": "structural"}, "3200": {"sv": "Deformation", "en": "Deformation", "category": "structural"}, "3300": {"sv": "Materialförlust", "en": "Material loss", "category": "structural"}, "4100": {"sv": "Saknad del", "en": "Missing part", "category": "component"}, "4200": {"sv": "Trasig del", "en": "Broken part", "category": "component"}, "4300": {"sv": "Lossnad del", "en": "Loose part", "category": "component"}, "5100": {"sv": "Fysisk blockering", "en": "Physical blockage", "category": "blockage"}, "5200": {"sv": "Synlig blockering", "en": "Visual blockage", "category": "blockage"}, "6100": {"sv": "Vegetation", "en": "Vegetation", "category": "environmental"}, "6200": {"sv": "Vattenskada", "en": "Water damage", "category": "environmental"}, "6300": {"sv": "Isskada", "en": "Ice damage", "category": "environmental"}, } @classmethod def get_description(cls, code: str, lang: str = "sv") -> str: """Hämta beskrivning på angivet språk""" if code in cls.CODES: return cls.CODES[code].get(lang, cls.CODES[code]["en"]) return "Okänd felkod" @classmethod def get_category(cls, code: str) -> str: """Hämta kategori""" return cls.CODES.get(code, {}).get("category", "unknown") ``` --- ### Dag 13-14: BOM (Lager 4, förenklat) **Uppgift:** Komponentstruktur (1 nivå) ```python # bom_structure.py from pydantic import BaseModel from typing import List, Optional class Component(BaseModel): goid: str name: str quantity: int = 1 unit: str = "st" # Livscykel installation_date: Optional[str] = None expected_lifespan: Optional[int] = None # år # Status status: str = "active" # active, retired, replaced class BOM(BaseModel): """Bill of Materials för infrastrukturobjekt""" parent_goid: str parent_name: str components: List[Component] def get_active_components(self) -> List[Component]: """Hämta aktiva komponenter""" return [c for c in self.components if c.status == "active"] def get_component_by_type(self, component_type: str) -> List[Component]: """Hämta komponenter av specifik typ""" return [ c for c in self.components if c.goid.split('-')[-2] == component_type ] # Exempel: Gatlykta street_light_bom = BOM( parent_goid="BEL-STR-LED-0001", parent_name="Gatlykta Drottningholm", components=[ Component(goid="BEL-STR-FND-CON-0001", name="Fundament", quantity=1), Component(goid="BEL-STR-BLT-GAL-0001", name="Förankringsbultar M24", quantity=4), Component(goid="BEL-STR-POL-GAL-0001", name="Stolpe 6m", quantity=1), Component(goid="BEL-STR-ARM-LED-0001", name="LED-armatur", quantity=1), Component(goid="BEL-STR-DRV-LED-0001", name="Drivdon", quantity=1), ] ) ``` --- ## Vecka 5-6: Fas 3 Intelligens (Lager 8, 9) ### Dag 15-17: Riskmodell (Lager 8) **Uppgift:** Riskberäkning ```python # risk_model.py from typing import Dict from pydantic import BaseModel class RiskScores(BaseModel): safety: int = 0 # 0-10 economic: int = 0 # 0-10 operational: int = 0 # 0-10 legal: int = 0 # 0-10 environmental: int = 0 # 0-10 aesthetic: int = 0 # 0-10 class RiskWeights: """Vikter per objekttyp""" DEFAULT = { "safety": 0.3, "economic": 0.2, "operational": 0.2, "legal": 0.1, "environmental": 0.1, "aesthetic": 0.1 } BRIDGE = { "safety": 0.4, "economic": 0.2, "operational": 0.2, "legal": 0.1, "environmental": 0.05, "aesthetic": 0.05 } WINDOW = { "safety": 0.1, "economic": 0.2, "operational": 0.1, "legal": 0.1, "environmental": 0.1, "aesthetic": 0.4 } def calculate_risk(scores: RiskScores, object_type: str = "default") -> Dict: """Beräkna sammanlagd risk""" weights = getattr(RiskWeights, object_type.upper(), RiskWeights.DEFAULT) total = sum( getattr(scores, dim) * weight for dim, weight in weights.items() ) return { "total": round(min(10, max(0, total)), 2), "breakdown": scores.dict(), "weights": weights, "level": _risk_level(total) } def _risk_level(score: float) -> str: if score >= 8: return "critical" if score >= 6: return "high" if score >= 4: return "medium" if score >= 2: return "low" return "minimal" ``` --- ### Dag 18-21: Relationer (Lager 9, förenklat) **Uppgift:** Enkla relationer ```python # relations.py from typing import List, Dict from pydantic import BaseModel class Relation(BaseModel): type: str # part_of, owned_by, adjacent_to target_goid: str target_name: Optional[str] = None class ObjectGraph: """Enkel kunskapsgraf för IOM""" def __init__(self): self.relations: Dict[str, List[Relation]] = {} def add_relation(self, from_goid: str, relation: Relation): """Lägg till relation""" if from_goid not in self.relations: self.relations[from_goid] = [] self.relations[from_goid].append(relation) def get_related(self, goid: str, relation_type: Optional[str] = None) -> List[Relation]: """Hämta relaterade objekt""" relations = self.relations.get(goid, []) if relation_type: relations = [r for r in relations if r.type == relation_type] return relations def get_owners(self, goid: str) -> List[str]: """Hämta ägare för objekt""" owners = [] for from_goid, relations in self.relations.items(): for rel in relations: if rel.target_goid == goid and rel.type == "owned_by": owners.append(from_goid) return owners ``` --- ## Vecka 7-10: Fas 4 Vision (Lager 10) ### Dag 22-30: Digital tvilling **Uppgift:** Dashboard och API ```python # digital_twin_api.py from fastapi import FastAPI, HTTPException from typing import List, Optional import asyncio app = FastAPI(title="IOM Digital Twin API") @app.get("/objects/{goid}") async def get_object(goid: str): """Hämta komplett objekt med historik""" obj = await db.objects.find_one({"goid": goid}) if not obj: raise HTTPException(status_code=404, detail="Object not found") # Hämta observationer observations = await db.observations.find( {"object_goid": goid} ).sort("timestamp", -1).to_list(100) # Hämta relationer relations = await db.relations.find( {"from_goid": goid} ).to_list(100) return { "object": obj, "observations": observations, "relations": relations, "latest_condition": observations[0]["overall_condition"] if observations else None, "observation_count": len(observations) } @app.get("/objects/{goid}/timeline") async def get_timeline(goid: str, months: int = 12): """Hämta tidslinje för objekt""" observations = await db.observations.find( {"object_goid": goid} ).sort("timestamp", 1).to_list(1000) timeline = [] for obs in observations: timeline.append({ "date": obs["timestamp"], "condition": obs.get("overall_condition"), "findings": [f["type"] for f in obs.get("findings", [])], "risk_level": obs.get("risk_level"), "media_count": len(obs.get("media", [])) }) return {"goid": goid, "timeline": timeline} @app.get("/queries/condition-degradation") async def find_degrading_objects( domain: Optional[str] = None, min_observations: int = 2, threshold: float = 1.0 ): """Hitta objekt som försämrats över tid""" pipeline = [ {"$match": {"object_goid": {"$regex": f"^{domain}"}} if domain else {}}, {"$group": { "_id": "$object_goid", "first_condition": {"$first": "$overall_condition"}, "last_condition": {"$last": "$overall_condition"}, "count": {"$sum": 1} }}, {"$match": { "count": {"$gte": min_observations}, "$expr": {"$gte": [ {"$subtract": ["$first_condition", "$last_condition"]}, threshold ]} }} ] results = await db.observations.aggregate(pipeline).to_list(100) return results ``` --- ## Teknisk stack | Komponent | Teknik | |---|---| | API | FastAPI (Python) | | Databas | PostgreSQL + PostGIS (geodata) | | Cache | Redis | | Bildlagring | S3 | | AI-integration | REST API till befintlig AI-tjänst | | Dokumentation | OpenAPI / Swagger | --- ## Milestones | Vecka | Milestone | Kriterier | |---|---|---| | 2 | Taxonomi + GOID | Kan skapa och validera objekt-ID | | 4 | Observationer | Kan spara och hämta observationer med bilder | | 6 | Metadata + Felkoder | Kan klassificera och söka på felkoder | | 8 | Risk + Relationer | Kan beräkna risk och följa relationer | | 10 | Digital tvilling | Dashboard med tidslinje och trender | --- ## Nästa steg 1. **Dag 1:** Sätt upp repo och CI/CD 2. **Dag 2:** Implementera taxonomi 3. **Dag 3:** Implementera GOID-generator 4. **Dag 4:** Sätt upp databas 5. **Dag 5:** Implementera observation-API **Total tid till MVP:** 2 veckor **Total tid till full IOM:** 10 veckor