# Integral — ML Verification Notebook + React GraphQL Integration + Map UI This document contains three deliverables you requested: **(A) ML verification notebook** (pothole detection & verification), **(B) React dashboard GraphQL queries & subscription wiring**, and **(C) Map UI (Leaflet)** integration. Each section includes runnable code, dependency lists, and quickstart instructions. --- ## A — ML Verification Notebook (Jupyter, Python) **Purpose:** Train a pothole detection model (segmentation + bbox) and produce a confidence score per detection for the verification-service. Exports inference results to the `detection-service` endpoint or writes directly to the Postgres `objects` table. **Notes:** Use labeled images from vehicle dashcams, crowd-sourced uploads, and satellite tiles. This notebook uses PyTorch + torchvision and a simple U-Net-style segmentation + simple classifier for confidence. ### Dependencies ```bash pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118 # or CPU wheel pip install jupyterlab opencv-python scikit-learn geopandas rasterio shapely matplotlib tqdm requests pip install albumentations ``` ### Notebook (save as `ml_verification.ipynb` — here shown as linear Python cells) ```python # cell 1: imports import os import json from pathlib import Path import random import numpy as np import cv2 import torch import torch.nn as nn from torch.utils.data import Dataset, DataLoader import torchvision.transforms as T import albumentations as A from sklearn.model_selection import train_test_split from tqdm import tqdm import requests # cell 2: config DATA_DIR = Path('data') IMAGES_DIR = DATA_DIR/'images' MASKS_DIR = DATA_DIR/'masks' # segmentation masks where potholes marked BATCH_SIZE = 8 NUM_EPOCHS = 20 DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu') MODEL_DIR = Path('model') MODEL_DIR.mkdir(parents=True, exist_ok=True) # cell 3: dataset class PotholeDataset(Dataset): def __init__(self, image_paths, mask_paths, transforms=None): self.images = image_paths self.masks = mask_paths self.transforms = transforms def __len__(self): return len(self.images) def __getitem__(self, idx): img = cv2.imread(str(self.images[idx]), cv2.IMREAD_COLOR) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) mask = cv2.imread(str(self.masks[idx]), cv2.IMREAD_GRAYSCALE) # normalize mask to 0/1 mask = (mask > 127).astype('float32') if self.transforms: augmented = self.transforms(image=img, mask=mask) img = augmented['image'] mask = augmented['mask'] img = img.astype('float32') / 255.0 img = np.transpose(img, (2,0,1)) img_t = torch.tensor(img, dtype=torch.float32) mask_t = torch.tensor(mask, dtype=torch.float32).unsqueeze(0) return img_t, mask_t # cell 4: simple U-Net model class DoubleConv(nn.Module): def __init__(self, in_c, out_c): super().__init__() self.net = nn.Sequential( nn.Conv2d(in_c, out_c, 3, padding=1), nn.ReLU(inplace=True), nn.Conv2d(out_c, out_c, 3, padding=1), nn.ReLU(inplace=True) ) def forward(self,x): return self.net(x) class UNet(nn.Module): def __init__(self, n_channels=3, n_classes=1): super().__init__() self.enc1 = DoubleConv(n_channels, 64) self.pool = nn.MaxPool2d(2) self.enc2 = DoubleConv(64,128) self.enc3 = DoubleConv(128,256) self.enc4 = DoubleConv(256,512) self.up3 = nn.ConvTranspose2d(512,256,2,stride=2) self.dec3 = DoubleConv(512,256) self.up2 = nn.ConvTranspose2d(256,128,2,stride=2) self.dec2 = DoubleConv(256,128) self.up1 = nn.ConvTranspose2d(128,64,2,stride=2) self.dec1 = DoubleConv(128,64) self.final = nn.Conv2d(64, n_classes, 1) def forward(self,x): e1 = self.enc1(x) e2 = self.enc2(self.pool(e1)) e3 = self.enc3(self.pool(e2)) e4 = self.enc4(self.pool(e3)) d3 = self.dec3(torch.cat([self.up3(e4), e3], dim=1)) d2 = self.dec2(torch.cat([self.up2(d3), e2], dim=1)) d1 = self.dec1(torch.cat([self.up1(d2), e1], dim=1)) out = self.final(d1) return torch.sigmoid(out) # cell 5: prepare data lists (you must provide matching images & masks filenames) image_files = sorted(list((IMAGES_DIR).glob('*.jpg'))) mask_files = sorted(list((MASKS_DIR).glob('*.png'))) train_imgs, val_imgs, train_masks, val_masks = train_test_split(image_files, mask_files, test_size=0.2, random_state=42) transform = A.Compose([ A.Resize(256,256), A.HorizontalFlip(p=0.5), A.RandomBrightnessContrast(p=0.3), ]) train_ds = PotholeDataset(train_imgs, train_masks, transforms=transform) val_ds = PotholeDataset(val_imgs, val_masks, transforms=A.Compose([A.Resize(256,256)])) train_loader = DataLoader(train_ds, batch_size=BATCH_SIZE, shuffle=True) val_loader = DataLoader(val_ds, batch_size=BATCH_SIZE, shuffle=False) # cell 6: training loop model = UNet().to(DEVICE) optimizer = torch.optim.Adam(model.parameters(), lr=1e-4) criterion = nn.BCELoss() for epoch in range(NUM_EPOCHS): model.train() running_loss = 0.0 for imgs, masks in tqdm(train_loader, desc=f'train {epoch}'): imgs = imgs.to(DEVICE); masks = masks.to(DEVICE) preds = model(imgs) loss = criterion(preds, masks) optimizer.zero_grad(); loss.backward(); optimizer.step() running_loss += loss.item() print(f'Epoch {epoch} loss {running_loss/len(train_loader):.4f}') # validation model.eval() val_loss = 0.0 with torch.no_grad(): for imgs, masks in val_loader: imgs = imgs.to(DEVICE); masks = masks.to(DEVICE) preds = model(imgs) val_loss += criterion(preds, masks).item() print(f'Val loss {val_loss/len(val_loader):.4f}') torch.save(model.state_dict(), MODEL_DIR/f'unet_epoch_{epoch}.pt') # cell 7: inference helper -> produce detections import scipy.ndimage as ndi def infer_and_extract(img_path, model, threshold=0.3, min_area=50): img = cv2.imread(img_path) h0,w0 = img.shape[:2] img_r = cv2.resize(img, (256,256)) x = np.transpose(img_r.astype('float32')/255.0, (2,0,1)) x = torch.tensor(x).unsqueeze(0).to(DEVICE) with torch.no_grad(): pred = model(x)[0,0].cpu().numpy() # resize mask back mask = cv2.resize((pred>threshold).astype('uint8'), (w0,h0)) labeled, n = ndi.label(mask) detections = [] for region in range(1,n+1): ys, xs = np.where(labeled==region) if len(xs) < min_area: continue x_min, x_max = xs.min(), xs.max() y_min, y_max = ys.min(), ys.max() area = len(xs) conf = pred[ys, xs].mean() # approximate confidence detections.append({ 'bbox': [int(x_min), int(y_min), int(x_max), int(y_max)], 'area': int(area), 'confidence': float(conf) }) return detections, mask # cell 8: export detections -> call detection-service DETECTION_API = os.getenv('DETECTION_API','http://localhost:3001/detect') def export_detection(location, image_url, detections, provenance): # choose highest confidence detection if not detections: return None top = max(detections, key=lambda d: d['confidence']) payload = { 'namespace':'satellite', 'type':'pothole-detection', 'timestamp':None, 'location': location, 'severity': int(min(5, max(1, int(top['area']/500)))), # heuristic 'images':[image_url], 'provenance': provenance } try: r = requests.post(DETECTION_API, json=payload, timeout=5) return r.json() except Exception as e: print('export failed', e) return None # usage example if __name__ == '__main__': model.load_state_dict(torch.load(MODEL_DIR/'unet_epoch_19.pt', map_location=DEVICE)) test_img = 'data/ground/test1.jpg' dets, mask = infer_and_extract(test_img, model) print(dets) # export with a dummy location/provenance print(export_detection({'lat':-29.12,'lon':26.22}, 'https://example.com/test1.jpg', dets, {'source':'ml-run','license':'CC0'})) ``` --- ## B — React Dashboard: GraphQL queries & subscriptions (Apollo Client) **Goal:** Wire the dashboard to the Apollo Server created earlier. Provide query examples, subscription usage, and UI integration (hooks + state). ### Dependencies ```bash npm install @apollo/client graphql subscriptions-transport-ws graphql-ws npm install leaflet react-leaflet ``` > Note: `graphql-ws` is recommended for modern websockets; for Apollo v3 use `subscriptions-transport-ws` or adapt to your server. ### Apollo client setup (src/apollo.js) ```js import { ApolloClient, InMemoryCache, HttpLink, split } from '@apollo/client'; import { GraphQLWsLink } from '@apollo/client/link/subscriptions'; import { createClient } from 'graphql-ws'; import { getMainDefinition } from '@apollo/client/utilities'; const httpLink = new HttpLink({ uri: 'http://localhost:4000/graphql' }); const wsLink = new GraphQLWsLink(createClient({ url: 'ws://localhost:4000/graphql' })); const splitLink = split( ({ query }) => { const def = getMainDefinition(query); return def.kind === 'OperationDefinition' && def.operation === 'subscription'; }, wsLink, httpLink ); export const client = new ApolloClient({ link: splitLink, cache: new InMemoryCache(), }); ``` ### Queries & Subscriptions (src/graphql/queries.js) ```js import { gql } from '@apollo/client'; export const LIST_FAULTS = gql` query ListInfraFaults($limit:Int,$offset:Int){ listInfraFaults(limit:$limit,offset:$offset){ id namespace type timestamp severity confirmed images provenance } } `; export const FAULT_CREATED = gql` subscription { faultCreated { id namespace type timestamp location severity confirmed images provenance } } `; export const FAULT_CONFIRMED = gql` subscription { faultConfirmed { id confirmed } } `; export const PAYOUT_UPDATED = gql` subscription { payoutUpdated { id faultId amountMinorUnits currency payeeId status txRef } } `; ``` ### React hook usage (src/hooks/useFaults.js) ```js import { useQuery, useSubscription } from '@apollo/client'; import { LIST_FAULTS, FAULT_CREATED, FAULT_CONFIRMED } from '../graphql/queries'; export function useFaults() { const { data, loading, error, fetchMore } = useQuery(LIST_FAULTS, { variables: { limit: 50, offset: 0 } }); useSubscription(FAULT_CREATED, { onSubscriptionData: ({ client, subscriptionData }) => { const newFault = subscriptionData.data.faultCreated; // optional: update cache or refetch client.cache.modify({ fields: { listInfraFaults(existing = []) { const newRef = client.cache.writeFragment({ data: newFault, fragment: gql`fragment NewFault on InfrastructureFault { id namespace type timestamp severity confirmed images provenance }` }); return [newRef, ...existing]; } } }); } }); useSubscription(FAULT_CONFIRMED, { onSubscriptionData: ({ client, subscriptionData }) => { const changed = subscriptionData.data.faultConfirmed; // update cache entry client.cache.modify({ id: client.cache.identify({ __typename: 'InfrastructureFault', id: changed.id }), fields: { confirmed() { return changed.confirmed; } } }); } }); return { data, loading, error, fetchMore }; } ``` ### Wiring into dashboard component (snippet) ```js import React from 'react'; import { useFaults } from './hooks/useFaults'; export default function InfraPanel(){ const { data, loading } = useFaults(); if(loading) return
Loading...
; return (
{data.listInfraFaults.map(f => (

{f.type} — severity {f.severity}

Confirmed: {String(f.confirmed)}

))}
); } ``` --- ## C — Map UI (Leaflet + React-Leaflet) with Satellite overlays **Goal:** Display pothole markers, heatmap, and satellite overlays (Sentinel tile layers or Mapbox). Clicking a marker opens verification panel and create/settle actions. ### Dependencies ```bash npm install leaflet react-leaflet leaflet.heat ``` Also add CSS in your app root for leaflet: ```css /* index.css */ .leaflet-container { height: 100%; width: 100%; } ``` ### Map component (src/components/MapView.jsx) ```jsx import React, { useEffect, useState } from 'react'; import { MapContainer, TileLayer, Marker, Popup, Circle } from 'react-leaflet'; import 'leaflet/dist/leaflet.css'; import L from 'leaflet'; // fix default icon issues in many bundlers delete L.Icon.Default.prototype._getIconUrl; L.Icon.Default.mergeOptions({ iconRetinaUrl: require('leaflet/dist/images/marker-icon-2x.png'), iconUrl: require('leaflet/dist/images/marker-icon.png'), shadowUrl: require('leaflet/dist/images/marker-shadow.png') }); import { useFaults } from '../hooks/useFaults'; export default function MapView(){ const { data } = useFaults(); const [center] = useState([-29.12,26.22]); return ( {/* Optional Satellite overlay with Mapbox (requires token): */} {/* */} {data && data.listInfraFaults.map(f => (
{f.type}

Severity: {f.severity}

Confirmed: {String(f.confirmed)}

{f.images && f.images.length>0 && pothole}
))}
); } ``` ### Heatmap (optional) Use `leaflet.heat` plugin. Convert faults into weighted points by severity. --- ## Deployment & Running 1. Start Postgres + Redis + Apollo server (see earlier `integral-apollo-server` canvas doc). Run migrations. 2. Start the Apollo client React app (`npm start`) with `client` configured to `http://localhost:4000/graphql` and WS `ws://localhost:4000/graphql`. 3. Prepare training data and run `ml_verification.ipynb` to train a model and generate detection exports to the detection-service endpoint. 4. Use the dashboard to observe faults appearing in real-time and interact with map UI to verify and settle payouts. --- ## Next suggestions - I can **export the ML notebook as an actual `.ipynb` file** and attach it for download. - I can **generate the full React project files** (components, hooks, package.json) in the canvas and zip them. - I can **produce a small sample dataset** (synthetic images + masks) so you can run training quickly. Which of those would you like me to produce now?