dochia-cycle-model / src /ModelParams.php
danielpetrica's picture
Dochia v0.1.0: model card, params and reference implementations
e215d3c
Raw History Blame Contribute Delete
5.81 kB
<?php
declare(strict_types=1);
namespace Dochia;
/**
* Model parameters. Mirrors params/*.json so the PHP reference and the Python
* reference use exactly the same numbers.
*/
final class ModelParams
{
public function __construct(
public readonly string $version = 'dochia-v1-literature',
public readonly float $popMean = 29.0,
public readonly float $popBetweenSd = 3.5,
public readonly float $withinSdPrior = 2.0,
public readonly float $priorWeight = 4.0,
public readonly float $minMean = 18.0,
public readonly float $maxMean = 90.0,
public readonly float $minWithinVar = 1.44,
public readonly float $maxWithinVar = 36.0,
public readonly float $lutealMean = 14.13,
public readonly float $lutealSd = 1.41,
public readonly int $fertileWindowDays = 6,
public readonly float $ovulationSdFloor = 0.35,
public readonly float $ovulationGridStep = 0.25,
public readonly float $varianceBlend = 1.0,
public readonly bool $momentMatch = false,
public readonly float $tailProbBase = 0.10,
public readonly float $tailProbMax = 0.40,
public readonly float $tailProbSdGain = 0.06,
public readonly float $tailProbRangeGain = 0.02,
public readonly float $tailProbGapBonus = 0.20,
public readonly float $tailShiftDays = 3.0,
public readonly float $tailSdMultiplier = 2.2,
public readonly float $trendEwma = 0.4,
public readonly float $trendThreshold = 3.0,
public readonly float $trendMaxShift = 3.0,
public readonly float $bodyScale = 1.0,
public readonly float $tailScaleLow = 1.0,
public readonly float $tailScaleHigh = 1.0,
public readonly float $tailAnchorLow = 0.10,
public readonly float $tailAnchorHigh = 0.90,
public readonly string $calibrationMethod = 'none',
public readonly int $minLength = 15,
public readonly int $maxLength = 120,
public readonly int $hazardMaxDay = 120,
public readonly int $abstainGapDays = 90,
public readonly float $abstainRecentSd = 6.0,
public readonly int $abstainMinCycles = 0,
) {
}
/** @param array<string, mixed> $raw */
public static function fromArray(array $raw): self
{
$pop = is_array($raw['population'] ?? null) ? $raw['population'] : [];
$luteal = is_array($raw['luteal'] ?? null) ? $raw['luteal'] : [];
$pred = is_array($raw['predictive'] ?? null) ? $raw['predictive'] : [];
$cal = is_array($raw['calibration'] ?? null) ? $raw['calibration'] : [];
$plaus = is_array($raw['plausible'] ?? null) ? $raw['plausible'] : [];
$ov = is_array($raw['ovulation'] ?? null) ? $raw['ovulation'] : [];
$ab = is_array($raw['abstain'] ?? null) ? $raw['abstain'] : [];
$haz = is_array($raw['hazard'] ?? null) ? $raw['hazard'] : [];
return new self(
version: (string) ($raw['version'] ?? 'dochia-v1-literature'),
popMean: (float) ($pop['mean'] ?? 29.0),
popBetweenSd: (float) ($pop['between_sd'] ?? 3.5),
withinSdPrior: (float) ($pop['within_sd_prior'] ?? 2.0),
priorWeight: (float) ($pop['prior_weight'] ?? 4.0),
minMean: (float) ($pop['min_mean'] ?? 18.0),
maxMean: (float) ($pop['max_mean'] ?? 90.0),
minWithinVar: (float) ($pop['min_within_var'] ?? 1.44),
maxWithinVar: (float) ($pop['max_within_var'] ?? 36.0),
lutealMean: (float) ($luteal['mean'] ?? 14.13),
lutealSd: (float) ($luteal['sd'] ?? 1.41),
fertileWindowDays: (int) ($raw['fertile_window_days'] ?? 6),
ovulationSdFloor: (float) ($ov['sd_floor'] ?? 0.35),
ovulationGridStep: (float) ($ov['grid_step'] ?? 0.25),
varianceBlend: (float) ($pred['variance_blend'] ?? 1.0),
momentMatch: (bool) ($pred['moment_match'] ?? false),
tailProbBase: (float) ($pred['tail_prob_base'] ?? 0.10),
tailProbMax: (float) ($pred['tail_prob_max'] ?? 0.40),
tailProbSdGain: (float) ($pred['tail_prob_sd_gain'] ?? 0.06),
tailProbRangeGain: (float) ($pred['tail_prob_range_gain'] ?? 0.02),
tailProbGapBonus: (float) ($pred['tail_prob_gap_bonus'] ?? 0.20),
tailShiftDays: (float) ($pred['tail_shift_days'] ?? 3.0),
tailSdMultiplier: (float) ($pred['tail_sd_multiplier'] ?? 2.2),
trendEwma: (float) ($pred['trend_ewma'] ?? 0.4),
trendThreshold: (float) ($pred['trend_threshold'] ?? 3.0),
trendMaxShift: (float) ($pred['trend_max_shift'] ?? 3.0),
bodyScale: (float) ($cal['body_scale'] ?? 1.0),
tailScaleLow: (float) ($cal['tail_scale_low'] ?? 1.0),
tailScaleHigh: (float) ($cal['tail_scale_high'] ?? 1.0),
tailAnchorLow: (float) ($cal['tail_anchor_low'] ?? 0.10),
tailAnchorHigh: (float) ($cal['tail_anchor_high'] ?? 0.90),
calibrationMethod: (string) ($cal['method'] ?? 'none'),
minLength: (int) ($plaus['min'] ?? 15),
maxLength: (int) ($plaus['max'] ?? 120),
hazardMaxDay: (int) ($haz['max_day'] ?? 120),
abstainGapDays: (int) ($ab['gap_days'] ?? 90),
abstainRecentSd: (float) ($ab['recent_sd'] ?? 6.0),
abstainMinCycles: (int) ($ab['min_cycles'] ?? 0),
);
}
public static function fromJson(string $path): self
{
$raw = json_decode((string) file_get_contents($path), true, 512, JSON_THROW_ON_ERROR);
if (! is_array($raw)) {
throw new \RuntimeException("Invalid model params JSON: {$path}");
}
return self::fromArray($raw);
}
}