glp-companion/lib/features/medication/domain/usecases/calculate_serum_level.dart

69 lines
2.4 KiB
Dart

import 'dart:math' as math;
import '../entities/dose_entry.dart';
import '../value_objects/glp1_compound.dart';
/// Evaluates the accumulated pharmacokinetic serum concentration of GLP-1
/// receptor agonists using the standard single-compartment open decay model:
///
/// ```text
/// C(t) = sum over i of D_i * (0.5) ^ ((t - t_i) / t_half)
/// ```
///
/// Where `D_i` is a dose administered at time `t_i` and `t_half` is the
/// compound's elimination half-life (Semaglutide 168h, Tirzepatide 120h,
/// Liraglutide 13h). Doses recorded with a future [DoseEntry.administeredAt]
/// are ignored, and unrecognized compounds fall back to
/// [defaultHalfLifeHours].
///
/// The use case is pure Dart with no dependencies, so it runs identically in
/// the domain layer, widget tests, and unit tests.
class CalculateSerumLevel {
const CalculateSerumLevel({this.defaultHalfLifeHours = 168});
/// Half-life assumed for compounds that are not in [Glp1Compound].
final double defaultHalfLifeHours;
/// Convenience entry point mirroring the mathematical notation
/// `C(t = [at])`.
double call({
required Iterable<DoseEntry> doses,
required DateTime at,
double? halfLifeHours,
}) =>
evaluate(doses: doses, at: at, halfLifeHours: halfLifeHours);
/// Computes the total active serum concentration (mg) at [at].
///
/// [halfLifeHours] optionally forces a single half-life for every dose,
/// bypassing per-compound resolution (useful for custom titration studies).
double evaluate({
required Iterable<DoseEntry> doses,
required DateTime at,
double? halfLifeHours,
}) {
var totalMg = 0.0;
for (final dose in doses) {
if (dose.doseMg <= 0) continue;
final elapsed = at.difference(dose.administeredAt);
// Doses scheduled in the future have not entered circulation yet.
if (elapsed.isNegative) continue;
final halfLife = halfLifeHours ?? _halfLifeFor(dose.compoundName);
if (halfLife <= 0) continue;
final elapsedHours = elapsed.inMilliseconds / Duration.millisecondsPerHour;
totalMg += dose.doseMg * math.pow(0.5, elapsedHours / halfLife).toDouble();
}
return totalMg;
}
/// Resolves the elimination half-life for a compound label, falling back to
/// [defaultHalfLifeHours] when the name is unknown.
double _halfLifeFor(String compoundName) =>
Glp1Compound.tryParse(compoundName)?.halfLifeHours ??
defaultHalfLifeHours;
}