Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions Algorithm Explanation.md
Original file line number Diff line number Diff line change
Expand Up @@ -605,14 +605,21 @@ $$
- 求解:每个假设用 Gauss-Newton(IRLS 权重)求 MAP,步长减半直到后验不变差;从原点和“残差中位数按先验比例分给 s 与 d”的起点出发(允许移动清除时与 `mipd` 一样从网格上每隔 5 个点起步),保留最好的解。漂移块是三对角的(每个化验只连到自己的锚点),所以用加边三对角矩阵的 Schur 补求解,每步 O(锚点数)。Laplace 近似(Tierney & Kadane 1986)给出各假设的边际似然与后验协方差;曲线取各假设对数校正量后验均值按后验概率的加权平均,方差取混合方差。
- 额外输出:`factorSdFn(t)` 是对数校正量的后验标准差,化验附近最小,化验变旧后增大;`latestInterval` 是最近一次化验时刻校准水平的 90% 区间。允许移动清除后,幅度与半衰期会互相抵换,只看幅度的区间反而变宽,所以报告的是化验真正约束住的水平。`discounted` 是后验平均 Student-t 权重低于 ½ 的化验数;`drift` 是无漂移、约两周、约三个月、约一年四类记忆的后验概率。
- 预测页:选用 `bayes` 时,每次蒙特卡洛模拟仍只抽一个标准正态数(与其他方法共用同一随机序列,其他方法的范围不变),但按 `factorSdFn` 逐小时缩放,取代来自拟合误差的常数。只有一个化验时拟合误差无定义,其他方法不加校准不确定性,`bayes` 仍按后验给出范围。
- `mcmc`(个人药代,本项目自有):不再把一个人当成“原模型乘一个校正”,而是给每个人一套自己的药代参数,E2(t) = Σ_r exp(s + u_r) · C_r(t; k, a)(`src/domain/calibration/mcmc.ts`)。
- C_r 是只算给药途径 r(注射、口服、舌下、凝胶、贴片)时的模型 E2。模型对剂量是线性的,所以按途径拆开再相加与原模拟完全一致(`routeCurves.ts`,测试核对到 1e-9),个人系数乘在各自那一份上是精确的,不需要额外模拟。s 是各途径共用的个人水平,u_r 是该途径相对这个水平的个人偏差(#22 的舌下吸收比例就是其中之一),k 是对数个人清除(同 `mipd` 的清除倍数),a 是对数个人吸收速度:所有一阶吸收速率(注射储库释放、口服肠道、舌下黏膜、凝胶皮肤)同乘 exp(a),零级释放的贴片不受影响。吸收速度改变的是峰谷的位置和高低,不是平均水平。
- 先验:s 用 `bayes` 的 Student-t(ν = 4,尺度 0.50);u_r ~ N(0, 0.35²),分层,只有一种途径时不采样 u(此时模型即 `mipd` 加吸收速度),两种以上时各途径的化验通过共用的 s 互相借力;k ~ N(0, 0.35²),a ~ N(0, 0.40²),二者都限制在网格内(kMul ∈ [0.5, 2],吸收倍数 ∈ [0.4, 2.5])。0.35 与 0.40 是选定值。化验至少 3 簇才允许移动清除(与其他方法相同),至少 4 簇才允许移动吸收速度(`MCMC_MIN_LABS_FOR_ABSORPTION`),因为吸收只体现在化验落在给药间隔的哪个位置。每个化验的对数误差与 `bayes` 相同(`labLogSd`),似然少于 3 个化验用高斯,达到 3 个起用 Student-t(ν = 4)。
- 网格:在 13 × 9 个 (k, a) 网格点上算出各途径在化验时刻的浓度,采样时在浓度上做双线性插值。
- 采样:途径之和在对数尺度上不是线性的,后验没有闭式解,所以用 4 条链的自适应 Metropolis(Haario, Saksman & Tamminen 2001):预热 2500 步内用链自身的协方差(× 2.38²/维数)更新提议分布,并把步长朝 0.234 的接受率调(Roberts, Gelman & Gilks 1997);预热后提议分布固定,再走 2500 步,每 5 步保留一个,共 2000 个样本。用 split R-hat(Gelman et al. 2013)检查各链是否一致,结果放在 `rHat`。随机数种子由化验数据决定,同样的化验永远给出同样的曲线。
- 曲线:取 400 个样本,它们落到的网格点(按质量最多 16 个)上用整段时间轴算各途径曲线,后验均值是这些曲线的线性组合,所以画出的正好是样本打分时用的那个模型的平均;`factorSdFn` 用其中 150 个样本的对数校准量算标准差。`latestInterval` 是最近一次化验时刻校准水平的 5% 与 95% 分位数,`discounted` 同 `bayes`。额外输出 `absMul`(吸收速度倍数,界面显示为吸收速度变化)与 `routeScales`(两种以上途径时各途径的个人水平倍数)。
- 局限:不建模随时间的漂移(需要漂移请用 `bayes`);每日口服经过雌酮库平滑,吸收速度加倍只让化验变化约 10%,在化验误差之内,所以口服的吸收速度大多仍由先验决定,注射的储库释放速度则能从化验中较清楚地读出。尚未用真实的定时化验数据验证,因此不是默认方法。
- **个人清除**(仅 `mipd` 与 `ekf`):化验至少分成 3 簇才允许移动清除(`clearanceIdentifiable`,`MIN_LABS_FOR_CLEARANCE = 3`),否则只拟合幅度。分簇按时间顺序进行:比当前簇第一个化验晚 6 h 以上(`LAB_CLUSTER_H`)的化验开启新的一簇。同一时刻的几次抽血只算一簇(它们说明不了水平下降得多快);每 5 h 抽一次的一串化验按簇首计为多簇,例如 0、5、10、15、20 h 的五个化验是三簇(簇首为 0、10、20 h)。是否允许移动清除按化验的前缀判断:拟合前 i 个化验时只看这 i 个。清除网格覆盖 kMul ∈ [0.5, 2](`KMUL_MIN` / `KMUL_MAX`),在对数尺度上取 21 个网格点,每个点把 `e2_kClear` 与 `e2_kClearInj` 同乘该倍数重跑一次模拟(`runSimulationWithParams`;吸收速率与口服雌酮库 `kE1Pool` 不变),拟合时在网格之间线性插值。网格外的预测值取端点值、斜率为 0,拟合出的对数清除截在网格范围内(`clampLogK`),因此报告的 kMul 始终在 [0.5, 2] 内,半衰期变化在 −50% 到 +100% 之间。校准后的曲线在相邻两个网格点的对数浓度之间按与拟合相同的方式插值(`fieldLogConcAt`),所以画出的就是拟合所用的那条曲线(此前取最近的网格点,与拟合曲线最多差约 5%,报告的 kMul 也可能超出所画曲线的范围)。报告的半衰期变化为 (1/kMul − 1) × 100%,界面显示的半衰期变化取自此值。
- **历史模式**(`CalibrationHistoryMode`,默认 `retrospective`):
- `retrospective`:用全部化验值重估整段历史。`mipd` 与 `ekf` 对整段曲线使用最终拟合(幅度倍数乘以“该清除下的曲线 / 原曲线”,该清除下的曲线由网格插值得到);`ou_kalman` 使用平滑结果,两次化验之间按 OU 桥插值;`bayes` 使用对全部化验的模型平均。新化验可能改写过去的估计。
- `forward`:因果滤波,每个时刻只用到当时为止的化验值。第一个化验之前倍数为 1;第 i 个化验之后使用前 i 个化验学到的参数(`ekf`、`mipd` 与 `bayes` 都用前 i 个化验重新拟合,是否允许移动清除也只看这 i 个化验,`bayes` 的漂移从第 i 个化验向后外推;`ou_kalman` 只用前向滤波)。在之后的日期再加化验时,四种方法的过去估计都不会改变。
- `retrospective`:用全部化验值重估整段历史。`mipd` 与 `ekf` 对整段曲线使用最终拟合(幅度倍数乘以“该清除下的曲线 / 原曲线”,该清除下的曲线由网格插值得到);`ou_kalman` 使用平滑结果,两次化验之间按 OU 桥插值;`bayes` 使用对全部化验的模型平均,`mcmc` 使用对全部化验的后验样本。新化验可能改写过去的估计。
- `forward`:因果滤波,每个时刻只用到当时为止的化验值。第一个化验之前倍数为 1;第 i 个化验之后使用前 i 个化验学到的参数(`ekf`、`mipd`、`bayes` 与 `mcmc` 都用前 i 个化验重新拟合,是否允许移动清除也只看这 i 个化验,`bayes` 的漂移从第 i 个化验向后外推;`ou_kalman` 只用前向滤波)。在之后的日期再加化验时,五种方法的过去估计都不会改变。
- **拟合误差**:至少 2 个化验值时,取实际绘制的曲线(模型 × `factorFn`)在各化验时刻的对数残差 RMSE,折算为 ±% 报告(`curveFitErrPct`),所有方法与历史模式都如此,因此与图上看到的一致。预测页的不确定范围也取自它(`bayes` 除外,见上)。
- **旧设置迁移**(`normalizeCalibrationMethod`):存储的 `average` 映射为 `mipd`,`adaptive` 映射为 `ekf`,无法识别的值也按 `mipd` 处理。
- **年龄**:填写了出生年份时,原始模型已按年龄调整(§17),清除网格的每个点也用同一出生年份重跑,所以拟合出的个人清除 kMul 与界面显示的半衰期变化都是相对年龄调整后的模型而言;年龄相当于把个人清除先验的中心移到该年龄的平均值。
- `ekf`、`ou_kalman` 与 `mipd` 参照 hrt.transmtf.com 提供的校准模型,在本项目中针对可从化验识别的两个参数(个人幅度与个人清除)重新实现(`logic.ts` 注释);`bayes` 是本项目自有的方法。
- `ekf`、`ou_kalman` 与 `mipd` 参照 hrt.transmtf.com 提供的校准模型,在本项目中针对可从化验识别的两个参数(个人幅度与个人清除)重新实现(`logic.ts` 注释);`bayes` 与 `mcmc` 是本项目自有的方法。

---

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ See [DONATE.md](./DONATE.md) for how donations are split (40% project, 30% @fant
## Lab results

- Log blood test results and see them on the curve next to the estimate
- Calibrate the estradiol model to you with EKF, OU-Kalman, MIPD or Full Bayes, or edit its parameters by hand
- Calibrate the estradiol model to you with EKF, OU-Kalman, MIPD, Full Bayes or Personal PK (a level per route, clearance and absorption speed, sampled with MCMC), or edit its parameters by hand

## Supplies

Expand Down
2 changes: 1 addition & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@
## 检查结果

- 记录抽血检查结果,并在曲线上与估算值对照
- 用 EKF、OU-Kalman、MIPD 或全贝叶斯按你的情况校准雌二醇模型,也可以手动编辑参数
- 用 EKF、OU-Kalman、MIPD、全贝叶斯或个人药代(每种给药途径各自的水平、清除和吸收速度,用 MCMC 采样)按你的情况校准雌二醇模型,也可以手动编辑参数

## 药量库存

Expand Down
1 change: 1 addition & 0 deletions docs/backup-schema-audit/BACKUP_TEST_VECTORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ it has no keys or signature to check.
| BTV-16 | Case-variant JSON keys | `"Mode"` or `"Format"` instead of `"mode"`/`"format"`; `"Events"` instead of `"events"`; both `"Mode":"transmasc"` and `"mode":"transfem"` in one object | Object keys are case-sensitive. `"Mode"` leaves `mode` missing → envelope error; `"Format"` leaves `format` missing; `"Events"` is not a known category → treated as missing and reported, never merged. | Go `encoding/json` matches envelope struct fields case-insensitively. If a case variant and the lowercase key both occur, both decode to the same field and the later JSON member wins (last-wins). Mode names inside the `modes` map and category keys (`events`, `labResults`, `doseTemplates`, `quickDoses`, `supplies`, `plan`) remain exact lowercase. | **TOOLS AUTHORITATIVE** | Neither side matches category keys case-insensitively; `"Events"` is ignored by both. Envelope fields can differ: Go may accept a spelling the website rejects, and case variants resolve last-wins in Go but lowercase-only in JS. |
| BTV-17 | Tools encrypted / inspect rules | Literal, compressed-plaintext, or signed-only OpenPGP payload; forged PKESK recipient advertisement; tampered ciphertext; trailing packet after the encrypted data packet | The website never encrypts/decrypts complete backups and holds no OpenPGP keys or passphrases; it only reads the plaintext bundle. | `decrypt` and `inspect` validate the outer sequence: one or more PKESK packets, exactly one integrity-protected encrypted data packet, then EOF. Only legal marker packets may precede the encrypted packet; missing recipient/payload, a second encrypted packet, or any trailing packet is rejected. `decrypt` verifies the MDC and that the supplied private key actually decrypts. `inspect` only reads the outer container and unauthenticated PKESK advertisements, always reports `recipientVerified:false`, and does not decrypt or check the MDC, so it cannot detect ciphertext tampering. | **TOOLS AUTHORITATIVE** | A `.hrtb` that passes `inspect` is not recipient proof or integrity proof; only a successful `decrypt` demonstrates those. A decrypted bundle still carries website-defined record semantics. |
| BTV-18 | Calibration method `bayes` | `"calibration":{"method":"bayes","historyMode":"retrospective"}` | Accepts and keeps `bayes` (Full Bayes calibration, added 2026-11-04). Older website versions report `calibration.method is invalid`. | Rejects anything outside `off`, `ekf`, `ou_kalman`, `mipd`. | **WEBSITE AUTHORITATIVE** | A bundle exported with Full Bayes selected cannot be encrypted by Tools until Tools accepts `bayes`; switching method first works. |
| BTV-19 | Calibration method `mcmc` | `"calibration":{"method":"mcmc","historyMode":"retrospective"}` | Accepts and keeps `mcmc` (Personal PK calibration, added 2026-11-11). Older website versions report `calibration.method is invalid`. | Rejects anything outside `off`, `ekf`, `ou_kalman`, `mipd`. | **WEBSITE AUTHORITATIVE** | A bundle exported with Personal PK selected cannot be encrypted by Tools until Tools accepts `mcmc`; switching method first works. |

## Source account

Expand Down
6 changes: 6 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ import Disclaimer from './pages/Disclaimer';
import OpSec from './pages/OpSec';
import PKDemos from './pages/PKDemos';
import ModelGuide from './pages/ModelGuide';
import CalibrationGuide from './pages/CalibrationGuide';
import MilkTeaEasterEgg from './pages/MilkTeaEasterEgg';
import CatStates from './pages/CatStates';
import Simulator from './pages/Simulator';
Expand Down Expand Up @@ -1057,10 +1058,15 @@ const AppContent = () => {
historyMode={calibrationHistoryMode}
setHistoryMode={setCalibrationHistoryMode}
calibration={calibration}
onOpenGuide={() => handleViewChange('lab-calibration-guide')}
onBack={() => handleViewChange('lab')}
/>
)}

{currentView === 'lab-calibration-guide' && (
<CalibrationGuide weight={weight} onBack={() => handleViewChange('lab-calibration')} />
)}

{currentView === 'settings' && (
<Settings
t={t}
Expand Down
Loading
Loading