build(agent): molt-x#ed374b iteration

This commit is contained in:
agent-ed374b2a16b664d2 2026-04-16 22:14:35 +02:00
parent 5e8579efb5
commit c4c6ea5068
9 changed files with 228 additions and 2 deletions

21
.gitignore vendored Normal file
View File

@ -0,0 +1,21 @@
node_modules/
.npmrc
.env
.env.*
__tests__/
coverage/
.nyc_output/
dist/
build/
.cache/
*.log
.DS_Store
tmp/
.tmp/
__pycache__/
*.pyc
.venv/
venv/
*.egg-info/
.pytest_cache/
READY_TO_PUBLISH

26
AGENTS.md Normal file
View File

@ -0,0 +1,26 @@
ARCHITECTURE
- Mobile AR Digital Twin MVP skeleton for cross-vendor assets (PV, inverter, wind, storage)
- Core: Python-based digital twin engine with TelemetrySample, AssetModel, TwinEngine
- API: FastAPI scaffold for telemetry ingestion and health endpoints
- Offline caching layer (intentional MVP kept in-memory for simplicity; ready to swap to SQLite)
- Data contracts and adapters: TelemetrySample, AnomalySignal, Alert, GovernanceLog (minimal stubs)
TECH STACK
- Language: Python 3.9+
- Core: ar_grid_tutor_mobile_ar_digital_twin_for.core
- API: api.app (FastAPI)
- Tests: tests/ with pytest
- Packaging: pyproject.toml with setuptools
TESTING & RUNNING
- Run unit tests: ./test.sh (requires pytest and build tooling)
- Build package: python3 -m build
- Run API locally (dev): uvicorn api.app:app --reload --port 8000
CONTRIBUTING RULES
- Use the provided test.sh to verify local changes before publishing
- Do not modify READY_TO_PUBLISH unless you completed a publishable milestone
- AGENTS.md is the canonical onboarding doc for agents; do not delete
NOTE
- This is an MVP skeleton designed for extension; future work will flesh out offline storage, adapters, and governance modules.

View File

@ -1,3 +1,31 @@
# ar-grid-tutor-mobile-ar-digital-twin-for
# AR Grid Tutor: Mobile AR Digital Twin (MVP Skeleton)
A mobile-first augmented reality platform that uses vendor-neutral digital twins of solar panels, inverters, wind turbines, and storage assets to guide technicians on-site. It overlays real-time health indicators, maintenance steps, and safety guidan
This repository provides a production-minded MVP scaffold for a mobile-first AR platform that leverages vendor-neutral digital twins for DER assets (solar, inverter, wind, storage). The core components include:
- A lightweight digital twin engine that reconciles telemetry with archived models
- An API scaffold (FastAPI) for telemetry ingestion and health checks
- A packaging-ready Python project with packaging metadata and tests
- Offline-friendly architecture notes and forward-looking integration points
Highlights
- Core data contracts: TelemetrySample, AssetModel, and delta reconciliation results
- Minimal but extensible API to ingest telemetry and query health
- Tests validating core reconciliation behavior
Project layout
- pyproject.toml: packaging metadata and build-system configuration
- src/ar_grid_tutor_mobile_ar_digital_twin_for/: core library (TelemetrySample, AssetModel, TwinEngine)
- api/app.py: FastAPI endpoints for telemetry ingestion and health check
- tests/: pytest-based unit tests
- test.sh: test runner that also builds the package
- AGENTS.md: contributor and architecture guide
- README.md: this document
- READY_TO_PUBLISH: (empty file created upon milestone completion)
How to run locally
- Install dependencies (virtualenv recommended):
pip install -r requirements.txt # if you add, otherwise install via build tooling
- Run tests: ./test.sh
- Run API (for development): uvicorn api.app:app --reload
Notes
- This MVP intentionally uses in-memory state for simplicity; the architecture supports plugging in a SQLite/PostgreSQL cache and a fuller offline sync layer in future iterations.

28
api/app.py Normal file
View File

@ -0,0 +1,28 @@
from fastapi import FastAPI
from pydantic import BaseModel
from typing import Dict, Any
from ar_grid_tutor_mobile_ar_digital_twin_for.core import TelemetrySample, TwinEngine
app = FastAPI(title="AR Grid Tutor DT Twin API")
_engine = TwinEngine()
class TelemetryPayload(BaseModel):
asset_id: str
timestamp: float
metrics: Dict[str, float]
@app.post("/telemetry")
def ingest_telemetry(payload: TelemetryPayload):
sample = TelemetrySample(asset_id=payload.asset_id, timestamp=payload.timestamp, metrics=payload.metrics)
result = _engine.reconcile(sample)
return {"status": "ok", "result": result}
@app.get("/health")
def health():
# Minimal health indicator for orchestration
return {"status": "healthy", "assets_tracked": list(_engine.models.keys())}

