diff --git a/CHANGELOG.md b/CHANGELOG.md index 59ce4d9..bc70187 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Domain-Specific Bases documentation (`docs/guides/domain-bases/`) + - Explains how to resolve SI dimensional degeneracies with extended bases + - Radiation Dosimetry: Gy vs Sv vs Gy(RBE) vs effective dose + - Pharmacology: drug potency, IU variations, morphine equivalents + - Clinical Chemistry: molarity vs osmolarity, Bq vs Hz, mEq vs mmol + - Classical Mechanics: torque vs energy, surface tension vs spring constant + - Thermodynamics: heat capacity vs entropy + ## [0.10.0] - 2026-03-01 ### Added diff --git a/docs/guides/domain-bases/classical-mechanics.md b/docs/guides/domain-bases/classical-mechanics.md new file mode 100644 index 0000000..1faff97 --- /dev/null +++ b/docs/guides/domain-bases/classical-mechanics.md @@ -0,0 +1,264 @@ +# Classical Mechanics Basis + +## The Degeneracy Problem + +Two classic pairs share SI dimensions: + +| Pair | Shared Dimension | Hidden Qualifier | +|------|------------------|------------------| +| Torque vs Energy | ML²T⁻² | Angle (per radian) | +| Surface tension vs Spring constant | MT⁻² | Geometric character | + +--- + +## Torque vs Energy + +### The Degeneracy + +| Quantity | SI Unit | SI Dimension | +|----------|---------|--------------| +| Energy | Joule (J) | ML²T⁻² | +| Torque | Newton-meter (N·m) | ML²T⁻² | + +**Same dimension.** Yet: +- Energy is a scalar (state function) +- Torque is a pseudovector (force × lever arm × sin θ) +- Torque is energy *per unit angle*: τ = dW/dθ + +### Extended Basis + +```python +from ucon.basis import Basis, BasisComponent, Vector +from ucon.core import Dimension, Unit +from fractions import Fraction + +MECHANICS = Basis("Mechanics", [ + BasisComponent("mass", "M"), # 0 + BasisComponent("length", "L"), # 1 + BasisComponent("time", "T"), # 2 + BasisComponent("angle", "A"), # 3 — the hidden qualifier +]) +``` + +### Dimensional Vectors + +| Quantity | Vector | Interpretation | +|----------|--------|----------------| +| Energy | M¹L²T⁻²A⁰ | Work done | +| Torque | M¹L²T⁻²A⁻¹ | Work per radian | +| Angle | M⁰L⁰T⁰A¹ | Rotation measure | +| Angular velocity | M⁰L⁰T⁻¹A¹ | Radians per second | +| Angular momentum | M¹L²T⁻¹A¹ | Rotational inertia × ω | + +### Implementation + +```python +# Dimensions +energy = Dimension( + vector=Vector(MECHANICS, (1, 2, -2, 0)), + name="energy" +) # M¹L²T⁻²A⁰ + +torque = Dimension( + vector=Vector(MECHANICS, (1, 2, -2, -1)), + name="torque" +) # M¹L²T⁻²A⁻¹ + +angle = Dimension( + vector=Vector(MECHANICS, (0, 0, 0, 1)), + name="angle" +) # A¹ + +# Units +joule = Unit(name="joule", shorthand="J", dimension=energy) +newton_meter = Unit(name="newton_meter", shorthand="N·m", dimension=torque) +radian = Unit(name="radian", shorthand="rad", dimension=angle) +``` + +### Dimensional Algebra + +**Torque × Angle = Energy:** +``` +Work = τ × θ +M¹L²T⁻²A⁰ = (M¹L²T⁻²A⁻¹) × (A¹) ✓ +``` + +**Energy / Angle = Torque:** +``` +τ = dW/dθ +M¹L²T⁻²A⁻¹ = (M¹L²T⁻²A⁰) / (A¹) ✓ +``` + +### Safety + +```python +joule(100) + newton_meter(50) +# raises: incompatible dimensions (A⁰ vs A⁻¹) + +# Correct: convert torque to energy via angle +work = newton_meter(50) * radian(2) # → 100 J +``` + +--- + +## Surface Tension vs Spring Constant + +### The Degeneracy + +| Quantity | SI Unit | SI Dimension | Physical Meaning | +|----------|---------|--------------|------------------| +| Spring constant (k) | N/m | MT⁻² | Force per displacement (1D) | +| Surface tension (γ) | N/m | MT⁻² | Force per length along interface (2D) | + +Both are N/m = kg/s², but: +- Spring constant: restoring force per unit *displacement* +- Surface tension: force per unit *length of edge* on a surface (or energy per area) + +### Alternative View + +| Quantity | Alternative Form | Shows | +|----------|------------------|-------| +| Spring constant | N/m | Force / length (1D linear) | +| Surface tension | J/m² | Energy / area (2D interfacial) | + +J/m² = (kg·m²/s²)/m² = kg/s² = N/m — same dimension. + +### Extended Basis + +```python +MECHANICS_EXTENDED = Basis("Mechanics-Extended", [ + BasisComponent("mass", "M"), + BasisComponent("length", "L"), + BasisComponent("time", "T"), + BasisComponent("angle", "A"), + BasisComponent("interface", "I"), # interfacial/surface character +]) +``` + +### Dimensional Vectors + +| Quantity | Vector | Interpretation | +|----------|--------|----------------| +| Spring constant | M¹L⁰T⁻²A⁰I⁰ | Linear restoring force | +| Surface tension | M¹L⁰T⁻²A⁰I¹ | Interfacial energy density | +| Interface factor | M⁰L⁰T⁰A⁰I¹ | Surface/interface qualifier | + +### Implementation + +```python +spring_constant_dim = Dimension( + vector=Vector(MECHANICS_EXTENDED, (1, 0, -2, 0, 0)), + name="spring_constant" +) # M¹T⁻²I⁰ + +surface_tension_dim = Dimension( + vector=Vector(MECHANICS_EXTENDED, (1, 0, -2, 0, 1)), + name="surface_tension" +) # M¹T⁻²I¹ + +# Units +newton_per_meter = Unit( + name="newton_per_meter", + shorthand="N/m", + dimension=spring_constant_dim +) + +joule_per_m2 = Unit( + name="joule_per_square_meter", + shorthand="J/m²", + dimension=surface_tension_dim +) +``` + +### Physical Context + +**Spring constant:** +- Hooke's law: F = -kx +- k has units N/m +- Describes 1D elastic deformation + +**Surface tension:** +- Capillary force: F = γL (force along edge of length L) +- γ has units N/m or equivalently J/m² +- Describes 2D interfacial energy +- Examples: water-air (~72 mN/m), mercury-air (~486 mN/m) + +### Why It Matters + +In multiphysics simulations (e.g., microfluidics), both appear: + +```python +# Droplet on a spring (hypothetical) +spring_force = k * displacement # N +surface_force = gamma * perimeter # N + +# Without dimensional distinction: +total = k + gamma # Dimensionally valid in SI, physically nonsense + +# With extended basis: +total = k + gamma # raises: incompatible (I⁰ vs I¹) +``` + +--- + +## Stiffness Variants + +The spring constant degeneracy extends to other stiffness measures: + +| Quantity | SI Dimension | Physical Context | +|----------|--------------|------------------| +| Spring constant | MT⁻² | Translational spring | +| Torsional stiffness | ML²T⁻²A⁻¹ | Rotational spring (torque per radian) | +| Bending stiffness (EI) | ML³T⁻² | Beam flexure | +| Surface tension | MT⁻² | Interface energy | + +With the extended basis, all are distinct: + +```python +# Torsional stiffness: torque per angle +torsional_stiffness = Dimension( + vector=Vector(MECHANICS_EXTENDED, (1, 2, -2, -2, 0)), + name="torsional_stiffness" +) # M¹L²T⁻²A⁻² (N·m per radian) +``` + +--- + +## Projection to SI + +When you need SI compatibility: + +```python +MECHANICS_TO_SI = ConstantAwareBasisTransform( + source=MECHANICS, + target=SI, + matrix=( + (1, 0, 0, ...), # M → M + (0, 1, 0, ...), # L → L + (0, 0, 1, ...), # T → T + (0, 0, 0, ...), # A → dimensionless (dropped) + ), + bindings=( + ConstantBinding( + source_component=MECHANICS["angle"], + target_expression=Vector(SI, (0, 0, 0, ...)), # dimensionless + constant_symbol="rad", + exponent=Fraction(1), + ), + ), +) +``` + +The binding records that angle was collapsed to dimensionless via the radian. + +--- + +## Summary + +| Degeneracy | Hidden Dimension | Resolution | +|------------|------------------|------------| +| Torque vs Energy | Angle (A) | Torque is M¹L²T⁻²A⁻¹ | +| Surface tension vs Spring constant | Interface (I) | Surface tension is M¹T⁻²I¹ | +| Torsional vs Linear stiffness | Angle (A) | Torsional is M¹L²T⁻²A⁻² | + +All three are instances of the same pattern: a geometric qualifier (angle, interface) is treated as dimensionless in SI but carries physical meaning that affects how quantities combine. diff --git a/docs/guides/domain-bases/clinical-chemistry.md b/docs/guides/domain-bases/clinical-chemistry.md new file mode 100644 index 0000000..626c6a9 --- /dev/null +++ b/docs/guides/domain-bases/clinical-chemistry.md @@ -0,0 +1,255 @@ +# Clinical Chemistry Basis + +## The Degeneracy Problem + +Several clinically distinct quantities share SI dimensions: + +| Pair | Shared Dimension | Risk | +|------|------------------|------| +| Molarity vs Osmolarity | mol/L (NL⁻³) | Fluid/electrolyte errors | +| Becquerel vs Hertz | s⁻¹ (T⁻¹) | Radiation vs frequency confusion | +| Specific activity (enzyme) vs Specific activity (radioactive) | mol/(s·kg) | Different meanings of "activity" | + +--- + +## Molarity vs Osmolarity + +Both express concentration, but they count different things: + +| Quantity | Definition | Example | +|----------|------------|---------| +| Molarity | Moles of formula units per liter | 1 M NaCl = 1 mol NaCl/L | +| Osmolarity | Moles of osmotically active particles per liter | 1 M NaCl ≈ 2 Osm (Na⁺ + Cl⁻) | + +**Clinical importance:** Plasma osmolarity (~285-295 mOsm/L) determines fluid shifts. Molarity alone doesn't capture dissociation. + +### Extended Basis + +```python +from ucon.basis import Basis, BasisComponent, Vector +from ucon.core import Dimension, Unit +from fractions import Fraction + +CHEM = Basis("Chemistry", [ + BasisComponent("amount", "N"), # 0 — moles + BasisComponent("volume", "V"), # 1 — liters + BasisComponent("particle_semantics", "Π"), # 2 — dissociation factor +]) + +# Dimensions +molarity = Dimension( + vector=Vector(CHEM, (1, -1, 0)), + name="molarity" +) # N¹V⁻¹Π⁰ + +osmolarity = Dimension( + vector=Vector(CHEM, (1, -1, 1)), + name="osmolarity" +) # N¹V⁻¹Π¹ + +dissociation_factor = Dimension( + vector=Vector(CHEM, (0, 0, 1)), + name="dissociation_factor" +) # Π¹ — i factor (van't Hoff) +``` + +### Van't Hoff Factor (i) + +| Solute | i | Particles | +|--------|---|-----------| +| Glucose | 1 | Does not dissociate | +| NaCl | 2 | Na⁺ + Cl⁻ | +| CaCl₂ | 3 | Ca²⁺ + 2Cl⁻ | +| MgSO₄ | 2 | Mg²⁺ + SO₄²⁻ | + +```python +# Conversion +nacl_molarity = molar(0.154) # 0.154 M NaCl (normal saline) +nacl_osmolarity = nacl_molarity * van_hoff(2) # ≈ 308 mOsm/L + +# Dimensional algebra +# N¹V⁻¹Π¹ = (N¹V⁻¹Π⁰) × (Π¹) ✓ +``` + +--- + +## Becquerel vs Hertz + +Both have dimension T⁻¹, but they measure fundamentally different phenomena: + +| Unit | Measures | Definition | +|------|----------|------------| +| Hertz (Hz) | Oscillation frequency | Cycles per second | +| Becquerel (Bq) | Radioactive decay rate | Disintegrations per second | + +**The problem:** 1 Hz ≠ 1 Bq, even though both are s⁻¹. + +### Extended Basis + +```python +TEMPORAL = Basis("Temporal", [ + BasisComponent("time", "T"), # 0 + BasisComponent("event_type", "E"), # 1 — oscillation vs decay +]) + +# Dimensions +frequency = Dimension( + vector=Vector(TEMPORAL, (-1, 0)), + name="frequency" +) # T⁻¹E⁰ + +activity = Dimension( + vector=Vector(TEMPORAL, (-1, 1)), + name="radioactive_activity" +) # T⁻¹E¹ + +# Units +hertz = Unit(name="hertz", shorthand="Hz", dimension=frequency) +becquerel = Unit(name="becquerel", shorthand="Bq", dimension=activity) + +# Safety +hertz(1000) + becquerel(1000) +# raises: incompatible dimensions +``` + +--- + +## Enzyme Activity vs Radioactive Activity + +Both can be expressed as "activity per mass" but mean different things: + +| Quantity | Unit | Meaning | +|----------|------|---------| +| Enzyme specific activity | kat/kg (mol·s⁻¹·kg⁻¹) | Catalytic turnover per mass | +| Radioactive specific activity | Bq/kg (s⁻¹·kg⁻¹) | Decays per mass | + +### Extended Basis + +```python +ACTIVITY = Basis("Activity", [ + BasisComponent("time", "T"), + BasisComponent("mass", "M"), + BasisComponent("amount", "N"), # moles (for enzyme) + BasisComponent("activity_type", "A"), # catalytic vs decay +]) + +# Enzyme specific activity: katal per kilogram +enzyme_specific = Dimension( + vector=Vector(ACTIVITY, (-1, -1, 1, 0)), + name="enzyme_specific_activity" +) # T⁻¹M⁻¹N¹A⁰ + +# Radioactive specific activity: becquerel per kilogram +radio_specific = Dimension( + vector=Vector(ACTIVITY, (-1, -1, 0, 1)), + name="radioactive_specific_activity" +) # T⁻¹M⁻¹N⁰A¹ +``` + +--- + +## Concentration Units in Clinical Labs + +Clinical laboratories use various concentration expressions: + +| Unit | Dimension | Use Case | +|------|-----------|----------| +| mg/dL | M/L | Glucose (US), lipids | +| mmol/L | N/L | Glucose (international), electrolytes | +| mEq/L | N/L | Electrolytes with charge | +| mOsm/L | N/L with Π¹ | Osmolality calculations | +| IU/L | Activity/L | Enzyme assays | + +**Problem:** mmol/L and mEq/L are dimensionally equivalent but semantically different (mEq accounts for ionic charge). + +### Milliequivalents + +```python +ELECTROLYTE = Basis("Electrolyte", [ + BasisComponent("amount", "N"), + BasisComponent("volume", "V"), + BasisComponent("charge", "Z"), # ionic charge factor +]) + +millimolar = Dimension( + vector=Vector(ELECTROLYTE, (1, -1, 0)), + name="millimolar" +) # N¹V⁻¹Z⁰ + +milliequivalent = Dimension( + vector=Vector(ELECTROLYTE, (1, -1, 1)), + name="milliequivalent" +) # N¹V⁻¹Z¹ +``` + +**Conversion:** +- For Na⁺ (charge +1): 140 mmol/L = 140 mEq/L +- For Ca²⁺ (charge +2): 2.5 mmol/L = 5 mEq/L +- For Mg²⁺ (charge +2): 1 mmol/L = 2 mEq/L + +--- + +## Practical Example: Anion Gap + +The anion gap calculation: + +``` +AG = [Na⁺] - [Cl⁻] - [HCO₃⁻] +``` + +All in mEq/L (charge-adjusted): + +```python +sodium = meq_per_l(140) # mEq/L +chloride = meq_per_l(104) # mEq/L +bicarbonate = meq_per_l(24) # mEq/L + +anion_gap = sodium - chloride - bicarbonate # 12 mEq/L + +# Dimensional safety: can only subtract same dimensions +# N¹V⁻¹Z¹ - N¹V⁻¹Z¹ - N¹V⁻¹Z¹ = N¹V⁻¹Z¹ ✓ +``` + +--- + +## Safety Guarantees + +```python +# These are now caught: + +molarity(0.154) + osmolarity(0.308) +# raises: incompatible (Π⁰ vs Π¹) + +hertz(60) + becquerel(1000) +# raises: incompatible (E⁰ vs E¹) + +mmol_per_l(140) + meq_per_l(140) +# raises: incompatible (Z⁰ vs Z¹) +# Even though numerically equal for Na⁺, must convert explicitly +``` + +--- + +## Clinical Decision Support + +With proper dimensions, you can build safer lab systems: + +```python +def calculate_osmolarity( + sodium: Number, # mmol/L + glucose: Number, # mmol/L + urea: Number, # mmol/L +) -> Number: + """Calculate serum osmolarity using standard formula.""" + # 2*Na + glucose + urea + # Each term must be converted to osmolarity (particle count) + + na_osm = sodium * van_hoff(2) # ×2 for dissociation + glucose_osm = glucose * van_hoff(1) # ×1 (no dissociation) + urea_osm = urea * van_hoff(1) # ×1 (no dissociation) + + return na_osm + glucose_osm + urea_osm + # Returns N¹V⁻¹Π¹ (osmolarity) +``` + +The dimensional system enforces that you can't forget the van't Hoff factor for sodium. diff --git a/docs/guides/domain-bases/index.md b/docs/guides/domain-bases/index.md new file mode 100644 index 0000000..e2ec6d7 --- /dev/null +++ b/docs/guides/domain-bases/index.md @@ -0,0 +1,116 @@ +# Domain-Specific Bases + +## The Problem: SI Degeneracies + +The SI system has seven base dimensions (eight in ucon, including information). This is sufficient for dimensional analysis in most contexts, but it fails to distinguish physically or clinically distinct quantities that happen to share the same dimensional formula. + +**Examples of degenerate pairs in SI:** + +| Quantity A | Quantity B | Shared Dimension | Risk | +|------------|------------|------------------|------| +| Gray (absorbed dose) | Sievert (equivalent dose) | L²T⁻² | Radiation safety errors | +| Torque | Energy | ML²T⁻² | Mechanical analysis errors | +| Fentanyl (mg) | Morphine (mg) | M | Fatal dosing errors | +| Molarity | Osmolarity | NL⁻³ | Clinical chemistry errors | +| Heat capacity | Entropy | ML²T⁻²Θ⁻¹ | Thermodynamic confusion | + +These aren't edge cases — they represent real sources of error in safety-critical domains. + +--- + +## The Solution: Extended Bases + +ucon allows you to define custom bases with additional dimensions that capture domain-specific semantics. When a "hidden qualifier" is promoted to a real basis component, degenerate quantities become structurally distinguishable. + +**The pattern:** + +1. Identify the degenerate pair +2. Find the hidden qualifier (what makes them physically different?) +3. Add that qualifier as a new basis component +4. Redefine dimensions with explicit exponents for the new component + +--- + +## Domain Guides + +| Domain | Degeneracies Addressed | Guide | +|--------|------------------------|-------| +| [Radiation Dosimetry](radiation-dosimetry.md) | Gy vs Sv vs Gy(RBE) vs effective dose | Extended dose basis | +| [Pharmacology](pharmacology.md) | Drug mass vs potency, IU variations | Potency-aware basis | +| [Clinical Chemistry](clinical-chemistry.md) | Molarity vs Osmolarity, Bq vs Hz | Particle semantics | +| [Classical Mechanics](classical-mechanics.md) | Torque vs energy, surface tension vs spring constant | Geometric qualifiers | +| [Thermodynamics](thermodynamics.md) | Heat capacity vs entropy | Statistical mechanics | + +--- + +## General Approach + +```python +from ucon.basis import Basis, BasisComponent, Vector +from ucon.core import Dimension, Unit +from fractions import Fraction + +# 1. Define your extended basis +MY_DOMAIN = Basis("MyDomain", [ + BasisComponent("existing_dim_1", "X"), + BasisComponent("existing_dim_2", "Y"), + BasisComponent("hidden_qualifier", "Q"), # NEW +]) + +# 2. Create dimensions with explicit Q exponents +quantity_a = Dimension( + vector=Vector(MY_DOMAIN, (Fraction(1), Fraction(-1), Fraction(0))), + name="quantity_a" +) # X¹Y⁻¹Q⁰ + +quantity_b = Dimension( + vector=Vector(MY_DOMAIN, (Fraction(1), Fraction(-1), Fraction(1))), + name="quantity_b" +) # X¹Y⁻¹Q¹ — now distinguishable! + +# 3. Create units +unit_a = Unit(name="unit_a", dimension=quantity_a) +unit_b = Unit(name="unit_b", dimension=quantity_b) + +# 4. Safety guaranteed +unit_a(1.0) + unit_b(1.0) # raises: incompatible dimensions +``` + +--- + +## When to Use Extended Bases + +**Use an extended basis when:** + +- Two quantities have identical SI dimensions but different physical meaning +- Conflating them would be a safety or correctness error +- Domain experts treat them as fundamentally different +- You need to track a qualifier through calculations (not just label units) + +**Don't use an extended basis when:** + +- The distinction is purely notational (e.g., km vs m — same dimension, different scale) +- A pseudo-dimension suffices (e.g., radians vs pure ratio at the unit level) +- The overhead isn't justified for your use case + +--- + +## Relationship to Pseudo-Dimensions + +ucon also supports pseudo-dimensions for lighter-weight tagging: + +```python +ANGLE = Dimension.pseudo("angle", name="angle", symbol="θ") +``` + +Pseudo-dimensions: +- Tag units without affecting dimensional algebra with real dimensions +- Prevent adding radians to pure ratios +- Don't survive into compound quantities (torque still equals energy) + +Extended bases: +- Full dimensional tracking through all operations +- Torque ≠ energy even in derived quantities +- More overhead, more safety + +Choose based on your needs. diff --git a/docs/guides/domain-bases/pharmacology.md b/docs/guides/domain-bases/pharmacology.md new file mode 100644 index 0000000..0ca4b23 --- /dev/null +++ b/docs/guides/domain-bases/pharmacology.md @@ -0,0 +1,261 @@ +# Pharmacology Basis + +## The Degeneracy Problem + +In SI, drug amounts are measured in mass (kg or mg). But: + +| Drug | Dose | Equivalent Effect | +|------|------|-------------------| +| Morphine | 10 mg | Baseline | +| Fentanyl | 0.1 mg | Same as 10 mg morphine | +| Codeine | 100 mg | Same as 10 mg morphine | + +**All are "milligrams" in SI. Treating them as equivalent is fatal.** + +Similar problems exist with: +- International Units (IU) — different for each substance +- Bioavailability — IV vs oral doses +- Receptor binding — agonist vs antagonist potency + +--- + +## The Extended Pharmacology Basis + +```python +from ucon.basis import Basis, BasisComponent, Vector +from ucon.core import Dimension, Unit +from fractions import Fraction + +PHARMA = Basis("Pharma", [ + BasisComponent("mass", "M"), # 0 — physical mass + BasisComponent("potency", "P"), # 1 — biological activity + BasisComponent("bioavailability", "F"), # 2 — fraction reaching systemic circulation +]) +``` + +--- + +## Dimensional Vectors + +| Quantity | Vector | Interpretation | +|----------|--------|----------------| +| Drug mass | M¹P⁰F⁰ | Raw substance amount | +| Potency factor | M⁰P¹F⁰ | Activity per unit mass | +| Bioactive dose | M¹P¹F⁰ | Mass × potency | +| Systemically available dose | M¹P¹F¹ | Mass × potency × bioavailability | + +--- + +## Implementation + +```python +# Dimensions +drug_mass = Dimension( + vector=Vector(PHARMA, (1, 0, 0)), + name="drug_mass" +) + +potency_factor = Dimension( + vector=Vector(PHARMA, (0, 1, 0)), + name="potency_factor" +) + +bioactive_dose = Dimension( + vector=Vector(PHARMA, (1, 1, 0)), + name="bioactive_dose" +) + +systemic_dose = Dimension( + vector=Vector(PHARMA, (1, 1, 1)), + name="systemic_dose" +) + +# Units with potency baked in +morphine_mg = Unit( + name="morphine_mg", + shorthand="mg(morphine)", + dimension=bioactive_dose +) # potency = 1 (reference) + +fentanyl_mg = Unit( + name="fentanyl_mg", + shorthand="mg(fentanyl)", + dimension=bioactive_dose +) # potency = 100 + +codeine_mg = Unit( + name="codeine_mg", + shorthand="mg(codeine)", + dimension=bioactive_dose +) # potency = 0.1 +``` + +--- + +## Morphine Milligram Equivalents (MME) + +The CDC uses MME for opioid risk assessment: + +```python +# Conversion factors to MME +OPIOID_MME = { + "morphine": 1.0, + "fentanyl": 100.0, # 0.1 mg fentanyl = 10 MME + "oxycodone": 1.5, + "hydrocodone": 1.0, + "codeine": 0.15, + "methadone": 4.0, # variable, dose-dependent + "buprenorphine": 30.0, +} + +# With dimensional tracking: +fentanyl_dose = fentanyl_mg(0.1) +mme = fentanyl_dose.to(morphine_equivalent) # → 10 MME + +# CDC risk thresholds +# ≥50 MME/day: increased overdose risk +# ≥90 MME/day: avoid or justify +``` + +--- + +## International Units (IU) + +IU is a measure of biological activity, not mass. Each substance has its own definition: + +| Substance | 1 IU = | Notes | +|-----------|--------|-------| +| Insulin | 0.0347 mg (human) | Based on blood glucose effect | +| Vitamin D | 0.025 μg | Based on antirachitic activity | +| Vitamin A | 0.3 μg retinol | Based on growth assay | +| Heparin | ~0.002 mg | Based on anticoagulation | +| Penicillin | 0.6 μg | Historical bioassay | + +**Problem:** 1 IU of insulin ≠ 1 IU of vitamin D, but SI treats both as dimensionless counts. + +**Solution:** Model IU as substance-specific potency: + +```python +# Each IU type is a distinct unit with P¹ dimension +insulin_iu = Unit(name="insulin_IU", dimension=potency_factor) +vitamin_d_iu = Unit(name="vitamin_d_IU", dimension=potency_factor) + +# These cannot be added +insulin_iu(100) + vitamin_d_iu(1000) +# raises: incompatible (different substances, even though both are "IU") +``` + +For full safety, extend the basis with a substance tag: + +```python +PHARMA_TYPED = Basis("Pharma-Typed", [ + BasisComponent("mass", "M"), + BasisComponent("potency", "P"), + BasisComponent("bioavailability", "F"), + BasisComponent("substance_class", "S"), # insulin=1, vitamin_d=2, etc. +]) +``` + +--- + +## Bioavailability + +The fraction of drug reaching systemic circulation: + +| Route | Typical F | +|-------|-----------| +| IV | 1.0 (by definition) | +| IM | 0.8-1.0 | +| Oral | 0.1-0.9 (highly variable) | +| Transdermal | 0.1-0.5 | +| Sublingual | 0.3-0.8 | + +```python +# Oral morphine vs IV morphine +oral_morphine = Unit( + name="oral_morphine_mg", + dimension=systemic_dose # M¹P¹F¹ +) +# F ≈ 0.3 for oral morphine + +iv_morphine = Unit( + name="iv_morphine_mg", + dimension=systemic_dose +) +# F = 1.0 for IV + +# 30 mg oral ≈ 10 mg IV (same systemic exposure) +``` + +--- + +## Dimensional Algebra + +**Mass → Bioactive dose:** +``` +bioactive = mass × potency +M¹P¹F⁰ = (M¹P⁰F⁰) × (P¹) ✓ +``` + +**Bioactive → Systemic:** +``` +systemic = bioactive × bioavailability +M¹P¹F¹ = (M¹P¹F⁰) × (F¹) ✓ +``` + +--- + +## Safety Guarantees + +```python +# These are now dimension mismatches: + +morphine_mg(10) + fentanyl_mg(0.1) +# Must convert to common equivalents first + +oral_morphine(30) + iv_morphine(10) +# Must account for bioavailability + +insulin_iu(100) + vitamin_d_iu(1000) +# Cannot add different IU types +``` + +--- + +## Clinical Decision Support + +With dimensional tracking, you can build safer systems: + +```python +def check_mme_risk(prescriptions: list[Number]) -> str: + """Check total daily MME against CDC thresholds.""" + total_mme = sum( + dose.to(morphine_equivalent) + for dose in prescriptions + ) + + if total_mme.quantity >= 90: + return "HIGH RISK: ≥90 MME/day" + elif total_mme.quantity >= 50: + return "ELEVATED RISK: ≥50 MME/day" + else: + return "Standard risk" +``` + +The dimension system ensures all inputs are converted to a common basis before summation — no accidental fentanyl + morphine addition. + +--- + +## Corticosteroid Equivalents + +Similar pattern for steroid conversions: + +| Drug | Equivalent Dose | Relative Potency | +|------|-----------------|------------------| +| Prednisone | 5 mg | 1.0 (reference) | +| Prednisolone | 5 mg | 1.0 | +| Methylprednisolone | 4 mg | 1.25 | +| Dexamethasone | 0.75 mg | 6.7 | +| Hydrocortisone | 20 mg | 0.25 | + +The same extended basis applies — mass alone is insufficient. diff --git a/docs/guides/domain-bases/radiation-dosimetry.md b/docs/guides/domain-bases/radiation-dosimetry.md new file mode 100644 index 0000000..4ea4e36 --- /dev/null +++ b/docs/guides/domain-bases/radiation-dosimetry.md @@ -0,0 +1,214 @@ +# Radiation Dosimetry Basis + +## The Degeneracy Problem + +In SI, all of these have dimension L²T⁻² (equivalent to J/kg): + +| Quantity | Unit | What It Measures | +|----------|------|------------------| +| Absorbed dose | Gray (Gy) | Energy deposited per mass | +| Equivalent dose | Sievert (Sv) | Biologically weighted dose (radiation type) | +| Effective dose | Sievert (Sv) | Whole-body risk-weighted dose | +| RBE-weighted dose | Gy(RBE) | Therapeutically weighted dose | +| Kerma | Gray (Gy) | Kinetic energy released in matter | +| Specific energy | J/kg | Generic energy per mass | + +**All dimensionally identical. All clinically distinct.** + +Confusing Gray with Sievert, or equivalent dose with effective dose, is a medical error. + +--- + +## The Extended Dose Basis + +```python +from ucon.basis import Basis, BasisComponent, Vector +from ucon.core import Dimension, Unit +from fractions import Fraction + +DOSE = Basis("Dose", [ + BasisComponent("energy", "E"), # 0 + BasisComponent("mass", "M"), # 1 + BasisComponent("radiation_weighting", "R"), # 2 — w_R (regulatory) + BasisComponent("rbe", "B"), # 3 — RBE (therapeutic) + BasisComponent("tissue_weighting", "T"), # 4 — w_T (organ sensitivity) +]) +``` + +--- + +## Dimensional Vectors + +| Quantity | Vector | Interpretation | +|----------|--------|----------------| +| Absorbed dose (Gy) | E¹M⁻¹R⁰B⁰T⁰ | Raw energy deposition | +| Equivalent dose (Sv) | E¹M⁻¹R¹B⁰T⁰ | w_R weighted (organ level) | +| Effective dose (Sv) | E¹M⁻¹R¹B⁰T¹ | w_R × w_T weighted (whole body) | +| RBE-weighted dose Gy(RBE) | E¹M⁻¹R⁰B¹T⁰ | Therapeutic RBE weighted | + +--- + +## Implementation + +```python +# Dimensions +absorbed_dose = Dimension( + vector=Vector(DOSE, (1, -1, 0, 0, 0)), + name="absorbed_dose", + symbol="D" +) + +equivalent_dose = Dimension( + vector=Vector(DOSE, (1, -1, 1, 0, 0)), + name="equivalent_dose", + symbol="H" +) + +effective_dose = Dimension( + vector=Vector(DOSE, (1, -1, 1, 0, 1)), + name="effective_dose", + symbol="E_eff" +) + +rbe_weighted_dose = Dimension( + vector=Vector(DOSE, (1, -1, 0, 1, 0)), + name="rbe_weighted_dose", + symbol="D_RBE" +) + +# Weighting factor dimensions +radiation_weighting = Dimension( + vector=Vector(DOSE, (0, 0, 1, 0, 0)), + name="radiation_weighting" +) # R¹ — w_R values: gamma=1, alpha=20 + +tissue_weighting = Dimension( + vector=Vector(DOSE, (0, 0, 0, 0, 1)), + name="tissue_weighting" +) # T¹ — w_T values: lung=0.12, gonads=0.08, etc. + +rbe_factor = Dimension( + vector=Vector(DOSE, (0, 0, 0, 1, 0)), + name="rbe_factor" +) # B¹ — RBE values: proton≈1.1, carbon≈2-3 + +# Units +gray = Unit(name="gray", shorthand="Gy", dimension=absorbed_dose) +sievert_eq = Unit(name="sievert_equivalent", shorthand="Sv", dimension=equivalent_dose) +sievert_eff = Unit(name="sievert_effective", shorthand="Sv_eff", dimension=effective_dose) +gray_rbe = Unit(name="gray_rbe", shorthand="Gy(RBE)", dimension=rbe_weighted_dose) +``` + +--- + +## Dimensional Algebra + +**Absorbed → Equivalent:** +``` +H = D × w_R +E¹M⁻¹R¹B⁰T⁰ = (E¹M⁻¹R⁰B⁰T⁰) × (R¹) ✓ +``` + +**Equivalent → Effective:** +``` +E_eff = H × w_T +E¹M⁻¹R¹B⁰T¹ = (E¹M⁻¹R¹B⁰T⁰) × (T¹) ✓ +``` + +**Absorbed → RBE-weighted:** +``` +D_RBE = D × RBE +E¹M⁻¹R⁰B¹T⁰ = (E¹M⁻¹R⁰B⁰T⁰) × (B¹) ✓ +``` + +--- + +## Safety Guarantees + +```python +# These are now compile-time errors (dimension mismatch): + +gray(2.0) + sievert_eq(1.0) +# raises: incompatible dimensions (R⁰ vs R¹) + +sievert_eq(1.0) + sievert_eff(0.5) +# raises: incompatible dimensions (T⁰ vs T¹) + +gray(2.0) + gray_rbe(2.2) +# raises: incompatible dimensions (B⁰ vs B¹) +``` + +--- + +## Weighting Factor Values + +### Radiation Weighting Factors (w_R) + +| Radiation Type | w_R | +|----------------|-----| +| Photons (all energies) | 1 | +| Electrons, muons | 1 | +| Protons | 2 | +| Alpha particles | 20 | +| Neutrons | 5-20 (energy dependent) | + +### Tissue Weighting Factors (w_T) + +| Tissue | w_T | +|--------|-----| +| Bone marrow, colon, lung, stomach, breast | 0.12 | +| Gonads | 0.08 | +| Bladder, liver, esophagus, thyroid | 0.04 | +| Skin, bone surface, brain, salivary glands | 0.01 | +| Remainder | 0.12 (distributed) | + +### RBE Values (Therapeutic) + +| Particle | RBE (typical) | +|----------|---------------| +| Photons | 1.0 (reference) | +| Protons | 1.1 (clinical convention) | +| Carbon ions | 2-3 (varies with LET) | + +--- + +## Clinical Context + +### Radiation Protection (ICRP) + +Uses **w_R** and **w_T** for regulatory dose limits: +- Occupational limit: 20 mSv/year (effective dose) +- Public limit: 1 mSv/year (effective dose) + +### Radiation Therapy + +Uses **RBE** for treatment planning: +- Proton therapy: Gy(RBE) = Gy × 1.1 +- Carbon ion therapy: Gy(RBE) = Gy × RBE(LET) + +**The distinction matters:** A patient receiving 60 Gy(RBE) proton therapy has a different physical dose than 60 Gy photon therapy, and both differ from regulatory effective dose calculations. + +--- + +## Projection to SI + +If you need to collapse back to SI (L²T⁻²), use a `ConstantAwareBasisTransform` with bindings that record which weighting factors were absorbed: + +```python +DOSE_TO_SI = ConstantAwareBasisTransform( + source=DOSE, + target=SI, + matrix=(...), # Maps E¹M⁻¹ → L²T⁻² + bindings=( + ConstantBinding( + source_component=DOSE["radiation_weighting"], + target_expression=Vector(SI, (0, 0, 0, ...)), # dimensionless + constant_symbol="w_R", + exponent=Fraction(1), + ), + # ... similar for tissue_weighting, rbe + ), +) +``` + +The bindings enable round-trip conversion while tracking which weighting was applied. diff --git a/docs/guides/domain-bases/thermodynamics.md b/docs/guides/domain-bases/thermodynamics.md new file mode 100644 index 0000000..8466741 --- /dev/null +++ b/docs/guides/domain-bases/thermodynamics.md @@ -0,0 +1,243 @@ +# Thermodynamics Basis + +## The Degeneracy Problem + +Heat capacity and entropy share the same SI dimension: + +| Quantity | SI Unit | SI Dimension | Physical Meaning | +|----------|---------|--------------|------------------| +| Heat capacity (C) | J/K | ML²T⁻²Θ⁻¹ | Energy required to raise temperature | +| Entropy (S) | J/K | ML²T⁻²Θ⁻¹ | Microstate counting / disorder | + +**Same dimension. Different physics.** + +- Heat capacity: dQ/dT — a process-dependent response function +- Entropy: ∫dQ_rev/T — a state function measuring disorder + +--- + +## Conceptual Difference + +### Heat Capacity + +- **Question answered:** "How much energy to raise temperature by 1 K?" +- **Type:** Response function (can depend on process: C_p vs C_v) +- **Formula:** C = dQ/dT +- **Intensive form:** Specific heat c = C/m or molar heat C_m = C/n + +### Entropy + +- **Question answered:** "How many microstates correspond to this macrostate?" +- **Type:** State function (path-independent) +- **Formula:** S = k_B ln Ω (statistical), dS = dQ_rev/T (thermodynamic) +- **Intensive form:** Specific entropy s = S/m or molar entropy S_m = S/n + +--- + +## Extended Basis + +```python +from ucon.basis import Basis, BasisComponent, Vector +from ucon.core import Dimension, Unit +from fractions import Fraction + +THERMO = Basis("Thermodynamics", [ + BasisComponent("mass", "M"), # 0 + BasisComponent("length", "L"), # 1 + BasisComponent("time", "T"), # 2 + BasisComponent("temperature", "Θ"), # 3 + BasisComponent("statistical", "Σ"), # 4 — microstate/entropy character +]) +``` + +--- + +## Dimensional Vectors + +| Quantity | Vector | Interpretation | +|----------|--------|----------------| +| Energy | M¹L²T⁻²Θ⁰Σ⁰ | Mechanical/thermal energy | +| Heat capacity | M¹L²T⁻²Θ⁻¹Σ⁰ | Thermal response | +| Entropy | M¹L²T⁻²Θ⁻¹Σ¹ | Statistical/microstate measure | +| Boltzmann constant | M¹L²T⁻²Θ⁻¹Σ¹ | Entropy per microstate (k_B) | +| Temperature | M⁰L⁰T⁰Θ¹Σ⁰ | Thermal intensity | + +--- + +## Implementation + +```python +# Dimensions +energy = Dimension( + vector=Vector(THERMO, (1, 2, -2, 0, 0)), + name="energy" +) + +heat_capacity = Dimension( + vector=Vector(THERMO, (1, 2, -2, -1, 0)), + name="heat_capacity" +) # ML²T⁻²Θ⁻¹Σ⁰ + +entropy = Dimension( + vector=Vector(THERMO, (1, 2, -2, -1, 1)), + name="entropy" +) # ML²T⁻²Θ⁻¹Σ¹ + +statistical_factor = Dimension( + vector=Vector(THERMO, (0, 0, 0, 0, 1)), + name="statistical_factor" +) # Σ¹ + +# Units +joule_per_kelvin_heat = Unit( + name="joule_per_kelvin_C", + shorthand="J/K", + dimension=heat_capacity +) + +joule_per_kelvin_entropy = Unit( + name="joule_per_kelvin_S", + shorthand="J/K", + dimension=entropy +) +``` + +--- + +## The Boltzmann Constant + +k_B connects temperature to energy via entropy: + +``` +S = k_B ln Ω +``` + +In the extended basis, k_B has dimension Σ¹ — it's the bridge between statistical mechanics (microstates) and thermodynamics (temperature). + +When we set k_B = 1 (natural units for statistical mechanics), we're collapsing the statistical dimension: + +```python +# Forward: Σ¹ → dimensionless via k_B +# Inverse: dimensionless → Σ¹ by dividing by k_B +``` + +--- + +## Dimensional Algebra + +**Heat absorbed from capacity:** +``` +Q = C × ΔT +M¹L²T⁻²Θ⁰Σ⁰ = (M¹L²T⁻²Θ⁻¹Σ⁰) × (Θ¹) ✓ +``` + +**Entropy from heat (reversible):** +``` +ΔS = Q_rev / T +M¹L²T⁻²Θ⁻¹Σ¹ = (M¹L²T⁻²Θ⁰Σ⁰) / (Θ¹) × (Σ¹) ← needs statistical factor +``` + +Wait — this reveals something important. The formula dS = dQ/T doesn't automatically give you entropy's statistical character. The Σ¹ comes from the *definition* of entropy as a statistical quantity, not from the algebra. + +This is analogous to torque vs energy: the angle factor comes from the physics (τ = dW/dθ), not from pure dimensional analysis. + +--- + +## Related Degeneracies + +### Specific Heat vs Specific Entropy + +| Quantity | SI Dimension | Extended | +|----------|--------------|----------| +| Specific heat | L²T⁻²Θ⁻¹ | L²T⁻²Θ⁻¹Σ⁰ | +| Specific entropy | L²T⁻²Θ⁻¹ | L²T⁻²Θ⁻¹Σ¹ | + +### Gas Constant vs Entropy + +The gas constant R = 8.314 J/(mol·K) has the same dimension as molar entropy: + +| Quantity | SI Dimension | Extended | +|----------|--------------|----------| +| Gas constant R | ML²T⁻²Θ⁻¹N⁻¹ | ML²T⁻²Θ⁻¹N⁻¹Σ⁰ | +| Molar entropy | ML²T⁻²Θ⁻¹N⁻¹ | ML²T⁻²Θ⁻¹N⁻¹Σ¹ | + +R is a proportionality constant; molar entropy is a state function. + +--- + +## Safety Guarantees + +```python +# These are now caught: + +heat_capacity_water + entropy_water +# raises: incompatible dimensions (Σ⁰ vs Σ¹) + +# Must be explicit about what you're computing: +total_entropy = entropy_system + entropy_surroundings # ✓ (both Σ¹) +total_capacity = C_water + C_container # ✓ (both Σ⁰) +``` + +--- + +## Free Energy Functions + +The statistical dimension clarifies thermodynamic potentials: + +| Potential | Definition | Entropy Term | +|-----------|------------|--------------| +| Internal energy U | — | No explicit S | +| Helmholtz F | U - TS | Contains Σ¹ via S | +| Gibbs G | H - TS | Contains Σ¹ via S | +| Enthalpy H | U + PV | No explicit S | + +When computing G = H - TS: +- H has dimension M¹L²T⁻²Σ⁰ (energy) +- T has dimension Θ¹ +- S has dimension M¹L²T⁻²Θ⁻¹Σ¹ + +``` +TS: Θ¹ × M¹L²T⁻²Θ⁻¹Σ¹ = M¹L²T⁻²Σ¹ +``` + +**Problem:** H (Σ⁰) and TS (Σ¹) have different dimensions! + +**Resolution:** Gibbs energy inherits the statistical character: +``` +G = H - TS → G has dimension M¹L²T⁻²Σ¹ +``` + +Or we recognize that H implicitly carries Σ⁰ and the subtraction is valid only when we track that TS "converts" to pure energy via the temperature multiplication. + +This is a deep point — the extended basis reveals that free energies are intrinsically statistical quantities, not pure mechanical energies. + +--- + +## Practical Value + +**Textbook sanity check:** + +> "The entropy of the system increased by 50 J/K." +> "The heat capacity is 50 J/K." + +In SI, these are dimensionally indistinguishable. In the extended basis: +- First statement: Σ¹ quantity changed +- Second statement: Σ⁰ property measured + +**Simulation validation:** + +In molecular dynamics, you compute entropy from microstate sampling (statistical) and heat capacity from energy fluctuations (thermodynamic response). Conflating them is a methodological error the extended basis can catch. + +--- + +## Summary + +| Degeneracy | Hidden Dimension | Resolution | +|------------|------------------|------------| +| Heat capacity vs Entropy | Statistical (Σ) | Entropy is ML²T⁻²Θ⁻¹Σ¹ | +| Gas constant vs Molar entropy | Statistical (Σ) | Molar entropy carries Σ¹ | +| Specific heat vs Specific entropy | Statistical (Σ) | Same pattern, intensive | + +The statistical dimension captures whether a quantity describes: +- **Σ⁰**: Bulk thermal response (heat capacity, gas constant) +- **Σ¹**: Microstate counting (entropy, Boltzmann constant) diff --git a/mkdocs.yml b/mkdocs.yml index 572f18b..03bdffb 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -62,6 +62,13 @@ nav: - Domain Walkthroughs: - guides/domain-walkthroughs/index.md - Nursing Dosage: guides/domain-walkthroughs/nursing-dosage.md + - Domain-Specific Bases: + - guides/domain-bases/index.md + - Radiation Dosimetry: guides/domain-bases/radiation-dosimetry.md + - Pharmacology: guides/domain-bases/pharmacology.md + - Clinical Chemistry: guides/domain-bases/clinical-chemistry.md + - Classical Mechanics: guides/domain-bases/classical-mechanics.md + - Thermodynamics: guides/domain-bases/thermodynamics.md - Scientific Computing: - NumPy Arrays: guides/numpy-arrays.md - Pandas Integration: guides/pandas-integration.md