#!/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