Skip to content

Commit 25dbfce

Browse files
CreatmanCEOclaude
andcommitted
fix: interference layer, anomaly recommendations, baseline rounding, card limit
FIX 3: InterferenceLayer — pulsed lines between wells <2km apart - Red for active-active pairs, gray for inactive - Width proportional to combined yield - Added to LayerControls as "Interference" checkbox FIX 5: Differentiated anomaly recommendations by severity - >40% decline: "URGENT: Immediate rehabilitation" - 25-40%: "Schedule assessment within 2 weeks" - 15-25%: "Monitor closely" - Sensor fault: "Deploy field team to replace sensor" FIX 6: Rounded sensor fault baseline to 2 decimal places FIX 7: Network scan limited to top 10 anomalies + summary card when scanning all wells (prevents 30+ card flooding) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent b46b1d2 commit 25dbfce

6 files changed

Lines changed: 177 additions & 3 deletions

File tree

backend/eval/results/openrouter_deepseek_deepseek-chat-v3-0324_results.jsonl

Lines changed: 48 additions & 0 deletions
Large diffs are not rendered by default.

backend/eval/results/summary.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"openrouter/deepseek/deepseek-chat-v3-0324": {
3+
"model": "openrouter/deepseek/deepseek-chat-v3-0324",
4+
"total_cases": 96,
5+
"correct_tool_calls": 66,
6+
"accuracy": 0.6875,
7+
"schema_valid_count": 38,
8+
"schema_compliance": 0.3958333333333333,
9+
"latency_p50": 2862.5,
10+
"latency_p95": 10011.75,
11+
"total_cost_usd": 0.03359899999999999,
12+
"cost_per_request": 0.00034998958333333323,
13+
"total_tokens_in": 226010,
14+
"total_tokens_out": 7001,
15+
"avg_tokens_per_request": 2427.1979166666665,
16+
"error_count": 0,
17+
"error_rate": 0.0
18+
}
19+
}

backend/tools/detect_anomalies.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,11 @@ def _detect_debit_decline(df: pd.DataFrame, well_id: str) -> list[AnomalyCard]:
4040
value_current=round(mean_q4, 2),
4141
value_baseline=round(mean_q1, 2),
4242
change_pct=round(change_pct, 1),
43-
recommendation="Schedule pump inspection and well rehabilitation assessment",
43+
recommendation=(
44+
"URGENT: Immediate well rehabilitation required. Consider reducing pumping rate." if change_pct < -40
45+
else "Schedule pump test and well assessment within 2 weeks." if change_pct < -25
46+
else "Monitor closely. Schedule inspection if decline continues."
47+
),
4448
))
4549
return cards
4650

@@ -108,9 +112,9 @@ def _detect_sensor_fault(df: pd.DataFrame, well_id: str) -> list[AnomalyCard]:
108112
title=f"Sensor fault in {col}",
109113
description=f"{col} shows {max_run} consecutive zero readings — likely sensor malfunction",
110114
value_current=0.0,
111-
value_baseline=float(np.mean(values[values != 0])) if (values != 0).any() else 0.0,
115+
value_baseline=round(float(np.mean(values[values != 0])), 2) if (values != 0).any() else 0.0,
112116
change_pct=-100.0,
113-
recommendation=f"Inspect {col} sensor; replace if confirmed faulty",
117+
recommendation=f"Deploy field team to inspect and replace faulty {col} sensor.",
114118
))
115119
return cards
116120

@@ -148,4 +152,25 @@ def detect_anomalies(well_id: str | None = None) -> list[AnomalyCard]:
148152
severity_order = {"critical": 0, "high": 1, "medium": 2, "low": 3}
149153
cards.sort(key=lambda c: severity_order.get(c.severity, 4))
150154

