-- ------------------------------------------------------------------
-- Title: Hourly Sequential Organ Failure Assessment (SOFA)
--
-- Adapted from the MIMIC-IV upstream
--   concepts/score/sofa.sql
-- and ported to MIMIC-III v1.3 vanilla PostgreSQL.
--
-- Produces one row per (icustay_id, hr) for every hour of the ICU
-- stay, with both the per-component score AT that hour and the
-- 24-hour rolling MAX of each component (which is the value used by
-- Sepsis-3).  Final column `sofa_24hours` is the sum of the six
-- 24-hour rolling maxes.
--
-- Differences vs. MIMIC-IV upstream (search this file for "PORT NOTE"):
--   1. ID column is `icustay_id`, not `stay_id`.
--   2. There is no `icustay_hourly` derived table in MIMIC-III; we
--      build the hourly grid inline with `generate_series`.
--   3. There is no `mimic_derived.ventilation` with a fine-grained
--      `ventilation_status='InvasiveVent'` flag in MIMIC-III; we use
--      the lumped `ventilation_durations` table, so any active
--      ventilation row is treated as invasive ventilation for the
--      purpose of the PaO2:FiO2 vent/novent split.
--   4. We replicate MIMIC-IV's `urine_output_rate` adjustment inline
--      in `sofa_uo`: we materialise both `uo_24hr` (sum) and
--      `uo_tm_24hr` (count of distinct hours that actually had a UO
--      observation in the past 24 h), and the renal CASE in (11)
--      uses
--         GREATEST(uo_24hr, 0) * 24.0 / uo_tm_24hr
--      only when uo_tm_24hr BETWEEN 22 AND 30, falling back to
--      creatinine alone otherwise.  The GREATEST(_, 0) clip prevents
--      patients on continuous bladder irrigation (which the upstream
--      `urine_output.sql` subtracts as a negative volume) from being
--      mis-scored as oliguric.
--   5. Vasopressor rates come from the upstream
--      durations/{epinephrine,norepinephrine,dopamine,dobutamine}_dose.sql
--      tables, which already merge CareVue + MetaVision and convert
--      to mcg/kg/min.
--
-- Implementation note: each measurement class is materialised into
-- its own narrow staging table.  This avoids forcing the planner to
-- optimise a single ~10-way CTE join, lets each scan of the giant raw
-- tables (`chartevents`, `labevents`, `outputevents`,
-- `inputevents_*`) run independently, and lets you `EXPLAIN ANALYZE`
-- each step in isolation.
--
-- Dependencies:
--   postgres-functions.sql
--   durations/ventilation_durations.sql
--   durations/{dobutamine,dopamine,epinephrine,norepinephrine}_dose.sql
--   fluid_balance/urine_output.sql
--   sepsis/blood_gas_arterial.sql
--   sepsis/gcs_all.sql
-- ------------------------------------------------------------------

-- 1. Hourly grid: one row per (icustay_id, hr) for the entire stay.
DROP TABLE IF EXISTS sofa_grid;
CREATE TABLE sofa_grid AS
SELECT ie.subject_id, ie.hadm_id, ie.icustay_id
     , gs.hr
     , ie.intime + ((gs.hr - 1) * INTERVAL '1 hour') AS starttime
     , ie.intime + (gs.hr        * INTERVAL '1 hour') AS endtime
FROM   icustays ie
     , LATERAL generate_series(
         1,
         GREATEST(1,
           CEIL(EXTRACT(EPOCH FROM (ie.outtime - ie.intime)) / 3600.0)::int
         )
       ) AS gs(hr)
WHERE  ie.outtime IS NOT NULL
  AND  ie.outtime > ie.intime;

CREATE INDEX IF NOT EXISTS sofa_grid_idx ON sofa_grid (icustay_id, hr);
CREATE INDEX IF NOT EXISTS sofa_grid_time_idx
  ON sofa_grid (icustay_id, starttime, endtime);
ANALYZE sofa_grid;

-- 2. Mean arterial pressure: minimum within the hour.
DROP TABLE IF EXISTS sofa_vs;
CREATE TABLE sofa_vs AS
SELECT g.icustay_id, g.hr
     , MIN(ce.valuenum) AS meanbp_min
