""" bioavailability.py — SimBiology BA 계산 로직 이식 모듈 ====================================================== SimBiology + MATLAB 피팅 워크플로를 순수 Python으로 재현. 대리모델(MLP/Neural ODE)이 예측한 림프·혈관 흡수곡선으로부터 생체이용률(Bioavailability, BA)을 계산한다. 워크플로 (원본 SimBiology 검증: BA=96.8% 재현 확인, 오차 0.4%p): 1. 누적 흡수곡선(c_lymph, c_vessel)을 이중지수로 피팅 y(t) = y0 + (y00-y0)*F*(1-exp(-k_fast*t)) + (y00-y0)*(1-F)*(1-exp(-k_slow*t)) 2. mass rate 유도: [A*exp(-B*t) + C*exp(-D*t)]*dose*0.01 A=(y00-y0)*F*k_fast, B=k_fast, C=(y00-y0)*(1-F)*k_slow, D=k_slow 3. SC 3구획 / IV 2구획 PK ODE 적분 4. BA = AUC_SC(central) / AUC_IV(central) * 100 SimBiology 모델 파라미터 (피하주사 SC 모델): 구획: Central(3.557 L) / Peripheral(1.807 L) / Central lymph(0.312 L) k_12=0.0992 중앙→말초 k_21=0.3448 말초→중앙 k_input=0.1920 림프→중앙 k_10=0.0043 중앙→제거(청소) 흐름: 혈관흡수→중앙 직접 / 림프흡수→림프구획→(k_input)→중앙 """ import numpy as np from scipy.optimize import curve_fit from scipy.integrate import odeint # numpy 버전 호환: 2.0+는 trapezoid, 이전은 trapz _trapz = np.trapezoid if hasattr(np, "trapezoid") else np.trapz # ── SimBiology 모델 파라미터 ── K_12 = 0.0992 # 중앙 → 말초 (1/hr) K_21 = 0.3448 # 말초 → 중앙 (1/hr) K_INPUT = 0.1920 # 림프 → 중앙 (1/hr) K_10 = 0.0043 # 중앙 → 제거 (1/hr) DEFAULT_DOSE = 40.0 # mg T_MAX_HR = 1000.0 # 적분 구간 (hr, ≈42일) N_STEPS = 20000 def biexp_cumulative(t, y0, y00, F, k_fast, k_slow): """이중지수 누적 흡수곡선 (MATLAB fittingpara_v3.m와 동일).""" return (y0 + (y00 - y0) * F * (1 - np.exp(-k_fast * t)) + (y00 - y0) * (1 - F) * (1 - np.exp(-k_slow * t))) def fit_biexp(t_hr, y): """ 누적곡선을 이중지수로 피팅. 여러 초기값을 시도해 robust하게. MATLAB 제약: Lower=[-1,0,0,0,0] (y0,y00,F,k_fast,k_slow), F∈[0,1]. 반환: (y0, y00, F, k_fast, k_slow) 또는 None(실패). """ y = np.clip(np.asarray(y, float), 0, None) lower = [-1, 0, 0, 0, 0] upper = [np.inf, np.inf, 1, np.inf, np.inf] best, best_sse = None, np.inf for k_fast0 in (0.1, 1.0, 8.0): for y00_0 in (max(y.max(), 1.0), max(y[-1], 1.0)): try: p0 = [0, y00_0, 0.01, k_fast0, 0.01] popt, _ = curve_fit(biexp_cumulative, t_hr, y, p0=p0, bounds=(lower, upper), maxfev=5000) sse = np.sum((y - biexp_cumulative(t_hr, *popt)) ** 2) if sse < best_sse: best_sse, best = sse, popt except Exception: continue return best def rate_params(popt): """피팅 파라미터 → mass rate 계수 (A,B,C,D).""" y0, y00, F, k_fast, k_slow = popt A = (y00 - y0) * F * k_fast B = k_fast C = (y00 - y0) * (1 - F) * k_slow D = k_slow return A, B, C, D def _sc_ode(y, t, kle, kve): c_cen, c_per, c_lym = y dc_cen = kve(t) + K_INPUT * c_lym - K_10 * c_cen - K_12 * c_cen + K_21 * c_per dc_per = K_12 * c_cen - K_21 * c_per dc_lym = kle(t) - K_INPUT * c_lym return [dc_cen, dc_per, dc_lym] def _iv_ode(y, t): c_cen, c_per = y return [-K_10 * c_cen - K_12 * c_cen + K_21 * c_per, K_12 * c_cen - K_21 * c_per] def _sc_ode_dyn(y, t, kle, kve, k10, k12, k21, k_input): """ 모델 3 = SimBiology (사용자 모델, 기본). 논문 Fig 2B를 전신 2구획(Central+Peripheral)으로 펼친 검증 모델. 혈관→중앙 직접, 림프→림프구획→중앙. 말초⇄중앙(k12,k21). BA 96.8% 재현. """ c_cen, c_per, c_lym = y dc_cen = kve(t) + k_input * c_lym - k10 * c_cen - k12 * c_cen + k21 * c_per dc_per = k12 * c_cen - k21 * c_per dc_lym = kle(t) - k_input * c_lym return [dc_cen, dc_per, dc_lym] def _sc_ode_m1(y, t, ka, k10): """ 모델 1 = 논문 Fig 1A (single-pathway). 림프·혈관 미구분, 단일 흡수 ka. 전신 1구획(회색 박스). """ (c_cen,) = y return [ka(t) - k10 * c_cen] def _sc_ode_m2(y, t, kle, kve, k10): """ 모델 2 = 논문 Fig 2A (dual-pathway, 림프 구획 없음). 혈관·림프 두 경로가 둘 다 전신으로 직행. 전신 1구획(회색 박스). """ (c_cen,) = y return [kve(t) + kle(t) - k10 * c_cen] def _sc_ode_m4(y, t, kle, kve, k10, k_input, k_rl=0.001): """ 모델 4 = 논문 Fig 3A (redistribution). 모델 3(림프 구획 경유)에 '전신→림프 재분포'(k_rl)를 추가. 전신에 도달한 약이 조직에서 림프로 배액되어 재순환(Kagan 2007). 전신 1구획. k_rl은 가정값(미검증). """ c_cen, c_lym = y dc_cen = kve(t) + k_input * c_lym - k10 * c_cen - k_rl * c_cen dc_lym = kle(t) - k_input * c_lym + k_rl * c_cen return [dc_cen, dc_lym] def _iv_ode_1pool(y, t, k10): """1구획 IV (논문 모델 1,2,4용). t=0 전량 투여 후 1차 제거.""" (c_cen,) = y return [-k10 * c_cen] def _iv_ode_dyn(y, t, k10, k12, k21): """약물별 PK 파라미터를 받는 동적 IV ODE (2구획, 모델 3용).""" c_cen, c_per = y return [-k10 * c_cen - k12 * c_cen + k21 * c_per, k12 * c_cen - k21 * c_per] def compute_bioavailability(c_lymph, c_vessel, t_min, dose=DEFAULT_DOSE, drug="IgG", model=3, return_detail=False): """ 림프·혈관 누적 흡수곡선 → 생체이용률(%). 인자: c_lymph, c_vessel : 누적 % mass 곡선 (대리모델 예측) t_min : 시간축 (분) dose : 투여량 (mg) drug : 약물 종류 ("IgG", "INS", "ALB") 또는 MW(숫자) 기본값 "IgG" (SimBiology 검증값) 반환: BA (%) 또는 return_detail=True 시 dict """ # 약물별 전신 PK 파라미터 로드 try: from drug_pk_params import get_drug_params pk = get_drug_params(drug) k12, k21 = pk["k12"], pk["k21"] k_input, k10 = pk["k_input"], pk["k10"] except Exception: # drug_pk_params.py 없으면 기본값(IgG) 사용 k12, k21, k_input, k10 = K_12, K_21, K_INPUT, K_10 t_hr = np.asarray(t_min, float) / 60.0 pl = fit_biexp(t_hr, c_lymph) pv = fit_biexp(t_hr, c_vessel) if pl is None or pv is None: raise RuntimeError("이중지수 피팅 실패 — 곡선을 확인하세요.") Al, Bl, Cl, Dl = rate_params(pl) Av, Bv, Cv, Dv = rate_params(pv) def kle(t): return (Al * np.exp(-Bl * t) + Cl * np.exp(-Dl * t)) * dose * 0.01 def kve(t): return (Av * np.exp(-Bv * t) + Cv * np.exp(-Dv * t)) * dose * 0.01 # 모델 1은 림프+혈관 합산 흡수를 따로 피팅 pt = fit_biexp(t_hr, np.asarray(c_lymph, float) + np.asarray(c_vessel, float)) if pt is not None: At, Bt, Ct, Dt = rate_params(pt) def ka(t): return (At * np.exp(-Bt * t) + Ct * np.exp(-Dt * t)) * dose * 0.01 else: ka = lambda t: kle(t) + kve(t) tt = np.linspace(0, T_MAX_HR, N_STEPS) # ── 모델 선택 (논문 Kagan 2014 기준) ── # 모델 1,2,4 = 전신 1구획(논문 회색박스) → IV도 1구획 # 모델 3 = SimBiology 2구획(Central+Peripheral) → IV 2구획 if model == 5: # 모델 5 = Fig 3B (7구획 비선형) — 현재 데이터로 구현 불가 raise NotImplementedError( "모델 5(7구획 비선형, Fig 3B)는 전후방 림프 분리 측정 데이터가 " "필요하여 현재 COMSOL 데이터(림프 단일 출력)로는 구현할 수 없습니다.") elif model == 1: # Fig 1A: 단일흡수, 전신 1구획 sc_full = odeint(_sc_ode_m1, [0], tt, args=(ka, k10)) sc = np.column_stack([sc_full[:, 0], np.zeros(len(tt)), np.zeros(len(tt))]) iv = odeint(_iv_ode_1pool, [dose], tt, args=(k10,)) elif model == 2: # Fig 2A: dual, 림프구획 없음, 전신 1구획 sc_full = odeint(_sc_ode_m2, [0], tt, args=(kle, kve, k10)) sc = np.column_stack([sc_full[:, 0], np.zeros(len(tt)), np.zeros(len(tt))]) iv = odeint(_iv_ode_1pool, [dose], tt, args=(k10,)) elif model == 4: # Fig 3A: redistribution, 림프구획 + 전신 1구획 sc_full = odeint(_sc_ode_m4, [0, 0], tt, args=(kle, kve, k10, k_input)) sc = np.column_stack([sc_full[:, 0], np.zeros(len(tt)), sc_full[:, 1]]) iv = odeint(_iv_ode_1pool, [dose], tt, args=(k10,)) else: # model == 3 (SimBiology, 기본) sc = odeint(_sc_ode_dyn, [0, 0, 0], tt, args=(kle, kve, k10, k12, k21, k_input)) iv = odeint(_iv_ode_dyn, [dose, 0], tt, args=(k10, k12, k21)) auc_sc = _trapz(sc[:, 0], tt) auc_iv = _trapz(iv[:, 0], tt) BA = auc_sc / auc_iv * 100.0 if return_detail: # 혈장(Central) 기준 약동학 지표 sc_plasma = sc[:, 0] cmax = float(sc_plasma.max()) peak_i = int(np.argmax(sc_plasma)) tmax = float(tt[peak_i]) # t½: 정점 이후 절반으로 감소하는 시간 (소실 반감기) after = sc_plasma[peak_i:] t_after = tt[peak_i:] if len(after) > 1 and cmax > 0: t_half = float(t_after[int(np.argmin(np.abs(after - cmax / 2)))] - tmax) else: t_half = float("nan") return { "BA": float(BA), "AUC_SC": float(auc_sc), "AUC_IV": float(auc_iv), "drug": drug, "model": model, "pk_params": dict(k10=k10, k12=k12, k21=k21, k_input=k_input), "lymph_rate_params": dict(A=Al, B=Bl, C=Cl, D=Dl), "vessel_rate_params": dict(A=Av, B=Bv, C=Cv, D=Dv), # 혈장 농도-시간 곡선 (BA 그래프용) "sc_curve": sc[:, 0], # SC 혈장 곡선 "iv_curve": iv[:, 0], # IV 혈장 곡선 "central_curve": sc[:, 0], # (하위호환) "time_hr": tt, # 혈장 기준 PK 지표 "Cmax": cmax, "Tmax_hr": tmax, "t_half_hr": t_half, } return float(BA) # ── 자체 검증 (원본 SimBiology BA=96.8% 재현) ── if __name__ == "__main__": # 원본 피팅값으로 직접 검증 dose = 40.0 def kle(t): return (10.02*np.exp(-8.868*t) + 2.407*np.exp(-0.035*t)) * dose * 0.01 def kve(t): return (0.919*np.exp(-0.035*t) + 1.061*np.exp(-1.576*t)) * dose * 0.01 tt = np.linspace(0, 1000, 20000) sc = odeint(_sc_ode, [0,0,0], tt, args=(kle, kve)) iv = odeint(_iv_ode, [dose,0], tt) ba = _trapz(sc[:,0],tt) / _trapz(iv[:,0],tt) * 100 print(f"검증: BA={ba:.1f}% (원본 SimBiology 96.8%, 오차 {abs(ba-96.8):.2f}%p)")