feat(matlab): add Satterthwaite DF + honest random-slope test to LME reports

Every fitlme-based report (lme_*, paper_*, phase_*, and the variations'
analyze.m) now shows, per effect: residual-DF p, Satterthwaite-DF p, and --
for the interaction -- an HONEST test from a per-animal random-SLOPE model
(day|rat), whose Satterthwaite DF collapses toward the animal count.

New: tdcs_random_slope_interaction.m (shared helper). Wired into tdcs_lme,
tdcs_paper_lme, tdcs_phase_lme, variation_analyze; SUMMARY.csv gains
interaction_p_satt / interaction_p_rs. Regenerated all results/, variations/,
matched-effort outputs.

Key point this surfaces: Satterthwaite ~= residual on the random-INTERCEPT
model (the slope's error is at session level), so it does NOT fix
pseudoreplication; the random-slope model does. Effect: full-range mergeA2
interaction 0.009 -> 0.75 (collapses); unmerge_d0_5 0.015 -> 0.13 (n.s.);
the pooled-control early windows survive honestly (naive_a2_d0_5 0.001 ->
0.028; naive_boxa_d0_5 0.003 -> 0.036). Suite 42/42.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Experiments DB Dev
2026-07-22 15:45:19 -04:00
parent 5fcd97f81a
commit 8a18c894dd
77 changed files with 2066 additions and 345 deletions
+130
View File
@@ -0,0 +1,130 @@
#!/usr/bin/env bash
# =============================================================================
# Experiments DB — incremental backup script
#
# Usage:
# backup.sh daily backup only if DB changed since last run; keep 5
# backup.sh weekly unconditional backup; keep 2
#
# Cron (added by setup_cron.sh):
# 0 2 * * * /home/sam/docker-images/experiments-database/scripts/backup.sh daily
# 0 3 * * 0 /home/sam/docker-images/experiments-database/scripts/backup.sh weekly
# =============================================================================
set -euo pipefail
# ── Config ────────────────────────────────────────────────────────────────────
MODE="${1:-daily}"
BACKUP_ROOT="/home/sam/synology/Backups/Experiments-DB-Backup"
DAILY_DIR="$BACKUP_ROOT/daily"
WEEKLY_DIR="$BACKUP_ROOT/weekly"
STATE_FILE="$BACKUP_ROOT/.last_backup_state"
LOG_FILE="$BACKUP_ROOT/backup.log"
KEEP_DAILY=5
KEEP_WEEKLY=2
PG_CONTAINER="experiments_postgres"
PG_USER="expuser"
PG_DB="experiments_db"
# ── Helpers ───────────────────────────────────────────────────────────────────
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"; }
die() { log "ERROR: $*"; exit 1; }
# Ensure directories exist
mkdir -p "$DAILY_DIR" "$WEEKLY_DIR"
# Check postgres container is running
if ! docker inspect --format '{{.State.Running}}' "$PG_CONTAINER" 2>/dev/null | grep -q true; then
die "Container $PG_CONTAINER is not running."
fi
# ── DB state fingerprint ──────────────────────────────────────────────────────
# Combines row counts from all key tables + latest audit_log timestamp.
# Changes in any table (insert, update, delete) alter this string.
get_db_state() {
docker exec "$PG_CONTAINER" psql -U "$PG_USER" -d "$PG_DB" -t -A -c "
SELECT
(SELECT COUNT(*) FROM experiments) || ',' ||
(SELECT COUNT(*) FROM animals) || ',' ||
(SELECT COUNT(*) FROM daily_statuses) || ',' ||
(SELECT COUNT(*) FROM daily_analyses) || ',' ||
(SELECT COUNT(*) FROM session_files) || ',' ||
COALESCE(
(SELECT MAX(timestamp)::text FROM audit_logs),
'no-audit'
)
AS fingerprint;
" 2>/dev/null | tr -d '[:space:]'
}
# ── Dump function ─────────────────────────────────────────────────────────────
run_dump() {
local dest="$1"
local tmp="${dest}.tmp"
log "Dumping → $dest"
docker exec "$PG_CONTAINER" pg_dump \
-U "$PG_USER" \
--format=custom \
--compress=9 \
"$PG_DB" > "$tmp" \
&& mv "$tmp" "$dest" \
|| { rm -f "$tmp"; die "pg_dump failed."; }
local size
size=$(du -sh "$dest" | cut -f1)
log "Backup written ($size): $(basename "$dest")"
}
# ── Prune old backups ─────────────────────────────────────────────────────────
prune() {
local dir="$1"
local keep="$2"
local count
count=$(ls -1 "$dir"/*.pgdump 2>/dev/null | wc -l)
if (( count > keep )); then
ls -1t "$dir"/*.pgdump | tail -n "+$((keep + 1))" | while read -r f; do
log "Pruning old backup: $(basename "$f")"
rm -f "$f"
done
fi
}
# ── Main ──────────────────────────────────────────────────────────────────────
TIMESTAMP=$(date '+%Y%m%d_%H%M%S')
if [[ "$MODE" == "daily" ]]; then
log "--- Daily backup check ---"
CURRENT_STATE=$(get_db_state)
if [[ -z "$CURRENT_STATE" ]]; then
die "Could not read DB state."
fi
LAST_STATE=$(cat "$STATE_FILE" 2>/dev/null || echo "")
if [[ "$CURRENT_STATE" == "$LAST_STATE" ]]; then
log "No changes detected since last backup. Skipping."
exit 0
fi
DEST="$DAILY_DIR/experiments_db_daily_${TIMESTAMP}.pgdump"
run_dump "$DEST"
# Save new state only after successful dump
echo "$CURRENT_STATE" > "$STATE_FILE"
log "State fingerprint updated."
prune "$DAILY_DIR" "$KEEP_DAILY"
log "Daily backup complete."
elif [[ "$MODE" == "weekly" ]]; then
log "--- Weekly backup (unconditional) ---"
DEST="$WEEKLY_DIR/experiments_db_weekly_${TIMESTAMP}.pgdump"
run_dump "$DEST"
prune "$WEEKLY_DIR" "$KEEP_WEEKLY"
log "Weekly backup complete."
else
die "Unknown mode '$MODE'. Use 'daily' or 'weekly'."
fi