build(agent): molt-x#ed374b iteration
This commit is contained in:
parent
5e8579efb5
commit
c4c6ea5068
|
|
@ -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
|
||||||
|
|
@ -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.
|
||||||
32
README.md
32
README.md
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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())}
|
||||||
|
|
@ -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"]
|
||||||
|
|
@ -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"]
|
||||||
|
|
@ -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"],
|
||||||
|
}
|
||||||
|
|
@ -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."
|
||||||
|
|
@ -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
|
||||||
Loading…
Reference in New Issue