Files
boc/IOM_IMPLEMENTATION_PLAN.md
T

687 lines
19 KiB
Markdown
Raw Normal View History

# 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