
Tooluniverse Computational Biophysics
- 181 installs
- 1.6k repo stars
- Updated August 4, 2026
- mims-harvard/tooluniverse
Integrate computational biophysics tools—molecular simulation, structural analysis, and related models—into ToolUniverse agent workflows for scientific research and discovery pipelines.
About
ToolUniverse computational biophysics from mims-harvard/tooluniverse enables integration of specialized biophysics computation tools—such as molecular simulation and structural analysis—into scientific agent workflows and research pipelines within the ToolUniverse ecosystem.
- Domain-specific biophysics computation tooling
- Hooks into ToolUniverse agent framework
- Supports molecular and structural analysis
- Enables reproducible scientific automation
Tooluniverse Computational Biophysics by the numbers
- 181 all-time installs (skills.sh)
- +5 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #688 of 2,064 Data Science & ML skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/mims-harvard/tooluniverse --skill tooluniverse-computational-biophysicsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 181 |
|---|---|
| repo stars | ★ 1.6k |
| Last updated | August 4, 2026 |
| Repository | mims-harvard/tooluniverse ↗ |
What it does
Integrate computational biophysics tools—molecular simulation, structural analysis, and related models—into ToolUniverse agent workflows for scientific research and discovery pipelines.
Files
Computational Biophysics & Quantitative Biology Skill
1. Recognize the Physical Process
The single most important step: identify what physical process the problem describes. In quantitative biology, almost every problem maps to one of these:
- Drug enters body → distributes → is eliminated: pharmacokinetics. Key quantities: dose, bioavailability, volume of distribution, clearance, half-life. The body is a compartment model.
- Radioactive tracer decays over time: nuclear medicine. Same math as drug elimination (exponential decay) but the rate constant is a physical property of the isotope, not a patient variable.
- Pathogen spreads through population: epidemiology. R₀ determines whether an epidemic grows or dies. Herd immunity threshold = 1 - 1/R₀. Every epidemic model starts here.
- Ligand binds receptor: binding equilibrium. At low [ligand], binding is linear. At saturation, all sites occupied. Kd = concentration at half-maximal binding. This same curve describes enzyme kinetics, drug-receptor occupancy, and surface adsorption.
- Contaminant enters environment: dilution + persistence. Two questions: what is the concentration after mixing (conservation of mass), and how long does it persist (exponential decay with environmental half-life)?
- Two populations differ genetically: population genetics. Fst measures differentiation. HWE tests if mating is random. Gene flow opposes drift.
- Neurons communicate in a network: computational neuroscience. Integrate-and-fire models, synaptic dynamics, balanced excitation/inhibition. Mean firing rate depends on input current relative to threshold.
Once you name the process, the mathematical structure follows. Solve algebraically first, substitute numbers second, and always check that units cancel correctly and the magnitude is physically reasonable.
2. Reasoning Patterns by Problem Type
These are not formulas. They are ways of thinking about what is happening physically.
Conservation / Dilution Problems
Something is being spread into a larger volume, or two streams are mixing. The total amount of substance is conserved. Think: amount_before = amount_after, where amount = concentration x volume. This covers serial dilutions, mixing streams, stock solution preparation, and environmental discharge into rivers.
Exponential Decay / Growth Problems
Something is disappearing (or growing) at a rate proportional to how much is currently there. The signature: "half-life" or "doubling time" appears in the problem. This single pattern covers drug clearance, radioactive decay, environmental persistence, bacterial growth, and epidemic doubling. The only things that change between applications are the rate constant and what is decaying.
Saturation / Binding Problems
Something binds to a limited number of sites. At low concentrations, binding is proportional to concentration. At high concentrations, sites fill up and adding more has diminishing effect. This covers receptor-ligand binding, enzyme kinetics, surface adsorption, and oxygen-hemoglobin curves. The shape is always hyperbolic: response = max_response x [thing] / ([thing] + half_max_constant).
Threshold / Crossover Problems
"At what point does X equal Y?" or "When does the concentration drop below the therapeutic level?" Set two expressions equal and solve. Examples: time to reach a target drug level, when an environmental concentration exceeds a safety limit, herd immunity threshold (where effective R drops to 1).
Ratio / Rate Problems
Output = input x time, or output = concentration x flow rate. Clearance, flux, dosing rate, and drip rate calculations are all just dimensional analysis: arrange the given quantities so the units work out.
Population Comparison Problems
Two groups are being compared. You need a measure of difference (Fst, odds ratio, relative risk) and a measure of whether the difference is real (p-value, confidence interval). Think: what is the effect size, and is it distinguishable from noise?
3. When to Compute vs. Estimate vs. Look Up
Compute carefully when:
- The answer affects a patient (drug dosing, diagnostic interpretation)
- The problem gives you exact numbers and asks for an exact answer
- You need to fit a curve to data (use scipy)
Estimate and state uncertainty when:
- The answer needs an order of magnitude (environmental risk, population-level)
- Input values are themselves uncertain (R0 estimates, BCF from log Kow regressions)
- Say: "This is approximately X, with the main uncertainty coming from Y"
Look up via ToolUniverse when:
- You need a physical constant: half-life, molecular weight, Kd, log Kow, allele frequency
- The user names a specific drug, compound, gene, or variant
- You want to validate your calculation against a known case
| Data needed | Tool to use |
|---|---|
| Molecular weight, log Kow, SMILES | PubChem_get_CID_by_compound_name, PubChem_get_compound_properties_by_CID |
| Drug PK properties, mechanism | ChEMBL_get_molecule |
| Binding affinity (Kd, Ki, IC50) | BindingDB_search_by_target |
| Allele frequencies | gnomad_get_variant, MyVariant_query_variants |
| Literature values (R0, BCF, etc.) | EuropePMC_search_articles |
Just compute when:
- The problem gives you all the numbers
- No specific real-world compound/gene is named
4. Python Computation Templates
CRITICAL: When a problem gives you numbers and asks for a numerical answer, WRITE AND RUN Python code using the Bash tool. Do not try to compute in your head — write a script, execute it, and report the result. Mental arithmetic on multi-step problems introduces errors. The templates below are starting points — adapt them to the specific problem, then EXECUTE.
Answer Format Rules: Match the precision and format the question expects. If data uses 2 decimal places, round to 2. For large numbers (>10^6), use scientific notation; if the question says "in units of 10^28", give just the coefficient. For small numbers, match the question's format (e.g., "1.776 × 10^-3" not "1.8e-3"). Give ONLY the number — no units or descriptions unless explicitly asked.
# Pattern for every computation problem:
# 1. Extract ALL given values from the problem — write them down with units
# 2. Identify EXACTLY what quantity the question asks for
# 3. Write a Python script connecting givens to the unknown
# 4. Run it with: python3 -c "..."
# 5. VERIFY: substitute your answer back into the original problem — does it make sense?
# e.g., if computing a drip rate, check: rate × time = total volume?
# e.g., if computing vaccine coverage, check: coverage × efficacy × population > herd immunity?Template 1: Exponential Decay / Growth
Covers: drug clearance, radioactive decay, environmental persistence, bacterial growth, epidemic doubling.
import numpy as np
def exponential_process(initial, half_life, time):
"""Amount remaining after exponential decay. For growth, use negative half_life."""
return initial * (0.5 ** (time / half_life))
def time_to_reach(initial, target, half_life):
"""Time for exponential process to reach a target value."""
return half_life * np.log2(initial / target)
# Examples — same math, different domains:
# Drug: 500 mg dose, t½ = 6 h, after 24 h → 31.25 mg
# Radioactive: 20 mCi Tc-99m, t½ = 6 h, after 12 h → 5 mCi
# Environmental: 100 ppm pesticide, t½ = 30 days, after 90 days → 12.5 ppmTemplate 2: Conservation / Dilution / Mixing
Covers: C1V1=C2V2, stream mixing, serial dilutions. Core logic: C1*V1 = C2*V2 (pass 3 knowns, solve for 4th). For mixing n streams: final_conc = sum(Ci*Qi) / sum(Qi).
Template 3: Threshold / Equilibrium Solver
Covers: when drug drops below therapeutic level, herd immunity threshold. Use scipy.optimize.brentq(lambda x: func(x) - target, lo, hi) to find the crossover point.
Template 4: Saturation / Binding Curve
Covers: receptor binding, enzyme kinetics, adsorption, dose-response. Shape: response = Rmax * C / (C + Kd). Fit with scipy.optimize.curve_fit using p0=[median(C), max(response)].
Template 5: Statistical Comparison
Covers: HWE chi-square, contingency tables, group comparisons. Use scipy.stats.chisquare(observed, expected) for goodness-of-fit, stats.ttest_ind/ttest_rel for group comparisons.
Template 6: Rate / Dimensional Analysis
Covers: IV drip rate, clearance, flux, dosing rate. Core: rate = amount / time, mass_rate = concentration * flow_rate. Arrange units to cancel correctly.
Template 7: Compartmental Models & R0
Covers: SIR/SEIR, R0 derivation. R0 = beta * N / gamma (basic SIR). General: R0 = transmission_rate * infectious_duration * susceptible_contacts. Derive by tracing one infected individual through all compartments.
5. Multiple-Choice Strategy
Multiple-choice questions in biophysics, pharmacology, and clinical medicine are frequently answered incorrectly not because of missing knowledge but because of process errors: skipping an option, confusing a letter with the text, or committing to the first plausible-sounding choice. Use the systematic approach below every time.
The Mandatory MC Process
1. Read the stem twice. Identify the exact action being asked: "MOST likely", "FIRST step", "BEST describes", "EXCEPT". These qualifiers change the answer. 2. Force evaluation of every choice. For each option ask:
- Why would this be correct? — does it align with the core concept?
- Why would this be wrong? — does the reasoning contradict it, or is it only partially true?
3. Eliminate with explicit justification. Mark a choice eliminated only when you can state a factual reason (not just a feeling). 4. Count survivors. One survivor → that is your answer. Two or more → go back to the stem and look for the qualifier that distinguishes them. 5. Verify letter-to-text alignment. Before writing your answer, confirm the letter you intend to write corresponds to the option text you reasoned about. This catches the common error of reasoning "B is correct" but writing "C". 6. Quantitative MC: Calculate the exact answer FIRST using Python, THEN match to the closest option. Do not let the listed choices bias your computation — compute independently. 7. MC traps: "All/None of the above" is correct only ~25% of the time. Absolute language ("always", "never", "only") is usually wrong. The longest/most detailed option is correct more often. When two options are opposites, one is usually correct.
CRITICAL FOR BATCH PROCESSING: When answering multiple MC questions in sequence, do NOT rush. Apply the FULL elimination process to EVERY question. Common batch error: answering based on first impression without elimination. For each MC question, you MUST:
- Write out at least 2 eliminated options with reasons BEFORE selecting your answer
- If you cannot eliminate any options, that's a sign you need to LOOK UP information
mc_analyzer.py — Automated MC Scaffold
Located in skills/tooluniverse-computational-biophysics/scripts/mc_analyzer.py.
# Analysis mode: systematic elimination
python mc_analyzer.py --question "..." --choices "A:opt1,B:opt2,C:opt3,D:opt4" --reasoning "..."
# Verify mode: confirm letter-text alignment
python mc_analyzer.py --verify --answer "B" --question "..." --choices "A:opt1,B:opt2,C:opt3,D:opt4"Analysis mode scans reasoning for elimination signals, reports survivor count. Verify mode checks letter-text alignment. Use for any scored MC question.
---
6. Bundled Scripts
These ready-to-run scripts live in skills/tooluniverse-computational-biophysics/scripts/. Use them via the Bash tool instead of computing by hand — they include verification steps and handle edge cases.
epidemiology.py — Epidemiology calculations (5 types via --type)
Preferred: Use ToolUniverse tools (via MCP/SDK) instead of the script:
Epidemiology_r0_herdtool -- R0 and herd immunity thresholdEpidemiology_vaccine_coveragetool -- Vaccine coverage from field dataEpidemiology_nnttool -- Number needed to treatEpidemiology_diagnostictool -- Diagnostic test performance (2x2 table)Epidemiology_bayesiantool -- Bayesian pre/post-test probability
Fallback: Pure stdlib script. Types: r0_herd, vaccine_coverage, nnt, diagnostic, bayesian.
python epidemiology.py --type r0_herd --R0 3.5 --VE 0.90
python epidemiology.py --type nnt --control_rate 0.30 --treatment_rate 0.20
python epidemiology.py --type diagnostic --tp 90 --fp 10 --tn 880 --fn 20
python epidemiology.py --type bayesian --prevalence 0.01 --sensitivity 0.95 --specificity 0.90Key formulas: r0_herd: Hc=1-1/R0, Vc=Hc/VE. vaccine_coverage: derives VE from PCV/PPV field data. nnt: ARR=control-treatment, NNT=1/ARR. diagnostic: full 2x2 table. bayesian: pre-test odds → LR → post-test probability.
herd_immunity.py — Legacy vaccination threshold
python herd_immunity.py --R0 4.2 --VE 0.94Formula: Vc = (1 - 1/R0) / VE. Prefer epidemiology.py --type r0_herd for new work.
radioactive_decay.py — Activity remaining / time to target / parent-daughter
Three modes: forward (--A0 --half_life --time), reverse (--A0 --half_life --target), parent-daughter (--parent_daughter with Bateman equation).
python radioactive_decay.py --A0 8 --half_life 67.3 --time 72 # forward
python radioactive_decay.py --A0 8 --half_life 67.3 --target 5 # reverse
python radioactive_decay.py --parent_daughter --half_life_parent 306.05 --half_life_daughter 40.27 --A1 1.4 --A2 2.1 --delta_t 336 --counted daughterFormulas: A(t) = A0 * 0.5^(t/t_half). Parent-daughter: Bateman equation. --counted: daughter/both/parent.
env_risk_assessment.py — Environmental risk (hazard quotient)
Computes HQ for soil contaminant exposure via food pathway. Food format: "name:intake_g:bioavailability:PUF:TSCF".
python env_risk_assessment.py --total_mass_ug 1e9 --area_m2 250000 --depth_m 0.6 \
--bulk_density 1500 --theta_w 0.35 --foc 0.03 --Koc 28.3 \
--food "fruit:300:0.5:0.1:5" --body_weight 80 --RfD 0.02Formulas: C_soil → Kd=foc*Koc → C_sw → DI per pathway → HQ = sum(DI)/BW/RfD.
fluid_calculations.py — Clinical fluid and dosing (4 types via --type)
Types: drip_rate, bsa_dose, maintenance, dilution. Preferred over legacy iv_drip_rate.py.
python fluid_calculations.py --type drip_rate --volume_ml 50 --time_min 60 --drop_factor 60
python fluid_calculations.py --type bsa_dose --dose_per_m2 25 --bsa 0.8
python fluid_calculations.py --type maintenance --weight_kg 22
python fluid_calculations.py --type dilution --c1 20 --v1 4.7 --v2 50Formulas: drip_rate = (vol/time)drop_factor. bsa_dose = dose_per_m2BSA. maintenance = Holliday-Segar. dilution = C1V1=C2V2.
enzyme_kinetics.py — Km/Vmax, Hill, Ki (3 types via --type)
Preferred: use EnzymeKinetics_calculate tool (via MCP/SDK) with type and data parameters. Fallback: run enzyme_kinetics.py directly.
Pure stdlib. Types: km_vmax (Lineweaver-Burk + nonlinear), hill (cooperativity), ki (competitive/uncompetitive/noncompetitive).
python enzyme_kinetics.py --type km_vmax --substrate "1,2,5,10,20" --velocity "0.5,0.8,1.2,1.5,1.7"
python enzyme_kinetics.py --type ki --substrate "1,2,5,10,20" --velocity_no_inh "0.5,0.8,1.2,1.5,1.7" --velocity_inh "0.3,0.5,0.8,1.0,1.1" --inhibitor_conc 5 --inhibition_type competitiveburn_fluids.py — Burn resuscitation + maintenance
Adult: Parkland (4*kg*%TBSA). Pediatric (<30kg): Galveston (5000*BSA*%TBSA + 2000*BSA).
python burn_fluids.py --weight_kg 80 --tbsa_pct 40
python burn_fluids.py --weight_kg 25 --age_years 7 --tbsa_pct 45 --bsa_m2 0.95Output: hourly rates (first 8h / next 16h), total volume, urine output target. 8h clock starts from burn time.
---
7. Combinatorics & Counting Problems
For genetics combinatorics (F2 haplotypes, genotype counts, specimen tallies) or any counting/permutation/combination problem: ALWAYS write and execute Python code. Never attempt to enumerate or count mentally — even simple-looking problems (e.g., "how many unique chromosomes from 5 SNPs") have subtleties that cause errors without code. Use itertools.product, itertools.combinations, or direct formulas, then verify the count.
8. Common Pitfalls to Flag
- Unit mismatch: mg vs g, mL vs L, hours vs seconds. Always write units next to every number and verify cancellation before computing.
- Mono- vs multi-exponential: Drug clearance is often biexponential (distribution + elimination phases). Simple half-life decay assumes one compartment. State this assumption.
- R0 vs Re: R0 = fully susceptible population. Re = R0 x fraction_susceptible. Most real-world questions want Re.
- Single-site estimates are noisy: One SNP's Fst, one patient's response, one measurement's Kd. Always note when genome-wide averages, population means, or replicate experiments would be more reliable.
- Regression estimates are order-of-magnitude: BCF from log Kow, toxicity from QSAR. Flag the uncertainty explicitly.
- SI units in simulations: Neuron models, diffusion, thermodynamics — always convert to SI (seconds, meters, joules, volts) before computing. Mixed ms/mV causes silent factor-of-1000 errors.
"""
Burn resuscitation fluid calculator.
Supports three resuscitation formulas:
1. Parkland (Baxter) formula — adults and older children:
Total 24h = 4 * weight_kg * %TBSA [mL of Lactated Ringer's]
First 8h : half the total (from TIME OF BURN, not admission)
Next 16h : other half
2. Modified Brooke formula — alternative for adults/pediatrics:
Total 24h = 2 * weight_kg * %TBSA [mL of LR]
First 8h : half the total
Next 16h : other half
For pediatric patients: add maintenance fluids (4-2-1 rule) on top.
3. Galveston formula — pediatric (BSA-based):
Total 24h = 5000 * BSA_m2 * (%TBSA / 100) + 2000 * BSA_m2
(resuscitation component + maintenance component)
First 8h : half the total
Next 16h : other half
Pediatric maintenance fluids (4-2-1 rule, added to Parkland/Brooke for children):
First 10 kg : 4 mL/kg/h
Next 10 kg : 2 mL/kg/h
Each kg > 20 : 1 mL/kg/h
Holliday-Segar maintenance fluid (daily method, shown for reference):
<= 10 kg : 100 mL/kg/day
10-20 kg : 1000 + 50 mL/kg/day above 10 kg
> 20 kg : 1500 + 20 mL/kg/day above 20 kg
Second 24h (not auto-calculated here):
Colloid: 0.3-0.5 mL/kg/%TBSA (start at ~18-24 h)
Free water to maintain urine output 0.5-1 mL/kg/h
Usage:
# Adult (80 kg, 40% TBSA) — Parkland (default for >= 30 kg):
python burn_fluids.py --weight_kg 80 --tbsa_pct 40
# Pediatric, modified Brooke + maintenance (25 kg, 45% TBSA):
python burn_fluids.py --weight_kg 25 --age_years 7 --tbsa_pct 45 --formula brooke
# Pediatric, Galveston (25 kg, 45% TBSA, BSA 0.95 m2):
python burn_fluids.py --weight_kg 25 --age_years 7 --tbsa_pct 45 --bsa_m2 0.95
# Adult, modified Brooke:
python burn_fluids.py --weight_kg 80 --tbsa_pct 40 --formula brooke
"""
import argparse
import sys
def maintenance_4_2_1(weight_kg: float) -> dict:
"""
Maintenance fluid rate by the 4-2-1 rule (hourly method).
Returns hourly rate (mL/h) and daily equivalent (mL/day).
First 10 kg : 4 mL/kg/h
Next 10 kg : 2 mL/kg/h
Each kg > 20 : 1 mL/kg/h
"""
if weight_kg <= 10:
hourly = 4.0 * weight_kg
elif weight_kg <= 20:
hourly = 40.0 + 2.0 * (weight_kg - 10.0)
else:
hourly = 60.0 + 1.0 * (weight_kg - 20.0)
return {"hourly_mL_per_h": hourly, "daily_mL": hourly * 24.0}
def holliday_segar(weight_kg: float) -> dict:
"""
Maintenance fluid by Holliday-Segar method (daily method).
Returns daily (mL/day) and hourly (mL/h) rates.
"""
if weight_kg <= 10:
daily = 100.0 * weight_kg
elif weight_kg <= 20:
daily = 1000.0 + 50.0 * (weight_kg - 10.0)
else:
daily = 1500.0 + 20.0 * (weight_kg - 20.0)
return {"daily_mL": daily, "hourly_mL_per_h": daily / 24.0}
def parkland(weight_kg: float, tbsa_pct: float) -> dict:
"""
Parkland (Baxter) formula for burn resuscitation.
Total 24h = 4 * weight_kg * %TBSA mL of LR.
"""
total_24h = 4.0 * weight_kg * tbsa_pct
first_8h_total = total_24h / 2.0
next_16h_total = total_24h / 2.0
return {
"formula": "Parkland",
"coefficient": 4,
"total_24h_mL": total_24h,
"first_8h_mL": first_8h_total,
"next_16h_mL": next_16h_total,
"rate_first_8h_mL_per_h": first_8h_total / 8.0,
"rate_next_16h_mL_per_h": next_16h_total / 16.0,
}
def modified_brooke(weight_kg: float, tbsa_pct: float) -> dict:
"""
Modified Brooke formula for burn resuscitation.
Total 24h = 2 * weight_kg * %TBSA mL of LR.
"""
total_24h = 2.0 * weight_kg * tbsa_pct
first_8h_total = total_24h / 2.0
next_16h_total = total_24h / 2.0
return {
"formula": "Modified Brooke",
"coefficient": 2,
"total_24h_mL": total_24h,
"first_8h_mL": first_8h_total,
"next_16h_mL": next_16h_total,
"rate_first_8h_mL_per_h": first_8h_total / 8.0,
"rate_next_16h_mL_per_h": next_16h_total / 16.0,
}
def galveston(bsa_m2: float, tbsa_pct: float) -> dict:
"""
Galveston formula for pediatric burn resuscitation.
Total 24h = 5000 * BSA_m2 * (%TBSA/100) + 2000 * BSA_m2 mL of LR.
"""
resuscitation_component = 5000.0 * bsa_m2 * (tbsa_pct / 100.0)
maintenance_component = 2000.0 * bsa_m2
total_24h = resuscitation_component + maintenance_component
first_8h_total = total_24h / 2.0
next_16h_total = total_24h / 2.0
return {
"formula": "Galveston",
"resuscitation_component_mL": resuscitation_component,
"maintenance_component_mL": maintenance_component,
"total_24h_mL": total_24h,
"first_8h_mL": first_8h_total,
"next_16h_mL": next_16h_total,
"rate_first_8h_mL_per_h": first_8h_total / 8.0,
"rate_next_16h_mL_per_h": next_16h_total / 16.0,
}
def main():
parser = argparse.ArgumentParser(
description="Burn resuscitation fluid calculator (Parkland / Modified Brooke / Galveston).",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
parser.add_argument("--weight_kg", type=float, required=True, help="Patient weight in kg.")
parser.add_argument(
"--tbsa_pct",
type=float,
required=True,
help="Total body surface area burned (%%TBSA), e.g. 45 for 45%%.",
)
parser.add_argument(
"--age_years", type=float, default=None, help="Patient age in years (triggers pediatric mode if < 14)."
)
parser.add_argument(
"--bsa_m2",
type=float,
default=None,
help="Body surface area in m2 (required for Galveston formula).",
)
parser.add_argument(
"--formula",
choices=["parkland", "brooke", "galveston"],
default=None,
help="Resuscitation formula: parkland (4 mL/kg/%%TBSA), brooke (2 mL/kg/%%TBSA), "
"galveston (BSA-based, pediatric). Default: galveston if pediatric + BSA given, "
"else parkland for adults.",
)
args = parser.parse_args()
weight = args.weight_kg
tbsa = args.tbsa_pct
# Validate inputs
if weight <= 0:
print("Error: weight_kg must be positive.")
sys.exit(1)
if not (0 < tbsa <= 100):
print(f"Error: tbsa_pct must be between 0 and 100 (got {tbsa}).")
sys.exit(1)
if args.bsa_m2 is not None and args.bsa_m2 <= 0:
print(f"Error: bsa_m2 must be positive (got {args.bsa_m2}).")
sys.exit(1)
# Determine if pediatric
is_pediatric = (args.age_years is not None and args.age_years < 14) or weight < 30
# Determine formula
if args.formula is not None:
formula = args.formula
elif is_pediatric and args.bsa_m2 is not None:
formula = "galveston"
elif is_pediatric:
formula = "brooke"
else:
formula = "parkland"
if formula == "galveston" and args.bsa_m2 is None:
print("Error: --bsa_m2 is required for the Galveston formula.")
print(" Provide patient BSA in m2, or use --formula parkland/brooke.")
sys.exit(1)
print("=" * 62)
print(" Burn Resuscitation Fluid Calculator")
print("=" * 62)
age_str = f"{args.age_years} y" if args.age_years is not None else "not specified"
pop_str = "Pediatric" if is_pediatric else "Adult"
print(f" Weight : {weight} kg")
print(f" Age : {age_str}")
print(f" %TBSA burned : {tbsa}%")
if args.bsa_m2 is not None:
print(f" BSA : {args.bsa_m2} m2")
print(f" Population : {pop_str}")
# ---- Compute resuscitation ----
if formula == "galveston":
r = galveston(args.bsa_m2, tbsa)
elif formula == "brooke":
r = modified_brooke(weight, tbsa)
else:
r = parkland(weight, tbsa)
# ---- Display formula details ----
print()
if formula == "galveston":
print(" Galveston Formula (LR):")
print(f" Resuscitation component : {r['resuscitation_component_mL']:.1f} mL")
print(f" = 5000 x {args.bsa_m2} m2 x {tbsa / 100:.2f}")
print(f" Maintenance component : {r['maintenance_component_mL']:.1f} mL")
print(f" = 2000 x {args.bsa_m2} m2")
elif formula == "brooke":
print(" Modified Brooke Formula (LR):")
print(f" Total = 2 x {weight} kg x {tbsa}% = {r['total_24h_mL']:.1f} mL")
else:
print(" Parkland Formula (LR):")
print(f" Total = 4 x {weight} kg x {tbsa}% = {r['total_24h_mL']:.1f} mL")
# ---- Maintenance for pediatric (Parkland/Brooke only) ----
maint_hourly = 0.0
add_maintenance = is_pediatric and formula != "galveston"
if add_maintenance:
m = maintenance_4_2_1(weight)
maint_hourly = m["hourly_mL_per_h"]
# Combined rates
resus_first_8h = r["rate_first_8h_mL_per_h"]
resus_next_16h = r["rate_next_16h_mL_per_h"]
total_first_8h_rate = resus_first_8h + maint_hourly
total_next_16h_rate = resus_next_16h + maint_hourly
print()
print(" +---------------------------------------------------------+")
print(f" | Total resuscitation 24h : {r['total_24h_mL']:>8.1f} mL |")
print(f" | First 8h (half) : {r['first_8h_mL']:>8.1f} mL |")
print(f" | resuscitation rate : {resus_first_8h:>8.1f} mL/h |")
if add_maintenance:
print(f" | + maintenance (4-2-1) : {maint_hourly:>8.1f} mL/h |")
print(f" | = TOTAL first 8h rate : {total_first_8h_rate:>8.1f} mL/h |")
print(f" | Next 16h (half) : {r['next_16h_mL']:>8.1f} mL |")
print(f" | resuscitation rate : {resus_next_16h:>8.1f} mL/h |")
if add_maintenance:
print(f" | + maintenance (4-2-1) : {maint_hourly:>8.1f} mL/h |")
print(f" | = TOTAL next 16h rate : {total_next_16h_rate:>8.1f} mL/h |")
print(" +---------------------------------------------------------+")
if add_maintenance:
print()
_print_421_breakdown(weight)
print()
print(" IMPORTANT: The 8h clock starts from TIME OF BURN,")
print(" not time of admission. Adjust if fluid was already given.")
# ---- Holliday-Segar maintenance (reference) ----
hs = holliday_segar(weight)
print()
print(" Holliday-Segar Maintenance (reference):")
if weight <= 10:
rule = f"100 x {weight} kg"
elif weight <= 20:
rule = f"1000 + 50 x {weight - 10:.0f} kg (above 10 kg)"
else:
rule = f"1500 + 20 x {weight - 20:.0f} kg (above 20 kg)"
print(f" Formula : {rule}")
print(f" Daily : {hs['daily_mL']:.1f} mL/day")
print(f" Hourly : {hs['hourly_mL_per_h']:.2f} mL/h")
if formula == "galveston":
print()
print(" Note: Galveston formula already includes maintenance.")
print(" Holliday-Segar shown above for reference only.")
# ---- Urine output target ----
uo_min = 0.5 * weight
uo_max = 1.0 * weight
print()
print(f" Urine output target : {uo_min:.1f}-{uo_max:.1f} mL/h (0.5-1 mL/kg/h)")
print(f" Fluid type : Lactated Ringer's (isotonic)")
print()
print(" Titrate infusion rate to urine output; above values are")
print(" initial estimates only. Reassess hourly.")
print("=" * 62)
def _print_421_breakdown(weight_kg: float) -> None:
"""Print the 4-2-1 maintenance calculation breakdown."""
print(" 4-2-1 Maintenance Breakdown:")
if weight_kg <= 10:
print(f" 4 x {weight_kg:.0f} kg = {4.0 * weight_kg:.0f} mL/h")
elif weight_kg <= 20:
extra = weight_kg - 10.0
print(f" 4 x 10 kg = 40 mL/h")
print(f" 2 x {extra:.0f} kg (next 10 kg) = {2.0 * extra:.0f} mL/h")
print(f" Total = {40.0 + 2.0 * extra:.0f} mL/h")
else:
extra = weight_kg - 20.0
print(f" 4 x 10 kg = 40 mL/h")
print(f" 2 x 10 kg (next 10 kg) = 20 mL/h")
print(f" 1 x {extra:.0f} kg (above 20 kg) = {1.0 * extra:.0f} mL/h")
print(f" Total = {60.0 + 1.0 * extra:.0f} mL/h")
if __name__ == "__main__":
main()
"""
Environmental risk assessment calculator for contaminated site exposure.
Computes hazard quotient (HQ) for human exposure to soil contaminants
via food ingestion pathway (soil → soil solution → plant → human).
Calculation steps:
1. Soil concentration: C_soil = total_mass / soil_mass
2. Soil-water partitioning: C_sw = C_soil * rho_b / (Kd * rho_b + theta_w)
where Kd = foc * Koc
3. Plant uptake: C_plant = C_sw * TSCF * PUF
4. Daily intake: DI = C_plant * intake_rate_g * bioavailability
5. Average daily dose: ADD = total_DI / body_weight
6. Hazard quotient: HQ = ADD / RfD
Key parameter notes:
- Koc (organic carbon partition coefficient) is compound-specific.
Common values: PFHxS ~36, PFOS ~200, PFOA ~70 L/kg.
- TSCF (transpiration stream concentration factor): dimensionless,
typically 0.1-10 for different compounds.
- PUF (plant uptake factor): fraction of root-zone contaminant
transferred to edible parts.
Usage:
# Basic: single food pathway
python env_risk_assessment.py \\
--total_mass_ug 1e9 --area_m2 250000 --depth_m 0.6 \\
--bulk_density 1500 --theta_w 0.35 --foc 0.03 --Koc 36 \\
--food "fruit:300:0.5:0.1:5" \\
--body_weight 80 --RfD 0.02
# Multiple food pathways
python env_risk_assessment.py \\
--total_mass_ug 1e9 --area_m2 250000 --depth_m 0.6 \\
--bulk_density 1500 --theta_w 0.35 --foc 0.03 --Koc 36 \\
--food "fruit:300:0.5:0.1:5" --food "legume:50:0.3:0.2:5" \\
--body_weight 80 --RfD 0.02
Food format: "name:intake_g_per_day:bioavailability:plant_uptake_factor:TSCF"
"""
import argparse
import sys
def compute_soil_concentration(total_mass_ug: float, area_m2: float,
depth_m: float, bulk_density_kg_m3: float) -> float:
"""Soil concentration in ug/kg. C_soil = total_mass / (area * depth * rho_b)."""
soil_mass_kg = area_m2 * depth_m * bulk_density_kg_m3
return total_mass_ug / soil_mass_kg
def compute_soil_solution(C_soil_ug_kg: float, bulk_density: float,
theta_w: float, Kd: float) -> float:
"""
Soil solution (pore water) concentration in ug/L.
C_sw = C_soil * rho_b / (Kd * rho_b + theta_w)
Note: rho_b in kg/m3, theta_w in L/L (= m3/m3), Kd in L/kg.
Result: ug/m3 internally, converted to ug/L by dividing by 1000.
"""
# C_soil [ug/kg] * rho_b [kg/m3] = ug/m3 in bulk soil
# Kd*rho_b [L/kg * kg/m3 = L/m3], theta_w [L/L = L/(0.001 m3)]
# Denominator: Kd*rho_b + theta_w (in consistent units)
# For the partition: C_sw(ug/L) = C_soil(ug/kg) * rho_b(kg/L) / (Kd(L/kg)*rho_b(kg/L) + theta_w(L/L))
# rho_b in kg/L = rho_b_kg_m3 / 1000
rho_b_kg_L = bulk_density / 1000.0
C_sw = C_soil_ug_kg * rho_b_kg_L / (Kd * rho_b_kg_L + theta_w)
return C_sw
def compute_daily_intake(C_sw_ug_L: float, TSCF: float, PUF: float,
intake_g_day: float, bioavailability: float) -> float:
"""
Daily intake in ug/day from one food pathway.
DI = C_sw * TSCF * PUF * intake_g * bioavailability
"""
return C_sw_ug_L * TSCF * PUF * intake_g_day * bioavailability
def compute_hazard_quotient(total_DI_ug_day: float, body_weight_kg: float,
RfD_ug_kg_day: float) -> float:
"""HQ = (DI / BW) / RfD."""
ADD = total_DI_ug_day / body_weight_kg
return ADD / RfD_ug_kg_day
def parse_food(food_str: str) -> dict:
"""Parse food specification: 'name:intake_g:bioavailability:PUF:TSCF'."""
parts = food_str.split(":")
if len(parts) != 5:
raise ValueError(
f"Food must have 5 colon-separated fields "
f"(name:intake_g:bioavailability:PUF:TSCF), got: {food_str}"
)
return {
"name": parts[0],
"intake_g": float(parts[1]),
"bioavailability": float(parts[2]),
"PUF": float(parts[3]),
"TSCF": float(parts[4]),
}
def main():
parser = argparse.ArgumentParser(
description="Environmental risk assessment: hazard quotient from soil contamination.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
parser.add_argument("--total_mass_ug", type=float, required=True,
help="Total contaminant mass in soil (ug).")
parser.add_argument("--area_m2", type=float, required=True,
help="Contaminated area (m2).")
parser.add_argument("--depth_m", type=float, required=True,
help="Contamination depth (m).")
parser.add_argument("--bulk_density", type=float, required=True,
help="Soil bulk density (kg/m3).")
parser.add_argument("--theta_w", type=float, required=True,
help="Volumetric water content (L water / L soil).")
parser.add_argument("--foc", type=float, required=True,
help="Fraction organic carbon in soil (0-1).")
parser.add_argument("--Koc", type=float, required=True,
help="Organic carbon partition coefficient (L/kg).")
parser.add_argument("--food", action="append", required=True,
help="Food pathway: 'name:intake_g:bioavailability:PUF:TSCF'. "
"Can be specified multiple times.")
parser.add_argument("--body_weight", type=float, required=True,
help="Body weight (kg).")
parser.add_argument("--RfD", type=float, required=True,
help="Reference dose (ug/kg body weight/day).")
args = parser.parse_args()
# Parse food pathways
foods = []
for f_str in args.food:
try:
foods.append(parse_food(f_str))
except ValueError as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
# Step 1: Soil concentration
C_soil = compute_soil_concentration(
args.total_mass_ug, args.area_m2, args.depth_m, args.bulk_density
)
soil_mass = args.area_m2 * args.depth_m * args.bulk_density
# Step 2: Soil-water partition
Kd = args.foc * args.Koc
C_sw = compute_soil_solution(C_soil, args.bulk_density, args.theta_w, Kd)
# Step 3-4: Daily intake per food pathway
food_results = []
total_DI = 0.0
for food in foods:
DI = compute_daily_intake(C_sw, food["TSCF"], food["PUF"],
food["intake_g"], food["bioavailability"])
food_results.append({**food, "DI": DI})
total_DI += DI
# Step 5-6: Dose and HQ
ADD = total_DI / args.body_weight
HQ = compute_hazard_quotient(total_DI, args.body_weight, args.RfD)
# --- Output ---
print("=" * 65)
print(" Environmental Risk Assessment — Hazard Quotient")
print("=" * 65)
print()
print(" Step 1: Soil Concentration")
print(f" Total contaminant : {args.total_mass_ug:.2e} ug")
print(f" Area : {args.area_m2:,.0f} m2")
print(f" Depth : {args.depth_m} m")
print(f" Bulk density : {args.bulk_density} kg/m3")
print(f" Soil mass : {soil_mass:,.0f} kg")
print(f" C_soil = {args.total_mass_ug:.2e} / {soil_mass:.2e}")
print(f" = {C_soil:.4f} ug/kg")
print()
print(" Step 2: Soil Solution Concentration (Kd-corrected)")
print(f" foc = {args.foc}, Koc = {args.Koc} L/kg")
print(f" Kd = foc * Koc = {Kd:.4f} L/kg")
print(f" theta_w = {args.theta_w} L/L")
rho_b_L = args.bulk_density / 1000.0
print(f" rho_b = {rho_b_L:.3f} kg/L")
denom = Kd * rho_b_L + args.theta_w
print(f" C_sw = C_soil * rho_b / (Kd*rho_b + theta_w)")
print(f" = {C_soil:.4f} * {rho_b_L:.3f} / ({Kd:.4f}*{rho_b_L:.3f} + {args.theta_w})")
print(f" = {C_soil * rho_b_L:.4f} / {denom:.4f}")
print(f" = {C_sw:.4f} ug/L")
print()
print(" Step 3-4: Daily Intake by Food Pathway")
for fr in food_results:
print(f" {fr['name']}:")
print(f" Intake = {fr['intake_g']:.0f} g/day, BF = {fr['bioavailability']}, "
f"PUF = {fr['PUF']}, TSCF = {fr['TSCF']}")
print(f" DI = {C_sw:.4f} * {fr['TSCF']} * {fr['PUF']} * {fr['intake_g']:.0f} * {fr['bioavailability']}")
print(f" = {fr['DI']:.4f} ug/day")
print(f" ----------------------------------------")
print(f" Total daily intake = {total_DI:.4f} ug/day")
print()
print(" Step 5: Average Daily Dose")
print(f" ADD = {total_DI:.4f} / {args.body_weight} = {ADD:.4f} ug/kg/day")
print()
print(" Step 6: Hazard Quotient")
print(f" RfD = {args.RfD} ug/kg/day")
print(f" HQ = ADD / RfD = {ADD:.4f} / {args.RfD}")
print(f" = {HQ:.1f}")
print()
if HQ > 1:
print(f" RESULT: HQ = {HQ:.1f} >> 1 — significant health risk.")
else:
print(f" RESULT: HQ = {HQ:.2f} < 1 — within acceptable risk level.")
print()
print(" Verification:")
HQ_check = (C_sw * sum(f["TSCF"] * f["PUF"] * f["intake_g"] * f["bioavailability"]
for f in foods)) / args.body_weight / args.RfD
print(f" HQ recomputed: {HQ_check:.1f} (matches: {abs(HQ_check - HQ) < 0.1})")
print("=" * 65)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Enzyme kinetics calculator: Km/Vmax, Hill coefficient, and Ki determination.
No external dependencies — uses only math stdlib.
Usage:
python enzyme_kinetics.py --type km_vmax --substrate "1,2,5,10,20" --velocity "0.5,0.8,1.2,1.5,1.7"
python enzyme_kinetics.py --type hill --substrate "0.1,0.5,1,2,5,10,50" --velocity "0.02,0.1,0.2,0.35,0.6,0.8,0.95"
python enzyme_kinetics.py --type ki --substrate "1,2,5,10,20" --velocity_no_inh "0.5,0.8,1.2,1.5,1.7" --velocity_inh "0.3,0.5,0.8,1.0,1.1" --inhibitor_conc 5 --inhibition_type competitive
"""
import argparse
import math
import sys
# ---------------------------------------------------------------------------
# Km / Vmax via Lineweaver-Burk linear regression (1/v vs 1/[S])
# ---------------------------------------------------------------------------
def _linreg(xs, ys):
"""Simple linear regression: y = slope*x + intercept. Returns (slope, intercept, r2)."""
n = len(xs)
sx = sum(xs)
sy = sum(ys)
sxx = sum(x * x for x in xs)
sxy = sum(x * y for x, y in zip(xs, ys))
denom = n * sxx - sx * sx
if abs(denom) < 1e-30:
raise ValueError("Degenerate data — all x values identical.")
slope = (n * sxy - sx * sy) / denom
intercept = (sy * sxx - sx * sxy) / denom
y_mean = sy / n
ss_tot = sum((y - y_mean) ** 2 for y in ys)
ss_res = sum((y - (slope * x + intercept)) ** 2 for x, y in zip(xs, ys))
r2 = 1.0 - ss_res / ss_tot if ss_tot > 0 else 0.0
return slope, intercept, r2
def calc_km_vmax(substrate, velocity):
"""Determine Km and Vmax using Lineweaver-Burk and direct nonlinear grid search."""
if len(substrate) != len(velocity):
raise ValueError("substrate and velocity must have the same length.")
if len(substrate) < 3:
raise ValueError("Need at least 3 data points.")
# --- Method 1: Lineweaver-Burk ---
inv_s = [1.0 / s for s in substrate]
inv_v = [1.0 / v for v in velocity]
slope, intercept, r2 = _linreg(inv_s, inv_v)
if intercept <= 0 or slope <= 0:
lb_vmax = None
lb_km = None
lb_note = "Lineweaver-Burk gave non-physical (negative) parameters — data may not follow Michaelis-Menten."
else:
lb_vmax = 1.0 / intercept
lb_km = slope * lb_vmax
lb_note = None
# --- Method 2: Nonlinear least-squares via grid + refinement ---
vmax_est = max(velocity) * 1.2
km_est = substrate[len(substrate) // 2]
best_sse = float("inf")
best_km, best_vmax = km_est, vmax_est
for vmax_mult in [x * 0.1 for x in range(5, 31)]:
for km_mult in [x * 0.1 for x in range(1, 51)]:
vm = max(velocity) * vmax_mult
km = max(substrate) * km_mult * 0.1
sse = sum((v - vm * s / (km + s)) ** 2 for s, v in zip(substrate, velocity))
if sse < best_sse:
best_sse = sse
best_km, best_vmax = km, vm
# Refine with smaller steps around best
for _ in range(3):
step_v = best_vmax * 0.05
step_k = best_km * 0.05
improved = False
for dv in [-step_v, 0, step_v]:
for dk in [-step_k, 0, step_k]:
vm = best_vmax + dv
km = best_km + dk
if vm <= 0 or km <= 0:
continue
sse = sum((v - vm * s / (km + s)) ** 2 for s, v in zip(substrate, velocity))
if sse < best_sse:
best_sse = sse
best_km, best_vmax = km, vm
improved = True
if not improved:
break
# R-squared for nonlinear fit
v_mean = sum(velocity) / len(velocity)
ss_tot = sum((v - v_mean) ** 2 for v in velocity)
nl_r2 = 1.0 - best_sse / ss_tot if ss_tot > 0 else 0.0
print("=" * 60)
print("MICHAELIS-MENTEN KINETICS: Km and Vmax")
print("=" * 60)
print(f"\nData points: {len(substrate)}")
print(f"[S] range: {min(substrate):.4g} - {max(substrate):.4g}")
print(f"v range: {min(velocity):.4g} - {max(velocity):.4g}")
print("\n--- Lineweaver-Burk (1/v vs 1/[S]) ---")
if lb_vmax is not None:
print(f" Vmax = {lb_vmax:.4g}")
print(f" Km = {lb_km:.4g}")
print(f" R² = {r2:.4f}")
else:
print(f" {lb_note}")
print("\n--- Nonlinear fit (grid search) ---")
print(f" Vmax = {best_vmax:.4g}")
print(f" Km = {best_km:.4g}")
print(f" R² = {nl_r2:.4f}")
print(f" SSE = {best_sse:.4g}")
print("\n--- Predicted vs Observed ---")
print(f" {'[S]':>10s} {'v_obs':>10s} {'v_pred':>10s} {'residual':>10s}")
for s, v in zip(substrate, velocity):
v_pred = best_vmax * s / (best_km + s)
print(f" {s:10.4g} {v:10.4g} {v_pred:10.4g} {v - v_pred:10.4g}")
print("\n--- Catalytic efficiency ---")
print(f" kcat/Km = Vmax/Km = {best_vmax / best_km:.4g} (units depend on [E] normalization)")
print("\nNote: For publication-quality fits, use scipy.optimize.curve_fit with proper error estimates.")
# ---------------------------------------------------------------------------
# Hill coefficient from cooperative binding data
# ---------------------------------------------------------------------------
def calc_hill(substrate, velocity):
"""Determine Hill coefficient from log-log linearization of binding data."""
if len(substrate) != len(velocity):
raise ValueError("substrate and velocity must have the same length.")
if len(substrate) < 3:
raise ValueError("Need at least 3 data points.")
vmax_est = max(velocity) * 1.1
# Hill linearization: log(v / (Vmax - v)) = nH * log([S]) - nH * log(K0.5)
# Use data points where 0.1*Vmax < v < 0.9*Vmax for reliable linearization
log_s = []
log_y = []
for s, v in zip(substrate, velocity):
if 0.1 * vmax_est < v < 0.9 * vmax_est:
y = v / (vmax_est - v)
if y > 0:
log_s.append(math.log10(s))
log_y.append(math.log10(y))
if len(log_s) < 2:
# Try broader range with adjusted Vmax
vmax_est = max(velocity) * 1.3
for s, v in zip(substrate, velocity):
if 0.05 * vmax_est < v < 0.95 * vmax_est:
y = v / (vmax_est - v)
if y > 0:
log_s.append(math.log10(s))
log_y.append(math.log10(y))
if len(log_s) < 2:
print("ERROR: Insufficient data points in the 10-90% saturation range for Hill analysis.")
print("Provide more data points spanning a wider concentration range.")
sys.exit(1)
slope, intercept, r2 = _linreg(log_s, log_y)
nH = slope
k05 = 10 ** (-intercept / nH) if abs(nH) > 1e-10 else float("inf")
print("=" * 60)
print("HILL ANALYSIS: Cooperative Binding")
print("=" * 60)
print(f"\nData points used for Hill plot: {len(log_s)} of {len(substrate)}")
print(f"Estimated Vmax: {vmax_est:.4g}")
print(f"\n--- Hill Parameters ---")
print(f" Hill coefficient (nH) = {nH:.3f}")
print(f" K0.5 = {k05:.4g}")
print(f" R² (Hill plot) = {r2:.4f}")
print(f"\n--- Interpretation ---")
if nH > 1.05:
print(f" nH = {nH:.2f} > 1 → POSITIVE cooperativity")
print(f" Binding at one site increases affinity at other sites.")
elif nH < 0.95:
print(f" nH = {nH:.2f} < 1 → NEGATIVE cooperativity")
print(f" Binding at one site decreases affinity at other sites.")
else:
print(f" nH = {nH:.2f} ≈ 1 → NO cooperativity (independent sites)")
print(f"\n--- Predicted vs Observed ---")
print(f" {'[S]':>10s} {'v_obs':>10s} {'v_pred':>10s}")
for s, v in zip(substrate, velocity):
v_pred = vmax_est * (s ** nH) / (k05 ** nH + s ** nH)
print(f" {s:10.4g} {v:10.4g} {v_pred:10.4g}")
print(f"\nNote: nH is an empirical parameter, not the number of binding sites.")
print(f"True number of sites must be determined by stoichiometry (ITC, AUC).")
# ---------------------------------------------------------------------------
# Ki from inhibition data
# ---------------------------------------------------------------------------
def calc_ki(substrate, velocity_no_inh, velocity_inh, inhibitor_conc, inhibition_type):
"""Determine Ki from paired velocity data with and without inhibitor."""
n = len(substrate)
if len(velocity_no_inh) != n or len(velocity_inh) != n:
raise ValueError("All data arrays must have the same length.")
if n < 3:
raise ValueError("Need at least 3 data points.")
# First, get Km and Vmax from uninhibited data
inv_s = [1.0 / s for s in substrate]
inv_v0 = [1.0 / v for v in velocity_no_inh]
slope0, intercept0, r2_0 = _linreg(inv_s, inv_v0)
if intercept0 <= 0 or slope0 <= 0:
print("ERROR: Uninhibited data does not give valid Km/Vmax. Check data.")
sys.exit(1)
vmax = 1.0 / intercept0
km = slope0 * vmax
# Get apparent parameters from inhibited data
inv_vi = [1.0 / v for v in velocity_inh]
slope_i, intercept_i, r2_i = _linreg(inv_s, inv_vi)
if intercept_i <= 0 or slope_i <= 0:
print("ERROR: Inhibited data does not give valid apparent parameters. Check data.")
sys.exit(1)
vmax_app = 1.0 / intercept_i
km_app = slope_i * vmax_app
inh_type = inhibition_type.lower().strip()
ki = None
print("=" * 60)
print(f"ENZYME INHIBITION ANALYSIS: {inhibition_type.title()}")
print("=" * 60)
print(f"\nInhibitor concentration: {inhibitor_conc}")
print(f"\n--- Uninhibited Parameters ---")
print(f" Vmax = {vmax:.4g}")
print(f" Km = {km:.4g}")
print(f" R² = {r2_0:.4f}")
print(f"\n--- Inhibited Apparent Parameters ---")
print(f" Vmax_app = {vmax_app:.4g}")
print(f" Km_app = {km_app:.4g}")
print(f" R² = {r2_i:.4f}")
if inh_type == "competitive":
# Competitive: Km_app = Km * (1 + [I]/Ki), Vmax unchanged
# Ki = [I] / (Km_app/Km - 1)
ratio = km_app / km
if ratio <= 1.0:
print(f"\nWARNING: Km_app ({km_app:.4g}) <= Km ({km:.4g}). Not consistent with competitive inhibition.")
else:
ki = inhibitor_conc / (ratio - 1.0)
print(f"\n--- Competitive Inhibition ---")
print(f" Km_app / Km = {ratio:.4f}")
print(f" Expected: Vmax_app ≈ Vmax (ratio = {vmax_app / vmax:.3f})")
elif inh_type == "uncompetitive":
# Uncompetitive: Vmax_app = Vmax / (1 + [I]/Ki), Km_app = Km / (1 + [I]/Ki)
ratio = vmax / vmax_app
if ratio <= 1.0:
print(f"\nWARNING: Vmax_app ({vmax_app:.4g}) >= Vmax ({vmax:.4g}). Not consistent with uncompetitive inhibition.")
else:
ki = inhibitor_conc / (ratio - 1.0)
print(f"\n--- Uncompetitive Inhibition ---")
print(f" Vmax / Vmax_app = {ratio:.4f}")
print(f" Km / Km_app = {km / km_app:.4f} (should ≈ {ratio:.4f})")
elif inh_type in ("noncompetitive", "non-competitive"):
# Pure noncompetitive: Vmax_app = Vmax / (1 + [I]/Ki), Km unchanged
ratio = vmax / vmax_app
if ratio <= 1.0:
print(f"\nWARNING: Vmax_app ({vmax_app:.4g}) >= Vmax ({vmax:.4g}). Not consistent with noncompetitive inhibition.")
else:
ki = inhibitor_conc / (ratio - 1.0)
print(f"\n--- Noncompetitive Inhibition ---")
print(f" Vmax / Vmax_app = {ratio:.4f}")
print(f" Expected: Km_app ≈ Km (ratio = {km_app / km:.3f})")
else:
print(f"\nERROR: Unknown inhibition type '{inhibition_type}'. Use: competitive, uncompetitive, noncompetitive.")
sys.exit(1)
if ki is not None:
print(f"\n Ki = {ki:.4g}")
print(f"\n--- Verification ---")
print(f" {'[S]':>10s} {'v_obs':>10s} {'v_pred':>10s} {'residual':>10s}")
for s, v_obs in zip(substrate, velocity_inh):
if inh_type == "competitive":
v_pred = vmax * s / (km * (1 + inhibitor_conc / ki) + s)
elif inh_type == "uncompetitive":
factor = 1 + inhibitor_conc / ki
v_pred = (vmax / factor) * s / (km / factor + s)
else: # noncompetitive
v_pred = (vmax / (1 + inhibitor_conc / ki)) * s / (km + s)
print(f" {s:10.4g} {v_obs:10.4g} {v_pred:10.4g} {v_obs - v_pred:10.4g}")
else:
print(f"\n Ki could not be determined — check data consistency with {inhibition_type} model.")
print(f"\nNote: Lineweaver-Burk analysis amplifies error at low [S]. For publication, use nonlinear regression.")
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def parse_floats(s):
"""Parse comma-separated float string."""
return [float(x.strip()) for x in s.split(",")]
def main():
parser = argparse.ArgumentParser(description="Enzyme kinetics calculator")
parser.add_argument("--type", required=True, choices=["km_vmax", "hill", "ki"],
help="Calculation type")
parser.add_argument("--substrate", help="Comma-separated substrate concentrations")
parser.add_argument("--velocity", help="Comma-separated velocity values")
parser.add_argument("--velocity_no_inh", help="Velocities without inhibitor (for ki)")
parser.add_argument("--velocity_inh", help="Velocities with inhibitor (for ki)")
parser.add_argument("--inhibitor_conc", type=float, help="Inhibitor concentration (for ki)")
parser.add_argument("--inhibition_type", default="competitive",
help="Type of inhibition: competitive, uncompetitive, noncompetitive")
args = parser.parse_args()
if args.type == "km_vmax":
if not args.substrate or not args.velocity:
parser.error("--substrate and --velocity required for km_vmax")
calc_km_vmax(parse_floats(args.substrate), parse_floats(args.velocity))
elif args.type == "hill":
if not args.substrate or not args.velocity:
parser.error("--substrate and --velocity required for hill")
calc_hill(parse_floats(args.substrate), parse_floats(args.velocity))
elif args.type == "ki":
if not args.substrate or not args.velocity_no_inh or not args.velocity_inh:
parser.error("--substrate, --velocity_no_inh, and --velocity_inh required for ki")
if args.inhibitor_conc is None:
parser.error("--inhibitor_conc required for ki")
calc_ki(
parse_floats(args.substrate),
parse_floats(args.velocity_no_inh),
parse_floats(args.velocity_inh),
args.inhibitor_conc,
args.inhibition_type,
)
if __name__ == "__main__":
main()
"""Epidemiology calculations: R0/herd immunity, NNT, diagnostic tests, Bayesian post-test probability,
and vaccine coverage threshold from field data.
Consolidates the most commonly needed epidemiology formulas into one script with
verified output. No external dependencies — pure Python stdlib.
Calculation types (--type):
r0_herd R0, effective reproduction number, and herd immunity threshold.
Requires: --R0 (and optionally --VE for vaccine-adjusted threshold,
--coverage to evaluate Re at a given vaccination fraction)
vaccine_coverage
Derive VE from field surveillance data using the screening method
(Farrington 1993), then compute required vaccination coverage Vc.
Requires: --R0 --PCV --PPV
PCV = proportion of cases that were vaccinated (0-1)
PPV = proportion of population vaccinated (0-1)
Formula: VE = 1 - [PCV*(1-PPV)] / [(1-PCV)*PPV]
Then: Vc = (1 - 1/R0) / VE
nnt Number needed to treat / number needed to harm.
Requires: --control_rate --treatment_rate
diagnostic Sensitivity, specificity, PPV, NPV, accuracy, likelihood ratios.
Requires: --tp --fp --tn --fn (2×2 contingency counts)
bayesian Post-test probability via Bayes' theorem.
Requires: --prevalence --sensitivity --specificity
Optional: --test_result {positive,negative} (default: positive)
Usage:
python epidemiology.py --type r0_herd --R0 3.5 --VE 0.90
python epidemiology.py --type r0_herd --R0 14 --VE 0.97 --coverage 0.95
python epidemiology.py --type vaccine_coverage --R0 3.0 --PCV 0.06 --PPV 0.35
python epidemiology.py --type nnt --control_rate 0.30 --treatment_rate 0.20
python epidemiology.py --type diagnostic --tp 90 --fp 10 --tn 880 --fn 20
python epidemiology.py --type bayesian --prevalence 0.01 --sensitivity 0.95 --specificity 0.90
python epidemiology.py --type bayesian --prevalence 0.05 --sensitivity 0.80 --specificity 0.95 --test_result negative
"""
import argparse
import math
import sys
# ---------------------------------------------------------------------------
# R0 / herd immunity
# ---------------------------------------------------------------------------
def r0_herd(R0: float, VE: float = 1.0, coverage: float | None = None) -> dict:
"""
Compute herd immunity threshold and effective reproduction number.
Args:
R0: Basic reproduction number (must be > 1).
VE: Vaccine efficacy as a fraction [0, 1]. Default 1.0 (perfect vaccine).
coverage: Optional vaccination fraction [0, 1] to evaluate Re at.
Returns dict with:
herd_threshold_perfect: Herd immunity threshold assuming 100% efficacy.
herd_threshold_ve: VE-adjusted minimum vaccination fraction.
Re_at_coverage: Effective R at the given coverage (if provided).
"""
if R0 <= 1:
raise ValueError(f"R0 must be > 1 for epidemic spread (got {R0}).")
if not 0 < VE <= 1:
raise ValueError(f"VE must be in (0, 1] (got {VE}).")
hc_perfect = 1.0 - 1.0 / R0
hc_ve = hc_perfect / VE # = (1 - 1/R0) / VE
result = {
"R0": R0,
"VE": VE,
"herd_threshold_perfect": hc_perfect,
"herd_threshold_ve_adjusted": hc_ve,
}
if coverage is not None:
if not 0 <= coverage <= 1:
raise ValueError(f"coverage must be in [0, 1] (got {coverage}).")
# Re = R0 * (1 - VE * coverage)
Re = R0 * (1.0 - VE * coverage)
result["coverage"] = coverage
result["Re_at_coverage"] = Re
result["epidemic_suppressed"] = Re < 1.0
return result
def print_r0_herd(res: dict) -> None:
print("=" * 60)
print(" R0 and Herd Immunity Analysis")
print("=" * 60)
print(f" Basic reproduction number (R0) : {res['R0']}")
print(f" Vaccine efficacy (VE) : {res['VE'] * 100:.1f}%")
print()
hc = res["herd_threshold_perfect"]
hc_ve = res["herd_threshold_ve_adjusted"]
print(" Herd Immunity Threshold (Hc = 1 - 1/R0):")
print(f" Perfect vaccine (VE=100%) : {hc * 100:.2f}% ({hc:.4f})")
print()
print(" VE-adjusted minimum vaccination coverage:")
print(f" Vc = Hc / VE = {hc:.4f} / {res['VE']:.2f}")
print(f" = {hc_ve:.4f} ({hc_ve * 100:.2f}%)")
if hc_ve > 1.0:
print(" WARNING: VE too low to achieve herd immunity even at 100% coverage.")
if "coverage" in res:
cov = res["coverage"]
Re = res["Re_at_coverage"]
print()
print(f" Effective R (Re) at {cov * 100:.1f}% vaccination coverage:")
print(f" Re = R0 × (1 - VE × coverage)")
print(f" = {res['R0']} × (1 - {res['VE']:.2f} × {cov:.2f})")
print(f" = {Re:.4f}")
suppressed = res["epidemic_suppressed"]
status = "SUPPRESSED (Re < 1)" if suppressed else "NOT suppressed (Re ≥ 1)"
print(f" Epidemic status: {status}")
print()
print(" Verification:")
check = 1.0 - 1.0 / res["R0"]
assert abs(check - res["herd_threshold_perfect"]) < 1e-9, "Herd threshold mismatch"
print(f" 1 - 1/R0 = {check:.6f} ✓")
print("=" * 60)
# ---------------------------------------------------------------------------
# Vaccine coverage threshold from field data (screening method)
# ---------------------------------------------------------------------------
def vaccine_coverage(R0: float, PCV: float, PPV: float) -> dict:
"""
Derive VE from observational field data using the screening method
(Farrington 1993), then compute the required vaccination coverage Vc.
The screening method estimates vaccine effectiveness from two readily
available surveillance numbers:
PCV = proportion of cases that were vaccinated [0, 1]
PPV = proportion of the population vaccinated [0, 1]
Formula:
VE = 1 - [PCV * (1 - PPV)] / [(1 - PCV) * PPV]
Hc = 1 - 1/R0 (herd immunity threshold, perfect vaccine)
Vc = Hc / VE (minimum coverage with real-world VE)
Args:
R0: Basic reproduction number (must be > 1).
PCV: Proportion of disease cases that are vaccinated (0, 1).
PPV: Proportion of total population that is vaccinated (0, 1).
Returns dict with VE, Hc, Vc, and intermediate values.
"""
if R0 <= 1:
raise ValueError(f"R0 must be > 1 for epidemic spread (got {R0}).")
if not 0 < PCV < 1:
raise ValueError(f"PCV must be in (0, 1) exclusive (got {PCV}).")
if not 0 < PPV < 1:
raise ValueError(f"PPV must be in (0, 1) exclusive (got {PPV}).")
# Screening method: VE = 1 - [PCV*(1-PPV)] / [(1-PCV)*PPV]
numerator = PCV * (1.0 - PPV)
denominator = (1.0 - PCV) * PPV
VE = 1.0 - numerator / denominator
if VE <= 0:
raise ValueError(
f"Derived VE = {VE:.4f} <= 0 — vaccination appears ineffective or harmful "
f"with PCV={PCV}, PPV={PPV}."
)
Hc = 1.0 - 1.0 / R0
Vc = Hc / VE
# Re at the current PPV with derived VE
Re_current = R0 * (1.0 - VE * PPV)
return {
"R0": R0,
"PCV": PCV,
"PPV": PPV,
"VE": VE,
"herd_threshold_perfect": Hc,
"Vc": Vc,
"Re_current": Re_current,
}
def print_vaccine_coverage(res: dict) -> None:
print("=" * 60)
print(" Vaccine Coverage Threshold (Screening Method)")
print("=" * 60)
print(f" Basic reproduction number (R0) : {res['R0']}")
print(f" Proportion of cases vaccinated (PCV) : {res['PCV']}")
print(f" Proportion of population vaccinated (PPV): {res['PPV']}")
print()
PCV = res["PCV"]
PPV = res["PPV"]
VE = res["VE"]
Hc = res["herd_threshold_perfect"]
Vc = res["Vc"]
print(" Step 1 — Derive VE via screening method (Farrington 1993):")
print(f" VE = 1 - [PCV * (1-PPV)] / [(1-PCV) * PPV]")
print(f" = 1 - [{PCV} * {1-PPV:.4f}] / [{1-PCV:.4f} * {PPV}]")
num = PCV * (1 - PPV)
den = (1 - PCV) * PPV
print(f" = 1 - {num:.6f} / {den:.6f}")
print(f" = 1 - {num/den:.6f}")
print(f" = {VE:.4f} ({VE * 100:.2f}%)")
print()
print(" Step 2 — Herd immunity threshold (perfect vaccine):")
print(f" Hc = 1 - 1/R0 = 1 - 1/{res['R0']} = {Hc:.4f} ({Hc * 100:.2f}%)")
print()
print(" Step 3 — Required vaccination coverage:")
print(f" Vc = Hc / VE = {Hc:.4f} / {VE:.4f}")
print(f" = {Vc:.4f} ({Vc * 100:.1f}%)")
if Vc > 1.0:
print(" WARNING: Vc > 100% — herd immunity unachievable at this VE.")
print()
Re = res["Re_current"]
print(f" Current situation (PPV = {PPV:.0%}):")
print(f" Re = R0 * (1 - VE * PPV) = {res['R0']} * (1 - {VE:.4f} * {PPV})")
print(f" = {Re:.4f}")
status = "suppressed" if Re < 1.0 else "NOT suppressed"
print(f" Epidemic: {status}")
print()
print(" Verification:")
Re_at_Vc = res["R0"] * (1.0 - VE * Vc)
print(f" Re at Vc = R0*(1 - VE*Vc) = {Re_at_Vc:.6f} (should be ~1.00)")
VE_check = 1.0 - (PCV * (1 - PPV)) / ((1 - PCV) * PPV)
assert abs(VE_check - VE) < 1e-9, "VE mismatch"
print(f" VE recomputed: {VE_check:.6f} matches")
print("=" * 60)
# ---------------------------------------------------------------------------
# NNT / NNH
# ---------------------------------------------------------------------------
def nnt(control_rate: float, treatment_rate: float) -> dict:
"""
Compute NNT, ARR, RRR, RR, and odds ratio.
Args:
control_rate: Event rate in the control group [0, 1].
treatment_rate: Event rate in the treatment group [0, 1].
Returns dict with NNT/NNH, ARR, RRR, RR, OR.
"""
for name, val in [("control_rate", control_rate), ("treatment_rate", treatment_rate)]:
if not 0 <= val <= 1:
raise ValueError(f"{name} must be in [0, 1] (got {val}).")
ARR = control_rate - treatment_rate # Absolute Risk Reduction (positive = benefit)
RR = treatment_rate / control_rate if control_rate > 0 else float("nan")
RRR = 1.0 - RR if not math.isnan(RR) else float("nan")
# Odds ratio
odds_control = control_rate / (1.0 - control_rate) if control_rate < 1 else float("inf")
odds_treatment = treatment_rate / (1.0 - treatment_rate) if treatment_rate < 1 else float("inf")
OR = odds_treatment / odds_control if odds_control > 0 else float("nan")
if abs(ARR) < 1e-12:
NNT = float("inf")
label = "NNT"
elif ARR > 0:
NNT = 1.0 / ARR
label = "NNT" # treatment reduces risk — number needed to treat
else:
NNT = 1.0 / abs(ARR)
label = "NNH" # treatment increases risk — number needed to harm
return {
"control_rate": control_rate,
"treatment_rate": treatment_rate,
"ARR": ARR,
"RR": RR,
"RRR": RRR,
"OR": OR,
"NNT": NNT,
"NNT_label": label,
}
def print_nnt(res: dict) -> None:
print("=" * 60)
print(" NNT / NNH Analysis")
print("=" * 60)
print(f" Control group event rate : {res['control_rate'] * 100:.2f}%")
print(f" Treatment group event rate : {res['treatment_rate'] * 100:.2f}%")
print()
arr = res["ARR"]
print(f" Absolute Risk Reduction (ARR) : {arr * 100:.4f}%")
rr = res["RR"]
if not math.isnan(rr):
print(f" Relative Risk (RR) : {rr:.4f}")
rrr = res["RRR"]
print(f" Relative Risk Reduction (RRR) : {rrr * 100:.2f}%")
or_ = res["OR"]
if not math.isnan(or_) and not math.isinf(or_):
print(f" Odds Ratio (OR) : {or_:.4f}")
print()
label = res["NNT_label"]
nnt_val = res["NNT"]
if math.isinf(nnt_val):
print(f" {label}: ∞ (treatment has no effect on event rate)")
else:
print(f" {label} = 1 / |ARR| = 1 / {abs(arr):.6f} = {nnt_val:.2f}")
print(f" Rounded up: {math.ceil(nnt_val)} patients")
if label == "NNT":
print(f" Interpretation: Treat {math.ceil(nnt_val)} patients to prevent 1 event.")
else:
print(f" Interpretation: Treat {math.ceil(nnt_val)} patients to cause 1 additional event (harm).")
print()
print(" Verification:")
check = abs(1.0 / arr) if abs(arr) > 1e-12 else float("inf")
if not math.isinf(check):
assert abs(check - nnt_val) < 1e-6, "NNT mismatch"
print(f" 1 / |ARR| = {check:.4f} ✓")
print("=" * 60)
# ---------------------------------------------------------------------------
# Diagnostic test metrics
# ---------------------------------------------------------------------------
def diagnostic(tp: int, fp: int, tn: int, fn: int) -> dict:
"""
Compute diagnostic test performance metrics from a 2×2 table.
Args:
tp: True positives (disease+, test+)
fp: False positives (disease-, test+)
tn: True negatives (disease-, test-)
fn: False negatives (disease+, test-)
Returns dict with sensitivity, specificity, PPV, NPV, accuracy, LR+, LR-.
"""
for name, val in [("tp", tp), ("fp", fp), ("tn", tn), ("fn", fn)]:
if val < 0:
raise ValueError(f"{name} must be >= 0 (got {val}).")
n_disease_pos = tp + fn
n_disease_neg = fp + tn
n_total = n_disease_pos + n_disease_neg
if n_disease_pos == 0:
raise ValueError("No disease-positive cases (tp + fn = 0). Cannot compute sensitivity.")
if n_disease_neg == 0:
raise ValueError("No disease-negative cases (fp + tn = 0). Cannot compute specificity.")
sensitivity = tp / n_disease_pos # True Positive Rate
specificity = tn / n_disease_neg # True Negative Rate
prevalence = n_disease_pos / n_total
PPV = tp / (tp + fp) if (tp + fp) > 0 else float("nan") # Positive Predictive Value
NPV = tn / (tn + fn) if (tn + fn) > 0 else float("nan") # Negative Predictive Value
accuracy = (tp + tn) / n_total
LR_pos = sensitivity / (1.0 - specificity) if specificity < 1.0 else float("inf")
LR_neg = (1.0 - sensitivity) / specificity if specificity > 0.0 else float("nan")
DOR = LR_pos / LR_neg if LR_neg and LR_neg > 0 else float("nan")
return {
"tp": tp, "fp": fp, "tn": tn, "fn": fn,
"n_disease_pos": n_disease_pos,
"n_disease_neg": n_disease_neg,
"n_total": n_total,
"prevalence": prevalence,
"sensitivity": sensitivity,
"specificity": specificity,
"PPV": PPV,
"NPV": NPV,
"accuracy": accuracy,
"LR_pos": LR_pos,
"LR_neg": LR_neg,
"DOR": DOR,
}
def print_diagnostic(res: dict) -> None:
print("=" * 60)
print(" Diagnostic Test Performance")
print("=" * 60)
print(" 2×2 Contingency Table:")
print(f" True Positives (TP) : {res['tp']:>6}")
print(f" False Positives (FP) : {res['fp']:>6}")
print(f" True Negatives (TN) : {res['tn']:>6}")
print(f" False Negatives (FN) : {res['fn']:>6}")
print(f" Total : {res['n_total']:>6}")
print(f" Disease prevalence : {res['prevalence'] * 100:.2f}%")
print()
print(" Performance Metrics:")
print(f" Sensitivity (TPR) = TP / (TP+FN) = {res['tp']} / {res['n_disease_pos']}")
print(f" = {res['sensitivity']:.4f} ({res['sensitivity'] * 100:.2f}%)")
print(f" Specificity (TNR) = TN / (TN+FP) = {res['tn']} / {res['n_disease_neg']}")
print(f" = {res['specificity']:.4f} ({res['specificity'] * 100:.2f}%)")
ppv = res["PPV"]
npv = res["NPV"]
n_pos_test = res["tp"] + res["fp"]
n_neg_test = res["tn"] + res["fn"]
if not math.isnan(ppv):
print(f" PPV = TP / (TP+FP) = {res['tp']} / {n_pos_test}")
print(f" = {ppv:.4f} ({ppv * 100:.2f}%)")
if not math.isnan(npv):
print(f" NPV = TN / (TN+FN) = {res['tn']} / {n_neg_test}")
print(f" = {npv:.4f} ({npv * 100:.2f}%)")
print(f" Accuracy = (TP+TN) / N = {res['tp'] + res['tn']} / {res['n_total']}")
print(f" = {res['accuracy']:.4f} ({res['accuracy'] * 100:.2f}%)")
print()
lrp = res["LR_pos"]
lrn = res["LR_neg"]
if not math.isinf(lrp):
print(f" Likelihood Ratio+ = Sens / (1-Spec) = {lrp:.4f}")
if not math.isnan(lrn) and lrn:
print(f" Likelihood Ratio- = (1-Sens) / Spec = {lrn:.4f}")
dor = res["DOR"]
if not math.isnan(dor) and not math.isinf(dor):
print(f" Diagnostic Odds Ratio (DOR) = {dor:.2f}")
print()
print(" Interpretation:")
se = res["sensitivity"]
sp = res["specificity"]
if se >= 0.95:
print(f" High sensitivity ({se * 100:.1f}%): few missed cases; good rule-out test.")
elif se < 0.80:
print(f" Low sensitivity ({se * 100:.1f}%): many missed cases (false negatives).")
if sp >= 0.95:
print(f" High specificity ({sp * 100:.1f}%): few false alarms; good rule-in test.")
elif sp < 0.80:
print(f" Low specificity ({sp * 100:.1f}%): many false positives.")
if not math.isnan(ppv):
print(f" PPV {ppv * 100:.1f}%: of those testing positive, {ppv * 100:.1f}% truly have the disease.")
if not math.isnan(npv):
print(f" NPV {npv * 100:.1f}%: of those testing negative, {npv * 100:.1f}% truly do not.")
print()
print(" Verification:")
check_se = res["tp"] / (res["tp"] + res["fn"])
check_sp = res["tn"] / (res["tn"] + res["fp"])
assert abs(check_se - se) < 1e-9, "Sensitivity mismatch"
assert abs(check_sp - sp) < 1e-9, "Specificity mismatch"
print(f" Sensitivity recomputed: {check_se:.6f} ✓")
print(f" Specificity recomputed: {check_sp:.6f} ✓")
print("=" * 60)
# ---------------------------------------------------------------------------
# Bayesian post-test probability
# ---------------------------------------------------------------------------
def bayesian(prevalence: float, sensitivity: float, specificity: float,
test_result: str = "positive") -> dict:
"""
Compute post-test probability via Bayes' theorem.
Args:
prevalence: Pre-test probability (prior) of disease [0, 1].
sensitivity: P(test+ | disease+) [0, 1].
specificity: P(test- | disease-) [0, 1].
test_result: "positive" or "negative".
Returns dict with pre-test odds, LR, post-test odds, and post-test probability.
"""
for name, val in [("prevalence", prevalence), ("sensitivity", sensitivity),
("specificity", specificity)]:
if not 0 <= val <= 1:
raise ValueError(f"{name} must be in [0, 1] (got {val}).")
if test_result not in ("positive", "negative"):
raise ValueError("test_result must be 'positive' or 'negative'.")
pre_test_odds = prevalence / (1.0 - prevalence) if prevalence < 1 else float("inf")
if test_result == "positive":
LR = sensitivity / (1.0 - specificity) if specificity < 1.0 else float("inf")
else:
LR = (1.0 - sensitivity) / specificity if specificity > 0.0 else float("nan")
post_test_odds = pre_test_odds * LR
post_test_prob = post_test_odds / (1.0 + post_test_odds) if not math.isinf(post_test_odds) else 1.0
# Alternative: direct formula via Bayes
if test_result == "positive":
p_test_given_disease = sensitivity
p_test_given_no_disease = 1.0 - specificity
else:
p_test_given_disease = 1.0 - sensitivity
p_test_given_no_disease = specificity
p_test = p_test_given_disease * prevalence + p_test_given_no_disease * (1.0 - prevalence)
ptp_direct = (p_test_given_disease * prevalence / p_test) if p_test > 0 else float("nan")
return {
"prevalence": prevalence,
"sensitivity": sensitivity,
"specificity": specificity,
"test_result": test_result,
"pre_test_odds": pre_test_odds,
"LR": LR,
"post_test_odds": post_test_odds,
"post_test_prob": post_test_prob,
"post_test_prob_direct": ptp_direct,
"p_test": p_test,
}
def print_bayesian(res: dict) -> None:
print("=" * 60)
print(" Bayesian Post-Test Probability")
print("=" * 60)
print(f" Pre-test probability (prevalence) : {res['prevalence'] * 100:.2f}%")
print(f" Sensitivity : {res['sensitivity'] * 100:.2f}%")
print(f" Specificity : {res['specificity'] * 100:.2f}%")
print(f" Test result : {res['test_result'].upper()}")
print()
pre_odds = res["pre_test_odds"]
LR = res["LR"]
post_odds = res["post_test_odds"]
post_prob = res["post_test_prob"]
print(" Step 1 — Convert prevalence to pre-test odds:")
print(f" Pre-test odds = {res['prevalence']:.4f} / (1 - {res['prevalence']:.4f})")
print(f" = {pre_odds:.4f}")
print()
lr_label = "LR+" if res["test_result"] == "positive" else "LR-"
if res["test_result"] == "positive":
lr_formula = f"Sensitivity / (1 - Specificity) = {res['sensitivity']:.4f} / {1 - res['specificity']:.4f}"
else:
lr_formula = f"(1 - Sensitivity) / Specificity = {1 - res['sensitivity']:.4f} / {res['specificity']:.4f}"
print(f" Step 2 — Likelihood Ratio ({lr_label}):")
print(f" {lr_label} = {lr_formula}")
if not math.isnan(LR) and not math.isinf(LR):
print(f" = {LR:.4f}")
elif math.isinf(LR):
print(f" = ∞ (perfect specificity)")
print()
print(" Step 3 — Post-test odds = pre-test odds × LR:")
if not math.isinf(post_odds):
print(f" = {pre_odds:.4f} × {LR:.4f} = {post_odds:.4f}")
else:
print(f" = ∞")
print()
print(" Step 4 — Convert post-test odds to probability:")
if not math.isinf(post_odds):
print(f" Post-test prob = post-test odds / (1 + post-test odds)")
print(f" = {post_odds:.4f} / {1 + post_odds:.4f}")
print(f" = {post_prob:.4f} ({post_prob * 100:.2f}%)")
else:
print(f" Post-test prob = 100% (certain)")
print()
print(" Interpretation:")
delta = post_prob - res["prevalence"]
direction = "increased" if delta > 0 else "decreased"
print(
f" A {res['test_result']} test result {direction} disease probability "
f"from {res['prevalence'] * 100:.2f}% to {post_prob * 100:.2f}%."
)
if res["test_result"] == "positive" and post_prob < 0.5:
print(" PPV < 50%: most positive tests are false positives at this prevalence.")
elif res["test_result"] == "negative" and (1.0 - post_prob) > 0.99:
print(" NPV > 99%: a negative result nearly rules out disease.")
print()
print(" Verification (direct Bayes formula):")
direct = res["post_test_prob_direct"]
if not math.isnan(direct):
print(f" P(disease | test {res['test_result']}) via direct formula = {direct:.6f}")
assert abs(direct - post_prob) < 1e-6, f"Bayesian mismatch: {direct} vs {post_prob}"
print(" Matches odds-ratio method ✓")
print("=" * 60)
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def _build_parser() -> argparse.ArgumentParser:
p = argparse.ArgumentParser(
description="Epidemiology calculations: R0/herd immunity, NNT, diagnostics, Bayes.",
epilog=(
"Examples:\n"
" python epidemiology.py --type r0_herd --R0 3.5 --VE 0.90\n"
" python epidemiology.py --type nnt --control_rate 0.30 --treatment_rate 0.20\n"
" python epidemiology.py --type diagnostic --tp 90 --fp 10 --tn 880 --fn 20\n"
" python epidemiology.py --type bayesian --prevalence 0.01 "
"--sensitivity 0.95 --specificity 0.90\n"
),
formatter_class=argparse.RawDescriptionHelpFormatter,
)
p.add_argument(
"--type",
required=True,
choices=["r0_herd", "vaccine_coverage", "nnt", "diagnostic", "bayesian"],
help="Calculation type.",
)
# r0_herd
p.add_argument("--R0", type=float, help="Basic reproduction number (r0_herd).")
p.add_argument("--VE", type=float, default=1.0, help="Vaccine efficacy [0,1] (r0_herd).")
p.add_argument("--coverage", type=float, help="Vaccination coverage fraction to evaluate Re (r0_herd).")
# vaccine_coverage (screening method)
p.add_argument("--PCV", type=float, help="Proportion of cases vaccinated (vaccine_coverage).")
p.add_argument("--PPV", type=float, help="Proportion of population vaccinated (vaccine_coverage).")
# nnt
p.add_argument("--control_rate", type=float, help="Event rate in control group (nnt).")
p.add_argument("--treatment_rate", type=float, help="Event rate in treatment group (nnt).")
# diagnostic
p.add_argument("--tp", type=int, help="True positives (diagnostic).")
p.add_argument("--fp", type=int, help="False positives (diagnostic).")
p.add_argument("--tn", type=int, help="True negatives (diagnostic).")
p.add_argument("--fn", type=int, help="False negatives (diagnostic).")
# bayesian
p.add_argument("--prevalence", type=float, help="Pre-test probability / disease prevalence (bayesian).")
p.add_argument("--sensitivity", type=float, help="Test sensitivity (bayesian, diagnostic).")
p.add_argument("--specificity", type=float, help="Test specificity (bayesian, diagnostic).")
p.add_argument(
"--test_result",
choices=["positive", "negative"],
default="positive",
help="Test result to condition on (bayesian, default: positive).",
)
return p
def main() -> None:
parser = _build_parser()
args = parser.parse_args()
calc_type = args.type
try:
if calc_type == "r0_herd":
if args.R0 is None:
parser.error("--type r0_herd requires --R0.")
res = r0_herd(args.R0, VE=args.VE, coverage=args.coverage)
print_r0_herd(res)
elif calc_type == "vaccine_coverage":
if args.R0 is None:
parser.error("--type vaccine_coverage requires --R0.")
if args.PCV is None or args.PPV is None:
parser.error("--type vaccine_coverage requires --PCV and --PPV.")
res = vaccine_coverage(args.R0, args.PCV, args.PPV)
print_vaccine_coverage(res)
elif calc_type == "nnt":
if args.control_rate is None or args.treatment_rate is None:
parser.error("--type nnt requires --control_rate and --treatment_rate.")
res = nnt(args.control_rate, args.treatment_rate)
print_nnt(res)
elif calc_type == "diagnostic":
for flag in ("--tp", "--fp", "--tn", "--fn"):
attr = flag.lstrip("-")
if getattr(args, attr) is None:
parser.error(f"--type diagnostic requires {flag}.")
res = diagnostic(args.tp, args.fp, args.tn, args.fn)
print_diagnostic(res)
elif calc_type == "bayesian":
for flag, attr in [("--prevalence", "prevalence"),
("--sensitivity", "sensitivity"),
("--specificity", "specificity")]:
if getattr(args, attr) is None:
parser.error(f"--type bayesian requires {flag}.")
res = bayesian(args.prevalence, args.sensitivity, args.specificity, args.test_result)
print_bayesian(res)
except ValueError as exc:
print(f"ERROR: {exc}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
"""General clinical fluid and dosing calculations.
Consolidates multiple calculation types into one script with --type subcommands.
Usage:
# IV drip rate (replaces iv_drip_rate.py)
python fluid_calculations.py --type drip_rate --volume_ml 50 --time_min 60 --drop_factor 60
# Drug added to bag (additive or equal-volume replacement)
python fluid_calculations.py --type drip_rate --drug_ml 4.7 --saline_ml 50 --time_min 60 --drop_factor 60
python fluid_calculations.py --type drip_rate --drug_ml 4.7 --saline_ml 50 --time_min 60 --drop_factor 60 --replace_equal
# BSA-based dose (e.g. chemotherapy)
python fluid_calculations.py --type bsa_dose --dose_per_m2 25 --bsa 0.8
python fluid_calculations.py --type bsa_dose --dose_per_m2 25 --bsa 0.8 --days 3
# Maintenance fluids — Holliday-Segar (pediatric and adult)
python fluid_calculations.py --type maintenance --weight_kg 22
# Dilution — C1V1 = C2V2 (find any one unknown)
python fluid_calculations.py --type dilution --c1 20 --v1 4.7 --v2 50
python fluid_calculations.py --type dilution --c1 20 --v1 4.7 --c2 1.88
python fluid_calculations.py --type dilution --c2 5 --v2 100 --v1 25
Supported --type values: drip_rate, bsa_dose, maintenance, dilution
"""
import argparse
import sys
# ---------------------------------------------------------------------------
# Calculation functions
# ---------------------------------------------------------------------------
def calc_drip_rate(
volume_ml: float | None,
drug_ml: float | None,
saline_ml: float | None,
time_min: float,
drop_factor: float,
replace_equal: bool = False,
) -> dict:
"""
IV drip rate from total volume, infusion time, and drop factor.
Returns mL/min, drops/min (exact and rounded).
"""
if volume_ml is not None:
total = volume_ml
mix_note = f"Total volume: {volume_ml} mL"
elif drug_ml is not None and saline_ml is not None:
if replace_equal:
total = saline_ml
mix_note = (
f"Equal-volume replacement: removed {drug_ml} mL saline, "
f"added {drug_ml} mL drug → total stays {saline_ml} mL"
)
else:
total = drug_ml + saline_ml
mix_note = f"Additive: {drug_ml} mL drug + {saline_ml} mL saline = {total} mL"
else:
raise ValueError("Provide --volume_ml, or both --drug_ml and --saline_ml.")
rate_ml_min = total / time_min
drops_per_min = rate_ml_min * drop_factor
return {
"mix_note": mix_note,
"total_volume_mL": total,
"time_min": time_min,
"drop_factor": drop_factor,
"rate_mL_per_min": round(rate_ml_min, 4),
"drops_per_min": round(drops_per_min, 2),
"drops_per_min_rounded": round(drops_per_min),
"rate_mL_per_h": round(rate_ml_min * 60, 2),
"verification": f"{rate_ml_min:.4f} mL/min × {time_min} min = {rate_ml_min * time_min:.2f} mL (should equal {total})",
}
def calc_bsa_dose(
dose_per_m2: float,
bsa: float,
days: int = 1,
) -> dict:
"""
BSA-based dose (e.g. chemotherapy, targeted therapy).
dose_per_m2: dose in any unit per m² per administration
bsa: patient body surface area in m²
days: number of days (course total; each day gets one administration at the per-day dose)
"""
single_dose = dose_per_m2 * bsa
total_course = single_dose * days
return {
"bsa_m2": bsa,
"dose_per_m2": dose_per_m2,
"single_dose": round(single_dose, 4),
"days": days,
"total_course_dose": round(total_course, 4),
"verification": f"{dose_per_m2} × {bsa} m² = {single_dose:.4f} per day × {days} day(s) = {total_course:.4f} total",
}
def calc_maintenance(weight_kg: float) -> dict:
"""
Maintenance fluid requirements by two methods:
Holliday-Segar (daily method):
<= 10 kg : 100 mL/kg/day
10-20 kg : 1000 + 50 mL/kg/day above 10 kg
> 20 kg : 1500 + 20 mL/kg/day above 20 kg
4-2-1 rule (hourly method):
First 10 kg : 4 mL/kg/h
Next 10 kg : 2 mL/kg/h
Each kg > 20 : 1 mL/kg/h
"""
# Holliday-Segar (daily)
if weight_kg <= 10:
hs_daily = 100.0 * weight_kg
hs_rule = f"100 x {weight_kg} kg"
elif weight_kg <= 20:
hs_daily = 1000.0 + 50.0 * (weight_kg - 10.0)
hs_rule = f"1000 + 50 x {weight_kg - 10:.1f} kg (above 10 kg)"
else:
hs_daily = 1500.0 + 20.0 * (weight_kg - 20.0)
hs_rule = f"1500 + 20 x {weight_kg - 20:.1f} kg (above 20 kg)"
hs_hourly = hs_daily / 24.0
# 4-2-1 rule (hourly)
if weight_kg <= 10:
rule421_hourly = 4.0 * weight_kg
rule421_desc = f"4 x {weight_kg} kg"
elif weight_kg <= 20:
rule421_hourly = 40.0 + 2.0 * (weight_kg - 10.0)
rule421_desc = f"4x10 + 2x{weight_kg - 10:.1f} kg"
else:
rule421_hourly = 60.0 + 1.0 * (weight_kg - 20.0)
rule421_desc = f"4x10 + 2x10 + 1x{weight_kg - 20:.1f} kg"
rule421_daily = rule421_hourly * 24.0
return {
"weight_kg": weight_kg,
"holliday_segar": {
"formula": "Holliday-Segar",
"rule_applied": hs_rule,
"daily_mL": round(hs_daily, 1),
"hourly_mL_per_h": round(hs_hourly, 2),
},
"four_two_one": {
"formula": "4-2-1 rule",
"rule_applied": rule421_desc,
"hourly_mL_per_h": round(rule421_hourly, 2),
"daily_mL": round(rule421_daily, 1),
},
"verification": (
f"Holliday-Segar: {hs_rule} = {hs_daily:.1f} mL/day = {hs_hourly:.2f} mL/h | "
f"4-2-1: {rule421_desc} = {rule421_hourly:.2f} mL/h = {rule421_daily:.1f} mL/day"
),
}
def calc_dilution(
c1: float | None = None,
v1: float | None = None,
c2: float | None = None,
v2: float | None = None,
) -> dict:
"""
C1·V1 = C2·V2 dilution equation.
Provide exactly three values; the fourth is calculated.
Units are arbitrary but must be consistent (same concentration unit, same volume unit).
"""
knowns = {"c1": c1, "v1": v1, "c2": c2, "v2": v2}
unknowns = [k for k, v in knowns.items() if v is None]
if len(unknowns) != 1:
raise ValueError(f"Provide exactly 3 of c1, v1, c2, v2 (got {4 - len(unknowns)} known values).")
target = unknowns[0]
if target == "c1":
val = (c2 * v2) / v1 # type: ignore[operator]
elif target == "v1":
val = (c2 * v2) / c1 # type: ignore[operator]
elif target == "c2":
val = (c1 * v1) / v2 # type: ignore[operator]
else: # v2
val = (c1 * v1) / c2 # type: ignore[operator]
result = dict(c1=c1, v1=v1, c2=c2, v2=v2)
result[target] = round(val, 6)
verification_lhs = result["c1"] * result["v1"] # type: ignore[operator]
verification_rhs = result["c2"] * result["v2"] # type: ignore[operator]
return {
**result,
"solved_for": target,
"solved_value": round(val, 6),
"verification": f"C1·V1 = {verification_lhs:.4f} | C2·V2 = {verification_rhs:.4f} (should be equal)",
}
# ---------------------------------------------------------------------------
# Output helpers
# ---------------------------------------------------------------------------
def _print_drip_rate(r: dict) -> None:
print("=" * 60)
print(" IV Drip Rate")
print("=" * 60)
print(f" {r['mix_note']}")
print(f" Infusion time : {r['time_min']} min")
print(f" Drop factor : {r['drop_factor']} drops/mL")
print()
print(f" ┌─────────────────────────────────────────┐")
print(f" │ Rate : {r['rate_mL_per_min']:.4f} mL/min │")
print(f" │ Rate : {r['rate_mL_per_h']:.2f} mL/h │")
print(f" │ Drip rate : {r['drops_per_min']:.1f} drops/min │")
print(f" │ Drip rate : {r['drops_per_min_rounded']} drops/min (rounded) │")
print(f" └─────────────────────────────────────────┘")
print()
print(f" Verification: {r['verification']}")
print("=" * 60)
def _print_bsa_dose(r: dict) -> None:
print("=" * 60)
print(" BSA-Based Dose")
print("=" * 60)
print(f" BSA : {r['bsa_m2']} m²")
print(f" Dose per m² : {r['dose_per_m2']} (units as given)")
print()
print(f" ┌─────────────────────────────────────────┐")
print(f" │ Single dose : {r['single_dose']:.4f} │")
if r["days"] > 1:
print(f" │ Course days : {r['days']} │")
print(f" │ Total course : {r['total_course_dose']:.4f} │")
print(f" └─────────────────────────────────────────┘")
print()
print(f" Verification: {r['verification']}")
print("=" * 60)
def _print_maintenance(r: dict) -> None:
hs = r["holliday_segar"]
f21 = r["four_two_one"]
print("=" * 60)
print(" Maintenance Fluids")
print("=" * 60)
print(f" Weight : {r['weight_kg']} kg")
print()
print(" Holliday-Segar (daily method):")
print(f" Rule : {hs['rule_applied']}")
print(f" Daily : {hs['daily_mL']:.1f} mL/day")
print(f" Hourly : {hs['hourly_mL_per_h']:.2f} mL/h")
print()
print(" 4-2-1 Rule (hourly method):")
print(f" Rule : {f21['rule_applied']}")
print(f" Hourly : {f21['hourly_mL_per_h']:.2f} mL/h")
print(f" Daily equiv. : {f21['daily_mL']:.1f} mL/day")
print()
print(f" Verification: {r['verification']}")
print("=" * 60)
def _print_dilution(r: dict) -> None:
print("=" * 60)
print(" Dilution — C1·V1 = C2·V2")
print("=" * 60)
print(f" C1 = {r['c1']} V1 = {r['v1']}")
print(f" C2 = {r['c2']} V2 = {r['v2']}")
print()
print(f" ┌─────────────────────────────────────────┐")
print(f" │ {r['solved_for'].upper()} = {r['solved_value']:<35}│")
print(f" └─────────────────────────────────────────┘")
print()
print(f" Verification: {r['verification']}")
print("=" * 60)
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def main() -> None:
parser = argparse.ArgumentParser(
description="Clinical fluid and dosing calculator.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
parser.add_argument(
"--type",
required=True,
choices=["drip_rate", "bsa_dose", "maintenance", "dilution"],
help="Calculation type.",
)
# drip_rate parameters
parser.add_argument("--volume_ml", type=float, help="[drip_rate] Total infusion volume (mL).")
parser.add_argument("--drug_ml", type=float, help="[drip_rate] Drug solution volume added to bag (mL).")
parser.add_argument("--saline_ml", type=float, help="[drip_rate] Saline bag volume (mL).")
parser.add_argument("--time_min", type=float, help="[drip_rate] Infusion time (minutes).")
parser.add_argument("--drop_factor", type=float, help="[drip_rate] Drop factor (drops/mL).")
parser.add_argument(
"--replace_equal",
action="store_true",
help="[drip_rate] Equal-volume replacement: remove drug_ml of saline before adding drug.",
)
# bsa_dose parameters
parser.add_argument("--dose_per_m2", type=float, help="[bsa_dose] Dose per m² per administration.")
parser.add_argument("--bsa", type=float, help="[bsa_dose] Patient BSA (m²).")
parser.add_argument("--days", type=int, default=1, help="[bsa_dose] Number of administration days (default 1).")
# maintenance parameters
parser.add_argument("--weight_kg", type=float, help="[maintenance] Patient weight (kg).")
# dilution parameters
parser.add_argument("--c1", type=float, default=None, help="[dilution] Initial concentration.")
parser.add_argument("--v1", type=float, default=None, help="[dilution] Initial volume.")
parser.add_argument("--c2", type=float, default=None, help="[dilution] Final concentration.")
parser.add_argument("--v2", type=float, default=None, help="[dilution] Final volume.")
args = parser.parse_args()
try:
if args.type == "drip_rate":
if args.time_min is None or args.drop_factor is None:
parser.error("drip_rate requires --time_min and --drop_factor.")
result = calc_drip_rate(
args.volume_ml, args.drug_ml, args.saline_ml,
args.time_min, args.drop_factor, args.replace_equal,
)
_print_drip_rate(result)
elif args.type == "bsa_dose":
if args.dose_per_m2 is None or args.bsa is None:
parser.error("bsa_dose requires --dose_per_m2 and --bsa.")
if args.bsa <= 0:
parser.error("--bsa must be positive.")
result = calc_bsa_dose(args.dose_per_m2, args.bsa, args.days)
_print_bsa_dose(result)
elif args.type == "maintenance":
if args.weight_kg is None:
parser.error("maintenance requires --weight_kg.")
if args.weight_kg <= 0:
parser.error("--weight_kg must be positive.")
result = calc_maintenance(args.weight_kg)
_print_maintenance(result)
elif args.type == "dilution":
result = calc_dilution(args.c1, args.v1, args.c2, args.v2)
_print_dilution(result)
except ValueError as exc:
print(f"Error: {exc}")
sys.exit(1)
if __name__ == "__main__":
main()
"""
Herd immunity threshold calculator.
Computes the minimum vaccination coverage (Vc) required to achieve herd immunity,
accounting for vaccine efficacy (VE).
Formula:
Vc = (1 - 1/R0) / VE
Where:
R0 = basic reproduction number (average infections caused by one case in fully susceptible pop)
VE = vaccine efficacy (fraction of vaccinated individuals who are protected, 0–1)
Vc = minimum fraction of the population that must be vaccinated
Usage:
python herd_immunity.py --R0 4.2 --VE 0.94
python herd_immunity.py --R0 14 --VE 0.97 # measles
python herd_immunity.py --R0 2.5 --VE 0.85 # seasonal flu
"""
import argparse
import sys
def herd_immunity_threshold(R0: float) -> float:
"""Herd immunity threshold with perfect (100%) vaccine: Hc = 1 - 1/R0."""
if R0 <= 1:
raise ValueError(f"R0 must be > 1 for an epidemic to occur (got {R0}).")
return 1.0 - 1.0 / R0
def vaccination_coverage_needed(R0: float, VE: float) -> float:
"""
Minimum vaccination coverage Vc = (1 - 1/R0) / VE.
This is the fraction of the total population that must be vaccinated so that
the effective reproduction number Re drops to 1 (herd immunity).
"""
if not (0 < VE <= 1):
raise ValueError(f"VE must be between 0 (exclusive) and 1 (got {VE}).")
Hc = herd_immunity_threshold(R0)
return Hc / VE
def effective_R(R0: float, VE: float, coverage: float) -> float:
"""
Effective reproduction number given vaccination coverage and efficacy.
Re = R0 * (1 - VE * coverage)
"""
return R0 * (1.0 - VE * coverage)
def main():
parser = argparse.ArgumentParser(
description="Calculate herd immunity vaccination coverage requirement.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
parser.add_argument(
"--R0",
type=float,
required=True,
help="Basic reproduction number (must be > 1).",
)
parser.add_argument(
"--VE",
type=float,
required=True,
help="Vaccine efficacy as a fraction, e.g. 0.94 for 94%%.",
)
parser.add_argument(
"--check_coverage",
type=float,
default=None,
metavar="FRAC",
help="Optional: evaluate Re at this vaccination coverage fraction.",
)
args = parser.parse_args()
R0 = args.R0
VE = args.VE
# Validate inputs
if R0 <= 1:
print(f"Error: R0 must be > 1 (got {R0}). With R0 ≤ 1 the pathogen cannot sustain an epidemic.")
sys.exit(1)
if not (0 < VE <= 1):
print(f"Error: VE must be between 0 (exclusive) and 1 (got {VE}).")
sys.exit(1)
Hc = herd_immunity_threshold(R0)
Vc = vaccination_coverage_needed(R0, VE)
print("=" * 55)
print(" Herd Immunity Threshold Calculator")
print("=" * 55)
print(f" Input R0 : {R0}")
print(f" Input VE : {VE:.1%}")
print("-" * 55)
print(f" Herd immunity threshold (perfect vaccine)")
print(f" Hc = 1 - 1/R0 = {Hc:.4f} ({Hc:.1%})")
print()
print(f" Required vaccination coverage (VE-adjusted)")
print(f" Vc = Hc / VE = {Vc:.4f} ({Vc:.1%})")
if Vc > 1.0:
print()
print(f" WARNING: Vc > 100% — herd immunity is mathematically")
print(f" unachievable at this VE level. A more efficacious vaccine")
print(f" or additional non-pharmaceutical interventions are required.")
else:
print()
# Show Re at exact Vc coverage
Re_at_Vc = effective_R(R0, VE, Vc)
print(f" Verification: Re at Vc coverage = {Re_at_Vc:.4f} (should be ≈ 1.00)")
# Optional: evaluate Re at a user-supplied coverage
if args.check_coverage is not None:
cov = args.check_coverage
if not (0 <= cov <= 1):
print(f"\n Error: --check_coverage must be 0–1 (got {cov}).")
else:
Re_check = effective_R(R0, VE, cov)
status = "epidemic suppressed" if Re_check < 1 else "epidemic can grow"
print()
print(f" At {cov:.1%} coverage → Re = {Re_check:.4f} ({status})")
print("=" * 55)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Calculate IV drip rate from infusion parameters.
Usage: python iv_drip_rate.py --volume_ml 50 --time_min 60 --drop_factor 60
python iv_drip_rate.py --drug_ml 4.7 --saline_ml 50 --time_min 60 --drop_factor 60
"""
import argparse
def main():
parser = argparse.ArgumentParser(description="IV drip rate calculator")
parser.add_argument('--volume_ml', type=float, help='Total volume to infuse (mL)')
parser.add_argument('--drug_ml', type=float, help='Volume of drug solution added (mL)')
parser.add_argument('--saline_ml', type=float, help='Volume of saline bag (mL)')
parser.add_argument('--time_min', type=float, required=True, help='Infusion time (minutes)')
parser.add_argument('--drop_factor', type=float, required=True, help='Drop factor (drops/mL)')
parser.add_argument('--replace_equal', action='store_true',
help='In clinical practice, remove equal volume of saline before adding drug')
args = parser.parse_args()
if args.volume_ml:
total = args.volume_ml
elif args.drug_ml and args.saline_ml:
if args.replace_equal:
# Clinical practice: remove drug_ml of saline, then add drug_ml of drug
# Total volume stays at saline_ml
total = args.saline_ml
print(f"Equal-volume replacement: removed {args.drug_ml} mL saline, added {args.drug_ml} mL drug")
print(f"Total volume = {args.saline_ml} mL (unchanged)")
else:
total = args.drug_ml + args.saline_ml
print(f"Additive: {args.drug_ml} mL drug + {args.saline_ml} mL saline = {total} mL total")
else:
print("Provide either --volume_ml or both --drug_ml and --saline_ml")
return
rate_ml_min = total / args.time_min
drops_per_min = rate_ml_min * args.drop_factor
print(f"\nRate: {rate_ml_min:.4f} mL/min")
print(f"Drip rate: {drops_per_min:.1f} drops/min")
print(f"Rounded: {round(drops_per_min)} drops/min")
if __name__ == '__main__':
main()
"""
mc_analyzer.py — Systematic multiple-choice question analyzer.
Usage (analysis mode):
python mc_analyzer.py \\
--question "Which of the following best describes..." \\
--choices "A:option1,B:option2,C:option3,D:option4" \\
--reasoning "My analysis..."
Usage (verify mode):
python mc_analyzer.py --verify \\
--answer "B" \\
--question "Which of the following..." \\
--choices "A:option1,B:option2,C:option3,D:option4"
The analyzer forces systematic evaluation of every choice before committing to an answer.
"""
import argparse
import re
import sys
import textwrap
# ---------------------------------------------------------------------------
# Parsing helpers
# ---------------------------------------------------------------------------
def parse_choices(choices_str: str) -> dict[str, str]:
"""
Parse a comma-separated "LETTER:text" string into an ordered dict.
Accepts formats:
A:first option,B:second option,C:third option
A: first option, B: second option
Colons inside option text are preserved (split only on the first colon
after the single-character letter key).
"""
result: dict[str, str] = {}
# Split on commas that are immediately followed by a letter and a colon.
parts = re.split(r",\s*(?=[A-Za-z]:)", choices_str)
for part in parts:
part = part.strip()
if not part:
continue
colon_idx = part.index(":")
letter = part[:colon_idx].strip().upper()
text = part[colon_idx + 1:].strip()
result[letter] = text
return result
def wrap(text: str, width: int = 78, indent: str = " ") -> str:
return textwrap.fill(text, width=width, initial_indent=indent,
subsequent_indent=indent)
# ---------------------------------------------------------------------------
# Display helpers
# ---------------------------------------------------------------------------
def print_header(title: str) -> None:
bar = "=" * 72
print(f"\n{bar}")
print(f" {title}")
print(bar)
def print_section(title: str) -> None:
print(f"\n--- {title} ---")
# ---------------------------------------------------------------------------
# Core analysis
# ---------------------------------------------------------------------------
def analyse(question: str, choices: dict[str, str], reasoning: str) -> None:
"""
Walk through each choice systematically:
1. Display all choices.
2. For each choice print guiding questions.
3. Apply elimination based on the reasoning text.
4. Report surviving candidates or a single answer.
"""
print_header("MULTIPLE-CHOICE SYSTEMATIC ANALYZER")
# ---- Question display ------------------------------------------------
print_section("QUESTION")
print(wrap(question, indent=" "))
# ---- Choices display -------------------------------------------------
print_section("ALL CHOICES")
for letter, text in choices.items():
print(f" [{letter}] {text}")
# ---- Per-choice diagnostic questions ---------------------------------
print_section("PER-CHOICE EVALUATION FRAMEWORK")
print(" For each choice, consider BOTH angles before deciding.\n")
for letter, text in choices.items():
print(f" [{letter}] {text}")
print(f" WHY CORRECT? → Does this align with the core concept?")
print(f" Is it the most specific/complete answer?")
print(f" WHY WRONG? → Does the reasoning contradict this choice?")
print(f" Is it partially true but missing a key element?")
print()
# ---- Reasoning display -----------------------------------------------
print_section("PROVIDED REASONING")
print(wrap(reasoning, indent=" "))
# ---- Elimination pass ------------------------------------------------
print_section("ELIMINATION PASS")
print(" Scanning reasoning for explicit mentions of each choice ...\n")
reasoning_lower = reasoning.lower()
eliminated: list[str] = []
surviving: list[str] = []
# Elimination detection strategy
# -----------------------------------------------------------------------
# Single-letter matching in free text is noisy ("a", "b", "c" appear as
# articles and mid-word substrings). We use two complementary patterns:
#
# Pattern 1 — anchored reference: the choice letter appears as a
# standalone token at the start of a sentence / after punctuation /
# inside brackets, AND a negative keyword appears within ±30 chars.
# Anchor chars: start-of-string, [.!?\n] + optional space, or [(].
#
# Pattern 2 — explicit elimination phrase: unambiguous multi-word phrases
# that contain the letter (e.g. "eliminate a", "rule out b").
#
# NEG_WORDS are only applied when a valid anchor is found, keeping the
# window tight (±30 chars) to avoid cross-sentence false positives.
NEG_WORDS = [
"not the answer", "is not", "does not", "cannot", "would not",
"incorrect", "wrong", "eliminate", "rule out", "discard", "exclude",
"not correct", "not right", "is wrong", "is incorrect",
]
# Explicit multi-word phrases that already include the letter unambiguously.
EXPLICIT_TEMPLATES = [
r"eliminate\s+{l}\b",
r"rule\s+out\s+{l}\b",
r"discard\s+{l}\b",
r"exclude\s+{l}\b",
r"\[{L}\]\s+is\s+(not|incorrect|wrong)",
r"\({L}\)\s+is\s+(not|incorrect|wrong)",
r"{L}\s+is\s+incorrect",
r"{L}\s+is\s+wrong",
r"{L}\s+is\s+not\s+correct",
r"{L}\s+is\s+not\s+the\s+answer",
r"not\s+{L}\b",
r"not\s+\[?{L}\]?\)",
]
# Anchor pattern: letter appears right after sentence boundary or bracket.
ANCHOR_RE = r"(?:(?:^|[.!?\n]\s*)(?:\(?\[?)|\[|\()\s*{l}(?:\s|[)\]:.,])"
for letter in choices:
ll = letter.lower()
L = letter.upper()
is_eliminated = False
match_reason = ""
# --- Pattern 1: anchored letter + negative keyword in tight window ---
anchor_positions = [
m.start() for m in re.finditer(
ANCHOR_RE.replace("{l}", re.escape(ll)),
reasoning_lower, re.MULTILINE,
)
]
for pos in anchor_positions:
window_start = max(0, pos - 10)
window_end = min(len(reasoning_lower), pos + 80)
window = reasoning_lower[window_start:window_end]
for neg in NEG_WORDS:
if neg in window:
is_eliminated = True
match_reason = f"'{neg}' near anchored [{L}]"
break
if is_eliminated:
break
# --- Pattern 2: explicit elimination phrases ---
if not is_eliminated:
for tmpl in EXPLICIT_TEMPLATES:
pattern = tmpl.replace("{l}", re.escape(ll)).replace("{L}", re.escape(L))
if re.search(pattern, reasoning_lower, re.IGNORECASE):
is_eliminated = True
match_reason = f"explicit elimination phrase for [{L}]"
break
if is_eliminated:
eliminated.append(letter)
print(f" [{letter}] ELIMINATED — {match_reason}")
else:
surviving.append(letter)
print(f" [{letter}] SURVIVING — no clear elimination signal found")
# ---- Verdict ---------------------------------------------------------
print_section("VERDICT")
if len(surviving) == 0:
print(" WARNING: All choices appear eliminated — check reasoning for errors.")
print(" Surviving candidates: (none — logic contradiction detected)")
elif len(surviving) == 1:
answer = surviving[0]
print(f" ANSWER: [{answer}] — only one choice survives elimination.")
print(f" Text: {choices[answer]}")
print()
print(" CONFIDENCE CHECK:")
print(f" Re-read the question with [{answer}] in mind.")
print(f" Does '{choices[answer]}' directly answer what was asked?")
print(f" If yes → commit to [{answer}].")
else:
print(f" NEEDS MORE REASONING — {len(surviving)} choices survive:")
for letter in surviving:
print(f" [{letter}] {choices[letter]}")
print()
print(" NEXT STEPS:")
print(" 1. Compare surviving choices pairwise — what distinguishes them?")
print(" 2. Re-read the question stem for a specific qualifier")
print(" (e.g., 'MOST likely', 'FIRST step', 'BEST describes').")
print(" 3. Apply the most restrictive interpretation of the question.")
print(" 4. If still tied, reason from first principles about which is")
print(" MORE complete, MORE specific, or the DIRECT mechanism.")
# ---- Pitfall reminders -----------------------------------------------
print_section("COMMON MC PITFALLS")
print(" - 'All of the above' traps: true individually but not as a set.")
print(" - Distractors that are correct in a DIFFERENT context.")
print(" - Double negatives: 'which is NOT an example of NOT X?' = X.")
print(" - Qualifiers matter: 'always', 'never', 'most', 'best', 'first'.")
print(" - The longest or most detailed answer is not automatically right.")
print()
# ---------------------------------------------------------------------------
# Verify mode
# ---------------------------------------------------------------------------
def verify(answer: str, question: str, choices: dict[str, str]) -> None:
"""
Re-read the question and selected answer letter; check for consistency.
"""
print_header("CONFIDENCE CHECKER — VERIFY MODE")
answer = answer.strip().upper()
print_section("QUESTION")
print(wrap(question, indent=" "))
print_section("SELECTED ANSWER")
if answer not in choices:
print(f" ERROR: [{answer}] is not among the given choices: "
f"{', '.join(choices.keys())}")
sys.exit(1)
print(f" [{answer}] {choices[answer]}")
print_section("CONSISTENCY CHECK")
print(" Verify each of the following manually:\n")
checks = [
f"Does [{answer}] directly answer what the question asks?",
f"Is '{choices[answer]}' the most COMPLETE answer (not just partially true)?",
f"Would a different choice be eliminated if [{answer}] is correct?",
f"Does [{answer}] address the SPECIFIC qualifier in the question "
f"(most, first, best, always, never)?",
f"If you substituted [{answer}] back into the question, does the "
f"sentence make logical sense?",
]
for i, check in enumerate(checks, start=1):
print(f" [{i}] {wrap(check, indent=' ').strip()}")
print()
print_section("LETTER vs. TEXT ALIGNMENT")
print(f" You selected letter [{answer}].")
print(f" The text for [{answer}] is: \"{choices[answer]}\"")
print()
print(" Confirm: Is this the text your reasoning concluded was correct?")
print(" If your reasoning said a DIFFERENT letter, you have a mismatch.")
print(" Common cause: reasoning names B but you wrote C — re-check.")
print()
print_section("ALL CHOICES FOR REFERENCE")
for letter, text in choices.items():
marker = " ← YOUR CHOICE" if letter == answer else ""
print(f" [{letter}] {text}{marker}")
print()
# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Systematic multiple-choice analyzer. Forces evaluation of "
"every option before committing to an answer.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=textwrap.dedent("""\
Examples:
# Analysis mode
python mc_analyzer.py \\
--question "Which drug class inhibits ACE?" \\
--choices "A:Beta-blockers,B:ACE inhibitors,C:ARBs,D:Statins" \\
--reasoning "Beta-blockers act on beta receptors, not ACE ..."
# Verify mode
python mc_analyzer.py --verify \\
--answer B \\
--question "Which drug class inhibits ACE?" \\
--choices "A:Beta-blockers,B:ACE inhibitors,C:ARBs,D:Statins"
"""),
)
parser.add_argument(
"--verify", action="store_true",
help="Run in confidence-checker mode instead of full analysis.",
)
parser.add_argument(
"--question", required=True,
help="The full MC question text (quote the entire string).",
)
parser.add_argument(
"--choices", required=True,
help='Comma-separated letter:text pairs, e.g. "A:opt1,B:opt2,C:opt3".',
)
parser.add_argument(
"--reasoning",
help="Your analysis or reasoning string (required in analysis mode).",
)
parser.add_argument(
"--answer",
help="The answer letter to verify (required in --verify mode).",
)
return parser
def main() -> None:
parser = build_parser()
args = parser.parse_args()
choices = parse_choices(args.choices)
if not choices:
print("ERROR: Could not parse --choices. "
"Expected format: \"A:text,B:text,C:text\"")
sys.exit(1)
if args.verify:
if not args.answer:
parser.error("--verify mode requires --answer LETTER")
verify(args.answer, args.question, choices)
else:
if not args.reasoning:
parser.error("Analysis mode requires --reasoning TEXT")
analyse(args.question, choices, args.reasoning)
if __name__ == "__main__":
main()