FROM   sofa_grid g
LEFT   JOIN chartevents ce
       ON  ce.icustay_id = g.icustay_id
       AND ce.charttime >  g.starttime
       AND ce.charttime <= g.endtime
       AND ce.itemid IN (456, 52, 6702, 443, 220052, 220181, 225312)
       AND ce.valuenum > 0 AND ce.valuenum < 300
       AND COALESCE(ce.error, 0) = 0
GROUP  BY g.icustay_id, g.hr;
CREATE INDEX IF NOT EXISTS sofa_vs_idx ON sofa_vs (icustay_id, hr);
ANALYZE sofa_vs;

-- 3. GCS: minimum within the hour (uses gcs_all carry-forward logic).
DROP TABLE IF EXISTS sofa_gcs;
CREATE TABLE sofa_gcs AS
SELECT g.icustay_id, g.hr
     , MIN(gc.gcs) AS gcs_min
FROM   sofa_grid g
LEFT   JOIN gcs_all gc
       ON  gc.icustay_id = g.icustay_id
       AND gc.charttime >  g.starttime
       AND gc.charttime <= g.endtime
GROUP  BY g.icustay_id, g.hr;
CREATE INDEX IF NOT EXISTS sofa_gcs_idx ON sofa_gcs (icustay_id, hr);
ANALYZE sofa_gcs;

-- 4. Bilirubin: maximum within the hour.
DROP TABLE IF EXISTS sofa_bili;
CREATE TABLE sofa_bili AS
SELECT g.icustay_id, g.hr
     , MAX(le.valuenum) AS bilirubin_max
FROM   sofa_grid g
INNER  JOIN icustays ie
       ON ie.icustay_id = g.icustay_id
LEFT   JOIN labevents le
       ON  le.subject_id = ie.subject_id
       AND le.hadm_id    = ie.hadm_id
       AND le.charttime  >  g.starttime
       AND le.charttime  <= g.endtime
       AND le.itemid = 50885
       AND le.valuenum IS NOT NULL
       AND le.valuenum > 0
GROUP  BY g.icustay_id, g.hr;
CREATE INDEX IF NOT EXISTS sofa_bili_idx ON sofa_bili (icustay_id, hr);
ANALYZE sofa_bili;

-- 5. Creatinine: maximum within the hour.
DROP TABLE IF EXISTS sofa_cr;
CREATE TABLE sofa_cr AS
SELECT g.icustay_id, g.hr
     , MAX(le.valuenum) AS creatinine_max
FROM   sofa_grid g
INNER  JOIN icustays ie
       ON ie.icustay_id = g.icustay_id
LEFT   JOIN labevents le
       ON  le.subject_id = ie.subject_id
       AND le.hadm_id    = ie.hadm_id
       AND le.charttime  >  g.starttime
       AND le.charttime  <= g.endtime
       AND le.itemid = 50912
       AND le.valuenum IS NOT NULL
       AND le.valuenum > 0
       AND le.valuenum < 150  -- sanity (mg/dL)
GROUP  BY g.icustay_id, g.hr;
CREATE INDEX IF NOT EXISTS sofa_cr_idx ON sofa_cr (icustay_id, hr);
ANALYZE sofa_cr;

-- 6. Platelets: minimum within the hour.
DROP TABLE IF EXISTS sofa_plt;
CREATE TABLE sofa_plt AS
SELECT g.icustay_id, g.hr
     , MIN(le.valuenum) AS platelet_min
FROM   sofa_grid g
INNER  JOIN icustays ie
       ON ie.icustay_id = g.icustay_id
LEFT   JOIN labevents le
       ON  le.subject_id = ie.subject_id
       AND le.hadm_id    = ie.hadm_id
       AND le.charttime  >  g.starttime
       AND le.charttime  <= g.endtime
       AND le.itemid = 51265
       AND le.valuenum IS NOT NULL
       AND le.valuenum > 0
GROUP  BY g.icustay_id, g.hr;
CREATE INDEX IF NOT EXISTS sofa_plt_idx ON sofa_plt (icustay_id, hr);
ANALYZE sofa_plt;

