feature:
Full SOFA Score: Data Layer + Scoring Engine Glasgow Coma Scale: Data Layer + Scoring Engine
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
public static class SofaCalculator
|
||||
{
|
||||
public static readonly IReadOnlyList<string> SofaObservationCodes = new[]
|
||||
{
|
||||
"PAO2_MMHG", "FIO2_PCT", "PLATELET_K_UL", "BILIRUBIN_MG_DL",
|
||||
"CREATININE_MG_DL", "URINE_OUTPUT_ML_H",
|
||||
"SYSTOLIC_BP", "DIASTOLIC_BP", "SPO2", "SUPPLEMENTAL_O2"
|
||||
};
|
||||
|
||||
public static readonly IReadOnlySet<string> SofaCodeSet =
|
||||
new HashSet<string>(SofaObservationCodes);
|
||||
|
||||
public static bool IsSofaCode(string observationCode) =>
|
||||
SofaCodeSet.Contains(observationCode);
|
||||
|
||||
// GCS components and gcs.scored also trigger SOFA re-score (CNS organ system)
|
||||
public static bool TriggersRescore(string observationCode) =>
|
||||
IsSofaCode(observationCode) || GcsCalculator.IsGcsCode(observationCode);
|
||||
|
||||
public static string CacheKey(Guid encounterId, string code) =>
|
||||
$"sofa:{encounterId}:{code}";
|
||||
|
||||
// --- 1. Respiratory: PaO2/FiO2 ratio ---
|
||||
public static int ScoreRespiratory(decimal? pao2, decimal? fio2, bool onMechanicalVent)
|
||||
{
|
||||
if (pao2 is null || fio2 is null || fio2 == 0) return 0;
|
||||
var ratio = pao2.Value / (fio2.Value / 100m);
|
||||
return ratio switch
|
||||
{
|
||||
>= 400 when onMechanicalVent => 0,
|
||||
>= 400 => 0,
|
||||
>= 300 => 1,
|
||||
>= 200 => 2,
|
||||
>= 100 when onMechanicalVent => 3,
|
||||
>= 100 => 2,
|
||||
_ when onMechanicalVent => 4,
|
||||
_ => 3
|
||||
};
|
||||
}
|
||||
|
||||
// SpO2/FiO2 proxy when PaO2 unavailable (Rice et al. 2007)
|
||||
public static int ScoreRespiratoryFromSpo2(decimal? spo2, decimal? fio2, bool onMechanicalVent)
|
||||
{
|
||||
if (spo2 is null || fio2 is null || fio2 == 0) return 0;
|
||||
var sfRatio = spo2.Value / (fio2.Value / 100m);
|
||||
return sfRatio switch
|
||||
{
|
||||
>= 315 => 0,
|
||||
>= 235 => 1,
|
||||
>= 150 => 2,
|
||||
>= 67 when onMechanicalVent => 3,
|
||||
>= 67 => 2,
|
||||
_ when onMechanicalVent => 4,
|
||||
_ => 3
|
||||
};
|
||||
}
|
||||
|
||||
// --- 2. Coagulation: Platelet count (k/µL) ---
|
||||
public static int ScoreCoagulation(decimal? platelets)
|
||||
{
|
||||
if (platelets is null) return 0;
|
||||
return platelets.Value switch
|
||||
{
|
||||
>= 150 => 0,
|
||||
>= 100 => 1,
|
||||
>= 50 => 2,
|
||||
>= 20 => 3,
|
||||
_ => 4
|
||||
};
|
||||
}
|
||||
|
||||
// --- 3. Liver: Bilirubin (mg/dL) ---
|
||||
public static int ScoreLiver(decimal? bilirubin)
|
||||
{
|
||||
if (bilirubin is null) return 0;
|
||||
return bilirubin.Value switch
|
||||
{
|
||||
< 1.2m => 0,
|
||||
< 2.0m => 1,
|
||||
< 6.0m => 2,
|
||||
< 12.0m => 3,
|
||||
_ => 4
|
||||
};
|
||||
}
|
||||
|
||||
// --- 4. Cardiovascular: MAP and vasopressor dose ---
|
||||
public static int ScoreCardiovascular(decimal? map, VasopressorInfo? vasopressor)
|
||||
{
|
||||
if (vasopressor is not null)
|
||||
{
|
||||
return vasopressor.DrugName.ToUpperInvariant() switch
|
||||
{
|
||||
"DOPAMINE" when vasopressor.DoseUgKgMin > 15m => 4,
|
||||
"EPINEPHRINE" when vasopressor.DoseUgKgMin > 0.1m => 4,
|
||||
"NOREPINEPHRINE" when vasopressor.DoseUgKgMin > 0.1m => 4,
|
||||
"DOPAMINE" when vasopressor.DoseUgKgMin > 5m => 3,
|
||||
"EPINEPHRINE" => 3,
|
||||
"NOREPINEPHRINE" => 3,
|
||||
"DOPAMINE" => 2,
|
||||
"DOBUTAMINE" => 2,
|
||||
_ => 1
|
||||
};
|
||||
}
|
||||
|
||||
if (map is null) return 0;
|
||||
return map.Value < 70m ? 1 : 0;
|
||||
}
|
||||
|
||||
// --- 5. CNS: Glasgow Coma Scale (from Phase 25) ---
|
||||
public static int ScoreCns(int? gcsTotal) =>
|
||||
GcsCalculator.ToSofaCnsScore(gcsTotal ?? 15);
|
||||
|
||||
// --- 6. Renal: Creatinine (mg/dL) or urine output (mL/day) ---
|
||||
public static int ScoreRenal(decimal? creatinine, decimal? urineOutputMlPerDay)
|
||||
{
|
||||
var creatScore = creatinine switch
|
||||
{
|
||||
null => 0,
|
||||
< 1.2m => 0,
|
||||
< 2.0m => 1,
|
||||
< 3.5m => 2,
|
||||
< 5.0m => 3,
|
||||
_ => 4
|
||||
};
|
||||
|
||||
var urineScore = urineOutputMlPerDay switch
|
||||
{
|
||||
null => 0,
|
||||
< 200m => 4,
|
||||
< 500m => 3,
|
||||
_ => 0
|
||||
};
|
||||
|
||||
return Math.Max(creatScore, urineScore);
|
||||
}
|
||||
|
||||
public static SofaResult ComputeTotal(
|
||||
int respiratory, int coagulation, int liver,
|
||||
int cardiovascular, int cns, int renal) =>
|
||||
new(
|
||||
Total: respiratory + coagulation + liver + cardiovascular + cns + renal,
|
||||
Respiratory: respiratory,
|
||||
Coagulation: coagulation,
|
||||
Liver: liver,
|
||||
Cardiovascular: cardiovascular,
|
||||
Cns: cns,
|
||||
Renal: renal);
|
||||
|
||||
// Count organ systems with component data for baseline eligibility
|
||||
public static int CountPopulatedOrganSystems(
|
||||
bool hasRespiratory,
|
||||
bool hasCoagulation,
|
||||
bool hasLiver,
|
||||
bool hasCardiovascular,
|
||||
bool hasCns,
|
||||
bool hasRenal) =>
|
||||
new[] { hasRespiratory, hasCoagulation, hasLiver,
|
||||
hasCardiovascular, hasCns, hasRenal }
|
||||
.Count(hasData => hasData);
|
||||
}
|
||||
Reference in New Issue
Block a user