69 lines
2.4 KiB
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;
|
|
} |