-- 7. PaO2/FiO2: split into vent / novent based on whether the patient
--    is on invasive ventilation at the time of the blood gas.  We
--    take the worst (lowest) PaO2:FiO2 of each kind during the hour.
DROP TABLE IF EXISTS sofa_pf;
CREATE TABLE sofa_pf AS
WITH pafi AS (
  SELECT bg.icustay_id, bg.charttime
       , CASE WHEN vd.icustay_id IS NULL THEN bg.pao2fio2 END AS pao2fio2_novent
       , CASE WHEN vd.icustay_id IS NOT NULL THEN bg.pao2fio2 END AS pao2fio2_vent
  FROM   blood_gas_arterial bg
  LEFT   JOIN ventilation_durations vd
         ON  bg.icustay_id = vd.icustay_id
         AND bg.charttime >= vd.starttime
         AND bg.charttime <= vd.endtime
  WHERE  bg.pao2fio2 IS NOT NULL
)
SELECT g.icustay_id, g.hr
     , MIN(p.pao2fio2_novent) AS pao2fio2_novent
     , MIN(p.pao2fio2_vent)   AS pao2fio2_vent
FROM   sofa_grid g
LEFT   JOIN pafi p
       ON  p.icustay_id = g.icustay_id
       AND p.charttime  >  g.starttime
       AND p.charttime  <= g.endtime
GROUP  BY g.icustay_id, g.hr;
CREATE INDEX IF NOT EXISTS sofa_pf_idx ON sofa_pf (icustay_id, hr);
ANALYZE sofa_pf;

-- 8. Urine output: sum over the preceding 24 h plus a count of how many
--    distinct hours actually had a UO observation (`uo_tm_24hr`).  This
--    matches the MIMIC-IV `urine_output_rate` table's two columns and
--    lets the renal CASE distinguish "no data" from "really oliguric".
--    The renal score in (11) requires uo_tm_24hr to be in a plausible
--    range before the UO branch fires; otherwise it falls back to
--    creatinine alone.  See port note #3 in the file header.
DROP TABLE IF EXISTS sofa_uo;
CREATE TABLE sofa_uo AS
SELECT g.icustay_id, g.hr
     , SUM(uo.value)                                       AS uo_24hr
     , COUNT(DISTINCT date_trunc('hour', uo.charttime))    AS uo_tm_24hr
FROM   sofa_grid g
LEFT   JOIN urine_output uo
       ON  uo.icustay_id = g.icustay_id
       AND uo.charttime  >  DATETIME_SUB(g.endtime, INTERVAL '24' HOUR)
       AND uo.charttime  <= g.endtime
GROUP  BY g.icustay_id, g.hr;
CREATE INDEX IF NOT EXISTS sofa_uo_idx ON sofa_uo (icustay_id, hr);
ANALYZE sofa_uo;

-- 9. Vasopressor rates: take the maximum rate of each pressor active at
--    `endtime`.  Each upstream dose table is (icustay_id, starttime,
--    endtime, vaso_rate, vaso_amount).
DROP TABLE IF EXISTS sofa_vaso;
CREATE TABLE sofa_vaso AS
SELECT g.icustay_id, g.hr
     , MAX(epi.vaso_rate) AS rate_epinephrine
     , MAX(nor.vaso_rate) AS rate_norepinephrine
     , MAX(dop.vaso_rate) AS rate_dopamine
     , MAX(dob.vaso_rate) AS rate_dobutamine
FROM   sofa_grid g
LEFT   JOIN epinephrine_dose epi
       ON  epi.icustay_id = g.icustay_id
       AND g.endtime >  epi.starttime
       AND g.endtime <= epi.endtime
LEFT   JOIN norepinephrine_dose nor
       ON  nor.icustay_id = g.icustay_id
       AND g.endtime >  nor.starttime
       AND g.endtime <= nor.endtime
LEFT   JOIN dopamine_dose dop
       ON  dop.icustay_id = g.icustay_id
       AND g.endtime >  dop.starttime
       AND g.endtime <= dop.endtime
LEFT   JOIN dobutamine_dose dob
       ON  dob.icustay_id = g.icustay_id
       AND g.endtime >  dob.starttime
       AND g.endtime <= dob.endtime