155+
# When scanning all wells, limit output to top anomalies to avoid flooding UI
156+
if well_id is None and len(cards) > 10:
157+
total = len(cards)
158+
by_severity = {}
159+
for c in cards:
160+
by_severity[c.severity] = by_severity.get(c.severity, 0) + 1
161+
summary = ", ".join(f"{v} {k}" for k, v in by_severity.items())
162+
# Keep top 10, add summary card
163+
cards = cards[:10]
164+
cards.append(AnomalyCard(
165+
severity="low",
166+
well_id="NETWORK",
167+
anomaly_type="debit_decline",
168+
title=f"Network summary: {total} anomalies detected",
169+
description=f"Showing top 10 by severity. Total breakdown: {summary}.",
170+
value_current=0,
171+
value_baseline=0,
172+
change_pct=0,
173+
recommendation="Use 'detect_anomalies' on individual wells for detailed analysis.",
174+
))
175+
151176
return cards
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
'use client';
2+
3+
import { useMemo } from "react";
4+
import { Source, Layer } from "react-map-gl/maplibre";
5+
import type { WellsGeoJSON } from "@/types";
6+
import type { FeatureCollection, LineString } from "geojson";
7+
8+
interface Props {
9+
wellsGeoJSON: WellsGeoJSON;
10+
}
11+
12+
const INTERFERENCE_RADIUS_KM = 2;
13+
14+
function haversineKm(lat1: number, lon1: number, lat2: number, lon2: number): number {
15+
const R = 6371;
16+
const dLat = ((lat2 - lat1) * Math.PI) / 180;
17+
const dLon = ((lon2 - lon1) * Math.PI) / 180;
18+
const a =
19+
Math.sin(dLat / 2) ** 2 +
20+
Math.cos((lat1 * Math.PI) / 180) * Math.cos((lat2 * Math.PI) / 180) * Math.sin(dLon / 2) ** 2;
21+
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
22+
}
23+
24+
export function InterferenceLayer({ wellsGeoJSON }: Props) {
25+
const linesGeoJSON = useMemo<FeatureCollection<LineString>>(() => {
26+
const features: FeatureCollection<LineString>["features"] = [];
27+
const wells = wellsGeoJSON.features;
28+
29+
for (let i = 0; i < wells.length; i++) {
30+
for (let j = i + 1; j < wells.length; j++) {
31+
const [lon1, lat1] = wells[i].geometry.coordinates;
32+
const [lon2, lat2] = wells[j].geometry.coordinates;
33+
const dist = haversineKm(lat1, lon1, lat2, lon2);
34+
35+
if (dist < INTERFERENCE_RADIUS_KM) {
36+
const bothActive = wells[i].properties.status === "active" && wells[j].properties.status === "active";
37+
const totalYield = wells[i].properties.current_yield_ls + wells[j].properties.current_yield_ls;
38+
39+
features.push({
40+
type: "Feature",
41+
geometry: {
42+
type: "LineString",
43+
coordinates: [[lon1, lat1], [lon2, lat2]],
44+
},
45+
properties: {
46+
well_a: wells[i].properties.id,
47+
well_b: wells[j].properties.id,
48+
distance_km: Math.round(dist * 100) / 100,
49+
total_yield: totalYield,
50+
both_active: bothActive,
51+
width: Math.max(1, Math.min(4, totalYield / 15)),
52+
},
53+
});
54+
}
55+
}
56+
}
57+
58+
return { type: "FeatureCollection", features };
59+
}, [wellsGeoJSON]);
60+
61+
return (
62+
<Source id="interference-lines" type="geojson" data={linesGeoJSON}>
63+
<Layer
64+
id="interference-lines-layer"
65+
type="line"
66+
paint={{
67+
"line-color": ["case", ["get", "both_active"], "#ef4444", "#9ca3af"],
68+
"line-width": ["get", "width"],
69+
"line-dasharray": [4, 3],
70+
"line-opacity": 0.6,
71+
}}
72+
/>
73+
</Source>
74+
);
75+
}

frontend/src/components/Map/LayerControls.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useMapStore } from "@/stores/mapStore";
55
const LAYERS = [
66
{ id: "wells", label: "Wells", icon: "⬤" },
77
{ id: "depression_cones", label: "Depression Cones", icon: "◎" },
8+
{ id: "interference", label: "Interference", icon: "⟷" },
89
];
910

1011
export function LayerControls() {

frontend/src/components/Map/WellsMap.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type { WellProperties } from "@/types";
99
import { WellPopup } from "./WellPopup";
1010
import { LayerControls } from "./LayerControls";
1111
import { DepressionConeLayer } from "./DepressionConeLayer";
12+
import { InterferenceLayer } from "./InterferenceLayer";
1213

1314
const MAP_STYLE = "https://tiles.openfreemap.org/styles/positron";
1415

@@ -154,6 +155,11 @@ export function WellsMap() {
154155
<DepressionConeLayer wellsGeoJSON={wellsGeoJSON} />
155156
)}
156157

158+
{/* Interference lines */}
159+
{activeLayers.includes("interference") && wellsGeoJSON && (
160+
<InterferenceLayer wellsGeoJSON={wellsGeoJSON} />
161+
)}
162+
157163
{/* Well popup */}
158164
{popupWell && (
159165
<WellPopup

0 commit comments

Comments
 (0)