Files
boc/IOM_IMPLEMENTATION_PLAN.md
T
Bernt bae705aa97 ARCHITECTURE: NFC roadmap, edge AI, audit logging
- Add NFC ePassport roadmap (ICAO 9303, eIDAS)
- Add TensorFlow.js edge face detection (BlazeFace)
- Add structured audit logger (GDPR-compliant)
- Risk scoring support

Part of KYC Apple Native UX v1.1.0
2026-06-29 16:24:48 +00:00

19 KiB

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

# 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

# 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

# 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

# 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

# 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å)

# 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

# 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

# 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

# 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