WHERE  epi.icustay_id IS NOT NULL
   OR  nor.icustay_id IS NOT NULL
   OR  dop.icustay_id IS NOT NULL
   OR  dob.icustay_id IS NOT NULL
GROUP  BY g.icustay_id, g.hr;
CREATE INDEX IF NOT EXISTS sofa_vaso_idx ON sofa_vaso (icustay_id, hr);
ANALYZE sofa_vaso;

-- 10. Wide assembly: left-join all of the above onto the grid.
DROP TABLE IF EXISTS sofa_wide;
CREATE TABLE sofa_wide AS
SELECT g.subject_id, g.hadm_id, g.icustay_id, g.hr
     , g.starttime, g.endtime
     , vs.meanbp_min
     , gc.gcs_min
     , bi.bilirubin_max
     , cr.creatinine_max
     , pl.platelet_min
     , pf.pao2fio2_novent
     , pf.pao2fio2_vent
     , uo.uo_24hr
     , uo.uo_tm_24hr
     , va.rate_epinephrine
     , va.rate_norepinephrine
     , va.rate_dopamine
     , va.rate_dobutamine
FROM   sofa_grid g
LEFT   JOIN sofa_vs   vs ON vs.icustay_id = g.icustay_id AND vs.hr = g.hr
LEFT   JOIN sofa_gcs  gc ON gc.icustay_id = g.icustay_id AND gc.hr = g.hr
LEFT   JOIN sofa_bili bi ON bi.icustay_id = g.icustay_id AND bi.hr = g.hr
LEFT   JOIN sofa_cr   cr ON cr.icustay_id = g.icustay_id AND cr.hr = g.hr
LEFT   JOIN sofa_plt  pl ON pl.icustay_id = g.icustay_id AND pl.hr = g.hr
LEFT   JOIN sofa_pf   pf ON pf.icustay_id = g.icustay_id AND pf.hr = g.hr
LEFT   JOIN sofa_uo   uo ON uo.icustay_id = g.icustay_id AND uo.hr = g.hr
LEFT   JOIN sofa_vaso va ON va.icustay_id = g.icustay_id AND va.hr = g.hr;
CREATE INDEX IF NOT EXISTS sofa_wide_idx ON sofa_wide (icustay_id, hr);
ANALYZE sofa_wide;

