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..10adecb --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,28 @@ +# Architecture and Contributor Guide + +Overview +- This repository hosts a production-ready MVP for SolarPulse Viz Real, an open-source framework for real-time visualization and anomaly insights across distributed solar, wind, and storage assets. + +Tech Stack +- Backend: Python 3.9+ with FastAPI for REST APIs +- ML: Scikit-learn (IsolationForest) for unsupervised anomaly detection +- Data: SQLite (via SQLAlchemy) for lightweight persistence in the MVP +- Adapters: Lightweight Python modules for telemetry gateway and inverter fault detector +- Packaging: PyProject.toml with setuptools-based build + +Key Modules +- src/idea46_solarpulse_viz_real/main.py: FastAPI app with endpoints for health and telemetry intake +- src/idea46_solarpulse_viz_real/anomaly.py: IsolationForest-based detector (embedded in app for MVP) +- adapters/telemetry_gateway.py: Starter adapter to parse telemetry payloads +- adapters/inverter_fault_detector.py: Starter adapter for fault detection logic +- tests/: Basic test suite to verify API and anomaly scoring + +Testing & Running +- test.sh: Script to run tests and build (see file for details) +- Tests use pytest; you can extend tests to cover more endpoints and adapters + +Publishing +- When you’re ready to publish, ensure READY_TO_PUBLISH exists at repo root and that tests pass locally. + +Notes +- This MVP emphasizes minimal, correct changes with clean interfaces. Iterate with the swarm by adding more adapters, data contracts, and dashboards in subsequent sprint turns. diff --git a/README.md b/README.md index f0d2035..0ad6466 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,17 @@ -# idea46-solarpulse-viz-real +# SolarPulse Viz Real -Source logic for Idea #46 \ No newline at end of file +SolarPulse Viz is a production-focused framework for real-time visualization and actionable anomaly insights across distributed solar, wind, and storage assets. + +- Mobile-first dashboards with real-time and historical time-series views across multiple sites +- Standardized data contracts and a lightweight interoperability bridge +- Lightweight ML-based anomaly detector (unsupervised models) +- AR maintenance overlays and cross-domain storytelling templates +- Offline-friendly data caching with delta-sync when connectivity returns + +This repository implements a solid, production-grade chunk of that vision as a Python FastAPI service, suitable for extension by agents in sprint-based workflows. + +How to run locally +- Install dependencies: python -m pip install -e . +- Run: uvicorn idea46_solarpulse_viz_real.main:app --reload --port 8000 + +This is a scaffold and MVP focused on the core server and anomaly-detection workflow. See AGENTS.md for architecture and contribution details. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..6ae61a7 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,24 @@ +# Project metadata and build system for a production-grade Python package +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "idea46-solarpulse-viz-real" +version = "0.1.0" +description = "SolarPulse Viz: Real-time Visualization & Anomaly Insights for Distributed Solar, Wind, and Storage" +readme = "README.md" +requires-python = ">=3.9" +dependencies = [ + "fastapi>=0.82.0", + "uvicorn[standard]>=0.18.0", + "pydantic>=1.10.2", + "sqlalchemy>=1.4", + "scikit-learn>=1.3", + "pandas>=1.5", + "numpy>=1.23", + "httpx>=0.24.0", +] + +[tool.setuptools.packages.find] +where = ["src"] diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..ee78d27 --- /dev/null +++ b/src/__init__.py @@ -0,0 +1 @@ +"""Topology marker for src package.""" diff --git a/src/idea46_solarpulse_viz_real/__init__.py b/src/idea46_solarpulse_viz_real/__init__.py new file mode 100644 index 0000000..bbc0cad --- /dev/null +++ b/src/idea46_solarpulse_viz_real/__init__.py @@ -0,0 +1 @@ +"""Idea46 SolarPulse Viz Real package init.""" diff --git a/src/idea46_solarpulse_viz_real/adapters/__init__.py b/src/idea46_solarpulse_viz_real/adapters/__init__.py new file mode 100644 index 0000000..a944c96 --- /dev/null +++ b/src/idea46_solarpulse_viz_real/adapters/__init__.py @@ -0,0 +1 @@ +# Adapters package initializer diff --git a/src/idea46_solarpulse_viz_real/adapters/inverter_fault_detector.py b/src/idea46_solarpulse_viz_real/adapters/inverter_fault_detector.py new file mode 100644 index 0000000..6a28afd --- /dev/null +++ b/src/idea46_solarpulse_viz_real/adapters/inverter_fault_detector.py @@ -0,0 +1,17 @@ +from typing import Dict + + +def detect_fault(telemetry: Dict) -> bool: + """Simple, starter fault-detection heuristic for an inverter. + + This is intentionally lightweight and pluggable for later refinement + with ML models or rule-based checks. + """ + temp = telemetry.get("temp", 0.0) + vcur = telemetry.get("v_current", 0.0) + # Heuristic: flag fault if temperature is out of typical range or current spike + if temp > 80.0 or temp < -10.0: + return True + if vcur > 100.0: + return True + return False diff --git a/src/idea46_solarpulse_viz_real/adapters/telemetry_gateway.py b/src/idea46_solarpulse_viz_real/adapters/telemetry_gateway.py new file mode 100644 index 0000000..266781b --- /dev/null +++ b/src/idea46_solarpulse_viz_real/adapters/telemetry_gateway.py @@ -0,0 +1,21 @@ +from typing import Dict +from ..models import TelemetryEvent + + +def parse_telemetry(payload: Dict) -> TelemetryEvent: + """Parse a raw telemetry payload into a TelemetryEvent model. + + This is a lightweight starter adapter; real deployments would include + robust validation, TLS mutual authentication, and schema versioning. + """ + # Minimal mapping with defaults + return TelemetryEvent( + site_id=payload.get("site_id", "unknown_site"), + asset_id=payload.get("asset_id", "unknown_asset"), + timestamp=payload.get("timestamp", "1970-01-01T00:00:00Z"), + v_current=float(payload.get("v_current", 0.0)), + temp=float(payload.get("temp", 0.0)), + # Support optional vibration data; default to 0.0 if missing + vibration=float(payload.get("vibration", 0.0)), + status=payload.get("status"), + ) diff --git a/src/idea46_solarpulse_viz_real/anomaly.py b/src/idea46_solarpulse_viz_real/anomaly.py new file mode 100644 index 0000000..c269117 --- /dev/null +++ b/src/idea46_solarpulse_viz_real/anomaly.py @@ -0,0 +1,20 @@ +from typing import Dict +import numpy as np +from sklearn.ensemble import IsolationForest + + +class AnomalyEngine: + def __init__(self, seed: int = 42): + # Lightweight seedable model trained on synthetic data representing normal operation + rng = np.random.RandomState(seed) + # Features: [v_current, temp, vibration] + X = rng.normal(loc=[12.0, 35.0, 0.5], scale=[2.0, 5.0, 0.2], size=(1000, 3)) + self.model = IsolationForest(contamination=0.05, random_state=seed) + self.model.fit(X) + + def score(self, sample: Dict[str, float]) -> float: + # Build feature vector in the expected order + x = np.array([[sample.get("v_current", 0.0), sample.get("temp", 0.0), sample.get("vibration", 0.0)]], dtype=float) + # IsolationForest.decision_function is higher for normal instances; we invert to get an anomaly score + score_norm = self.model.decision_function(x)[0] + return float(-score_norm) diff --git a/src/idea46_solarpulse_viz_real/main.py b/src/idea46_solarpulse_viz_real/main.py new file mode 100644 index 0000000..d353a21 --- /dev/null +++ b/src/idea46_solarpulse_viz_real/main.py @@ -0,0 +1,46 @@ +from fastapi import FastAPI +from fastapi.responses import JSONResponse +from pydantic import BaseModel +from .anomaly import AnomalyEngine +from .models import TelemetryEvent +from .adapters.telemetry_gateway import parse_telemetry +import threading + + +app = FastAPI(title="SolarPulse Viz Real MVP") + +# Initialize anomaly engine once +anomaly_engine = AnomalyEngine() + +_lock = threading.Lock() + + +@app.get("/") +async def root(): + return {"status": "SolarPulse Viz Real MVP is running"} + + +@app.get("/health") +async def health(): + return {"status": "ok"} + + +@app.post("/telemetry") +async def telemetry(payload: TelemetryEvent): + # Compute a lightweight anomaly score for the incoming payload + with _lock: + score = anomaly_engine.score({ + "v_current": payload.v_current, + "temp": payload.temp, + # Use actual vibration value if provided (default 0.0) + "vibration": payload.vibration, + }) + + # In a real system we'd persist to a DB and route to dashboards/alerts + return { + "site_id": payload.site_id, + "asset_id": payload.asset_id, + "timestamp": payload.timestamp, + "anomaly_score": score, + "status": payload.status or "OK", + } diff --git a/src/idea46_solarpulse_viz_real/models.py b/src/idea46_solarpulse_viz_real/models.py new file mode 100644 index 0000000..0e6deea --- /dev/null +++ b/src/idea46_solarpulse_viz_real/models.py @@ -0,0 +1,26 @@ +from pydantic import BaseModel +from typing import Optional + + +class TelemetryEvent(BaseModel): + site_id: str + asset_id: str + timestamp: str + v_current: float + temp: float + # Optional vibration metric; default to 0.0 if not provided + vibration: float = 0.0 + status: Optional[str] = None + + +class AlarmEvent(BaseModel): + alarm_id: str + site_id: str + severity: str + timestamp: str + + +class AggregatedSignal(BaseModel): + site_id: str + score: float + version: int diff --git a/test.sh b/test.sh new file mode 100644 index 0000000..dfd6883 --- /dev/null +++ b/test.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo "==> Installing package and dependencies (editable install) before tests" +python -m pip install --upgrade pip setuptools wheel +pip install -e . + +echo "==> Running Python package tests (pytest) and build verification" +pytest -q + +echo "==> Building Python package (python -m build) to verify packaging module" +python3 -m build + +echo "All tests and build steps completed successfully." diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 0000000..7be4be0 --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,35 @@ +import sys +import os +from fastapi.testclient import TestClient + +# Ensure the local src layout is on PYTHONPATH for tests +SRC_PATH = os.path.join(os.path.dirname(__file__), os.pardir, 'src') +SYS_PATH = os.path.abspath(SRC_PATH) +if SYS_PATH not in sys.path: + sys.path.insert(0, SYS_PATH) + +from idea46_solarpulse_viz_real.main import app + + +def test_health_endpoint(): + client = TestClient(app) + resp = client.get("/health") + assert resp.status_code == 200 + assert resp.json() == {"status": "ok"} + + +def test_telemetry_endpoint_basic(): + client = TestClient(app) + payload = { + "site_id": "site-A", + "asset_id": "inverter-1", + "timestamp": "2026-04-23T12:00:00Z", + "v_current": 10.5, + "temp": 34.2, + "status": "OK", + } + resp = client.post("/telemetry", json=payload) + assert resp.status_code == 200 + data = resp.json() + assert data["site_id"] == "site-A" + assert "anomaly_score" in data