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 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 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; }