-- 11. Per-hour component scores (no rolling window yet).
DROP TABLE IF EXISTS sofa_components;
CREATE TABLE sofa_components AS
SELECT w.*
     -- Respiration
     , CASE
         WHEN pao2fio2_vent   < 100 THEN 4
         WHEN pao2fio2_vent   < 200 THEN 3
         WHEN pao2fio2_novent < 300 THEN 2
         WHEN pao2fio2_vent   < 300 THEN 2
         WHEN pao2fio2_novent < 400 THEN 1
         WHEN pao2fio2_vent   < 400 THEN 1
         WHEN COALESCE(pao2fio2_vent, pao2fio2_novent) IS NULL THEN NULL
         ELSE 0
       END AS respiration

     -- Coagulation
     , CASE
         WHEN platelet_min <  20 THEN 4
         WHEN platelet_min <  50 THEN 3
         WHEN platelet_min < 100 THEN 2
         WHEN platelet_min < 150 THEN 1
         WHEN platelet_min IS NULL THEN NULL
         ELSE 0
       END AS coagulation

     -- Liver (mg/dL)
     , CASE
         WHEN bilirubin_max >= 12.0 THEN 4
         WHEN bilirubin_max >=  6.0 THEN 3
         WHEN bilirubin_max >=  2.0 THEN 2
         WHEN bilirubin_max >=  1.2 THEN 1
         WHEN bilirubin_max IS NULL THEN NULL
         ELSE 0
       END AS liver

     -- Cardiovascular
     , CASE
         WHEN rate_dopamine > 15 OR rate_epinephrine >  0.1 OR rate_norepinephrine >  0.1 THEN 4
         WHEN rate_dopamine >  5 OR rate_epinephrine <= 0.1 OR rate_norepinephrine <= 0.1 THEN 3
         WHEN rate_dopamine >  0 OR rate_dobutamine >  0 THEN 2
         WHEN meanbp_min < 70 THEN 1
         WHEN COALESCE(meanbp_min, rate_dopamine, rate_dobutamine,
                       rate_epinephrine, rate_norepinephrine) IS NULL THEN NULL
         ELSE 0
       END AS cardiovascular

     -- CNS (GCS)
     , CASE
         WHEN gcs_min >= 13 AND gcs_min <= 14 THEN 1
         WHEN gcs_min >= 10 AND gcs_min <= 12 THEN 2
         WHEN gcs_min >=  6 AND gcs_min <=  9 THEN 3
         WHEN gcs_min <   6                   THEN 4
         WHEN gcs_min IS NULL                 THEN NULL
         ELSE 0
       END AS cns

     -- Renal
     -- We scale uo_24hr to a 24h-equivalent only when the rolling
     -- window has at least 22 distinct hours of observations
     -- (matching MIMIC-IV's `urine_output_rate` upper-bound check).
     -- If fewer than 22 valid hours exist, the UO branch is treated
     -- as missing and the renal score falls back to creatinine alone.
     -- We also clip GU-irrigant negative net values to zero so a
     -- patient on continuous bladder irrigation isn't mis-scored as
     -- oliguric.
     , CASE
         WHEN creatinine_max >= 5.0                          THEN 4
         WHEN uo_tm_24hr BETWEEN 22 AND 30
              AND GREATEST(uo_24hr, 0) * 24.0 / uo_tm_24hr < 200 THEN 4
         WHEN creatinine_max >= 3.5 AND creatinine_max < 5.0 THEN 3
         WHEN uo_tm_24hr BETWEEN 22 AND 30
              AND GREATEST(uo_24hr, 0) * 24.0 / uo_tm_24hr < 500 THEN 3
         WHEN creatinine_max >= 2.0 AND creatinine_max < 3.5 THEN 2
         WHEN creatinine_max >= 1.2 AND creatinine_max < 2.0 THEN 1
         WHEN creatinine_max IS NULL
              AND NOT (uo_tm_24hr BETWEEN 22 AND 30)         THEN NULL
         ELSE 0
       END AS renal
FROM   sofa_wide w;
CREATE INDEX IF NOT EXISTS sofa_components_idx
  ON sofa_components (icustay_id, hr);
ANALYZE sofa_components;

-- 12. Final hourly SOFA: 24-hour rolling MAX per component, summed.
DROP TABLE IF EXISTS sofa_hourly;
CREATE TABLE sofa_hourly AS
SELECT s.subject_id, s.hadm_id, s.icustay_id, s.hr
     , s.starttime, s.endtime
     , s.respiration, s.coagulation, s.liver
     , s.cardiovascular, s.cns, s.renal
     , COALESCE(MAX(s.respiration) OVER w, 0)   AS respiration_24hours
     , COALESCE(MAX(s.coagulation) OVER w, 0)   AS coagulation_24hours
     , COALESCE(MAX(s.liver) OVER w, 0)         AS liver_24hours
     , COALESCE(MAX(s.cardiovascular) OVER w, 0) AS cardiovascular_24hours
     , COALESCE(MAX(s.cns) OVER w, 0)           AS cns_24hours
     , COALESCE(MAX(s.renal) OVER w, 0)         AS renal_24hours
     , COALESCE(MAX(s.respiration) OVER w, 0)
     + COALESCE(MAX(s.coagulation) OVER w, 0)
     + COALESCE(MAX(s.liver) OVER w, 0)
     + COALESCE(MAX(s.cardiovascular) OVER w, 0)
     + COALESCE(MAX(s.cns) OVER w, 0)
     + COALESCE(MAX(s.renal) OVER w, 0)
       AS sofa_24hours
FROM   sofa_components s
WINDOW w AS (
  PARTITION BY s.icustay_id
  ORDER BY     s.hr
  ROWS BETWEEN 23 PRECEDING AND CURRENT ROW
);
CREATE INDEX IF NOT EXISTS sofa_hourly_idx
  ON sofa_hourly (icustay_id, hr);
CREATE INDEX IF NOT EXISTS sofa_hourly_time_idx
  ON sofa_hourly (icustay_id, endtime);
ANALYZE sofa_hourly;
