제4장
바이오의약품이 표적에 얼마나 강하게 결합하는가, 몸속에 얼마나 오래 머무는가, 주어진 농도가 얼마만큼의 효과를 내는가. 이 세 질문을 떠받치는 식과 BiopharmaSDK의 구현, 그리고 그 구현을 믿어서는 안 되는 지점을 다룬다.
제3장에서는 WIA-BIO-016의 타입 모델이 곧 이 표준의 데이터 형식이라는 점을 설명하였다. 이 장은 기록에서 계산으로 넘어간다. 표준의 계산 API는 작다. BiopharmaSDK의 메서드 네 개(calculateBindingAffinity, calculateKinetics, calculatePharmacokinetics, calculatePharmacodynamics)가 있고, 그중 세 개는 독립 함수로도 내보내진다. 여기에 셸 도구 wia-bio-016의 calc-affinity, calc-pk, simulate-pk, simulate-pkpd 명령이 더해진다. 그 밑에 깔린 것은 중개연구자라면 누구나 이미 쓰고 있는 교과서적 관계, 즉 질량작용의 법칙, 1차 소실, 힐(Hill) 방정식이다.
식이 단순하므로 정작 중요한 것은 계산 자체가 아니라 계산을 둘러싼 약속이다. 각 필드가 어떤 단위를 기대하는지, 어떤 필드가 기록만 되고 무시되는지, 어떤 출력이 물리량이고 어떤 출력이 참조 코드 작성자가 정한 휴리스틱인지가 핵심이다. 이 장에 인용한 SDK 출력은 모두 v1.0 소스를 실제로 실행해 얻은 값이며, 코드가 명세서나 약리학과 어긋나는 곳은 모두 명시하였다. 4.6절에서 그 결함을 모아 정리하고, 4.7절에서 명세서 부록 A의 예제를 다시 계산한다.
calculateBindingAffinity()결합 부위 하나가 항원과 가역적으로 반응하는 경우(Ab + Ag ⇌ Ab·Ag), 질량작용의 법칙으로부터 명세서 §4.1.1의 평형 해리상수가 정의된다.
Kd = [Ab]free × [Ag]free / [Ab·Ag] (단위: M = mol/L)
Kd는 농도다. 가장 쓸모 있는 해석은, 리간드가 과량일 때 결합 부위의 절반이 점유되는 유리 리간드 농도가 곧 Kd라는 것이다. 따라서 Kd가 작을수록 결합이 강하다. Kd는 표준 결합 자유에너지와도 ΔG° = RT·ln(Kd/c°)(c° = 1 M)로 직결된다. 25 °C에서 RT는 약 2.48 kJ/mol이므로 Kd 1 nM은 ΔG° ≈ −51.4 kJ/mol(약 −12.3 kcal/mol)에 해당하고, Kd를 10배 개선할 때마다 얻는 에너지는 약 5.7 kJ/mol(1.36 kcal/mol)에 불과하다. 친화도 성숙 연구가 백분율이 아니라 "자릿수"로 개선을 말하는 까닭이 이 로그 관계에 있다.
실험에서는 보통 넣어 준 총량을 알고 복합체의 양을 측정한다. 유리 상태의 양은 질량 보존으로 구한다.
[Ab]free = [Ab]total − [Ab·Ag]
[Ag]free = [Ag]total − [Ab·Ag]
calculateBindingAffinity()가 하는 일이 정확히 이것이다. 입력 타입 BindingAffinity에는 antibodyConc, antigenConc, complexConc(모두 M 단위)와 필수 필드 temperature(°C), 선택 필드 pH, buffer가 있다. 메서드는 각 총량에서 복합체를 빼고 Kd를 계산한 뒤, kd, freeAntibody, freeAntigen, bindingStrength, fractionBound(복합체 ÷ 총 항체), therapeuticSuitability의 여섯 필드를 가진 BindingAffinityResult를 돌려준다.
복합체를 빼는 일은 농도가 Kd와 비슷한 영역에서 특히 중요하다. 단순한 교과서식 처리는 유리 리간드가 총 리간드와 같다고 가정하는데, 이는 리간드가 결합 부위보다 훨씬 많을 때만 성립한다. 이 가정이 깨지는 상황(리간드 고갈)에서 총량과 Kd로부터 형성될 복합체 양을 예측하는 정방향 문제는 이차방정식의 정확한 해를 필요로 한다.
[Ab·Ag] = ( S − √(S² − 4·[Ab]total·[Ag]total) ) / 2, 단 S = [Ab]total + [Ag]total + Kd
SDK는 이 정방향 함수를 제공하지 않지만, 검산 도구로는 매우 유용하다. SDK가 돌려준 Kd를 이 식에 다시 넣으면 측정된 복합체 농도가 재현되어야 한다. 4.7절에서 부록 A.1로 이 검산을 수행한다.
이 메서드는 두 가지 경우에 코드 BIO007(INVALID_PARAMETERS)의 BiopharmaError를 던진다. 하나는 총량 중 하나가 0 이하이거나 복합체가 음수인 경우이고, 다른 하나는 복합체가 어느 한쪽 총량을 넘는 경우다. 복합체가 정확히 0이면 받아들여지며, 이때 결과는 kd: Infinity, bindingStrength: 'very-low', fractionBound: 0이다. 산술적으로는 일관되지만 사용자 화면에서는 별도의 방어 처리가 필요하다.
temperature, pH, buffer는 기록만 되고 계산에는 쓰이지 않는다. 순수한 평형 계산이라면 문제가 없지만, 25 °C에서 잰 값이든 37 °C에서 잰 값이든 같은 레코드는 같은 Kd를 낸다는 뜻이기도 하다. 친화도는 온도에 따라 달라지고 분석 완충액의 영향도 받으므로, 이 필드들은 결과와 함께 모든 비교와 보고서로 전달되어야 한다. 이들은 Kd를 바꾸는 입력이 아니라 Kd를 해석 가능하게 만드는 메타데이터다.
명세서 §4.1.2는 친화도 구간을 다섯 개로 나누고, SDK는 명세서의 상한 없는 "낮음" 구간을 100 nM에서 한 번 더 쪼개 여섯 개를 구현한다. 아래 표는 세 출처를 나란히 놓은 것이다. CLI 열은 4.6절에서 다룰 결함을 미리 반영한 것으로, 배포된 셸 스크립트에서는 마지막 라벨을 제외한 모든 라벨에 도달할 수 없다.
| Kd 범위 | 명세서 §4.1.2 라벨 | bindingStrength(SDK) | CLI calc-affinity | therapeuticSuitability |
|---|---|---|---|---|
| < 1 pM | Ultra-high(초고) | ultra-high | ULTRA-HIGH(도달 불가) | true |
| 1 pM – 100 pM | Very high(매우 높음) | very-high | VERY HIGH(도달 불가) | true |
| 100 pM – 1 nM | High(높음) | high | HIGH(도달 불가) | true |
| 1 nM – 10 nM | Moderate(보통) | moderate | MODERATE(도달 불가) | true |
| 10 nM – 100 nM | Low(낮음) | low | LOW | 정확히 10 nM일 때만 true |
| ≥ 100 nM | Low(낮음) | very-low | LOW | false |
코드에서 도출되지만 명세서에는 적혀 있지 않은 경계 규칙이 두 가지 있다. 첫째, 모든 구간은 하한을 포함한다. SDK가 엄격한 "미만" 비교를 쓰므로 Kd가 정확히 1 nM이면 high가 아니라 moderate로 분류된다. 둘째, therapeuticSuitability는 BIOPHARMA_CONSTANTS.MIN_THERAPEUTIC_KD(10 nM) "이하"를 쓰는 반면 분류는 "미만"을 쓴다. 그래서 Kd가 정확히 10 nM이면 low이면서 동시에 적합으로 보고된다. 양쪽 모두 20 nM, 복합체 10 nM으로 SDK를 실행하면 실제로 kd: 1e-8, bindingStrength: 'low', therapeuticSuitability: true가 나온다. 인위적인 사례이지만, 적합성 시험이 반드시 고정해 두어야 할 바로 그런 경계다.
구간 라벨과 10 nM 플래그는 모두 이 표준의 관례일 뿐 규제 기준이 아니다. 친화도가 "충분한지"는 표적의 양, 필요한 점유율, 투여 가능한 용량, 작용 기전에 달려 있다. 작용제 항체, 차단 항체, 세포 내 유입이 필요한 ADC는 요구 조건이 서로 크게 다를 수 있다. therapeuticSuitability는 스크리닝 데이터를 거르는 분류 플래그로 다루어야 하며, 분자가 효과를 낼 것이라는 판정으로 읽어서는 안 된다.
calculateKinetics()평형값은 시간 경과를 감춘다. 결합은 2차 반응으로 결합속도상수 kon(M⁻¹s⁻¹)을 가지며, 해리는 1차 반응으로 해리속도상수 koff(s⁻¹)를 가진다. 평형에서는 두 속도가 같아지므로 명세서 §4.2.1의 관계가 나온다.
Kd = koff / kon
복합체 반감기 = ln 2 / koff
calculateKinetics(kon, koff)는 두 상수 중 하나라도 양수가 아니면 BIO007을 던지고, 그렇지 않으면 바로 이 두 값을 KineticParameters 레코드에 담아 돌려준다. 명세서는 치료용 항체의 전형적인 범위로 kon 10⁵–10⁶ M⁻¹s⁻¹, koff 10⁻³–10⁻⁵ s⁻¹를 제시한다. 이 범위 안에서 예시로 고른 값으로 SDK를 두 번 실행해 보면 동역학이 왜 추가 정보를 주는지 알 수 있다.
| 예시 | kon (M⁻¹s⁻¹) | koff (s⁻¹) | Kd (SDK) | 복합체 반감기 (SDK) |
|---|---|---|---|---|
| K-1 | 1 × 10⁵ | 1 × 10⁻⁴ | 1 × 10⁻⁹ M (1 nM) | 6,931초 ≈ 1.9시간 |
| K-2 | 1 × 10⁶ | 1 × 10⁻⁵ | 1 × 10⁻¹¹ M (10 pM) | 69,315초 ≈ 19.3시간 |
Kd의 100배 차이는 결합 쪽 10배와 해리 쪽 10배가 합쳐진 결과다. Kd가 같은 두 분자라도 복합체가 유지되는 시간은 자릿수 단위로 다를 수 있으며, 체류 시간이 기전에 중요한 경우에는 이 차이가 결정적이다. 따라서 특성분석 레코드에는 비율인 Kd만이 아니라 kon과 koff를 함께 남겨야 한다.
구현상 주의할 점이 하나 있다. calculateKinetics()는 실제 사용한 장비와 온도와 무관하게 결과에 항상 method: 'spr', temperature: 25를 기록한다. KineticParameters 타입은 'spr' | 'bli' | 'ifa' | 'elisa' | 'kinexA'를 허용하지만 계산기는 그 정보를 받지 못한다. 반환 객체를 분석 레코드로 저장하는 코드는 두 필드를 실제 메타데이터로 덮어써야 한다. 그러지 않으면 37 °C에서 수행한 BLI 측정이 25 °C SPR로 보관된다. 이 메서드는 독립 함수로 내보내지지 않으므로 SDK 인스턴스를 통해서만 호출할 수 있다.
표면 플라즈몬 공명(SPR)과 바이오레이어 간섭법(BLI)은 표면에 포획된 질량에 비례하는 신호로 결합을 측정한다. 1:1 상호작용의 정상상태에서 명세서 §4.2.2는 다음 식을 제시한다.
Response = Rmax × C / (Kd + C)
C는 용액 중 분석물 농도이고, Rmax는 고정화된 부위가 모두 점유되었을 때의 신호다. C = Kd이면 응답은 Rmax의 절반이고, C = 9·Kd이면 Rmax의 90%다. 농도별 평탄부 응답을 이 식에 맞추는 방법을 정상상태 친화도 분석이라 한다.
평탄부에 다가가는 과정은 관측 속도 kobs = kon·C + koff의 단일 지수함수를 따른다. 예시 K-1에서 C = 1 nM이면 kobs = 10⁵ × 10⁻⁹ + 10⁻⁴ = 2 × 10⁻⁴ s⁻¹이고, 평형의 95%에 도달하는 데 약 ln 20 / kobs ≈ 15,000초, 즉 4시간 가까이 걸린다. 실제 주입 시간은 이보다 훨씬 짧다. 그래서 고친화도 항체는 평탄부를 기다리지 않고 결합·해리 구간을 동역학적으로 피팅하여 특성분석하는 것이 일반적이다. 도달하지 못한 정상상태에서 보고한 Kd는 흔하지만 피할 수 있는 오류다.
IgG에는 똑같은 결합 팔이 두 개 있다. 항원이 표면이나 세포막에 충분한 밀도로 놓여 있으면, 분자 전체가 떨어져 나가기 전에 한쪽 팔이 다시 결합할 수 있고, 겉보기 해리는 Fab 팔 하나의 해리보다 훨씬 느려진다. 이렇게 나타나는 기능적 친화도, 곧 결합활성은 항원 밀도, 기하 구조, 항체 형식에 따라 고유의 1가 친화도를 한 자릿수 이상 넘어설 수 있다. 명세서 §4.3은 1가 Kd 대비 10–1000배라는 일반적인 증강 범위를 제시하지만, 실제로 두루 적용되는 배수는 없으며 효과의 크기는 분석 구성에 달려 있다.
데이터 교환의 관점에서 이는 동역학 레코드가 측정 방향과 함께 있어야만 해석 가능하다는 뜻이다. 항체를 포획하고 단량체 항원을 흘려 보냈는지(고유 친화도에 가까움), 항원을 고정하고 2가 항체를 분석물로 썼는지(결합활성이 섞인 겉보기 값)를 알아야 한다. KineticParameters에는 방향이나 결합가를 담을 필드가 없다. 타입이 확장되기 전까지 구현자는 이 정보를 잃지 말고 별도의 분석 설명에 담아 두어야 한다.
calculatePharmacokinetics()SDK는 가장 단순한 약동학 모델을 쓴다. 부피 Vd인 잘 섞인 구획 하나에서 약물이 청소율 CL의 1차 소실로 제거된다는 모델이다. 정맥 일시주사 후 농도는 C(t) = C0·e^(−ke·t)로 감소한다. 명세서 §5.1의 관계는 모두 이 모델에서 나온다.
AUC = F × Dose / CL (Dose가 mg, CL이 L/h이면 mg·h/L)
ke = CL / Vd (h⁻¹)
t½ = ln 2 / ke = 0.693 × Vd / CL (h)
C0 = Dose / Vd (mg/L, 정맥 일시주사에만 해당)
입력 타입 PKParameters가 단위를 고정한다. dose는 mg, bioavailability는 0~1, clearance는 L/h, volumeOfDistribution은 L이며, 필수 필드 route('iv' | 'sc' | 'im' | 'oral' | 'inhalation')와 선택 필드 patientWeight(kg)가 있다. 용량·청소율·분포용적 중 하나가 양수가 아니거나 생체이용률이 [0, 1]을 벗어나면 BIO007을 던진다. 결과 PKResult에는 auc, halfLife(시간), c0, ke, dosingInterval(시간)이 담긴다. 타입에는 선택 필드 cmax, tmax도 선언되어 있으나 메서드가 채우지는 않는다.
숫자를 쓰기 전에 짚어 둘 단순화가 세 가지 있다. 첫째, route와 patientWeight는 어떤 계산에도 쓰이지 않는다. 생체이용률은 AUC에만 들어가고, c0는 투여 경로와 상관없이 Dose/Vd로 계산된다. 피하 투여에는 애초에 C0가 없다. 주사 부위에서의 흡수가 느려서 최고 농도는 며칠 뒤에 나타난다. route: 'sc'와 예시용 F = 0.7로 SDK를 실행하면 AUC는 그만큼 줄어들지만 C0는 정맥 투여와 똑같이 보고된다. PKResult를 소비하는 쪽은 정맥 일시주사가 아니면 c0를 무시해야 한다.
둘째, 단일클론항체는 1구획으로 잘 설명되는 경우가 드물다. 정맥 투여 후 농도-시간 곡선은 대개 분포상 뒤에 느린 말단상이 이어지는 모양이며, 2구획 모델로 피팅한다. 셋째, 막 표적에 대한 항체에서는 표적 결합 자체가 소실 경로가 될 수 있다. 이러한 표적 매개 약물 처분(TMDD) 때문에 청소율이 농도에 의존하게 된다. 표적이 포화되지 않은 저농도에서는 빠르고, 포화된 뒤에는 느리다. 이 경우 CL 값 하나는 그것을 추정한 농도 범위에서만 유효하다.
신장 여과 한계보다 작은 단백질은 대개 몇 시간 안에 신장으로 제거된다. IgG는 사구체 여과를 통과하기에는 너무 크며, 주로 음세포작용과 리소좀 분해로 제거된다. IgG를 오래 살게 하는 것은 신생아 Fc 수용체 FcRn이다. 약산성인 엔도솜으로 들어온 항체의 Fc 부위가 FcRn에 결합하고, 이 복합체가 세포 표면으로 재순환되면 혈액의 중성 pH에서 항체가 분해되지 않고 순환계로 되돌아간다. 이 pH 의존적 회수 덕분에 IgG1, IgG2, IgG4 항체의 반감기는 대체로 2~3주에 이르고, 산성 pH에서 FcRn 결합을 강화하는 Fc 공학으로 반감기를 늘릴 수 있으며, Fc가 없는 절편은 훨씬 빨리 사라진다. SDK 상수는 이 배경을 IGG_HALF_LIFE: 21(일)과 FCRN_KD: 200e-9(M, pH 6.0)로 담고 있다. 두 값 모두 특정 제품의 성질이 아니라 모델의 참조값으로 다루어야 한다.
같은 생물학이 분포용적에도 한계를 정한다. 150 kDa 단백질은 모세혈관 벽을 느리게 통과하므로 항체의 Vd는 혈장 부피에 간질 공간 일부를 더한 정도다. 명세서가 mAb에 대해 제시한 3–8 L 범위는 이와 부합하며, BIOPHARMA_CONSTANTS.PLASMA_VOLUME은 3.5 L다.
dosingInterval은 약동학적 양이 아니다. 참조 구현에 하드코딩된 세 규칙이 만들어 낸 값이다.
if (halfLife < 12) dosingInterval = max(4, ceil(halfLife / 2)) // 시간
else if (halfLife < 48) dosingInterval = 24
else dosingInterval = ceil(halfLife)
코드 주석은 "보통 반감기의 1~2배"라고 적고 있으나 첫 번째 규칙과 맞지 않는다. 어느 규칙도 실제로 투여 요법을 정하는 요소인 치료 범위, 목표 농도, 노출-반응 관계를 보지 않는다. 이 필드는 시뮬레이션한 후보들을 "수 시간", "하루", "수 주" 범주로 나누는 데에만 쓸모가 있다. 목표 농도에서 용량을 도출하는 방법은 다음 절에서 다룬다.
농도 축을 로그로 두면 1구획 소실은 반감기마다 절반이 되는 직선이다. 4.7절의 두 예제는 C0와 ke 값이 같고 시간 단위만 다르므로 같은 그림 하나로 표현된다. 바로 이 우연의 일치가 4.4절의 교훈이다.
C (mg/L, 로그 눈금)
20.0 ┤●
│ ╲
10.0 ┤ ●
│ ╲
5.0 ┤ ●
│ ╲
2.5 ┤ ●
│ ╲
1.25 ┤ ●
└┬───────┬───────┬───────┬───────┬──→ 시간
0 1 t½ 2 t½ 3 t½ 4 t½
부록 A.2 단백질 : 1 t½ = 17.3시간 (CL = 0.2 L/h, Vd = 5 L)
IgG 예시 (4.7절) : 1 t½ = 17.3일 (CL = 0.2 L/day, Vd = 5 L)
명세서 §5.3은 고전적인 두 투여식을 제시한다. 부하 용량은 분포용적을 한 번에 목표 농도까지 채우고, 유지 용량은 투여 간격 τ 동안 제거된 양을 보충한다.
부하 용량 = Vd × Css / F
유지 용량 = CL × Css,avg × τ / F
명세서는 유지 용량 식에서 F를 빼고 적었는데, 이는 정맥 투여에서만 맞다. 다른 경로라면 부하 용량과 마찬가지로 생체이용률로 나누어야 한다. Css,avg는 한 투여 간격 동안의 평균 정상상태 농도로, 한 간격의 AUC를 τ로 나눈 값이다. 다구획 약물에서는 부하 식의 "Vd"를 신중히 골라야 한다(초기 과다 농도를 피하려면 중심 구획 부피, 모든 조직을 채우려면 정상상태 분포용적). 1구획 계산은 첫 추정치일 뿐이다.
부하 용량 없이 반복 투여하면 약물은 정상상태를 향해 축적된다. 한 간격에서 정상상태 노출과 첫 투여 노출의 비인 축적비는 R = 1 / (1 − e^(−ke·τ))이며, 정상상태로 다가가는 속도는 반감기만으로 결정된다. 반감기 1회 뒤 약 50%, 약 3.3회 뒤 90%, 5회 뒤 97%에 이른다. 반감기가 약 17일인 분자라면 평균 농도가 정상상태 값의 10% 이내로 들어오기까지 대략 두 달이 걸린다. 반감기가 긴 바이오의약품에서 부하 용량이나 초기 집중 투여가 흔한 실질적 이유가 이것이다.
계산 예시(예시 값, 정맥 투여, F = 1): CL = 0.2 L/day, Vd = 5 L이면 ke = 0.04 day⁻¹, t½ ≈ 17.3일이다. 예시 목표 Css,avg를 20 mg/L, τ를 14일로 두면 유지 용량은 0.2 × 20 × 14 = 2주마다 56 mg이고, 부하 용량 5 × 20 = 100 mg이면 곧바로 목표에 도달한다. 이 간격의 축적비는 1 / (1 − e^(−0.56)) ≈ 2.3이며, 부하 용량이 없으면 정상상태의 90%에 이르기까지 약 57일(반감기 3.3회)이 걸린다. 이 숫자들은 실제 제품을 나타내지 않는다. 식들이 어떻게 맞물리는지를 보여 주기 위한 값이다.
SDK는 시간(hour) 단위에 고정되어 있다. CL은 L/h, t½와 dosingInterval은 시간, AUC는 mg·h/L다. 그런데 항체의 청소율은 보통 하루 단위로 보고되고, 용량은 mg/kg으로 표시되는 경우가 많다. 아래 표는 데이터가 SDK에 들어가기 전에 반드시 거쳐야 할 변환을 정리한 것이다. PKParameters 타입에는 단위 필드가 없으므로 변환을 빠뜨려도 아무 경고가 나오지 않는다.
| 보고된 양 | SDK가 요구하는 단위 | 변환 |
|---|---|---|
| CL (L/day) | L/h | 24로 나눈다 |
| CL (mL/h/kg) | L/h | 체중(kg)을 곱하고 1000으로 나눈다 |
| 용량 (mg/kg) | mg | 체중(kg)을 곱한다. patientWeight는 자동 적용되지 않는다 |
| 농도 (µg/mL) | mg/L | 수치가 같다(1 µg/mL = 1 mg/L) |
| SDK의 AUC (mg·h/L) | 보고용 mg·day/L | 24로 나눈다 |
SDK의 halfLife (h) | 보고용 일(day) | 24로 나눈다 |
SDK에는 투여 요법 계산기가 없으므로 애플리케이션이 직접 작성해야 한다. 변수 이름에 단위를 담은 최소한의 도우미는 다음과 같다. 이 코드는 WIA-BIO-016의 일부가 아니다.
// SDK에 포함되지 않음: 애플리케이션 측 도우미(정맥 투여 또는 F를 아는 경우)
function regimen(clLperDay: number, vdL: number, cssMgPerL: number,
tauDays: number, f = 1) {
const keperDay = clLperDay / vdL;
return {
loadingMg: (vdL * cssMgPerL) / f,
maintenanceMg: (clLperDay * cssMgPerL * tauDays) / f,
accumulation: 1 / (1 - Math.exp(-keperDay * tauDays)),
halfLifeDays: Math.LN2 / keperDay,
};
}
regimen(0.2, 5, 20, 14);
// => { loadingMg: 100, maintenanceMg: 56, accumulation: 2.33, halfLifeDays: 17.3 }
calculatePharmacodynamics()약력학은 농도와 효과를 잇는다. 가장 많이 쓰이는 것은 시그모이드 Emax 모델, 곧 힐 방정식이다.
E = E0 + Emax × Cⁿ / (EC50ⁿ + Cⁿ)
E0는 기저 효과, Emax는 약물이 낼 수 있는 최대 효과, EC50은 Emax의 절반을 내는 농도, n은 곡선의 가파름을 정하는 힐 계수다. n = 1(명세서 §5.2.1의 일반 Emax 모델)이면 최대 효과의 10%에서 90%로 가는 데 농도가 81배 늘어야 하고, n = 2이면 9배면 된다. EC50과 Emax는 데이터에서 추정하는 값이며, 특정 계에서의 특정 평가변수를 기술한다. 혈장 바이오마커의 EC50과 임상 반응의 EC50은 같은 숫자가 아니다.
calculatePharmacodynamics()는 PDParameters 레코드를 받는다. concentration과 ec50(둘 다 mg/L로 문서화됨), emax, 선택 필드 hillCoefficient(기본값 1)와 baseline(기본값 0)이다. 농도나 Emax가 음수이거나 EC50이 양수가 아니면 BIO007을 던지고, 그렇지 않으면 effect, receptorOccupancy, effectPercentage, therapeuticRange, withinRange를 돌려준다. effect는 힐 방정식을 올바르게 구현한 값이다. 나머지 네 출력에는 4.6절의 단서가 붙는다.
수용체 점유율, 즉 표적 부위 중 약물이 결합한 비율은 4.1절과 같은 질량작용에서 나온다. 유리 약물이 표적보다 과량이면 다음과 같다.
RO = C / (Kd + C)
여기서 C와 Kd는 같은 단위여야 한다. 혈청 항체 농도는 질량 단위로, Kd는 몰 단위로 보고되므로 분자량이 필요하다. 약 150 kDa인 IgG에서 1 nM은 10⁻⁹ mol/L × 150,000 g/mol = 1.5 × 10⁻⁴ g/L, 즉 0.15 mg/L(0.15 µg/mL)이다. SDK의 convertConcentration() 도우미로는 이 변환을 할 수 없다. 몰 단위끼리만 변환하며, 질량 단위가 오면 분자량을 넘겨도 BIO007("Unit conversion not implemented for these units")을 던진다.
이렇게 계산한 점유율은 이상화된 값이다. 평형, 유리 약물 농도, 그리고 보통 25 °C의 정제된 계에서 시험관 내로 측정한 Kd를 가정한다. 표적의 교체 속도와, 조직 농도가 혈장 농도보다 훨씬 낮을 수 있다는 사실도 무시한다. 명세서 §5.2.2는 "치료 효과를 위한" 목표 점유율로 80~90% 이상을 제시하는데, 이는 차단 항체에서 흔히 쓰는 계획상의 가정이지 일반 규칙이 아니다. 중요한 점유율 수준은 기전에 따라 다르며, 가능하면 계산이 아니라 측정(예: 혈액 세포에 대한 유세포 분석)으로 확인하는 것이 바람직하다.
참조 구현은 한 줄씩 읽을 수 있을 만큼 짧고, 그렇게 읽으면 WIA-BIO-016 v1.0 위에 무언가를 만드는 사람이라면 알아야 할 문제들이 드러난다. 숨겨진 것은 없다. 구현이 표준의 우연한 실수가 아니라 의도를 재현하도록 여기에 모아 둔다.
| # | 위치 | 결함 | 결과 | 우회 방법 |
|---|---|---|---|---|
| D1 | calculatePharmacodynamics | 점유율을 C/(Kd + C)가 아니라 C/(EC50 + C)로 계산 | 점유율이 효과 곡선을 그대로 따라감. EC50 ≠ Kd이면(거의 항상) 틀림 | Kd로부터 직접 계산하되 C와 Kd의 단위를 맞춘다 |
| D2 | calculatePharmacodynamics | therapeuticRange = 0.5×EC50 ~ 9×EC50, 주석은 "Emax의 50~90%" | n = 1이면 하한은 Emax의 50%가 아니라 33%. n = 2이면 범위가 20%~98.8% | 표시용 휴리스틱으로만 쓰고, 범위는 노출-반응 데이터로 정한다 |
| D3 | calculatePharmacodynamics | effectPercentage = effect ÷ Emax × 100, effect에 기저값 포함 | 기저값이 있으면 100%를 넘고, 검증이 허용하는 Emax = 0에서는 NaN | (effect − E0) ÷ Emax를 쓰고 Emax = 0을 거부한다 |
| D4 | calculateKinetics | method: 'spr', temperature: 25 하드코딩 | 저장된 레코드가 분석 조건을 잘못 기록 | 두 필드를 실제 분석 메타데이터로 덮어쓴다 |
| D5 | calculatePharmacokinetics | route, patientWeight 무시, dosingInterval은 휴리스틱 | 피하·경구 투여에도 C0를 보고. 간격이 치료 범위와 무관 | c0는 정맥 일시주사에만 쓰고, 요법은 4.4절처럼 도출한다 |
| D6 | README | 존재하지 않는 simulatePharmacokinetics를 import | 예제가 컴파일되지 않음 | calculatePharmacokinetics를 쓴다 |
| D7 | index.ts, types.ts | 심볼을 두 번 내보냄(선언부와 끝의 export { … } 블록) | tsc가 TS2323/TS2484로 실패하여 배포본 그대로는 빌드 불가 | 끝의 재내보내기 블록을 삭제한다 |
| D8 | wia-bio-016 calc-affinity | bc가 1e-9 같은 과학적 표기를 해석하지 못함 | Kd가 0으로 출력되거나 분류 비교가 실패하여 결과가 항상 "LOW" | SDK를 쓰거나, 제7장처럼 bc를 awk로 바꾼다 |
| D9 | 명세서 부록 A | A.1을 "High affinity", A.2를 "BID or TID"로 해석 | A.1은 명세서 자신의 표 및 코드와 모순. A.2는 코드의 24시간 출력과 모순 | 4.7절의 수정된 해석을 쓴다 |
D1은 그럴듯한 숫자를 조용히 만들어 내므로 자세히 볼 필요가 있다. Kd = 1 nM(0.15 mg/L)인 150 kDa 항체, EC50 1 mg/L, 농도 9 mg/L이면 SDK는 receptorOccupancy: 0.9를 보고한다. 결합식으로 계산하면 9 / (0.15 + 9) ≈ 0.984다. 이 차이는 반올림 문제가 아니다. 두 숫자는 서로 다른 질문에 대한 답이다. 점유율은 친화도로 정해지지만, EC50은 신호 증폭, 수용체 예비능, 선택한 평가변수 등 결합 이후의 모든 과정에 좌우된다. 기능 평가에서 흔히 그렇듯 EC50이 Kd보다 크면 SDK는 점유율을 과소평가하고, EC50이 Kd보다 작으면 과대평가한다.
D7은 이 장의 예제를 직접 돌려 보려는 모든 사람에게 영향을 준다. v1.0 소스를 TypeScript 5로 컴파일하면 첫 번째 중복 내보내기에서 멈추고, Node.js의 내장 TypeScript 로더도 같은 이유로 모듈을 거부한다. 모든 심볼이 선언된 자리에서 이미 내보내지고 있으므로, index.ts 끝의 export { BiopharmaSDK }; 한 줄과 types.ts 끝의 export type { … } / export { … } 블록만 지우면 된다. README의 npm install @wia/bio-016 줄도 의도의 표명으로 읽어야 한다. 아래 예제는 배포된 패키지가 아니라 api/typescript를 로컬에서 빌드했다고 가정한다.
D8은 명령줄 도구의 다른 문제와 함께 제7장에서 다루지만, 이 장에 미치는 영향은 간단하다. calc-affinity 명령은 세 농도를 모두 일반 십진수로 넣을 때만 Kd를 올바르게 계산한다. 그렇게 해도 분류 줄은 1e-12, 100e-12 같은 식으로 적힌 임계값과 비교하는데 bc가 이것도 거부하므로, 출력되는 구간은 항상 "LOW", 적합성은 항상 "BORDERLINE/UNSUITABLE"이 된다. 기본값이 일반 십진수인 calc-pk 명령은 SDK와 같은 숫자를 재현한다.
부록 A.1은 항체 1 nM, 항원 10 nM, 복합체 0.9 nM을 제시한다. 유리 상태는 0.1 nM과 9.1 nM이므로 Kd = 0.1 × 9.1 / 0.9 ≈ 1.01 nM이다. SDK로 실행하면 다음과 같다.
import { BiopharmaSDK } from '../api/typescript/src'; // 로컬 빌드, D7 수정
const sdk = new BiopharmaSDK();
const a1 = sdk.calculateBindingAffinity({
antibodyConc: 1e-9, // M, 총량
antigenConc: 1e-8, // M, 총량
complexConc: 0.9e-9, // M, 측정값
temperature: 25, // 기록만 되고 계산에 쓰이지 않음
});
console.log(a1);
// {
// kd: 1.011111111111112e-9, // ≈ 1.01 nM
// freeAntibody: 1.0000000000000007e-10, // ≈ 0.1 nM
// freeAntigen: 9.1e-9,
// bindingStrength: 'moderate',
// fractionBound: 0.8999999999999999,
// therapeuticSuitability: true
// }
명세서는 이 결과를 "High affinity (nM range)"로 해석한다. 이는 1–10 nM을 "Moderate"로 규정한 명세서 자신의 §4.1.2 표와도, 1.01 nM을 moderate로 분류하는 SDK와도 모순된다. 올바른 해석은 이렇다. WIA-BIO-016 구간 기준으로 보통 친화도이며, 표준의 10 nM 스크리닝 임계값보다 낮으므로 therapeuticSuitability는 true다. 1 nM이 특정 개발 프로그램에 충분한지는 표적에 따라 따로 따져야 할 문제다.
4.1절의 이차방정식으로 고리를 닫을 수 있다. 총량 1 nM과 10 nM, Kd = 1.0111 nM이면 S = 12.0111 nM, √(S² − 40) ≈ 10.2111 nM이므로 예측 복합체는 (12.0111 − 10.2111) / 2 = 0.900 nM으로 입력과 일치한다. 적합성 시험은 바로 이 왕복 검산을 쓸 수 있다. 구한 Kd를 정방향 식에 다시 넣었을 때 부동소수점 허용오차 안에서 측정 복합체가 재현되지 않는 구현은 틀린 것이다. SDK가 유리 항체를 1.0000000000000007e-10으로 출력한다는 점도 눈여겨보자. 거의 같은 두 수를 이진 부동소수점으로 빼면 정확하지 않으므로, 시험은 엄격한 일치가 아니라 상대 허용오차로 비교해야 한다.
부록 A.2는 정맥 투여 100 mg, CL = 0.2 L/h, Vd = 5 L을 쓴다. 명세서는 이를 가상의 소형 치료 단백질이라 설명한다. SDK는 그 계산을 그대로 재현한다. 아래 표에는 같은 수치의 청소율을 하루 단위로 표현한 IgG 유사 사례, 즉 0.2 L/day를 추가하였다. 이는 단일클론항체에서 전형적인 크기 수준이다. SDK에는 0.2 / 24 ≈ 0.00833 L/h로 넣어야 한다.
const a2 = sdk.calculatePharmacokinetics({
dose: 100, bioavailability: 1, clearance: 0.2,
volumeOfDistribution: 5, route: 'iv',
});
// { auc: 500, halfLife: 17.325, c0: 20, ke: 0.04, dosingInterval: 24 }
const igg = sdk.calculatePharmacokinetics({
dose: 100, bioavailability: 1, clearance: 0.2 / 24, // 0.2 L/day
volumeOfDistribution: 5, route: 'iv',
});
// { auc: 12000, halfLife: 415.8, c0: 20, ke: 0.001667, dosingInterval: 416 }
| 출력 | A.2 단백질 (CL 0.2 L/h) | IgG 유사 예시 (CL 0.2 L/day) |
|---|---|---|
| AUC | 500 mg·h/L | 12,000 mg·h/L (= 500 mg·day/L) |
| ke | 0.04 h⁻¹ | 0.00167 h⁻¹ (= 0.04 day⁻¹) |
| t½ | 17.3시간 | 415.8시간 ≈ 17.3일 |
| C0 | 20 mg/L | 20 mg/L |
| 24시간 후 C (C0·e^(−ke·t)) | ≈ 7.7 mg/L | ≈ 19.2 mg/L |
dosingInterval (휴리스틱) | 24시간 | 416시간 ≈ 17일 |
여기서 세 가지 교훈을 얻는다. 첫째, A.2에 대한 명세서의 해석 "Dosing frequency: BID or TID"는 반감기 12~48시간이면 무조건 24시간 간격을 돌려주는 참조 코드와 맞지 않는다. 둘 다 투여 권고가 아니며, 적합성 시험은 산문이 아니라 코드에 문서화된 규칙을 시험해야 한다. 둘째, A.2를 일반화해서는 안 된다. 0.2 L/h의 청소율은 전형적인 항체 청소율보다 훨씬 빠르며, 항체라면 반감기가 하루도 되지 않는다. 셋째, 두 열은 청소율의 시간 단위만 다르고, 파생되는 모든 양이 정확히 24배씩 차이 난다. 시간 단위를 기대하는 필드에 하루 단위 청소율을 넣으면 반감기가 24배 짧게 나오고, 아무 오류도 발생하지 않는다. 이 API를 잘못 쓰는 가장 흔한 길이 바로 이 단위 실수 하나다.
예시로, IgG 사례의 표적 Kd가 1 nM(0.15 mg/L)이고 어떤 바이오마커 반응의 EC50이 1 mg/L, n = 1이라고 하자. 투여 24시간 뒤 농도 약 19.2 mg/L에서 힐 방정식은 19.2 / (1 + 19.2) ≈ Emax의 95%를 준다. SDK는 effect로 같은 값을 보고하고, D1 때문에 receptorOccupancy도 0.95로 보고한다. 결합식으로는 19.2 / (0.15 + 19.2) ≈ 0.992다. 포화 근처에서는 두 값이 비슷해 보이지만 투여 간격의 최저점에서는 벌어진다. 9·Kd에 해당하는 1.35 mg/L에서 실제 점유율은 90%인데 SDK의 식은 1.35 / 2.35 ≈ 57%를 준다. 용량 선택 논리에서 최저 목표 농도를 정하는 경우처럼 점유율이 의사결정을 이끄는 곳에서는 반드시 Kd로 계산해야 한다.
moderate이고, therapeuticSuitability는 10 nM 이하 스크리닝 플래그다.c0를 무시한다.dosingInterval과 therapeuticRange는 휴리스틱이다. receptorOccupancy는 EC50을 쓰므로 Kd로 다시 계산한다.simulatePharmacokinetics가 아니라 calculatePharmacokinetics를 호출하며, CLI의 친화도 구간 출력에 의존하지 않는다.bindingStrength 구간을 제시하라.calculateBindingAffinity()가 총량에서 복합체를 빼는 이유는 무엇이며, 유리 ≈ 총량으로 가정하면 언제 Kd가 눈에 띄게 틀어지는가?clearance: 0.25로 입력하였다. Vd = 6 L일 때 SDK가 보고할 반감기와 올바른 값은 각각 얼마인가?therapeuticRange의 하한은 Emax의 몇 %에 해당하는가? 이것이 코드 주석의 주장과 다른 이유는 무엇인가?