nx_stability.nx
buildroot/runtime/nx_stability.nx
about
nx_stability.nx -- ICH Q1A(R2)/Q1E STABILITY rung: the temperature physics
that turns "how long is it good for" into a computed number instead of a
printed guess. Serves the peptide / supplement / frozen-dessert lanes
alike, because degradation kinetics does not care what the product is.
===== WHY THIS EXISTS ============================================
nx_shelf_life already models Q10 scaling, but ONLY at decade-aligned
temperature steps -- and the actual regulatory question is 25 C vs 40 C,
a FIFTEEN degree gap. The organ returned -1 for the one comparison the
whole category rests on. This rung supplies the Arrhenius layer beneath
it and the ICH machinery above it.
===== THE KEYSTONE: Ea IS RECOVERED, NOT ASSERTED ================
Every stability model needs an activation energy, and asserting one is
how a plausible-looking shelf life becomes a lie. We do not assert it.
ICH Q1A(R2) declares that 6 months at the accelerated condition (40 C)
supports a 24-month re-test period at the long-term condition (25 C).
Two conditions the regulation itself calls EQUIVALENT uniquely determine
the kinetics between them:
acceleration factor = 24/6 = 4 over dT = 15 C
Q10 = 4^(10/15) = 2.512 (published pharma/food band: 2 - 3)
Ea = R*ln(4)*T1*T2/dT = 71.6 kJ/mol (published band: 60 - 100)
BOTH recovered figures land inside independently published bands that
were never used as inputs. That is the liar-kill: the regulation's own
arithmetic agrees with the thermal-degradation literature. This is the
same discipline nx_thermal_process used to recover the pasteurisation
z-value from the PMO's two equivalent schedules.
===== ⚠THE RECOVERED Ea IS LUMPED, AND SAYING SO IS THE POINT =====
ICH's two conditions are 25 C / 60% RH and 40 C / 75% RH. They differ in
TEMPERATURE **AND IN HUMIDITY**. Two conditions differing in two variables
cannot separate two parameters -- the system is underdetermined -- so the
figure recovered above is an EFFECTIVE activation energy that carries the
moisture contribution inside it, not a pure thermal Ea.
dependencies 2 imports · 2 importers
imports: nx_syscalls.nxnx_pow10.nx
imported by: nx_labsci_svc.nxnx_stability_rh.nx
structs
| none |
consts
| 83 | const STAB_MAGIC_10000: i64 = 10000 |
| 84 | const STAB_MAGIC_1000000: i64 = 1000000 |
| 85 | const STAB_MAGIC_100000: i64 = 100000 |
| 87 | const STAB_INVALID: i64 = 0 - 1 |
| 90 | const STAB_R_MILLI: i64 = 8314 |
| 93 | const STAB_LN10_MILLI: i64 = 2303 |
| 95 | const STAB_KELVIN_OFF_CENTI: i64 = 27315 |
| 100 | const STAB_ICH_LONG_C: i64 = 25 |
| 101 | const STAB_ICH_LONG_MONTHS: i64 = 24 |
| 102 | const STAB_ICH_ACCEL_C: i64 = 40 |
| 103 | const STAB_ICH_ACCEL_MONTHS: i64 = 6 |
| 107 | const STAB_SIGNIFICANT_LOSS_PCT: i64 = 5 |
| 110 | const STAB_PUB_Q10_LO_Q3: i64 = 2000 |
| 111 | const STAB_PUB_Q10_HI_Q3: i64 = 3000 |
| 112 | const STAB_PUB_EA_LO_J: i64 = 60000 |
| 113 | const STAB_PUB_EA_HI_J: i64 = 100000 |
| 184 | const STAB_ICH_LONG_RH_PCT: i64 = 60 |
| 185 | const STAB_ICH_ACCEL_RH_PCT: i64 = 75 |
functions
| 118 | func stab_c_to_centi_kelvin(c: i64) -> i64 |
| 129 | func stab_q10_from_equivalence_q3(t_lo_c: i64, months_lo: i64, t_hi_c: i64, months_hi: i64) -> i64 |
| 147 | func stab_ea_from_equivalence(t_lo_c: i64, months_lo: i64, t_hi_c: i64, months_hi: i64) -> i64 |
| 170 | func stab_ich_q10_q3() -> i64 called by 2: lab_stability_shelfstab_recovered_q10_in_published_band calls 1: stab_q10_from_equivalence_q3 |
| 175 | func stab_ich_ea_j() -> i64 |
| 190 | func stab_ea_is_lumped_with_humidity() -> i64 called by 1: lab_stability_shelf |
| 198 | func stab_conditions_needed_to_separate() -> i64 |
| 207 | func stab_projection_on_recovered_path(target_rh_pct: i64) -> i64 |
| 214 | func stab_recovered_q10_in_published_band() -> i64 calls 1: stab_ich_q10_q3 |
| 223 | func stab_recovered_ea_in_published_band() -> i64 calls 1: stab_ich_ea_j |
| 236 | func stab_accel_factor_q3(q10_q3: i64, dt_c: i64) -> i64 |
| 249 | func stab_e10_milli_kelvin(ea_j: i64) -> i64 called by 1: stab_mkt_centi_kelvin |
| 257 | func stab_e10_over_t_milli(e10_mk: i64, t_centi_k: i64) -> i64 called by 1: stab_mkt_centi_kelvin |
| 267 | func stab_convexity_ratio_milli(ea_j: i64, t_centi_k: i64) -> i64 |
| 280 | func stab_mkt_centi_kelvin(temps_c: *i64, hours: *i64, n: i64, ea_j: i64) -> i64 |
| 321 | func stab_mean_centi_kelvin(temps_c: *i64, hours: *i64, n: i64) -> i64 |
| 346 | func stab_potency_retained_permil(t_months: i64, shelf_months: i64, loss_pct: i64) -> i64 |
| 367 | func stab_overage_permil(retained_permil: i64) -> i64 called by 1: lab_stability_shelf |
| 374 | func stab_is_significant_change(retained_permil: i64) -> i64 called by 1: lab_stability_shelf |
| 387 | func stab_accelerated_alone_sets_shelf_life() -> i64 called by 1: lab_stability_shelf |
| 392 | func stab_max_extrapolated_months(long_term_months: i64) -> i64 |