demeter

Autonomous Hydroponic Intelligence
commit c359a2f6f06ce4a751f675f6b387f279c040f8db
parent 12fc01b1a11f164df1e54528b54e785ba81f7a94
Author: maydayv7 <maydayv7@gmail.com>
Date:   Mon, 30 Mar 2026 00:08:21 +0530

Add File

Diffstat:
Asimulator/research_simulator.ipynb | 364+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 364 insertions(+), 0 deletions(-)

diff --git a/simulator/research_simulator.ipynb b/simulator/research_simulator.ipynb @@ -0,0 +1,364 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "header" + }, + "source": [ + "# Research Grade Hydroponics Simulator\n", + "\n", + "This notebook provides a comprehensive environment for simulating and managing a hydroponic crop system. It integrates theoretical physics, deep learning, and real-time synchronization with cloud services.\n", + "\n", + "**Key Features:**\n", + "* **Physics-Informed Core**: Combines first-principles differential equations with a Physics-Informed Neural Network (PINN) for \"biological chaos\" modeling.\n", + "* **Reinforcement Learning**: Built-in Gymnasium environment for training optimal control agents using Proximal Policy Optimization (PPO).\n", + "* **Azure Digital Twins**: Real-time telemetry synchronization with Azure-hosted Digital Twins.\n", + "* **Research API**: Exposes a FastAPI server with ngrok tunneling for remote telemetry and control." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dependencies" + }, + "source": [ + "## Setup & Dependencies" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "pip_installs" + }, + "outputs": [], + "source": [ + "!pip install torch azure-digitaltwins-core azure-identity Pillow numpy fastapi uvicorn nest_asyncio google-genai gymnasium stable-baselines3 pyngrok -q\n", + "!pip install diffusers transformers accelerate torchvision -q" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "imports" + }, + "outputs": [], + "source": [ + "import base64\n", + "import time\n", + "import os\n", + "import json\n", + "import threading\n", + "import uvicorn\n", + "import nest_asyncio\n", + "import numpy as np\n", + "import torch\n", + "import torch.nn as nn\n", + "import gymnasium as gym\n", + "from gymnasium import spaces\n", + "from io import BytesIO\n", + "from collections import deque\n", + "from dataclasses import dataclass, asdict\n", + "from fastapi import FastAPI\n", + "from pydantic import BaseModel\n", + "from PIL import Image\n", + "from stable_baselines3 import PPO\n", + "from azure.identity import DeviceCodeCredential\n", + "from azure.digitaltwins.core import DigitalTwinsClient\n", + "from pyngrok import ngrok\n", + "\n", + "nest_asyncio.apply()\n", + "\n", + "# Configuration\n", + "MODEL_PATH = \"models/PPO/lettuce_brain_v1.zip\"\n", + "HISTORY_LEN = 20\n", + "ADT_URL = \"simulator.api.krc.digitaltwins.azure.net\"\n", + "TWIN_ID = \"HydrophonicTank\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "physics_header" + }, + "source": [ + "## 1. Physics-Informed Architecture\n", + "\n", + "Defining the `ResidualPhysicsNet` which captures non-linear biological effects that standard textbook equations often miss." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "physics_net" + }, + "outputs": [], + "source": [ + "class ResidualPhysicsNet(nn.Module):\n", + " def __init__(self, state_dim=7, action_dim=4):\n", + " super().__init__()\n", + " self.net = nn.Sequential(\n", + " nn.Linear(state_dim + action_dim, 64),\n", + " nn.Tanh(),\n", + " nn.Linear(64, 64),\n", + " nn.ReLU(),\n", + " nn.Linear(64, state_dim)\n", + " )\n", + " with torch.no_grad():\n", + " self.net[-1].weight.mul_(0.01)\n", + "\n", + " def forward(self, state, action):\n", + " x = torch.cat([state, action], dim=-1)\n", + " return self.net(x)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "rl_header" + }, + "source": [ + "## 2. Gymnasium Environment\n", + "\n", + "Standardized RL interface for training control agents." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "hydro_env" + }, + "outputs": [], + "source": [ + "class HydroponicsEnv(gym.Env):\n", + " def __init__(self):\n", + " super().__init__()\n", + " self.high_obs = np.array([14.0, 5.0, 40.0, 50.0, 100.0, 5.0, 5000.0], dtype=np.float32)\n", + " self.low_obs = np.array([0.0, 0.0, 5.0, 0.0, 0.0, 0.0, 0.0], dtype=np.float32)\n", + " self.observation_space = spaces.Box(low=self.low_obs, high=self.high_obs, dtype=np.float32)\n", + " self.action_space = spaces.Box(low=0.0, high=1.0, shape=(4,), dtype=np.float32)\n", + " \n", + " self.state = None\n", + " self.residual_model = ResidualPhysicsNet(7, 4)\n", + "\n", + " def reset(self, seed=None, options=None):\n", + " super().reset(seed=seed)\n", + " self.state = np.array([\n", + " 6.0 + np.random.uniform(-0.2, 0.2), # pH\n", + " 1.5 + np.random.uniform(-0.1, 0.1), # EC\n", + " 20.0, 24.0, 60.0, 1.0, 10.0\n", + " ], dtype=np.float32)\n", + " return self.state, {}\n", + "\n", + " def _physics_prior(self, state, action):\n", + " ph, ec, w_temp, a_temp, hum, vpd, biomass = state\n", + " acid, base, nutes, fan = action\n", + " d_ph = (base * 0.5) - (acid * 0.5) + (0.01 * biomass / 1000)\n", + " uptake = 0.05 * biomass * vpd\n", + " d_ec = (nutes * 0.2) - (uptake / 100.0)\n", + " stress = np.abs(vpd - 1.0)\n", + " growth_rate = 0.1 * (1.0 - min(stress, 1.0))\n", + " d_biomass = biomass * growth_rate\n", + " d_temp = -1.0 * fan + 0.1 \n", + " d_hum = -5.0 * fan + 2.0\n", + " new_vpd = 0.61 * np.exp((17.27 * a_temp)/(a_temp+237.3)) * (1 - hum/100)\n", + " d_vpd = new_vpd - vpd\n", + " return np.array([d_ph, d_ec, 0, d_temp, d_hum, d_vpd, d_biomass], dtype=np.float32)\n", + "\n", + " def step(self, action):\n", + " action = np.array(action, dtype=np.float32)\n", + " d_physics = self._physics_prior(self.state, action)\n", + " with torch.no_grad():\n", + " st_t = torch.tensor(self.state, dtype=torch.float32)\n", + " at_t = torch.tensor(action, dtype=torch.float32)\n", + " d_residual = self.residual_model(st_t, at_t).numpy()\n", + " \n", + " self.state += d_physics + (d_residual * 0.1)\n", + " self.state = np.clip(self.state, self.low_obs, self.high_obs)\n", + " ph, ec, _, _, _, _, biomass = self.state\n", + " reward = -abs(ph - 6.0) * 10.0 - abs(ec - 1.5) * 5.0 + biomass * 0.1\n", + " terminated = bool(ph < 4.0 or ph > 8.0)\n", + " return self.state, float(reward), terminated, False, {}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "training_header" + }, + "source": [ + "## 3. Agent Training (PPO)\n", + "\n", + "Train the agent using Stable Baselines3. This simulates the optimization of resource usage for growth." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "training_loop" + }, + "outputs": [], + "source": [ + "if __name__ == \"__main__\":\n", + " env = HydroponicsEnv()\n", + " os.makedirs(\"models/PPO\", exist_ok=True)\n", + " os.makedirs(\"logs\", exist_ok=True)\n", + "\n", + " model = PPO(\"MlpPolicy\", env, verbose=1, tensorboard_log=\"logs\")\n", + " print(\"🤖 Training Agent...\")\n", + " model.learn(total_timesteps=10000)\n", + " \n", + " model.save(MODEL_PATH)\n", + " print(f\"💾 Model saved to: {MODEL_PATH}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "azure_header" + }, + "source": [ + "## 4. Cloud Integration (Azure Digital Twins)\n", + "\n", + "Logic for synchronizing local simulator state with a cloud-hosted Digital Twin." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "azure_sync" + }, + "outputs": [], + "source": [ + "try:\n", + " credential = DeviceCodeCredential()\n", + " client = DigitalTwinsClient(ADT_URL, credential)\n", + " \n", + " def sync_to_azure(state):\n", + " ph, ec, water_temp, air_temp, humidity, vpd, biomass = state\n", + " payload = {\n", + " \"ph\": float(ph),\n", + " \"ec\": float(ec),\n", + " \"water_temp\": float(water_temp),\n", + " \"air_temp\": float(air_temp),\n", + " \"humidity\": float(humidity),\n", + " \"vpd\": float(vpd),\n", + " \"biomass_g\": float(biomass)\n", + " }\n", + " client.publish_telemetry(TWIN_ID, payload)\n", + "\n", + " def initialize_twin_state():\n", + " initial_patch = [\n", + " {\"op\": \"add\", \"path\": \"/ph\", \"value\": 6.0},\n", + " {\"op\": \"add\", \"path\": \"/ec\", \"value\": 1.5},\n", + " {\"op\": \"add\", \"path\": \"/biomass_g\", \"value\": 10.0}\n", + " ]\n", + " client.update_digital_twin(TWIN_ID, initial_patch)\n", + " print(\"✅ Twin initialized!\")\n", + "except NameError:\n", + " print(\"⚠️ Azure packages or identity missing. Skipping initialization.\")\n", + "except Exception as e:\n", + " print(f\"⚠️ Azure error: {e}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "api_header" + }, + "source": [ + "## 5. Research Simulator API & Ngrok Gateway\n", + "\n", + "The simulation engine and FastAPI server provide a programmatic interface for remote monitoring and action." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "api_server" + }, + "outputs": [], + "source": [ + "@dataclass\n", + "class FarmStateData:\n", + " ph: float; ec: float; water_temp: float; air_temp: float\n", + " humidity: float; vpd: float; biomass_g: float; tank_volume_l: float\n", + "\n", + "class FarmAction(BaseModel):\n", + " acid_dosage_ml: float = 0.0\n", + " base_dosage_ml: float = 0.0\n", + " nutrient_dosage_ml: float = 0.0\n", + " fan_speed_pct: float = 0.0\n", + " debug_force_ph: float | None = None\n", + "\n", + "class DigitalTwin:\n", + " def __init__(self):\n", + " self.state = np.array([6.0, 1.5, 20.0, 24.0, 60.0, 1.0, 10.0], dtype=np.float32)\n", + " self.plant_health = 100.0\n", + " self.residual_model = ResidualPhysicsNet(7, 4)\n", + " self.history = {k: deque([v]*5, maxlen=HISTORY_LEN) for k,v in zip([\"ph\", \"ec\", \"water_temp\", \"air_temp\", \"humidity\", \"vpd\"], self.state[:6])}\n", + "\n", + " def step(self, action: FarmAction = None):\n", + " if action is None: action = FarmAction()\n", + " u = np.array([action.acid_dosage_ml/10.0, action.base_dosage_ml/10.0, action.nutrient_dosage_ml/20.0, action.fan_speed_pct/100.0], dtype=np.float32)\n", + " \n", + " ph, ec, wt, at, hum, vpd, bio = self.state\n", + " d_physics = np.array([(u[1]-u[0])*0.5 + 0.001*bio, u[2]*0.2 - (0.02*bio*vpd)/100.0, 0, 0.1 - u[3]*1.5, 1.0 - u[3]*5.0, 0, 0.1*bio*(1.0-abs(vpd-1.0))], dtype=np.float32)\n", + " \n", + " with torch.no_grad():\n", + " nn_delta = self.residual_model(torch.tensor(self.state), torch.tensor(u)).numpy()\n", + " \n", + " self.state += d_physics + (nn_delta * 0.05)\n", + " self.state[0] = action.debug_force_ph if action.debug_force_ph else self.state[0]\n", + " self.state[3:5] = np.clip(self.state[3:5], [0, 0], [50, 100])\n", + " \n", + " for i, k in enumerate(self.history.keys()): self.history[k].append(float(self.state[i]))\n", + " return Image.new('RGB', (512, 512), (50, 50, 50))\n", + "\n", + "app = FastAPI()\n", + "sim = DigitalTwin()\n", + "\n", + "@app.get(\"/simulation/state\")\n", + "async def get_state():\n", + " img = sim.step()\n", + " buf = BytesIO(); img.save(buf, format=\"PNG\")\n", + " return {\"sensor_window\": {k: list(v) for k,v in sim.history.items()}, \"metadata\": {\"health\": round(float(sim.plant_health), 1), \"biomass\": round(float(sim.state[6]), 2)}, \"image\": base64.b64encode(buf.getvalue()).decode(\"utf-8\")}\n", + "\n", + "@app.post(\"/simulation/action\")\n", + "async def handle_action(action: FarmAction):\n", + " sim.step(action)\n", + " try: sync_to_azure(sim.state)\n", + " except: pass\n", + " return {\"status\": \"success\", \"ph\": float(sim.state[0])}\n", + "\n", + "def run_api():\n", + " print(\"🚀 Research Simulator API Online (Port 3001)\")\n", + " uvicorn.run(app, host=\"0.0.0.0\", port=3001, log_level=\"error\")\n", + "\n", + "threading.Thread(target=run_api, daemon=True).start()\n", + "\n", + "# Exposed via Ngrok\n", + "try:\n", + " ngrok.set_auth_token(NGROK_TOKEN)\n", + " public_url = ngrok.connect(3001).public_url\n", + " print(f\"🌍 Tunnel Ready: {public_url}\")\n", + "except Exception as e: print(f\"⚠️ Ngrok failed: {e}\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +}