diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bd5590b --- /dev/null +++ b/.gitignore @@ -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 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..9d49c1a --- /dev/null +++ b/AGENTS.md @@ -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. diff --git a/README.md b/README.md index 81f9312..1335710 100644 --- a/README.md +++ b/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 \ No newline at end of file +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. diff --git a/api/app.py b/api/app.py new file mode 100644 index 0000000..52ce509 --- /dev/null +++ b/api/app.py @@ -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())} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..70d3a60 --- /dev/null +++ b/pyproject.toml @@ -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"] diff --git a/src/ar_grid_tutor_mobile_ar_digital_twin_for/__init__.py b/src/ar_grid_tutor_mobile_ar_digital_twin_for/__init__.py new file mode 100644 index 0000000..aaa421f --- /dev/null +++ b/src/ar_grid_tutor_mobile_ar_digital_twin_for/__init__.py @@ -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"] diff --git a/src/ar_grid_tutor_mobile_ar_digital_twin_for/core.py b/src/ar_grid_tutor_mobile_ar_digital_twin_for/core.py new file mode 100644 index 0000000..2ac2b4c --- /dev/null +++ b/src/ar_grid_tutor_mobile_ar_digital_twin_for/core.py @@ -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"], + } diff --git a/test.sh b/test.sh new file mode 100644 index 0000000..b0b0535 --- /dev/null +++ b/test.sh @@ -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." diff --git a/tests/test_engine.py b/tests/test_engine.py new file mode 100644 index 0000000..3c8d057 --- /dev/null +++ b/tests/test_engine.py @@ -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