#!/bin/bash
# Kaiju Probability Index (KPI)
# Composite anomaly score from seismic + thermal data
# Alias: kpi
#
# "If it hits 0.7, nobody goes home."
export DISPLAY=:0
# Cache locations (shared with other maritime scripts)
seismic_cache="$HOME/.cache/maritime/seismic/day.json"
sst_cache="$HOME/.cache/maritime/sst"
kpi_cache="$HOME/.cache/maritime/kpi"
kpi_report="$kpi_cache/report.txt"
kpi_history="$kpi_cache/history.csv"
# Ensure cache exists
mkdir -p "$kpi_cache"
# Weight factors for composite score
SEISMIC_WEIGHT=0.35
THERMAL_WEIGHT=0.30
FREQUENCY_WEIGHT=0.20
DEPTH_WEIGHT=0.15
# Thresholds
THRESHOLD_YELLOW=0.40
THRESHOLD_RED=0.70
# ══════════════════════════════════════════════════════════════════
# DATA COLLECTION FUNCTIONS
# ══════════════════════════════════════════════════════════════════
# Get seismic score (0.0 - 1.0)
# Based on: count of events, max magnitude, clustering
get_seismic_score() {
if [ ! -f "$seismic_cache" ]; then
echo "0.0"
return
fi
local count max_mag score
# Count events in last 24h
count=$(jq '.metadata.count // 0' "$seismic_cache" 2>/dev/null)
# Get max magnitude
max_mag=$(jq '[.features[].properties.mag // 0] | max // 0' "$seismic_cache" 2>/dev/null)
# Count significant events (M4.0+)
local significant
significant=$(jq '[.features[].properties.mag // 0 | select(. >= 4.0)] | length' "$seismic_cache" 2>/dev/null)
# Calculate score
# Base: events/500 (500 events = 0.5)
# Bonus: max_mag/10 (M10 = 1.0)
# Bonus: significant * 0.05
score=$(echo "scale=2; ($count / 500) * 0.4 + ($max_mag / 10) * 0.4 + ($significant * 0.05) * 0.2" | bc 2>/dev/null)
# Clamp to 0.0 - 1.0
if (( $(echo "$score > 1.0" | bc -l) )); then
score="1.0"
elif (( $(echo "$score < 0.0" | bc -l) )); then
score="0.0"
fi
echo "${score:-0.0}"
}
# Get thermal anomaly score (0.0 - 1.0)
# Placeholder - would need actual anomaly parsing
get_thermal_score() {
# For now, return a baseline value
# In production, this would parse SST anomaly data
# and calculate deviation from baseline
# Check if SST data is recent (within 24h)
if [ -f "$sst_cache/global.gif" ]; then
local age
age=$(( $(date +%s) - $(stat -c %Y "$sst_cache/global.gif" 2>/dev/null || echo 0) ))
if [ "$age" -lt 86400 ]; then
# Data is fresh - return moderate baseline
echo "0.25"
else
# Data is stale
echo "0.10"
fi
else
echo "0.10"
fi
}
# Get event frequency score (0.0 - 1.0)
# Based on rate of increase in seismic activity
get_frequency_score() {
if [ ! -f "$seismic_cache" ]; then
echo "0.0"
return
fi
# Count events in last hour vs last 24h average
local hour_count day_count hourly_avg ratio
# This would need the hourly cache - simplified for now
day_count=$(jq '.metadata.count // 0' "$seismic_cache" 2>/dev/null)
hourly_avg=$(echo "scale=2; $day_count / 24" | bc 2>/dev/null)
# Assume current hour is average (placeholder)
# In production, compare actual hourly counts
echo "0.20"
}
# Get depth anomaly score (0.0 - 1.0)
# Deep events are more concerning
get_depth_score() {
if [ ! -f "$seismic_cache" ]; then
echo "0.0"
return
fi
# Count deep events (>300km)
local deep_count total_count ratio
deep_count=$(jq '[.features[].geometry.coordinates[2] // 0 | select(. > 300)] | length' "$seismic_cache" 2>/dev/null)
total_count=$(jq '.features | length' "$seismic_cache" 2>/dev/null)
if [ "$total_count" -gt 0 ]; then
ratio=$(echo "scale=2; $deep_count / $total_count" | bc 2>/dev/null)
echo "${ratio:-0.0}"
else
echo "0.0"
fi
}
# ══════════════════════════════════════════════════════════════════
# KPI CALCULATION
# ══════════════════════════════════════════════════════════════════
calculate_kpi() {
local seismic_score thermal_score frequency_score depth_score
local kpi
seismic_score=$(get_seismic_score)
thermal_score=$(get_thermal_score)
frequency_score=$(get_frequency_score)
depth_score=$(get_depth_score)
# Weighted composite
kpi=$(echo "scale=3; \
($seismic_score * $SEISMIC_WEIGHT) + \
($thermal_score * $THERMAL_WEIGHT) + \
($frequency_score * $FREQUENCY_WEIGHT) + \
($depth_score * $DEPTH_WEIGHT)" | bc 2>/dev/null)
# Generate report
local status_icon status_text
if (( $(echo "$kpi >= $THRESHOLD_RED" | bc -l) )); then
status_icon=""
status_text="CRITICAL - Anomaly threshold exceeded"
elif (( $(echo "$kpi >= $THRESHOLD_YELLOW" | bc -l) )); then
status_icon=""
status_text="ELEVATED - Monitoring increased"
else
status_icon=""
status_text="NOMINAL - Standard monitoring"
fi
cat > "$kpi_report" << EOF
╔══════════════════════════════════════════════════════════════╗
║ KAIJU PROBABILITY INDEX (KPI) REPORT ║
╠══════════════════════════════════════════════════════════════╣
║ Timestamp: $(date '+%Y-%m-%d %H:%M:%S UTC' -u)
║ Status: $status_icon $status_text
╠══════════════════════════════════════════════════════════════╣
║ COMPOSITE INDEX: $kpi
╠══════════════════════════════════════════════════════════════╣
║ Component Scores:
║ Seismic Activity: $seismic_score (weight: $SEISMIC_WEIGHT)
║ Thermal Anomaly: $thermal_score (weight: $THERMAL_WEIGHT)
║ Event Frequency: $frequency_score (weight: $FREQUENCY_WEIGHT)
║ Depth Anomaly: $depth_score (weight: $DEPTH_WEIGHT)
╠══════════════════════════════════════════════════════════════╣
║ Thresholds:
║ Yellow Alert: >= $THRESHOLD_YELLOW
║ Red Alert: >= $THRESHOLD_RED
╚══════════════════════════════════════════════════════════════╝
EOF
# Log to history
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ),$kpi,$seismic_score,$thermal_score,$frequency_score,$depth_score" >> "$kpi_history"
echo "$kpi"
}
# ══════════════════════════════════════════════════════════════════
# OUTPUT FUNCTIONS
# ══════════════════════════════════════════════════════════════════
# Status bar output with color coding
tobar() {
local kpi
kpi=$(calculate_kpi)
local icon
if (( $(echo "$kpi >= $THRESHOLD_RED" | bc -l) )); then
icon=""
elif (( $(echo "$kpi >= $THRESHOLD_YELLOW" | bc -l) )); then
icon=""
else
icon=""
fi
echo "$icon ${kpi:0:4}"
}
# Show full report
show_report() {
if [ -f "$kpi_report" ]; then
cat "$kpi_report"
else
calculate_kpi > /dev/null
cat "$kpi_report"
fi
}
# Show history
show_history() {
local lines="${1:-20}"
echo "KPI History (last $lines entries):"
echo "════════════════════════════════════"
echo "Timestamp,KPI,Seismic,Thermal,Freq,Depth"
tail -n "$lines" "$kpi_history" 2>/dev/null || echo "No history available"
}
# Update underlying data sources
update_sources() {
echo "Updating data sources..."
subsea-seismic-monitor update 2>/dev/null &
sea-surface-temperature-anomaly update 2>/dev/null &
wait
calculate_kpi > /dev/null
echo "KPI recalculated."
show_report
}
# Display help
help() {
cat << EOF
Kaiju Probability Index (KPI)
Composite anomaly score from seismic + thermal + acoustic data
════════════════════════════════════════════════════════════════
Usage: kpi [OPTION]
Options:
(none) Show KPI value for status bar
report, r Show full KPI report
history, h [N] Show last N history entries (default: 20)
update, u Update source data and recalculate
calculate, c Force recalculation
help Show this help
Scoring Components:
Seismic (35%) USGS earthquake data - count, magnitude, clustering
Thermal (30%) SST anomaly deviation from baseline
Frequency (20%) Rate of change in seismic activity
Depth (15%) Proportion of deep (>300km) events
Thresholds:
Green < 0.40 Nominal - standard monitoring
Yellow >= 0.40 Elevated - increased monitoring
Red >= 0.70 Critical - anomaly threshold exceeded
"If it hits 0.7, nobody goes home."
EOF
}
# ══════════════════════════════════════════════════════════════════
# MAIN
# ══════════════════════════════════════════════════════════════════
case "$1" in
r*) show_report ;;
h*) shift; show_history "$1" ;;
u*) update_sources ;;
c*) calculate_kpi; show_report ;;
--help) help ;;
"") tobar ;;
*) help ;;
esac
# Status bar click handlers
case $BLOCK_BUTTON in
1) notify-send -t 60000 "Kaiju Probability Index" "$(show_report)" ;;
2) update_sources ;;
3) notify-send -t 30000 "KPI History" "$(show_history 10)" ;;
6) setsid -f "$TERMINAL" -e "$EDITOR" "$0" ;;
esac