Blood and hemolysis
Hemoglobin from a photo, eye yellowness, hemolysis, transfusions and iron.
Each algorithm is described with what it reads, its steps, the values it uses (with their place in the code), what it shows and its limits.
Entry and conversion of lab results
When you type a lab value, or when Trevor reads one in a document, it converts it to the unit of its catalog, checks that it is plausible and not already in the record. These rules serve hemolysis, iron, fetal hemoglobin and creatinine. The original unit is kept next to the converted value.
Where you see it: Confirmation messages (Noted: LDH 320 U/L on 07.10.2026), refusals with a reason, and display of each result in the unit you chose in settings, with the original unit in parentheses.
What the algorithm reads
- Typed value (2.5 mg/dL, 112, undetectable), optional unit, date (self-report, import, document read)
- Lab unit chosen in settings (standard, SI or conventional) (settings)
The calculation, step by step
The values used
| Name in the code | Value | Role | Where |
|---|---|---|---|
Hemoglobin | 3 to 20 g/dL ; g/L × 0.1 | Bounds and factor. | src/trevor/units.py:83-84 |
Total bilirubin | 1 to 600 µmol/L ; mg/dL × 17.1 | Bounds (1 to 600) and factor. | src/trevor/units.py:85-86 |
Indirect bilirubin | 0.5 to 600 µmol/L ; mg/dL × 17.1 | Bounds and factor. | src/trevor/units.py:87-88 |
Creatinine | 10 to 1500 µmol/L ; mg/dL × 88.4 | Bounds and factor. | src/trevor/units.py:89-90; src/trevor/royal/labs.py:22 |
Ferritin | 1 to 60000 µg/L ; ng/mL × 1 ; mg/L × 1000 | Bounds and factors. | src/trevor/units.py:91-92 |
Haptoglobin | 0 to 8 g/L ; mg/dL × 0.01 ; mg/L × 0.001 | Bounds and factors. | src/trevor/units.py:93-94 |
LDH | 30 to 6000 U/L ; UI/L × 1 | Bounds. | src/trevor/units.py:95-96 |
Reticulocytes | 0.5 to 1500 ×10⁹/L ; /µL × 0.001 ; G/L × 1 | Bounds and factors. | src/trevor/units.py:97-99 |
Hemoglobin S, fetal hemoglobin | 0 to 100 % | Bounds in percent. | src/trevor/units.py:100-101; src/trevor/royal/labs.py:21 |
MAX_BACK_DAYS | 5 years (hemolysis, HbF, creatinine); 10 years (iron) | Maximum age of an entered date. | src/trevor/hemolysis.py:262; src/trevor/royal/labs.py:19; src/trevor/iron.py:35 |
Hemoglobin without unit | above 25: divided by 10 | Heuristic for g/L with no unit. | src/trevor/hemolysis.py:289 |
What is shown
For each entry: either the value saved in the catalog unit with a confirmation sentence, or a refusal that gives the reason in one sentence (no number, unit not recognized, percentage refused, out of bounds, future or too old date, record not open), or Already in your record. No level or clinical threshold here.
A worked example
2.5 mg/dL of total bilirubin becomes 2.5 × 17.1 = 42.75 µmol/L. A bare 112 of hemoglobin becomes 11.2 g/dL. 2% of reticulocytes is refused. Undetectable haptoglobin becomes 0 g/L. 95 g/L of hemoglobin becomes 9.5 g/dL (results obtained by running normalize_lab).
What this algorithm cannot do
- Three functions write almost the same rule (hemolysis.normalize_lab, iron.normalize, royal/labs.normalize). Only the iron one accepts thousands written with a space (1 450); the other two would read 1.
- The threshold for reading hemoglobin as g/L differs by place: 25 on entry with no unit, 40 for HemoSnap values in hemolysis, 30 for the transfusion response.
- Today's date comes from the person's local time for hemolysis and iron, but from server time for HbF and creatinine (royal/labs.py).
- The text < 0,1 for haptoglobin is recognized only with that exact wording (with space and comma); <0.1 is read as 0.1.
- A bare number is read in your settings unit, which can change the value by a factor of 17.1 if the setting is not the lab's.
- The conversion factors are rounded clinical-practice values (17.1, not 17.104), flagged as such in units.py.
Code : src/trevor/hemolysis.py (LAB_ENTRY, normalize_lab, add_lab, items_from_parsed), src/trevor/iron.py (normalize, add), src/trevor/royal/labs.py (LAB_EXTRA, normalize, add_lab), src/trevor/units.py (LABS, lab_factors, entry_unit, original_reading, lab_to_unit, lab, lab_pair)
Hemolysis assessment: six markers against your own baseline
Trevor compares your latest value of each marker (total and indirect bilirubin, LDH, reticulocytes, haptoglobin, hemoglobin) with the median of your earlier values. It counts how many recent markers move in the direction of hemolysis and derives a level: stable, to watch, or suggests a hemolysis flare. It describes your data and makes no diagnosis.
Where you see it: Bot Hemolysis screen, web record page and app: one level sentence, the list of flagged markers, what is missing to conclude, and a reminder that these are not diagnoses.
What the algorithm reads
- Lab results in the record: bilirubin, indirect bilirubin, LDH, reticulocytes (absolute count), haptoglobin, hemoglobin, with dates (health record, import, self-report)
- Lab hemoglobin values entered in HemoSnap (not estimates) (self-report)
- Date of the last transfusion (health record)
The calculation, step by step
The values used
| Name in the code | Value | Role | Where |
|---|---|---|---|
PANEL_DAYS | 21 | Window around the most recent measurement in which markers count as one panel. | src/trevor/hemolysis.py:36 |
MIN_BASELINE | 2 | Earlier values required. | src/trevor/hemolysis.py:37 |
ROBUST_FROM | 4 | Earlier values from which the 2 robust SD test is added. | src/trevor/hemolysis.py:38 |
AFTER_TRANSFUSION_DAYS | 28 | Time after transfusion that triggers the warning. | src/trevor/hemolysis.py:39 |
MARKERS bilirubin / indirect | +30 % / +30 % | Relative rise thresholds. | src/trevor/hemolysis.py:55-56 |
MARKERS LDH | +25 % | Relative rise threshold. | src/trevor/hemolysis.py:57 |
MARKERS reticulocytes | +30 % | Relative rise threshold (absolute count). | src/trevor/hemolysis.py:58 |
MARKERS haptoglobin | drop of 50 % | Relative fall threshold. | src/trevor/hemolysis.py:59 |
MARKERS hemoglobin | drop of 1.0 g/dL | Absolute fall. | src/trevor/hemolysis.py:60 |
HAPTO_UNDETECTABLE | 0.1 g/L | Baseline below which haptoglobin is no longer compared. | src/trevor/hemolysis.py:62 |
Robust standard deviation floor | 0.08 × |baseline| | The robust SD never falls below 8% of the baseline. | src/trevor/hemolysis.py:95-97 |
What is shown
Five levels: none (no marker in the record), not enough data (fewer than 2 current markers), stable (0 flagged), to watch (1 flagged) and suggests a hemolysis flare (2 or more flagged). The to watch and flare levels invite you to talk to the care team; to watch adds that one marker alone is not enough to conclude. The delayed reaction note appears only for to watch and flare within 28 days after a transfusion.
A worked example
Total bilirubin: values 50, 55, 52, then 80 µmol/L. Baseline (median of 50, 55, 52) = 52. Threshold = 52 × 1.30 = 67.6. 80 exceeds it, and with only 3 earlier values the robust test does not apply: status flagged, +54%. With LDH also flagged within 21 days, the level becomes suggests a hemolysis flare (computed by running judge).
What this algorithm cannot do
- A current marker without a baseline (fewer than 2 earlier values) or with undetectable haptoglobin still counts among current markers: two markers with no baseline give the stable level although nothing was compared.
- Total and indirect bilirubin are two separate markers: one rise in bilirubin, measured in both forms, counts as two flags and is enough for suggests a flare.
- The thresholds are the same for everyone: age, sex, disease and assay reliability do not change them.
- The module cannot tell hemolysis from another cause: the module text recalls that LDH also rises with muscle or liver and reticulocytes with bleeding. Only a recent transfusion (28 days) is taken into account, through a message.
- The lab values are those you entered or imported; a wrong or misdated value skews the comparison.
Code : src/trevor/hemolysis.py (series, judge, lab_view, missing_markers), src/trevor/conversation/hemolysis_screens.py (_hemolysis_lines), src/trevor/transfusion.py (last_transfusion)
HemoSnap: hemoglobin estimated from a fingertip photo
HemoSnap estimates your hemoglobin from three photos of a fingertip placed over the camera lens and flash. A number appears only after calibration on one of your lab values. It is an experimental feature with no clinical validation and does not replace a blood test.
Where you see it: In the bot, My health menu, HemoSnap entry: disclaimer, intro screen, three photos, result with a range, then calibration (lab value, date, link to a transfusion). The same calculation serves the phone app (signals.py).
What the algorithm reads
- Three fingertip photos, flash on, finger covering lens and flash (photo)
- One or more lab hemoglobin values (3 to 20 g/dL) with their date (self-report)
- Answer to the question: just before, just after, or unrelated to a transfusion (self-report)
The calculation, step by step
The values used
| Name in the code | Value | Role | Where |
|---|---|---|---|
POP_SLOPE | 9.0 | Population slope (g/dL per index unit), used with a single point. | src/trevor/media/hemosnap.py:30 |
POP_INTERCEPT | 6.0 | Population intercept. It does not enter any estimate while there is no point: with no point, no number is given. | src/trevor/media/hemosnap.py:31 |
HB_MIN, HB_MAX | 3.0 and 20.0 g/dL | Bounds of the entered lab value and of the displayed range. | src/trevor/media/hemosnap.py:32 |
PLAUSIBLE | (4.0, 19.0) g/dL | An estimate outside this range is not shown. | src/trevor/media/hemosnap.py:33 |
GREEN_MIN | 35.0 | Minimum median green (0 to 255): below it, the index blows up. | src/trevor/media/hemosnap.py:34 |
SLOPE_RANGE | (3.0, 18.0) | Plausible personal slope. | src/trevor/media/hemosnap.py:35 |
MODEL_BAND_1PT / 2PT / 4PT | 1.4 / 0.8 / 0.6 g/dL | Model part of the uncertainty by number of points (1, 2 to 3, 4 or more). | src/trevor/media/hemosnap.py:42-44 |
MEAS_BAND_1SHOT | 0.8 g/dL | Noise of one shot, divided by the square root of the number of shots. | src/trevor/media/hemosnap.py:45 |
HEMO_SHOTS_NEEDED | 3 | Number of consecutive shots in the bot. | src/trevor/conversation/wearables_screen.py:792 |
Quality thresholds | brightness 40 to 240, red share 0.5, green 35, red margin 8 | Conditions of quality_check; a pixel counts as red if red exceeds green and blue by more than 8. | src/trevor/media/hemosnap.py:91,105-111 |
What is shown
The result reads: Estimate: about X g/dL, in a range of A to B g/dL, followed by the number of lab values in the calibration and a note saying the feature is experimental, not validated, and less reliable on dark skin. Internal levels: not calibrated (no number), out of range (no number), calibrated with 1, 2 or more points. Each numeric estimate is saved with its index.
A worked example
One lab point: index 0.5 for 9.0 g/dL. The slope stays 9.0 and the intercept becomes 4.5. Three shots give a median index of 0.6: 9.0 × 0.6 + 4.5 = 9.9 g/dL. Half-width: square root of (1.4² + 0.8²/3) = 1.5. Display: 9.9 g/dL, range 8.4 to 11.4 (computed by running the code).
What this algorithm cannot do
- It is not a validated medical device: no accuracy study is cited in the code or texts of the repository.
- Calibration ignores the date of the lab value and the transfusion link: both answers are stored but do not enter the line (fit_calibration receives only index and value pairs).
- The text shown after a just-after-transfusion value says Trevor accounts for the drop in hemoglobin afterwards. The HemoSnap code applies no such correction.
- Two points close in hemoglobin give an unstable slope; the 3 to 18 bound limits it without fixing it.
- The uncertainty depends on the number of points, not on how far apart their levels are. Two points at the same hemoglobin level still get the 0.8 g/dL band.
- On the phone (signals.py), the index is computed outside the server: the server only checks that it lies between -1 and 5.6, with no photo quality check.
- The claim that transillumination reduces melanin bias comes from the module documentation, with no measurement in the repository.
Code : src/trevor/media/hemosnap.py (extract_features, quality_check, fit_calibration, uncertainty_band, estimate), src/trevor/conversation/wearables_screen.py (hemosnap_process, on_hemosnap_calibrate, on_hemosnap_cal_date, on_hemosnap_cal_transf), src/trevor/storage/database.py (hemo_calibration_points), src/trevor/signals.py (_hemoglobin, hemosnap_reading)
Scleral yellow: index and estimated bilirubin
Trevor measures the yellow of the white of your eye from a photo and derives an index. After several photo and blood test pairs, it converts the index into an estimated bilirubin with an interval. Without calibration, it only says whether you are more yellow, the same, or less yellow than usual.
Where you see it: In the bot Hemolysis screen (Eye yellow line) and on the web page: saved index, estimated bilirubin with range, no clear yellow, trend, and the number of calibration pairs out of 5.
What the algorithm reads
- Photo of the white of the eye, no flash (photo)
- Photo of a white sheet of paper in the same light (optional) (photo)
- Lab bilirubin (µmol/L or mg/dL, between 3 and 400 µmol/L), entered on the photo or found in the record within 2 days (self-report, health record)
The calculation, step by step
The values used
| Name in the code | Value | Role | Where |
|---|---|---|---|
MIN_POINTS_PROVISIONAL | 3 | Minimum pairs for a provisional number. | src/trevor/media/scleral.py:37 |
MIN_POINTS_CALIBRATED | 5 | Pairs for a calibrated number. | src/trevor/media/scleral.py:38 |
MIN_R_CALIBRATED | 0.6 | Minimum correlation for calibrated (below 0.3 or negative slope: poor). | src/trevor/media/scleral.py:39,218-224 |
BAND_FLOOR | 8.0 µmol/L | Minimum half-width of the interval. | src/trevor/media/scleral.py:40 |
VISIBLE_ICTERUS | 35.0 µmol/L | Below this bilirubin, the sclera stays white to the eye. | src/trevor/media/scleral.py:41 |
BILI_MIN, BILI_MAX | 3.0 and 400.0 µmol/L | Display and entry bounds. | src/trevor/media/scleral.py:42 |
MG_DL_TO_UMOL | 17.1 | mg/dL to µmol/L conversion. | src/trevor/media/scleral.py:43 |
TREND_MIN_SHIFT, TREND_MIN_HISTORY | 1.5 b* units; 3 measurements | Minimum shift and minimum history for the trend. | src/trevor/media/scleral.py:44-45 |
MIN_SCLERA_FRACTION | 0.02 | Minimum share of the image occupied by the white of the eye. | src/trevor/media/scleral.py:46 |
EYE_MIN_L, BLOWN_L, EYE_MIN_LIGHT | 50 ; 97 ; 58 | Minimum pixel lightness, blown-out threshold, minimum median lightness. | src/trevor/media/scleral.py:47-49 |
MAX_BLOWN_IMAGE, MAX_GLARE, MAX_SPREAD | 0.30 ; 0.25 ; 8.0 | Share of blown-out image, share of glare around the white, maximum b* spread. | src/trevor/media/scleral.py:50-52 |
EYE_PAIR_DAYS | 2 | Maximum gap between the photo and a record bilirubin. | src/trevor/hemolysis.py:41 |
What is shown
Three calibration levels: none (fewer than 3 pairs, trend only), provisional (3 or 4 pairs), calibrated (5 or more pairs, correlation of at least 0.6), plus the poor stage (negative slope or correlation under 0.3, no number). The number is the estimated bilirubin rounded in µmol/L with its bounds. Without an estimate, the trend says more yellow, same as usual, or less yellow. A text reminds that the photo estimates only bilirubin and makes no diagnosis.
A worked example
Five (index, bilirubin) pairs: (1, 20), (2, 30), (3, 41), (4, 52), (5, 60). Slope 10.2, intercept 10, correlation 0.999: calibrated stage. New index 3.5: 10.2 × 3.5 + 10 = 45.7, i.e. 46 µmol/L. The computed interval is about 3.4, so the floor of 8 applies: 38 to 54 µmol/L. Index 0: 10 µmol/L, under 35, so no clear yellow (run with the code).
What this algorithm cannot do
- It is not a validated medical device. The repository document (docs/CALIBRAGE_HEMOLYSE.md) still lists an accuracy study, a hematology agreement and a regulatory qualification as to do.
- The index depends on the device, the light and pigmentation: personal calibration absorbs a stable offset, not a change of phone or place.
- Calibration pairs from photos taken with and without a sheet are mixed in the same line.
- With exactly 3 pairs, the Student interval (12.7 for 1 degree of freedom) is very wide before the floor applies.
- On the phone (signals.py eye_numbers), numbers arrive already computed: the server only checks bounds (b* from -20 to 80, lightness 0 to 100) and the spread is set to 0.
- The no clear yellow message relies on the 35 µmol/L threshold, a visible-jaundice value set in the code.
Code : src/trevor/media/scleral.py (extract_eye, extract_paper, index_of, fit, estimate, trend, to_umol), src/trevor/hemolysis.py (record_eye, save_eye, calibration_points, eye_view, set_lab), src/trevor/conversation/hemolysis_screens.py (_hemolysis_lines), src/trevor/signals.py (eye_numbers)
Reading a lab PDF
Trevor reads the words and their positions in a lab PDF that contains text, finds the hemolysis markers and attaches each value to its sampling date. Nothing is invented: a value is kept only if it is aligned under a date. You review the result before anything is saved.
Where you see it: Review screen after sending the PDF: one checkbox per value read, which you tick or untick before saving.
What the algorithm reads
- Lab PDF with text (HUG report, German-language follow-up report) (import)
The calculation, step by step
The values used
| Name in the code | Value | Role | Where |
|---|---|---|---|
MAX_PAGES | 300 | Beyond it, refusal. | src/trevor/media/lab_pdf.py:36 |
Row tolerance | 3.0 points | Maximum vertical gap within a line. | src/trevor/media/lab_pdf.py:121 |
Column tolerance | max(14, 0.45 × minimum gap between columns) | Maximum distance between a value and its column. | src/trevor/media/lab_pdf.py:166-167 |
Share of header dates | at least 2 dates and 80 % of the row | Condition for a header line. | src/trevor/media/lab_pdf.py:155 |
Column deduplication | 8 points | Two dates within 8 points of center form one column. | src/trevor/media/lab_pdf.py:158 |
Value cap | 60 | Values kept on return. | src/trevor/hemolysis.py:365 |
What is shown
A list of values (marker, value in the catalog unit, unit, date, below-limit flag, original unit) to review. No clinical level or threshold.
A worked example
none (reading depends on the layout of a real PDF, which is not in the repository). Only label recognition was run: Total bilirubin in µmol/l becomes bilirubin; Reticulocytes nb.abs. in o/oo and Free hemoglobin give nothing.
What this algorithm cannot do
- Only two layouts are recognized: dated-column table and current-with-earlier result on one line. Another layout can give an empty list or misaligned values, hence the review.
- Labels must match the expected patterns exactly (French, plus German for bilirubin, hemoglobin and LDH): another wording is ignored without warning.
- A < sign is remembered (below-limit flag) but the stored value is the written number, converted: haptoglobin < 73 mg/L becomes 0.073 g/L.
- If two values of the same marker fall on the same day, the last one read wins with no conflict warning.
Code : src/trevor/media/lab_pdf.py (read_lab_pdf, marker_of, _read_table, _read_inline, _columns, _split), src/trevor/hemolysis.py (read_pdf_items, normalize_lab)
Hemoglobin response to transfusions
Trevor compares your hemoglobin before and after each transfusion to estimate what one unit gives you, how long the gain lasts, and whether exchanges respect the dose and interval of their protocol. It describes your data and recommends no dose.
Where you see it: Your transfusion response card: dashboard web page, app and export.
What the algorithm reads
- Sessions with date, type, number of units (self-report)
- Lab hemoglobin (record and HemoSnap lab values) and HemoSnap estimates (health record, photo)
- Hemoglobin S in % (record) (health record)
- Weight on the care card (25 to 250 kg) (health record)
The calculation, step by step
The values used
| Name in the code | Value | Role | Where |
|---|---|---|---|
PRIOR_PER_UNIT | 1.0 g/dL per unit | Adult reference. | src/trevor/transfusion_response.py:35 |
PRIOR_SD | 0.4 | Uncertainty of the reference. | src/trevor/transfusion_response.py:36 |
LAB_SD, ESTIMATE_SD | 0.25 ; 1.0 g/dL | Measurement error of a lab and of a HemoSnap estimate. | src/trevor/transfusion_response.py:37 |
HBS_SD | 2.0 percentage points | Error of a hemoglobin S assay. | src/trevor/transfusion_response.py:38 |
HBS_TARGET | 30.0 % | Reference under which hemoglobin S is sought to be kept between exchanges; the code says to confirm with hematology. | src/trevor/transfusion_response.py:39 |
PRE_WINDOW, POST_WINDOW | 7 days before; days 1 to 7 after | Search windows. | src/trevor/transfusion_response.py:40-41 |
MIN_PAIRS_INDIVIDUAL | 3 | Pairs for a non-provisional personal number. | src/trevor/transfusion_response.py:42 |
RECENT_EXCHANGES | 6 | Exchanges considered for the share on target. | src/trevor/transfusion_response.py:43 |
PROTOCOLS exchange | units 6 to 9 (for 77 kg), weeks 5 to 6 | Targets of automated exchange. | src/trevor/transfusion_response.py:49 |
PROTOCOLS exchange_manual | units 3 to 5 (for 77 kg), weeks 2 to 3 | Targets of manual exchange. | src/trevor/transfusion_response.py:50 |
Half-life | days 1 to 60, excess > 0.3 g/dL, decay > 0.0001 per day | Calculation conditions. | src/trevor/transfusion_response.py:216-225 |
What is shown
Gain per unit in g/dL with its standard error, the raw slope, the number of pairs and the provisional flag; half-life in days; for each exchange type: last number of units and status (below, in range, above), target in units, share of the last 6 exchanges on target, median interval and its status (short, in range, long), units per kg; hemoglobin S: latest assay, 30% reference, slope per unit. Personal numbers are declared enough only with 3 pairs of simple transfusions.
A worked example
Three simple transfusions: 2 units (7.0 then 9.0 g/dL), 2 units (7.4 then 9.1), 3 units (6.8 then 10.1). Raw slope 1.018 g/dL per unit, standard error 0.086. After pulling toward 1.0 (SD 0.4): 1.017 with error 0.084. Automated exchange for 70 kg: target 5.5 to 8.2 units; 6 units is in range; median interval of 7 weeks: long (above 6.5); 6.5 weeks: in range (run with the code).
What this algorithm cannot do
- The dose and interval targets are starting values given by David (hematology): to be confirmed, according to the code. They are not validated recommendations.
- The 1 g/dL per unit reference is an adult reference: the code does not adapt it to children or weight.
- The module adjusts neither plasma volume, nor hemolysis, nor sampling delay, nor the spleen (docs/ANALYSE_REPONSE_TRANSFUSION.md).
- The pooled_within_slope function (cohort slope) exists in the file but is called nowhere in the code read: it influences no displayed number.
- The half-life uses only the last simple transfusion and assumes exponential decay.
- Exchanges are excluded from the jump per unit (the net gain is small); their efficacy is read only on hemoglobin S, if any.
- The median interval mixes all exchanges of the type, including the first ones, with no recent window.
- Weight comes from a free-text card field (77 kg) read by regular expression.
Code : src/trevor/transfusion_response.py (hb_readings, make_pairs, slope_through_origin, shrink, half_life_days, units_target, dose_status, interval_status, build), src/trevor/transfusion.py (payload, _response), src/trevor/dashboard/data.py
Iron follow-up: ferritin, MRI and ejection fraction
Trevor records the iron measurements your care team gives you (ferritin, heart and liver T2*, liver iron, ejection fraction), plots them and reads them against reference points from the Thalassaemia International Federation. Each entry resets the reminder for the annual exam. It gives no treatment instruction.
Where you see it: Iron follow-up screen and page for the relevant conditions (thalassemia): one line per measurement type with the latest value, its date, a reference sentence, the direction since the previous measurement and a small chart.
What the algorithm reads
- Ferritin (µg/L), heart and liver T2* (ms), liver iron LIC (mg/g), ejection fraction (%) (self-report, health record)
The calculation, step by step
The values used
| Name in the code | Value | Role | Where |
|---|---|---|---|
FERRITIN_THAL_WATCH | 1000.0 µg/L | Ferritin to watch. | src/trevor/value_alerts.py:48 |
FERRITIN_THAL_REPORT | 2500.0 µg/L | Ferritin to report. | src/trevor/value_alerts.py:49 |
CARDIAC_T2_ABNORMAL_MS | 20.0 ms | Heart T2* abnormal below. | src/trevor/iron.py:30 |
CARDIAC_T2_HIGH_RISK_MS | 10.0 ms | Heart T2* high risk below. | src/trevor/iron.py:31 |
LIC_MODERATE, LIC_SEVERE | 7.0 ; 15.0 mg/g | Moderate and severe overload thresholds. | src/trevor/iron.py:32-33 |
FLAT_PERCENT | 10.0 | Gap under which two measurements read stable. | src/trevor/iron.py:34 |
MAX_BACK_DAYS | 3650 | Maximum age of an entered date. | src/trevor/iron.py:35 |
What is shown
Per measurement type: latest value, date, reference sentence, direction and chart. Ferritin: 3 levels (under 1,000, 1,000 to 2,499, 2,500 or more). Heart T2*: 3 levels (20 or more, 10 to under 20, under 10). LIC: 3 levels (under 7, 7 to under 15, 15 or more). Liver T2* and ejection fraction: no reference, value only recorded and charted. The sentences invite you to talk with the care team.
A worked example
Ferritin 1,450 µg/L: level to watch (between 1,000 and 2,500). Last two ferritins 1,000 then 1,080: gap 8%, under 10%, so stable; 1,000 then 1,200: 20%, so up. Heart T2* 15 ms: abnormal. Chart of 1, 2, 3, 5: ▁▃▅█ (run with the code).
What this algorithm cannot do
- The iron.py file does not handle transferrin saturation: no saturation calculation or display exists in this module. Only ferritin, T2*, LIC and ejection fraction are tracked.
- All reference points are marked to be validated by a hematologist in the code, and apply to transfusion-dependent thalassemia. For the non-dependent form, the code invents no threshold.
- Direction compares only the last two values, ignoring the time gap between them.
- Ferritin also rises with inflammation; the module does not distinguish this.
- If two different values are entered on the same day for one type, only one is kept in the series.
Code : src/trevor/iron.py (normalize, reading, trend, spark, series, build, add), src/trevor/value_alerts.py (FERRITIN_THAL_WATCH, FERRITIN_THAL_REPORT), src/trevor/conversation/iron_screens.py, src/trevor/dashboard/phone_care_routes.py
Symptoms after a transfusion: alert level
You tick the symptoms you have felt since a transfusion. The server gives the declaration the level of the most serious symptom ticked: warning sign, to report, or noted. It then tells you whom to call, and flags a possible delayed reaction if the sign and timing fit.
Where you see it: After my transfusion screen: symptom list, advice text by level, declaration history. The server computes the level; the client does not choose it.
What the algorithm reads
- Symptoms ticked among 17 closed choices (self-report)
- Start date (today by default) and intensity (0 to 3) (self-report)
- Known transfusion dates (health record)
The calculation, step by step
The values used
| Name in the code | Value | Role | Where |
|---|---|---|---|
DELAYED_HINTS | dark_urine, jaundice, fatigue, pain, fever, back_pain | Signs suggesting delayed destruction of transfused red cells. | src/trevor/transfusion.py:62 |
DELAYED_WINDOW_DAYS | (1, 28) | Window from the next day to four weeks. | src/trevor/transfusion.py:63 |
RECENT_DAYS | 30 | An older declaration no longer appears in the record banner. | src/trevor/transfusion.py:64 |
MAX_NOTE | 280 | Note characters. | src/trevor/transfusion.py:66 |
INTENSITIES | 0, 1, 2, 3 | Unknown, mild, bothersome, very strong. | src/trevor/transfusion.py:68 |
SYMPTOMS | 6 urgent, 8 to report, 3 noted | Catalog and levels. | src/trevor/transfusion.py:40-57 |
What is shown
Three levels: warning sign (urgent), to report, noted. The urgent text tells you to call your country's emergency number. The delayed reaction note is added for urgent and to report when the delay is 1 to 28 days with a suggestive sign.
A worked example
Symptoms ticked: fever and nausea. Fever is urgent, nausea noted: the level is urgent. Tick only hives: to report. Yellow skin or eyes 10 days after the transfusion: delayed reaction possible; the same sign on the same day (delay 0): no (run with the code).
What this algorithm cannot do
- The level depends only on the ticked list: intensity and note are saved but do not change the level.
- The fever label says 38 °C or more, while the module text cites the 38.3 °C rule of the HUG protocol. The urgent level comes from the ticked box, not from a measured temperature.
- The catalog is closed: a symptom missing from the list can only be described as other symptom, at the noted level.
- The delayed reaction note does not appear for the noted level.
- The hematology service must still review the list before wide release, according to the module.
Code : src/trevor/transfusion.py (SYMPTOMS, tier_of, delayed_possible, linked_transfusion, record_reaction, advice_lines)
Transfusion history, interval and program
Trevor finds your last transfusion from the several places where you may have noted it, counts the days elapsed, computes the mean interval between transfusions and, if you have a regular program, says whether you are late or on time. It also records the number of units and the length of each session.
Where you see it: Bot My transfusion program screen (status, days and weeks elapsed, number of transfusions, mean interval, delay or next date), then questions about the number of units and the duration. Same data on the web page and in the app (transfusion.payload).
What the algorithm reads
- Date of the transfusion or exchange (today, or a written date) (self-report)
- Regular program yes or no, and interval in weeks (self-report)
- Number of units (1 to 30), session length (1 minute to 24 hours) (self-report)
- Last transfusion line on the emergency card (health record)
The calculation, step by step
The values used
| Name in the code | Value | Role | Where |
|---|---|---|---|
MAX_BACK_DAYS | 730 | Maximum age of a recorded transfusion. | src/trevor/transfusion.py:65 |
MAX_UNITS | 30 | Maximum units per session. | src/trevor/transfusion.py:67 |
MAX_MINUTES | 1440 | Maximum session length. | src/trevor/transfusion.py:111 |
DURATION_CHOICES_HOURS | 1 to 8 hours | Duration buttons. | src/trevor/conversation/carnet_screens.py:26 |
Delay threshold | 0.5 week | From this delay, the bot shows a delay message. | src/trevor/conversation/wearables_screen.py:644-650 |
Default interval | 4 weeks | Used if the program interval is empty or zero in the bot screen. | src/trevor/conversation/wearables_screen.py:644 |
Unit buttons | 1 to 6, number typed up to 30 | Choices offered. | src/trevor/conversation/wearables_screen.py:701-712 |
What is shown
For each transfusion: date, type, units, duration, date source (history, profile or card) and days since the previous one. Overall: date of the last, days and weeks elapsed, count, mean interval in weeks, regularity (standard deviation of the intervals in days, computed but not shown in the bot screen). With a program: running X weeks late (planned interval Y) or next transfusion on DD.MM.YYYY in Z weeks. No clinical threshold.
A worked example
none (the gap calculation reads directly in the code; no data set was run). Code reading: 6-week program, last transfusion 45 days ago, i.e. 6.4 weeks; delay = 6.4 - 6 = 0.4 week, under 0.5, so the bot shows the next date.
What this algorithm cannot do
- The bot screen reads only the history and the profile date; transfusion.py also reads the emergency card. A date known only from the card counts in the app and not in the bot screen.
- The time zone differs: Geneva in transfusion.py, the person's local time in the bot screen and in transfusion_stats.
- The mean interval mixes simple transfusions and exchanges, though their target rhythms differ.
- The program has no clinical threshold: the delay is a plain subtraction, unrelated to your hemoglobin.
- A transfusion older than two years cannot be recorded.
- The delay calculation (WearableService.transfusion_status) in the crisis risk model was not read here.
Code : src/trevor/transfusion.py (transfusion_dates, last_transfusion, history, record_transfusion, clean_units, clean_minutes), src/trevor/conversation/wearables_screen.py (screen_transfusion, on_transfusion_units), src/trevor/conversation/carnet_screens.py (screen_transfusion_minutes, on_transfusion_minutes), src/trevor/storage/database.py (transfusion_stats)
