unit_lab_convert
Convert laboratory results between mg/dL, mg/L or g/L and mmol/L or umol/L for glucose, total, HDL and LDL cholesterol, triglycerides (as triolein), creatinine, urea, BUN, uric acid and calcium, with exact factors derived from molar masses computed from the IUPAC/CIAAW atomic weights. Every output carries a disclaimer: not medical advice, verify with a clinician.
Install
/tools install unit_lab_convert /tools install unit_lab_convert@1.0.0
In a console session, or let the model find it in normal mode: axlr_catalog describes it and axlr_install_tool asks for the same install card. The card reads catalog axlr · unit_lab_convert 1.0.0 · verified once the index signature, the manifest digest and every file digest check out. Then the model runs it with axlr_run_tool unit_lab_convert; each run is approved as a local_exec.
Arguments
One JSON object on stdin, validated against the schema before the program starts.
| name | type | required | default | meaning |
|---|---|---|---|---|
analyte | string | no | one of "glucose", "cholesterol_total", "cholesterol_hdl", "cholesterol_ldl", "triglycerides", "creatinine", "urea", "bun", "uric_acid", "calcium" | |
value | number | string | no | Number or decimal string | |
from | string | no | one of "mg/dL", "mg/L", "g/L", "mmol/L", "umol/L", "µmol/L" | |
to | string | no | Default: the analyte's usual unit on the other side. one of "mg/dL", "mg/L", "g/L", "mmol/L", "umol/L", "µmol/L" | |
decimals | integer | no | 2 | ≥ 0; ≤ 12 |
list_analytes | boolean | no | List analytes, formulas, molar masses and factors |
Input schema (JSON)
{
"type": "object",
"additionalProperties": false,
"properties": {
"analyte": {
"type": "string",
"enum": [
"glucose",
"cholesterol_total",
"cholesterol_hdl",
"cholesterol_ldl",
"triglycerides",
"creatinine",
"urea",
"bun",
"uric_acid",
"calcium"
]
},
"value": {
"type": [
"number",
"string"
],
"description": "Number or decimal string"
},
"from": {
"type": "string",
"enum": [
"mg/dL",
"mg/L",
"g/L",
"mmol/L",
"umol/L",
"µmol/L"
]
},
"to": {
"type": "string",
"enum": [
"mg/dL",
"mg/L",
"g/L",
"mmol/L",
"umol/L",
"µmol/L"
],
"description": "Default: the analyte's usual unit on the other side"
},
"decimals": {
"type": "integer",
"minimum": 0,
"maximum": 12,
"default": 2
},
"list_analytes": {
"type": "boolean",
"description": "List analytes, formulas, molar masses and factors"
}
}
}
Example
Input
{
"analyte": "glucose",
"value": 100,
"from": "mg/dL"
}
Output (stdout)
{
"to": "mmol/L",
"result": "5.55",
"factor": "0.05550745",
"molar_mass_g_mol": "180.156"
}
Files
The command the install card registers is python3 unit_lab_convert.py, run in the workspace with PATH and HOME as its only environment. Each file's SHA-256 is in the signed index; the console checks them before showing the card and again before writing.
unit_lab_convert.py · sha256 86b07cd2018f75cc8001214cf6d14a1c06fbfc2325074f5deec508b26aca5dd4
#!/usr/bin/env python3
"""Convert common laboratory results between mass units (mg/dL, mg/L, g/L) and molar units
(mmol/L, umol/L) through the analyte's molar mass, exactly.
Input (JSON on stdin):
analyte one of ANALYTES (or list_analytes: true)
value number or decimal string
from unit of value: mg/dL | mg/L | g/L | mmol/L | umol/L (µmol/L)
to target unit; optional, default the analyte's usual unit on the other side
decimals optional, default 2 (digits after the point in "result")
Molar masses are computed here from the formula with the IUPAC/CIAAW conventional atomic
weights (CIAAW, Standard Atomic Weights, https://www.ciaaw.org/atomic-weights.htm, abridged
values as in the IUPAC Periodic Table of the Elements, 2022): H 1.008, C 12.011, N 14.007,
O 15.999, Ca 40.078.
"""
import json
import re
import sys
from decimal import ROUND_HALF_UP, Decimal, localcontext
from fractions import Fraction
DISCLAIMER = ("Not medical advice: a unit conversion only. Reference ranges and units differ between "
"laboratories; verify with a qualified clinician before any clinical use.")
ATOMIC = {"H": Fraction("1.008"), "C": Fraction("12.011"), "N": Fraction("14.007"), "O": Fraction("15.999"),
"Ca": Fraction("40.078")}
ATOMIC_SOURCE = "IUPAC/CIAAW conventional atomic weights (abridged, IUPAC Periodic Table 2022)"
# name: (formula used for the molar mass, mass unit usual, molar unit usual, what is counted, note)
ANALYTES = {
"glucose": ("C6H12O6", "mg/dL", "mmol/L", "glucose", None),
"cholesterol_total": ("C27H46O", "mg/dL", "mmol/L", "cholesterol", None),
"cholesterol_hdl": ("C27H46O", "mg/dL", "mmol/L", "cholesterol in HDL", None),
"cholesterol_ldl": ("C27H46O", "mg/dL", "mmol/L", "cholesterol in LDL", None),
"triglycerides": ("C57H104O6", "mg/dL", "mmol/L", "triglyceride as triolein",
"Triglycerides are a mixture; laboratories convert as triolein (885.45 g/mol), the convention "
"behind the usual factor 0.01129."),
"creatinine": ("C4H7N3O", "mg/dL", "umol/L", "creatinine", None),
"urea": ("CH4N2O", "mg/dL", "mmol/L", "urea", None),
"bun": ("N2", "mg/dL", "mmol/L", "urea (from blood urea nitrogen)",
"BUN is the mass of nitrogen in urea: one mole of urea carries two N atoms (28.014 g), so BUN in mg/dL "
"converts to mmol/L of urea. Urea in mg/dL is a different quantity (analyte 'urea', 60.06 g/mol)."),
"uric_acid": ("C5H4N4O3", "mg/dL", "umol/L", "uric acid", None),
"calcium": ("Ca", "mg/dL", "mmol/L", "calcium (total)", None),
}
# unit: (kind, factor to g/L for mass, or to mol/L for molar)
UNITS = {"mg/dL": ("mass", Fraction(1, 100)), "mg/L": ("mass", Fraction(1, 1000)), "g/L": ("mass", Fraction(1)),
"mmol/L": ("molar", Fraction(1, 1000)), "umol/L": ("molar", Fraction(1, 1000000))}
def fail(message):
print(json.dumps({"error": message, "disclaimer": DISCLAIMER}))
sys.exit(2)
def molar_mass(formula):
total = Fraction(0)
for symbol, count in re.findall(r"([A-Z][a-z]?)(\d*)", formula):
total += ATOMIC[symbol] * (int(count) if count else 1)
return total
def decimal_text(value, places):
with localcontext() as ctx:
ctx.prec = 60
exact = Decimal(value.numerator) / Decimal(value.denominator)
return str(exact.quantize(Decimal(1).scaleb(-places), rounding=ROUND_HALF_UP))
def unit_name(text, field):
if not isinstance(text, str):
fail(f"{field} must be one of {', '.join(UNITS)}")
text = text.strip().replace("µ", "u").replace("μ", "u")
if text not in UNITS:
fail(f"{field} must be one of {', '.join(UNITS)}")
return text
def factor(analyte, source, target):
"""result = value * factor, exactly."""
mass = molar_mass(ANALYTES[analyte][0])
kind_s, to_base_s = UNITS[source]
kind_t, to_base_t = UNITS[target]
base = to_base_s
if kind_s == "mass" and kind_t == "molar":
base = base / mass
elif kind_s == "molar" and kind_t == "mass":
base = base * mass
return base / to_base_t, mass
def main():
try:
request = json.load(sys.stdin)
except ValueError as error:
fail(f"stdin is not JSON: {error}")
if not isinstance(request, dict):
fail("input must be a JSON object")
extra = sorted(set(request) - {"analyte", "value", "from", "to", "decimals", "list_analytes"})
if extra:
fail(f"unknown field(s): {', '.join(extra)}")
if request.get("list_analytes"):
rows = []
for name, (formula, mass_unit, molar_unit, counted, note) in ANALYTES.items():
f, mass = factor(name, mass_unit, molar_unit)
rows.append({"analyte": name, "formula": formula, "molar_mass_g_mol": decimal_text(mass, 3),
"counted_as": counted, "usual": f"{mass_unit} -> {molar_unit}",
"factor": decimal_text(f, 6), "note": note})
print(json.dumps({"analytes": rows, "atomic_weights": {k: str(v) for k, v in ATOMIC.items()},
"source": ATOMIC_SOURCE, "disclaimer": DISCLAIMER}, ensure_ascii=False))
return
analyte = request.get("analyte")
if not isinstance(analyte, str) or analyte not in ANALYTES:
fail(f"analyte must be one of {', '.join(ANALYTES)}")
value = request.get("value")
if isinstance(value, bool) or not isinstance(value, (int, float, str)):
fail("value must be a number or a decimal string")
if isinstance(value, str) and not re.fullmatch(r"\s*[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d{1,3})?\s*", value):
fail("value must be a number or a decimal string")
try:
amount = Fraction(str(value).strip())
except (ValueError, ZeroDivisionError):
fail("value must be a number or a decimal string")
if amount > 10 ** 12:
fail("value is too large")
if amount < 0:
fail("value must not be negative")
source = unit_name(request.get("from"), "from")
formula, mass_unit, molar_unit, counted, note = ANALYTES[analyte]
if "to" in request:
target = unit_name(request.get("to"), "to")
else:
target = molar_unit if UNITS[source][0] == "mass" else mass_unit
decimals = request.get("decimals", 2)
if isinstance(decimals, bool) or not isinstance(decimals, int) or not 0 <= decimals <= 12:
fail("decimals must be an integer from 0 to 12")
f, mass = factor(analyte, source, target)
result = amount * f
out = {
"analyte": analyte, "counted_as": counted, "value": str(value), "from": source, "to": target,
"result": decimal_text(result, decimals), "result_exact": str(result),
"factor": decimal_text(f, 8), "formula": formula, "molar_mass_g_mol": decimal_text(mass, 3),
"notes": [note] if note else [], "source": ATOMIC_SOURCE, "disclaimer": DISCLAIMER,
}
print(json.dumps(out, ensure_ascii=False))
if __name__ == "__main__":
try:
main()
except SystemExit:
raise
except Exception as error: # last resort: a usage error, never a traceback
fail(f"invalid input ({type(error).__name__})")
Where it is served
The registry serves only AXLR clients, so a browser gets 403; fetch with a User-Agent that starts with axlr:
curl -A axlr-docs/1.0 -O https://registry.underpassai.com/tools/unit_lab_convert/1.0.0/manifest.json
curl -A axlr-docs/1.0 -O https://registry.underpassai.com/tools/unit_lab_convert/1.0.0/unit_lab_convert.py