feature:
Full SOFA Score: Data Layer + Scoring Engine Glasgow Coma Scale: Data Layer + Scoring Engine
This commit is contained in:
@@ -9,18 +9,15 @@ public static class News2Calculator
|
||||
};
|
||||
|
||||
public static RedisKey[] AllParameterKeys(Guid encounterId) =>
|
||||
ParameterCodes
|
||||
.Select(code => (RedisKey)$"news2:{encounterId}:{code}")
|
||||
.ToArray();
|
||||
ParameterCodes
|
||||
.Select(code => (RedisKey)$"news2:{encounterId}:{code}")
|
||||
.ToArray();
|
||||
|
||||
public static string ParameterKey(Guid encounterId, string code) =>
|
||||
$"news2:{encounterId}:{code}";
|
||||
|
||||
public static bool IsNews2Code(string observationCode) =>
|
||||
ParameterCodes.Contains(observationCode);
|
||||
|
||||
// --- Individual parameter scoring ---
|
||||
// Each method returns 0-3 per the official NEWS2 scoring table.
|
||||
ParameterCodes.Contains(observationCode) || GcsCalculator.IsGcsCode(observationCode);
|
||||
|
||||
public static int ScoreRespRate(decimal value) => value switch
|
||||
{
|
||||
@@ -28,16 +25,15 @@ public static class News2Calculator
|
||||
<= 11 => 1,
|
||||
<= 20 => 0,
|
||||
<= 24 => 2,
|
||||
_ => 3 // >= 25
|
||||
_ => 3
|
||||
};
|
||||
|
||||
// Scale 1 (standard). Scale 2 (hypercapnic respiratory failure) is not implemented.
|
||||
public static int ScoreSpo2(decimal value) => value switch
|
||||
{
|
||||
<= 91 => 3,
|
||||
<= 93 => 2,
|
||||
<= 95 => 1,
|
||||
_ => 0 // >= 96
|
||||
_ => 0
|
||||
};
|
||||
|
||||
public static int ScoreSystolicBp(decimal value) => value switch
|
||||
@@ -46,7 +42,7 @@ public static class News2Calculator
|
||||
<= 100 => 2,
|
||||
<= 110 => 1,
|
||||
<= 219 => 0,
|
||||
_ => 3 // >= 220
|
||||
_ => 3
|
||||
};
|
||||
|
||||
public static int ScoreHeartRate(decimal value) => value switch
|
||||
@@ -56,30 +52,30 @@ public static class News2Calculator
|
||||
<= 90 => 0,
|
||||
<= 110 => 1,
|
||||
<= 130 => 2,
|
||||
_ => 3 // >= 131
|
||||
_ => 3
|
||||
};
|
||||
|
||||
// AVPU: Alert=0, Voice/Pain/Unresponsive=3 (any non-Alert scores 3)
|
||||
public static int ScoreConsciousness(decimal value) => value switch
|
||||
{
|
||||
0 => 0, // Alert
|
||||
_ => 3 // Voice (1), Pain (2), Unresponsive (3)
|
||||
0 => 0,
|
||||
_ => 3
|
||||
};
|
||||
|
||||
public static int ScoreConsciousnessFromGcs(int gcsTotal) =>
|
||||
GcsCalculator.ToNews2ConsciousnessScore(gcsTotal);
|
||||
|
||||
public static int ScoreTemperature(decimal value) => value switch
|
||||
{
|
||||
<= 35.0m => 3,
|
||||
<= 36.0m => 1,
|
||||
<= 38.0m => 0,
|
||||
<= 39.0m => 1,
|
||||
_ => 2 // >= 39.1
|
||||
_ => 2
|
||||
};
|
||||
|
||||
// 0 = room air, 1 = on supplemental oxygen
|
||||
public static int ScoreSupplementalO2(decimal value) =>
|
||||
value >= 1 ? 2 : 0;
|
||||
|
||||
// Dispatch to the correct scoring function by observation code.
|
||||
public static int ScoreParameter(string observationCode, decimal value) =>
|
||||
observationCode switch
|
||||
{
|
||||
@@ -93,13 +89,12 @@ public static class News2Calculator
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(observationCode))
|
||||
};
|
||||
|
||||
// Determine risk level from total score and single-param-3 flag.
|
||||
public static string DetermineRiskLevel(int totalScore, bool hasSingleParamThree) =>
|
||||
totalScore switch
|
||||
{
|
||||
>= 7 => "HIGH",
|
||||
>= 5 => "MEDIUM",
|
||||
_ when hasSingleParamThree => "LOW_MEDIUM",
|
||||
_ => "LOW"
|
||||
>= 7 => "HIGH",
|
||||
>= 5 => "MEDIUM",
|
||||
_ when hasSingleParamThree => "LOW_MEDIUM",
|
||||
_ => "LOW"
|
||||
};
|
||||
}
|
||||
@@ -40,30 +40,51 @@ public class News2Detector
|
||||
return News2Result.NotNews2Code;
|
||||
|
||||
using var timer = _metrics.News2ScoringDuration.NewTimer();
|
||||
|
||||
var cache = _redis.GetDatabase();
|
||||
|
||||
// Compute the individual score and store in Redis
|
||||
var individualScore = News2Calculator.ScoreParameter(observationCode, value);
|
||||
var paramData = JsonSerializer.Serialize(new
|
||||
// GCS components are cached by GcsDetector — trigger re-score only
|
||||
if (!GcsCalculator.IsGcsCode(observationCode))
|
||||
{
|
||||
value,
|
||||
score = individualScore,
|
||||
recordedAt = DateTimeOffset.UtcNow
|
||||
}, CachedParamJsonOptions);
|
||||
await cache.StringSetAsync(
|
||||
News2Calculator.ParameterKey(encounterId, observationCode),
|
||||
paramData,
|
||||
TimeSpan.FromSeconds(News2TtlSeconds));
|
||||
var individualScore = News2Calculator.ScoreParameter(observationCode, value);
|
||||
var paramData = JsonSerializer.Serialize(new
|
||||
{
|
||||
value,
|
||||
score = individualScore,
|
||||
recordedAt = DateTimeOffset.UtcNow
|
||||
}, CachedParamJsonOptions);
|
||||
await cache.StringSetAsync(
|
||||
News2Calculator.ParameterKey(encounterId, observationCode),
|
||||
paramData,
|
||||
TimeSpan.FromSeconds(News2TtlSeconds));
|
||||
}
|
||||
|
||||
// Fetch all 7 parameter keys in one MGET round-trip
|
||||
return await TryComputeScoreAsync(encounterId, patientId, ct);
|
||||
}
|
||||
|
||||
private async Task<News2Result> TryComputeScoreAsync(
|
||||
Guid encounterId, Guid patientId, CancellationToken ct)
|
||||
{
|
||||
var cache = _redis.GetDatabase();
|
||||
var allKeys = News2Calculator.AllParameterKeys(encounterId);
|
||||
var allValues = await cache.StringGetAsync(allKeys);
|
||||
|
||||
// Check completeness — all 7 must be present
|
||||
var scores = new int?[7];
|
||||
for (int i = 0; i < 7; i++)
|
||||
{
|
||||
if (i == 4) // consciousness — GCS-first, AVPU-fallback
|
||||
{
|
||||
scores[4] = await ResolveConsciousnessScoreAsync(encounterId);
|
||||
if (scores[4] is null)
|
||||
{
|
||||
var present = allValues.Count(v => v.HasValue) + 0;
|
||||
_logger.LogDebug(
|
||||
"NEWS2 incomplete for encounter {Id}: consciousness missing ({Present}/7 present)",
|
||||
encounterId, present);
|
||||
return News2Result.IncompleteParameters(present);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!allValues[i].HasValue)
|
||||
{
|
||||
_logger.LogDebug(
|
||||
@@ -77,23 +98,15 @@ public class News2Detector
|
||||
scores[i] = cached?.Score;
|
||||
}
|
||||
|
||||
// All 7 present — compute aggregate
|
||||
var paramScores = scores.Select(s => s!.Value).ToArray();
|
||||
var totalScore = paramScores.Sum();
|
||||
var hasSingleParamThree = paramScores.Any(s => s == 3);
|
||||
var riskLevel = News2Calculator.DetermineRiskLevel(totalScore, hasSingleParamThree);
|
||||
|
||||
// Persist the score to PostgreSQL
|
||||
var scoreId = await PersistScoreAsync(
|
||||
await PersistScoreAsync(
|
||||
encounterId, patientId, totalScore, riskLevel,
|
||||
paramScores, hasSingleParamThree, ct);
|
||||
|
||||
_logger.LogInformation(
|
||||
"NEWS2 score {Score} ({Risk}) for encounter {Id} — components: {Components}",
|
||||
totalScore, riskLevel, encounterId,
|
||||
string.Join(",", News2Calculator.ParameterCodes.Zip(paramScores, (c, s) => $"{c}={s}")));
|
||||
|
||||
// Create alert if warranted
|
||||
var alertCreated = false;
|
||||
if (riskLevel == "HIGH")
|
||||
{
|
||||
@@ -112,6 +125,29 @@ public class News2Detector
|
||||
News2Outcome.ScoreComputed, totalScore, riskLevel, alertCreated, 7, hasSingleParamThree);
|
||||
}
|
||||
|
||||
private async Task<int?> ResolveConsciousnessScoreAsync(Guid encounterId)
|
||||
{
|
||||
var cache = _redis.GetDatabase();
|
||||
var gcsValues = await cache.StringGetAsync(GcsCalculator.AllComponentKeys(encounterId));
|
||||
|
||||
if (gcsValues.All(v => v.HasValue))
|
||||
{
|
||||
var eye = decimal.Parse(gcsValues[0]!);
|
||||
var verbal = decimal.Parse(gcsValues[1]!);
|
||||
var motor = decimal.Parse(gcsValues[2]!);
|
||||
var total = GcsCalculator.ComputeTotal(eye, verbal, motor)!.Value;
|
||||
return News2Calculator.ScoreConsciousnessFromGcs(total);
|
||||
}
|
||||
|
||||
var avpuVal = await cache.StringGetAsync(
|
||||
News2Calculator.ParameterKey(encounterId, "AVPU"));
|
||||
if (!avpuVal.HasValue)
|
||||
return null;
|
||||
|
||||
var cached = JsonSerializer.Deserialize<News2CachedParam>(avpuVal!, CachedParamJsonOptions);
|
||||
return cached?.Score;
|
||||
}
|
||||
|
||||
private async Task<Guid> PersistScoreAsync(
|
||||
Guid encounterId, Guid patientId,
|
||||
int totalScore, string riskLevel,
|
||||
|
||||
Reference in New Issue
Block a user