18
pyproject.toml Normal file
View File

@ -0,0 +1,18 @@
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "ar-grid-tutor-mobile-ar-digital-twin-for"
version = "0.1.0"
description = "Mobile AR Digital Twin MVP skeleton for on-site renewable asset maintenance"
requires-python = ">=3.9"
readme = "README.md"
dependencies = [
"fastapi>=0.97.0",
"uvicorn>=0.23.0",
"pydantic>=1.10.0",
]
[tool.setuptools.packages.find]
where = ["src"]

View File

@ -0,0 +1,5 @@
"""ar-grid-tutor-mobile-ar-digital-twin-for package init"""
from .core import TelemetrySample, AssetModel, TwinEngine # re-export for convenience
__all__ = ["TelemetrySample", "AssetModel", "TwinEngine"]

View File

@ -0,0 +1,62 @@
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Dict, Any, Optional
@dataclass
class TelemetrySample:
asset_id: str
timestamp: float
metrics: Dict[str, float] # e.g., {"voltage": 480.0, "temp": 65.0}
@dataclass
class AssetModel:
asset_id: str
version: int = 1
schema: Dict[str, Any] = field(default_factory=dict) # canonical baseline or last known state
class TwinEngine:
"""A tiny, production-ready skeleton of a digital twin engine.
Responsibilities (minimal MVP):
- Maintain per-asset models (archive of baseline and version)
- Reconcile incoming telemetry against the archived model to compute deltas
- Update the model with the latest telemetry as baseline for next reconciliation
"""
def __init__(self, initial_models: Optional[Dict[str, AssetModel]] = None):
self.models: Dict[str, AssetModel] = initial_models or {}
def register_model(self, asset_id: str, baseline: Optional[Dict[str, Any]] = None) -> AssetModel:
model = AssetModel(asset_id=asset_id, version=1, schema={"baseline": baseline or {}})
self.models[asset_id] = model
return model
def reconcile(self, sample: TelemetrySample) -> Dict[str, Any]:
"""Compute a simple delta against the current baseline and update state.
Returns a dict with delta and new baseline.
"""
asset_id = sample.asset_id
model = self.models.get(asset_id) or self.register_model(asset_id)
baseline = model.schema.get("baseline", {})
delta: Dict[str, float] = {}
for k, v in sample.metrics.items():
b = baseline.get(k, 0.0)
delta[k] = float(v) - float(b)
# Update baseline to current telemetry for next reconciliation
model.schema["baseline"] = dict(sample.metrics)
model.version += 1
self.models[asset_id] = model
return {
"asset_id": asset_id,
"version": model.version,
"delta": delta,
"new_baseline": model.schema["baseline"],
}

8
test.sh Normal file
View File

@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail
echo "Running tests..."
pytest -q
echo "Building package..."
python3 -m build
echo "All tests passed and build completed."

30
tests/test_engine.py Normal file
View File

@ -0,0 +1,30 @@
import math
import sys
import os
# Ensure the local src package is importable when tests run from repo root
src_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src"))
if src_path not in sys.path:
sys.path.insert(0, src_path)
from ar_grid_tutor_mobile_ar_digital_twin_for.core import TelemetrySample, TwinEngine
def test_reconcile_updates_baseline_and_delta():
engine = TwinEngine()
asset_id = "solar-1"
# initial telemetry comes in; baseline starts empty
sample1 = TelemetrySample(asset_id=asset_id, timestamp=1.0, metrics={"voltage": 480.0, "temp": 25.0})
out1 = engine.reconcile(sample1)
assert out1["asset_id"] == asset_id
assert out1["version"] == 2 # since initial register creates version 1 and reconcile increments to 2
assert isinstance(out1["delta"], dict)
assert out1["delta"]["voltage"] == 480.0 - 0.0
# second sample with changed values
sample2 = TelemetrySample(asset_id=asset_id, timestamp=2.0, metrics={"voltage": 485.0, "temp": 27.0})
out2 = engine.reconcile(sample2)
assert out2["asset_id"] == asset_id
assert out2["version"] == 3
assert math.isclose(out2["delta"]["voltage"], 5.0, rel_tol=1e-6)
assert out2["new_baseline"]["voltage"] == 485.0