File size: 5,814 Bytes
e215d3c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
<?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);
    }
}