본문으로 건너뛰기

다운스트림 수집: 크로마토그래피와 여과 스키드

📍 현재 위치: 2부 공정을 포착하기. 바이오리액터는 세포와 항체가 담긴 탱크 하나를 넘겨주었습니다. 이제 우리는 그 분자를 정제 스키드(skid)를 따라 추적하며, 완만한 추세보다는 결정(decision) 에 더 가까운 데이터를 포착하는 법을 배웁니다.

쉽게 말하면

업스트림 데이터는 긴 비행과 같습니다. 고도와 속도가 몇 시간에 걸쳐 천천히 흐르고, 여러분이 주로 알고 싶은 것은 평균값입니다. 다운스트림 데이터는 착륙과 같습니다. 중요한 모든 일은 단 몇 분의 날카로운 순간에 벌어집니다 — 기수를 드는 플레어, 접지, 그리고 제동 — 그리고 질문은 결코 "평균은 얼마였는가?"가 아니라 "우리는 올바른 순간에 올바른 일을 했는가, 그리고 그것을 증명할 수 있는가?"입니다. 크로마토그래피(chromatography) 한 회분(run)은 짧고 이름이 붙은 단계들의 연속이며, 그 가치 있는 기록은 우리가 어느 구간의 액체를 보관하기로 결정했는가 입니다.

이 장에서 다루는 내용

바이오리액터를 거친 뒤에도 수확된 배양액(broth)은 여전히 대부분 물, 세포 잔해, 그리고 숙주 세포 단백질(host-cell protein, 항체와 함께 만들어지는 CHO 세포 자신의 단백질)에 약간의 항체가 녹아 있는 상태입니다. 다운스트림 정제(downstream purification)는 그것을 순수한 원료 의약품(drug substance)으로 바꾸는 일련의 스키드 — 각각이 펌프, 밸브, 검출기를 한 프레임에 얹은, 자기 완결적이고 자동화된 크로마토그래피 또는 여과 기계 — 입니다. 단백질 A 포획(Protein A capture), 바이러스 불활화 및 여과(viral inactivation and filtration), 폴리싱(polishing) 크로마토그래피, 그리고 한외여과/정용여과(ultrafiltration/diafiltration, UF/DF) 입니다. 이 장에서는 그 데이터를 오픈 소스 도구로 포착하는 방법을 보여줍니다.

  • 다운스트림 추적선(trace)이 왜 단계가 풍부하고 결정을 담고 있는지, 그리고 각 스키드가 어떤 신호를 만들어내는지.
  • 스키드 PLC에서 OPC UA를 통해 UV, 전도도(conductivity), pH, 압력, 유량을 읽어내기.
  • 한 회분을 ISA-88 오퍼레이션(operation)과 페이즈(phase) 로 분할하고, 컬럼 부피(column volume, CV) 단위로 정규화하기.
  • GMP(Good Manufacturing Practice, 의약품을 안전하고 재현 가능하게 만들기 위한 법적 강제력을 가진 규칙)에 결정적인 두 가지 결정 — 풀링 윈도(pooling window)유지 시간(hold time) / 무결성 시험(integrity test) — 을 PostgreSQL(오픈 소스 관계형 데이터베이스)에 events.operation_event 행으로 기록하기.
  • 강화된(intensified) 다컬럼 연속 포획(multi-column continuous capture, 3MCC) 변형, 그리고 순수 오픈 소스 소프트웨어(open-source software, OSS)만으로는 부족해지는 지점.

우리는 동반 저장소(companion repo)의 실제로 테스트된 코드를, 시뮬레이터가 만들어내는 결정론적(deterministic) 크로마토그램(SIM_SEED=2026)에 대해 실행하고, 그로부터 나오는 정확한 숫자를 들여다봅니다.

다운스트림은 추세선이 아니라 상태 기계다

업스트림에서는 2주 동안 몇 초마다 태그(tag) 하나를 기록하며, 그 이야기는 느린 곡선 속에 있습니다. 다운스트림에서는 단일 단백질 A 사이클(cycle)이 약 한 시간 지속되고, 그 한 시간 안에서 컬럼(column)은 정해진 단계(step) 의 순서를 통과합니다. 평형화, 적재, 세척, 용출, 스트립, 세정입니다. 각 단계는 "정상"의 의미가 완전히 다릅니다. 적재 중에는 280 nm에서의 UV가 기준선(baseline) 근처에 머물고, 용출 중에는 농축된 항체가 컬럼에서 떨어져 나오면서 수천 밀리흡광도단위(milli-absorbance-unit, mAU)까지 치솟습니다. (280 nm에서의 UV 흡광도는 단백질을 검출하는 일꾼입니다 — 단백질은 그 파장의 자외선을 흡수하므로, 높은 UV 값은 많은 양의 항체가 지나가고 있다는 뜻입니다.) 정치 세정(clean-in-place, CIP) 중에는 — 전도도는 액체에 녹아 있는 이온의 양을 재는 척도인데 — 이온으로 가득 찬 강염기인 수산화나트륨을 컬럼에 흘려보내기 때문에 전도도가 뛰어오릅니다.

이것이 바로 ISA-88 / IEC 61512 가 작성된 이유입니다. 하나의 배치(batch)는 프로시저(procedure) → 유닛 프로시저(unit procedure) → 오퍼레이션(operation) → 페이즈(phase) 의 계층으로 구조화됩니다 [1] — 전체 레시피(프로시저)에서 시작해, 장비 한 대에서 벌어지는 일(유닛 프로시저)과 주요 처리 활동(오퍼레이션)을 거쳐, 가장 작은 이름 붙은 단계(페이즈)까지 내려옵니다. 우리의 목적에서 유용한 단위는 페이즈 — 평형화, 적재, 세척, 용출 — 이며, 데이터 포착의 임무는 연속적인 센서 추적선을 그 이름 붙은 윈도들로 잘라내어, 이후의 모든 질의가 "09:14:32의 UV는 얼마였는가?"가 아니라 "용출 중 UV는 얼마였는가?"를 물을 수 있게 하는 것입니다.

신호 자체는 스키드의 PLC(programmable logic controller, 장비를 구동하는 산업용 컴퓨터)에서 산업용 데이터 프로토콜인 OPC UA(IEC 62541) 를 통해 나옵니다 [2]. 바이오리액터에서 사용한 것과 같은, 자기 서술적이고 타임스탬프가 찍히며 품질 플래그가 달린 그 전송 방식입니다. 크로마토그래피 스키드는 보통 UV280(때로는 여러 파장의 UV), 전도도, pH, 입구 및 출구 압력, 유량, 그리고 스키드 자체 제어기가 실행 중인 현재 단계 번호를 노출합니다. 우리는 그 모두를 포착합니다. 엔지니어링 작업은 그것을 결정으로 바꾸는 일입니다.

시뮬레이션된 단백질 A 사이클

실제 10만 유로짜리 크로마토그래피 스키드를 노트북에 올려놓을 수는 없으므로, 동반 저장소에는 물리적으로 그럴듯한 사이클을 내보내는 결정론적 시뮬레이터가 들어 있습니다. 시뮬레이터의 핵심은 정직함입니다. 이 장의 모든 숫자는 여러분이 바이트 단위로 똑같이 재생성할 수 있는 파일에서 나옵니다. 단백질 A 친화성 포획(affinity capture)은 사실상 모든 CHO 유래 단일클론항체(monoclonal antibody, mAb)의 플랫폼 첫 단계 이므로 [3], 모델링하기에 적절한 대상입니다.

examples/sim/bioproc_sim/protein_a.py에서 사이클은 컬럼 부피(column volume) — 크로마토그래피의 자연스럽고 규모에 독립적인 시계(clock)(여기서 1 CV는 수지 베드 1리터이며, 항체가 달라붙는 작은 다공성 비드로 충전된 컬럼을 0.5 CV/min, 즉 분당 컬럼 부피의 절반에 해당하는 액체를 통과시키며 운전합니다) — 단위로 측정된 페이즈들의 목록으로 정의됩니다.

# examples/sim/bioproc_sim/protein_a.py
CV_ML = 1000.0 # column volume (mL); 1 L Protein A column
CV_PER_MIN = 0.5 # 0.5 CV/min -> 1 CV = 2 min

# phase -> (duration in CV)
PHASES = [
("Equilibration", 3.0),
("Load", 8.0), # load to ~80% of dynamic binding capacity to avoid breakthrough loss
("Wash", 4.0),
("Elution", 5.0),
("Strip", 3.0),
("CIP", 3.0),
]

시뮬레이터는 UV/전도도/pH 추적선을 페이즈 단위로 쌓아 올립니다. 짚어둘 만한 실제 크로마토그래피 물리 두 가지가 있습니다. 적재 중에는 끝부분 가까이에서 UV가 돌파 시그모이드(breakthrough sigmoid) 로 상승합니다 — 컬럼이 동적 결합 용량(dynamic binding capacity)을 향해 차오르고 있으며, 계속 적재하면 제품이 결합되지 않은 채 그대로 빠져나가기 시작합니다. 용출 중에는 낮은 pH 완충액이 항체를 컬럼에 붙들고 있던 단백질 A–Fc 결합을 끊어, 항체를 날카롭고 약간 끌리는 피크(peak)로 방출합니다.

# examples/sim/bioproc_sim/protein_a.py — the elution peak
lo, hi = seg("Elution")
emask = (cv >= lo) & (cv < hi)
x = cv[emask] - (lo + 0.8)
peak = 1850.0 * np.exp(-(x ** 2) / (2 * 0.45 ** 2)) * (1 + 0.5 * (x > 0) * np.exp(-x / 0.9))
uv[emask] = 4.0 + peak
ph[emask] = 3.3 + 0.4 * np.exp(-((cv[emask] - lo) / 1.5))

이것을 실행하면(python -m bioproc_sim.protein_a) 약 1 Hz의 추적선과 한 줄짜리 요약이 나옵니다. 다음은 datasets/protein_a_chromatogram.csv에 커밋된 첫 몇 행입니다 — 길고 정돈된, 정확히 히스토리언(historian)이 좋아하는 모양입니다(실제 크로마토그래피 시스템의 네이티브 결과 파일은 더 풍부한 독점(proprietary) 기록이며, 이 개방형 CSV는 거기서 내보낸 정돈된 형태입니다).

ts,time_s,volume_CV,UV280_mAU,conductivity_mS_cm,pH,phase,batch_id
2026-01-19 08:00:00+00:00,0,0.0,4.3,4.926,7.207,Equilibration,BATCH-2026-001
2026-01-19 08:00:01+00:00,1,0.0083,5.36,4.958,7.181,Equilibration,BATCH-2026-001
2026-01-19 08:00:02+00:00,2,0.0167,2.64,4.997,7.204,Equilibration,BATCH-2026-001

전체 사이클은 3,120행입니다. 흥미로운 행 — 용출 피크 — 은 CV 15.8 부근, pH 3.5에서 2,769.6 mAU 로 정점에 이릅니다. (pH는 용출 내내 낮은 pH 기울기로 — 대략 3.7에서 3.3까지 — 떨어지며, 피크 정점에서는 약 3.5입니다. 같은 단계를 두고 약 3.3과 약 3.5가 함께 인용되는 이유가 이것입니다.) 바로 그 하나의 숫자가, 이 장의 나머지가 작동시키고자 하는 대상입니다.

ISA-88 페이즈별로 주석이 달린 단백질 A 포획 크로마토그램으로, UV280, 전도도, pH 추적선과 용출 피크의 상승부 및 하강부 100 mAU 임계값 사이의 음영 처리된 풀링 윈도를 보여준다.

단일 단백질 A 결합-용출(bind-and-elute) 사이클. UV280(파란색)은 평형화, 적재, 세척 내내 기준선 근처에 머물다가 용출 중에 폭발하고, pH(초록색)는 항체를 방출하기 위해 약 3.3까지 떨어지며, 전도도(주황색)는 CIP 중에 치솟습니다. 음영 처리된 띠는 풀링 윈도 — 우리가 실제로 보관하는 용출액의 구간 — 입니다.

저자가 AI의 도움을 받아 직접 제작한 그림입니다.

크로마토그램은 행이 아니라 곡선이다

크로마토그램이 실제로 어떤 종류 의 것인지 정확히 짚어둘 가치가 있습니다. 그것이 어떻게 저장할 수 있는지를 좌우하기 때문입니다. 위의 정돈된 CSV는 편리하게 평탄화한 것이지만, 그 밑바탕의 객체는 행의 목록이 아니라 곡선 입니다. 용출 부피의 연속 함수로서의 흡광도이며, 다파장 검출기에서는 여러 곡선이 동시에 있는 것(각 파장의 UV 대 부피), 즉 작은 n차원 배열입니다. 진정한 정보의 대부분은 어느 한 시료가 아니라 그 곡선의 형태 — 선행 가장자리(leading edge), 피크 정점, 끌리는 어깨(tailing shoulder) — 안에 들어 있습니다.

이것은 이 책이 거듭 돌아오는 두 가지 이유에서 중요합니다. 첫째, BMIC 온톨로지 — 제조 용어와 그 관계의 의미를 고정하는, 공유되고 기계 판독 가능한 어휘(지식 그래프 장이 하나를 만들고, 4권 바이오의약품 제조를 위한 온톨로지가 온통 여기에 바쳐져 있습니다); IOF(Industrial Ontologies Foundry)가 OAGi/NIIMBL와 함께 2026-02에 공개 — 는 의도적으로 처방적(prescriptive) 입니다. 레시피, 규격(specification), 중요 공정 변수(critical process parameter)와 품질 속성(quality attribute)(CPP와 CQA — 여러분이 조절하는 다이얼과 반드시 맞춰야 하는 품질) — 회분이 무엇을 하기로 되어 있는지 — 를 모델링하며, 측정된 곡선 자체를 위한 클래스는 없습니다. 그래서 "크로마토그램을 BMIC에 저장한다"는 것은 불가능합니다. BMIC는 용출 단계의 CPP/CQA 목표를 알려주고, 추적선은 다른 곳에 자리합니다. 둘째, 전체 추적선을 충실한 원본으로 — 우리가 여기서 추출하는 몇 안 되는 파생 숫자(피크 높이, 풀 윈도)만이 아니라 — 보관하고 싶다면, 평탄한 JSON이나 CSV는 잘못된 컨테이너입니다. 배열 전체 를 위한 벤더 중립적 거처는 이 책이 분석 실험실: 기기, LIMS와 ELN(The Analytical Lab: Instruments, LIMS & ELN) 에서 다루는 바로 그 두 가지입니다. 스펙트럼, 크로마토그램, 곡선을 위해 정확히 만들어진 n차원 데이터 큐브(Data Cube)를 갖춘 Allotrope의 HDF5 기반 ADF, 또는 SeriesSet이 배열을 개방형 XML로 담는 AnIML 입니다. Allotrope ASM 는 JSON이고, JSON은 결과 — 2,769.6 mAU 피크나 1.917 CV 풀 같은 스칼라 — 를 위한 것이지, 그 뒤의 조밀한 곡선을 위한 것이 아닙니다. (그 장은 ADF, ASM, AnIML를 온전히 설명합니다. 여기서의 유일한 요점은, 우리가 PostgreSQL에 기록하는 결정을 담은 숫자와 보관용 곡선은 서로 다른 두 가지 일이며, 서로 다른 두 저장소가 맡는다는 것입니다.)

한 회분을 ISA-88 페이즈로 잘라내기

단백질 A의 여섯 페이즈 — 평형화(Equilibrate), 적재(Load), 세척(Wash), 용출(Elution), 스트립(Strip), CIP — 를 왼쪽에서 오른쪽으로 흐르게 배치하고 각각 컬럼 부피 윈도를 주석으로 단 다이어그램으로, 용출 박스에서 초록색 연결선이 내려와 15.0에서 16.917 CV의 풀링 윈도를 표시한다.

페이즈 상태 기계: 적재, 세척, 용출

이 사이클은 자유롭게 흐르는 추세가 아닙니다. 스키드 제어기가 행진해 통과하는 정해진 순서(sequence) 이며, 그 순서가 우리가 포착하는 모든 것의 척추입니다. 왼쪽에서 오른쪽으로 읽으면, 평형화 가 베드를 준비하고(0–3 CV), 적재 가 UV를 돌파 어깨까지 끌어올리며 항체를 결합하고(3–11 CV), 세척 이 느슨하게 결합된 불순물을 씻어내며(11–15 CV), 용출 이 pH를 떨어뜨려 제품을 날카로운 피크로 방출하고(15–20 CV), 이어 스트립CIP 가 컬럼을 재생합니다(20–26 CV). 각 전환은 제어기가 찍는 이산적 이벤트이며, 결정적으로 그 순서의 각 박스는 정확히 하나의 events.operation_event 행이 됩니다. 여섯 중 단 하나만이 결정을 담습니다. 풀링 윈도가 붙는 용출 행입니다. 나머지 다섯은 그 결정을 시간과 정체성 안에 위치시키는 순수한 장부 정리입니다. 이것이 이 모델이 추세선이 아니라 상태 기계(state machine) 인 이유입니다. 질문은 결코 "지금 값이 얼마인가?"가 아니라 "우리는 어느 이름 붙은 상태에 있으며, 그 상태에 있는 동안 무엇을 결정했는가?"입니다.

실제 플랜트에서는 스키드 제어기가 자신이 어느 단계에 있는지 이미 알고 있으며, 각 시료에 단계/페이즈 라벨을 찍어둡니다. 저장소의 시뮬레이터도 똑같이 합니다. 추적선의 모든 행은 phase 열을 지니고 있습니다. 여러분에게 여전히 필요한 것은, 그 조밀한 시료별 라벨을 배치 기록이 신경 쓰는 몇 안 되는 연속 윈도로 바꾸는 일입니다 — 그래서 저장소에는 정확히 그 일을 하는, 작고 따분하지만 견고한 압축기(collapser)가 들어 있습니다. 추적선을 따라 걸으면서, 라벨이 바뀌는 곳마다 새 페이즈 윈도가 시작됩니다.

(신호만으로의 재구성 — 오래된 스키드나 병합된 데이터에서 맞닥뜨릴 수 있듯, 단계 라벨이 전혀 없을 때 원시 UV/전도도/pH 추적선에서 페이즈 윈도를 유도하는 일 — 은 진정으로 더 어려운 문제이며 여기서는 범위를 벗어납니다. 아래 코드는 시료별 단계 라벨이 있다고 가정합니다. 물리로부터 페이즈를 추론하지는 않습니다.)

examples/chapters/10-downstream-chromatography/phase_detect.py에서.

# examples/chapters/10-downstream-chromatography/phase_detect.py
def detect_phases(trace: pd.DataFrame) -> pd.DataFrame:
"""Collapse the per-sample phase labels into contiguous phase windows."""
t = trace.copy()
# a new phase starts wherever the label changes
t["grp"] = (t["phase"] != t["phase"].shift()).cumsum()
rows = []
for _, g in t.groupby("grp"):
rows.append({
"phase": g["phase"].iloc[0],
"start_ts": g["ts"].iloc[0],
"end_ts": g["ts"].iloc[-1],
"start_CV": round(float(g["volume_CV"].iloc[0]), 3),
"end_CV": round(float(g["volume_CV"].iloc[-1]), 3),
"max_UV_mAU": round(float(g["UV280_mAU"].max()), 1),
})
return pd.DataFrame(rows)

(label != label.shift()).cumsum() 트릭이 핵심의 전부입니다. 페이즈 이름이 이전 행과 달라질 때마다 누적합이 1씩 올라가며, 동일한 라벨이 연속된 각 구간에 groupby 할 수 있는 고유한 그룹 ID를 부여합니다. 커밋된 추적선에 대해 python chapters/10-downstream-chromatography/phase_detect.py를 실행하면 정확히 다음이 출력됩니다.

phase start_CV end_CV max_UV_mAU
Equilibration 0.000 2.992 7.3
Load 3.000 10.992 64.0
Wash 11.000 14.992 35.2
Elution 15.000 19.992 2769.6
Strip 20.000 22.992 45.9
CIP 23.000 25.992 8.1

여섯 개의 페이즈, 각각 깔끔한 CV 윈도와 그 피크 UV를 갖췄습니다. 진단적 가치가 이미 떨어져 나오는 것을 눈여겨보세요. 적재 페이즈의 최대 UV인 64 mAU는 돌파 어깨(breakthrough shoulder) — 컬럼이 포화에 다가가고 있다는 조용한 조기 경보 — 입니다. 회분이 DBC의 약 81%(용량이 58 g인 컬럼 — 1 L 베드를 58 g/L로 — 에 47 g 적재)에서 적재를 멈추기 때문에 돌파 누출은 작게 유지됩니다 — 그러나 그 어깨 너머로 밀어붙이면 결합되지 않은 항체가 곧장 폐기로 흘러가며, 이것이 적재를 용량 아래로 제한하는 이유 전부입니다. 운영 환경에서는 이것에 알람을 걸 것입니다. (이 장의 pytest인 test_ch10_phase_detection_and_pooling은 이 압축이 정확히 이 여섯 페이즈에 순서대로 떨어지는지를 단언합니다.)

중요한 결정: 풀링 윈도

페이즈를 검출하는 일은 장부 정리입니다. GMP에 결정적인 결정 은 풀링(pooling)입니다. 용출 중에 떨어져 나오는 모든 액체 가운데, 어느 구간을 제품 풀(pool)로 거두고 어느 구간을 폐기로 보낼 것인가? 너무 일찍 거두면 불순물을 같이 가져오고, 너무 늦게 거두면 풀이 희석되거나 수율을 잃습니다. 이것이 가장 순수한 형태의 공정 분석 기술(process analytical technology, PAT) 입니다 — 오프라인 분석을 기다리는 대신, 실시간 UV 측정이 공정 중(in-process) 제어 결정을 이끄는 것 [4] — 이며, 학술 문헌은 이제 온라인 분석이 제품 품질 속성에 맞춰 컬럼 풀링 결정을 이끌고, 그 속성은 반드시 검증된 범위(공정을 실제로 운전하기에 앞서, 사전에 한계를 입증하고 문서화하여 합격 가능한 제품을 신뢰성 있게 내도록 한 범위) 안에 들어와야 한다고 명시적으로 밝힙니다 [5].

고전적이고 견고한 규칙은 UV 임계값 풀링(UV-threshold pooling) 입니다. 상승부에서 UV가 임계값을 넘으면 거두기를 시작하고, 하강부에서 임계값 아래로 다시 떨어지면 멈춥니다. 더 새로운 플랜트는 인라인 UV/Vis 다변량 보정(multivariate calibration, 흔히 스펙트럼 디컨볼루션(spectral deconvolution)이라 불림)을 사용하여 원시 흡광도가 아니라 농도 (심지어 불순물 함량까지)에 맞춰 풀링하며, 오프라인 피크 면적 적분보다 단계 수율을 훨씬 정확하게 계산합니다 [6] — 5권이 바로 이 크로마토그램을 가지고 분석 방법수집 크로마토그래피에서 발전시키는, 스펙트럼으로 학습하는 방법입니다. 저장소는 단순하고 방어 가능한 임계값 버전을 구현합니다.

# examples/chapters/10-downstream-chromatography/phase_detect.py
POOL_THRESHOLD_MAU = 100.0 # start/stop collecting the eluate at 100 mAU

def pooling_decision(trace: pd.DataFrame) -> dict:
"""Collect the elution peak between up-slope and down-slope UV thresholds."""
elute = trace[trace["phase"] == "Elution"]
above = elute[elute["UV280_mAU"] >= POOL_THRESHOLD_MAU]
if above.empty:
return {"pooled": False}
start_cv = float(above["volume_CV"].iloc[0])
stop_cv = float(above["volume_CV"].iloc[-1])
return {
"pooled": True,
"pool_start_CV": round(start_cv, 3),
"pool_stop_CV": round(stop_cv, 3),
"pool_CV": round(stop_cv - start_cv, 3),
"threshold_mAU": POOL_THRESHOLD_MAU,
"peak_UV_mAU": round(float(elute["UV280_mAU"].max()), 1),
}

우리 회분에서는 다음을 반환합니다.

pooling: {'pooled': True, 'pool_start_CV': 15.0, 'pool_stop_CV': 16.917,
'pool_CV': 1.917, 'threshold_mAU': 100.0, 'peak_UV_mAU': 2769.6}

우리는 CV 15.0과 CV 16.92 사이의 1.917 CV 구간 — 약 1.9리터의 용출액 — 을 보관합니다. (요약 CSV의 pool_volume_mL 1916.7과 검출기의 pool_CV 1.917은 같은 윈도입니다. CSV의 소수점 둘째 자리 반올림에서 16.92 − 15.0, 검출기의 소수점 셋째 자리에서 16.917 − 15.0일 뿐이며, 아래 적재 역가 각주가 5.5와 5.88을 조정하는 것과 똑같습니다.) 바로 그 단 하나의 기록이 검사관의 질문에 답합니다. 시뮬레이터가 기록하는 요약(datasets/protein_a_summary.csv)은 그 주위로 물질 수지(mass balance)를 닫아줍니다.

batch_id,step,column,cv_mL,load_titer_g_L,load_volume_L,mass_loaded_g,pool_start_CV,pool_stop_CV,pool_volume_mL,DBC_g_per_L,recovery_frac,eluted_mass_g,eluate_titer_g_L
BATCH-2026-001,ProteinA_capture,MabSelect-PA01,1000.0,5.88,8.0,47.0,15.0,16.92,1916.7,58.0,0.92,43.3,22.58

코드를 직접 돌려본다면 알아둘 점 하나. 커밋된 datasets/protein_a_summary.csv캠페인(campaign) 실행(make data)으로 생성되며, 이는 유가식(fed-batch) 라인의 최종 역가(titer) 약 5.88 g/L를 simulate(load_titer_g_L=5.88)에 넣습니다. 모듈을 그대로 부르는 명령 python -m bioproc_sim.protein_a는 함수 자체의 기본값 load_titer_g_L=5.5를 사용하므로 약간 다른 요약 행을 출력합니다(mass_loaded 44.0 g, eluted 40.5 g, eluate_titer 21.12 g/L). 크로마토그램 추적선과 풀링 숫자 — 피크 2,769.6 mAU, 풀 15.0 → 16.917 — 는 어느 쪽이든 동일합니다. 적재 역가는 UV 추적선이 아니라 물질 수지만 스케일하기 때문입니다.

47.0 g의 항체가 적재되고, 동적 결합 용량은 58 g/L, 단계 회수율 92%, 그리고 43.3 g이 22.58 g/L로 용출 되어 — 적재 대비 거의 4배(22.58 / 5.88 ≈ 3.8배) 농축되었습니다. 시뮬레이터의 물질 수지는 의도적으로 정직합니다. 컬럼이 실제로 결합한 것만 용출할 수 있으므로, 용출 질량은 회수율을 적용하기 전에 min(mass_loaded, DBC × CV)로 상한이 걸립니다(protein_a.pyeluted_g = bound_g * recovery를 보세요). 적재한 것보다 더 많이 회수했다고 암시하는 풀링 결정은 버그일 것입니다. 이 장의 pytest(test_ch10_phase_detection_and_pooling)는 이 결정 쪽을 지킵니다. 회분이 풀링되었는지, 그리고 제품 피크가 1000 mAU를 넘는지를 단언합니다.

풀링이 잘못될 때: 현장 기록

우리가 attributes 페이로드에 공을 들이는 이유는, 풀링 결정이 바로 다운스트림 배치가 손실되는 지점이며, 그 기록이 나중에 벌어지는 논쟁에서 살아남는 유일한 것이기 때문입니다. 두 가지 실패 양상을 짚어둘 가치가 있습니다.

첫째는 엉뚱한 곳에 놓인 윈도 입니다. 몇 CV 너무 일찍 풀링하면 선행 가장자리의 불순물 — 응집체(aggregate), 숙주 세포 단백질 — 을 제품에 끌어들이고, 너무 늦게 풀링하면 풀을 희석하고 수율을 흘립니다. PAT 문헌은 이제 온라인 분석이 반드시 검증된 범위 안에 들어와야 하는 제품 품질 속성에 맞춰 이 컬럼 풀링 결정을 이끈다고 명시적으로 밝힙니다 [5] — 그래서 방어 가능한 기록은 단지 "우리는 풀링했다"가 아니라 "우리는 검증된 띠 안인 pool_start_CV 15.0과 pool_stop_CV 16.917 사이에서 풀링했다"입니다. 원시 흡광도 임계값은 배치마다 피크 모양이 흐를 때 속을 수도 있으며, 바로 그것이 더 새로운 플랜트가 원시 mAU가 아니라 인라인으로 측정된 농도 에 맞춰 풀링하여 임계값 규칙보다 단계 수율을 훨씬 정확하게 계산하는 이유입니다 [6]. 어느 쪽이든, 저장된 threshold_mAUpeak_UV_mAU가 검토자가 윈도가 그곳에 떨어졌는지를 재구성하게 해주는 것입니다.

둘째는 윈도가 옳았더라도 신뢰할 수 없는 기록 입니다. PIC/S PI 041-1(국제 GMP 사찰기관들의 데이터 무결성 지침)은 풀링과 유지 시간 기록이 반드시 귀속 가능하고(attributable), 동시기적이며(contemporaneous), 완전해야(complete) 한다고 — 결정이 벌어진 대로, 식별 가능한 행위자에 의해, 누락 없이 기록되어야 한다고 — 단호하게 말합니다 [11]. 페이즈 검출기가 라이브로 기록하고, 추적선에서 가져온 start_ts/end_ts로 찍히며, 외래 키로 BATCH-2026-001PA01에 묶인 operation_event 행은 구성 자체로 동시기적귀속 가능 을 만족합니다. 다음 날 아침 기술자가 스프레드시트에 옮겨 적은 숫자는 둘 다 만족하지 못합니다. 그리고 최대 유지 시간이나 범위를 벗어난 풀링 이벤트는 정해진 한계를 가진 공정 중 제어이므로, 그 위반은 각주가 아니라 기록해야 하는 공정 중 제어 실패입니다 [8]excursion 이벤트 유형이 정확히 이를 위해 존재하며, 그것이 품질로 라우팅되는 곳이 5부입니다.

기록하기: PostgreSQL의 오퍼레이션 이벤트

페이즈와 풀링 결정은 그것들이 속한 배치 곁에 저장되지 않으면 쓸모가 없습니다. 저장소의 관계형 척추(PostgreSQL 17, examples/platform/compose/compose.yaml에 고정된 timescale/timescaledb:2.17.2-pg17 이미지)에는 바로 이를 위해 만들어진 테이블 하나가 있습니다 — 시계열(time-series) 스트림과 배치 맥락 사이의 다리입니다.

examples/platform/db/30-lab-events.sql에서.

-- examples/platform/db/30-lab-events.sql
CREATE TABLE events.operation_event (
event_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
batch_id text REFERENCES s88.batch,
unit_id text REFERENCES s88.unit,
event_type text NOT NULL, -- phase | pool | hold | excursion
phase text,
start_ts timestamptz NOT NULL,
end_ts timestamptz,
attributes jsonb NOT NULL DEFAULT '{}'
);
CREATE INDEX ON events.operation_event (batch_id, start_ts);

그 모양은 의도적입니다. 구조화된 열들(batch_id, unit_id, phase, 시간 윈도)은 여러분이 언제나 필터링하고 조인하는 것들을 담고, 개방형 attributes jsonb는 이벤트 유형에 따라 달라지는 이벤트별 페이로드(payload)를 담습니다 — 용출의 풀링 윈도, 필터의 무결성 시험 결과, 유지의 지속 시간입니다. 페이즈 검출기는 페이즈당 한 행을 내보내고, 풀 윈도를 용출 행에 붙입니다.

# examples/chapters/10-downstream-chromatography/phase_detect.py
phases["event_type"] = "phase"
# attach the pool window to the Elution row
phases["attributes"] = phases["phase"].map(
lambda p: pool if p == "Elution" else {})

그래서 Postgres에 안착하는 용출 행은 다음과 같습니다 — 구조화된 맥락에, 검토자(또는 이 책의 뒷부분에 나올 SPARQL 질의)가 스키마 마이그레이션(schema migration) 없이도 읽을 수 있는 자기 서술적 JSON 페이로드가 더해진 모습입니다.

{
"batch_id": "BATCH-2026-001",
"unit_id": "PA01",
"event_type": "phase",
"phase": "Elution",
"start_ts": "2026-01-19T08:30:00Z",
"end_ts": "2026-01-19T08:39:59Z",
"attributes": {
"pooled": true, "pool_start_CV": 15.0, "pool_stop_CV": 16.917,
"pool_CV": 1.917, "threshold_mAU": 100.0, "peak_UV_mAU": 2769.6
}
}

이것이 우리가 여기서 히스토리언이 아니라 PostgreSQL을 사용하는 이유의 핵심입니다 [7] — 히스토리언은 원시의 고빈도 센서 스트림을 저장하는 시계열 데이터베이스입니다. 히스토리언은 수백만 개의 원시 시료를 담지만, 이 테이블은 배치 기록과 감사 추적(audit trail)이 실제로 신경 쓰는 해석되고 결정을 담은 몇 안 되는 기록을 담습니다.

operation_event의 해부: 행 하나로서의 풀링 결정 하나

그 용출 행을 필드 하나하나 천천히 해부해볼 가치가 있습니다. 그 위의 모든 열이 짐을 지고 있으며, 구조화된 열과 attributes jsonb 사이의 분할이 곧 설계의 전부이기 때문입니다.

events.operation_event 행 하나를 해부하는 신원 카드로, 구조화된 열 event_id, batch_id, unit_id, event_type, phase, start_ts, end_ts가 위에 있고 그 아래로 풀링 페이로드를 담은 강조된 attributes jsonb 블록이 있으며, 보라색 패널이 s88.batch와 s88.unit으로 향하는 두 외래 키 엣지를 해독한다.

events.operation_event의 행 하나 — 용출 풀링 결정. 일곱 개의 구조화된 열은 여러분이 언제나 조인하고 필터링하는 것들을 담고, 초록색 attributes jsonb 블록은 이벤트 유형에 따라 달라지는 이벤트별 페이로드 — 여기서는 풀링 윈도 — 를 담습니다.

저자가 AI의 도움을 받아 직접 제작한 그림입니다.

30-lab-events.sql의 스키마에서 열을 따라 걸으면.

  • event_idbigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY. 데이터베이스가 발급하며, 여러분은 결코 제공하지 않습니다. 감사 항목이나 다운스트림 조인이 가리키는 안정적인 핸들입니다.
  • batch_idtext REFERENCES s88.batch. GMP 배치 기록으로 향하는 외래 키(foreign key) 입니다. BATCH-2026-001seed_cho_line.sql에 시드된, 출하된 골든 배치(lot L26001)입니다. 이 FK가 존재하지 않는 배치에 대해서는 결코 풀링 결정을 기록할 수 없도록 보장합니다.
  • unit_idtext REFERENCES s88.unit. 장비 계층으로 향하는 또 다른 외래 키입니다. PA01단백질 A 포획 스키드 (DOWNSTREAM 영역의 Cytiva ÄKTA 프로세스 유닛)로 해석됩니다. 이것이 3MCC를 작동시키는 열입니다. 세 개의 컬럼은 그저 하나의 batch_id 아래의 세 unit_id 값일 뿐입니다.
  • event_typetext NOT NULL. 판별자(discriminator)입니다. phase | pool | hold | excursion. 검토자(그리고 질의)에게 attributes에서 어떤 모양 의 페이로드를 기대할지 알려줍니다. 여기서는 phase입니다.
  • phasetext(nullable). ISA-88 단계 이름 — Elution — 으로, JSON을 파싱하지 않고도 WHERE phase = 'Elution' 할 수 있도록 구조화된 열로 복사되어 있습니다.
  • start_ts / end_tstimestamptz. 윈도입니다. start_tsNOT NULL(이벤트는 시작을 가져야 합니다)이고, end_ts는 nullable이어서 아직 진행 중인 열린 페이즈를 닫히기 전에 기록할 수 있습니다.
  • attributesjsonb NOT NULL DEFAULT '{}'. 개방형 페이로드입니다. 용출이 아닌 다섯 행에서는 빈 객체 {}이고, 용출 행에서는 풀링 결정 전체를 담습니다. pooled, threshold_mAU, peak_UV_mAU, pool_start_CV, pool_stop_CV, pool_CV. 그것이 카드의 초록색 블록입니다 — 이 행을 장부 정리에서 증거 로 바꾸는 그 한 필드입니다.

카드가 가르치는 교훈은 이 책이 되풀이하는 그것입니다. 여러분이 언제나 필터링하는 것은 열에, 이벤트에 따라 달라지는 것은 jsonb에 넣어라. 여러분은 풀링 이벤트를 항상 그 배치와 유닛에 조인하고 페이즈와 시간으로 잘라낼 것이므로 — 그것들은 인덱싱된((batch_id, start_ts)) 열입니다. 여러분은 풀링 윈도를 읽기 는 하지만 pool_CV로 데이터베이스를 필터링 하는 일은 드물 것이므로 — 그것은 jsonb에 자리합니다. 거기서는 새로운 이벤트 유형(hold, 필터 integrity 시험)이 스키마 마이그레이션 없이 자신의 키를 가져올 수 있습니다.

같은 행을 트리플로, 형상으로, 역량 질문으로

용출 행은 관계형 사실이지만, 지식 그래프 장은 같은 사실을 RDF(Resource Description Framework, 사실의 단위가 주어-술어-목적어 트리플(triple) 인 그래프 데이터 모델)로 들어 올리며, 거기서는 외래 키가 걸어갈 수 있는 엣지가 됩니다. 그러면 풀링 결정은 한 테이블에 매달린 jsonb 덩어리가 아니라 용출 이벤트에 관한 작은 트리플 묶음이 되며, 바로 그 재구성이 다운스트림 조사가 시스템들을 가로질러 질의하게 해줍니다. 두 트리플이 짐을 집니다. 하나는 이벤트를 그 유닛에 묶고, 하나는 윈도를 기록합니다.

# the pooling decision as RDF triples (bp: is the local bioprocess vocabulary)
bp:ev-PA01-elution-001 bp:onUnit bp:PA01 ;
bp:pooledFrom 15.0 ;
bp:pooledTo 16.917 ;
bp:partOfBatch bp:BATCH-2026-001 .

그래프는 bp:partOfBatchbp:onUnit을 암시된 조인이 아니라 엣지로 저장하므로, 시맨틱 장의 SPARQL 프로퍼티 경로는 실패한 원료 의약품 로트에서 이 풀링 이벤트를 거쳐 그것을 만든 배치와 컬럼까지 곧장 걸어갑니다 — 4권 바이오의약품 제조를 위한 온톨로지가 로트 계보의 척추로 삼는 디지털 스레드 순회입니다.

닫힌 세계(closed-world) 절반 — 용출 이벤트가 실제로 풀링 윈도를 기록했는가? — 이야말로 SHACL(Shapes Constraint Language, 형상 제약 언어 — 그래프가 요구된 구조를 갖췄는지 검증하는 W3C 표준) 형상(shape)이 검사하는 것이며, 4권 출하 관문 장이 출하 결정 전체를 거기서 세우는 그 관문입니다. 열린 세계 추론기는 누락된 pooledFrom을 그저 알 수 없음으로 다루지만, SHACL은 그것을 지금 당장의 실패로 다룹니다 — 풀링 윈도가 없는 풀링 이벤트는 열린 질문이 아니라 불완전한 배치 기록이기에, 바로 GMP 기록이 필요로 하는 규율입니다.

# an elution event must carry exactly one pooling window (closed-world)
bp:PoolingShape a sh:NodeShape ;
sh:targetClass bp:ElutionEvent ;
sh:property [ sh:path bp:pooledFrom ; sh:minCount 1 ; sh:maxCount 1 ;
sh:message "Elution event is missing its pooling-window start." ] ;
sh:property [ sh:path bp:pooledTo ; sh:minCount 1 ; sh:maxCount 1 ;
sh:message "Elution event is missing its pooling-window stop." ] .

그 형상은 데이터베이스 자신의 NOT NULL과 이 장의 test_ch10_phase_detection_and_pooling 단언을 그래프 방식으로 다시 진술한 것입니다. 세 가지 산출물 — 열 제약, 단위 시험, SHACL 형상 — 이 하나의 규칙을 강제합니다. 그리고 검토자가 실제로 그래프에 던지는 질문 — 어느 배치가 검증된 띠 밖에서 풀링했는가? — 은 역량 질문(competency question)(어휘가 반드시 답할 수 있어야 하는 질의를 가리키는 온톨로지 엔지니어의 용어로, 인수 시험으로 쓰임)이며, bp:pooledFrom/bp:pooledTo를 14.5–17.5 CV 윈도와 대조해 거르는 한 줄짜리 SPARQL ASKSELECT로 답할 수 있습니다. 4권은 그러한 질문 23개를 실행 가능한 PASS/FAIL 점검으로 바꾸는데, 풀링 윈도는 그 점검들이 딛고 설 수 있는 또 하나의 사실입니다. 같은 bp:onUnit 엣지가 (아래의) 3MCC를 스키마 변경이 아니라 그래프 문제로 만드는 것입니다. 세 개의 컬럼은 세 개의 bp:PA01/bp:PA02/bp:PA03 노드이고, 누적 풀에서 각 컬럼의 기여로 향하는 bp:derivedFrom 엣지가 전환을 가로질러 계보를 온전히 유지합니다.

이 행은 삼부작에서 어디서 오는가

operation_event 행은 생산 현장에서 내려진 물리적 결정의 오픈 소스 구현입니다. 그것이 서술하는 컬럼과 크로마토그램은 1권이 수집 크로마토그래피폴리싱 크로마토그래피 에서 만드는 포획과 폴리싱 단계이며, 같은 event_type 열거형은 이후 수확, 바이러스 불활화, 바이러스 여과의 유지 및 여과 단계를 기록합니다. 결정을 담은 UV 추적선 그 자체 — 우리가 방금 기록한 풀링 윈도가 나온 그 크로마토그래피 데이터 스테이션 — 는 2권이 공정 데이터가 태어나는 곳을 둘러보기 에서 틀 짓는 데이터 포인트입니다. 1권은 물리적 단계이고, 2권은 데이터 포인트와 그 열린 과제이며, 이 장은 그 고리를 닫는 코드와 행입니다.

유지 시간, 무결성 시험, 그리고 나머지 공정 열차

단백질 A는 첫 스키드일 뿐입니다. 같은 operation_event 패턴이 다운스트림 공정 열차(train)의 나머지를 기록하며, event_type 열거형(phase | pool | hold | excursion)은 바로 그것을 위해 만들어졌습니다.

  • 바이러스 불활화 는 낮은 pH 유지입니다 — 용출액을 검증된 낮은 pH 설정점(여기서는 약 3.5)으로 적정(titrate, 산을 더해 조정)한 뒤 검증된 최소 시간 동안 유지합니다. 낮은 pH가 세포 배양에서 묻어왔을 수 있는 외피 보유 바이러스(enveloped virus)를 불활화하여, 공정 열차에 전용 바이러스 안전 장벽을 제공하기 때문입니다. 그 유지에는 다음 단계로 넘어가기 전 정해진 최대 유지 시간 이 있으며, 최소와 최대 모두가 GMP에 결정적이고, 반드시 검증되어야 하며, 반드시 기록되어야 합니다 [8]. start_ts, end_ts, 그리고 {"target_min": 60, "actual_min": 64, "pH": 3.5}라는 attributes 페이로드를 갖춘 hold 이벤트가 증거 추적의 전부입니다.
  • 여과 (바이러스 여과, 멸균 여과)는 합격/불합격이 이산적이고 기록되는 이벤트인 사용 전/사용 후 무결성 시험 을 만들어냅니다. {"test": "bubble_point", "psi": 51, "spec_min_psi": 45, "result": "pass"}. 스키드의 압력과 유량 추세는 히스토리언으로 가고, 판정 은 여기로 옵니다.
  • 폴리싱 (양이온/음이온 교환)은 또 다른 결합-용출 또는 통과(flow-through) 크로마토그래피 단계입니다 — 같은 페이즈 검출 및 풀링 코드가, 다만 다른 임계값으로 적용됩니다.
  • UF/DF 는 원료 의약품을 농축하고 완충액을 교환합니다. 기록되는 결정은 충족된 농축 계수(concentration-factor)와 정용 부피(diavolume) 목표입니다.

정직한 메모. 최대 유지 시간 위반 은 정확히 품질 부서로 떠올라야 하는 종류의 excursion 이벤트입니다. 그것을 포착하는 일은 쉽습니다. 그것을 라우팅하고, 조사하고, 처분(disposition)하는 워크플로(workflow)가 규제 대상이며, 우리는 그것을 5부에서 만듭니다.

임계값이 학습된 모델이 될 때

100 mAU 규칙은 고정된 상수이며, 고정된 상수는 "그것이 승인된 숫자인가?"를 넘어선 검증이 필요 없습니다. 플랜트가 그 대신 인라인으로 측정된 농도 에 맞춰 풀링하는 순간 — 이 분야가 옮겨 가고 있는 스펙트럼 디컨볼루션 경로 — 임계값은 학습된 모델(인라인 UV/Vis 스펙트럼을 농도로 매핑하는 부분 최소 제곱(PLS) 등의 검정선)로 대체되며, 그 모델은 5권 바이오제조를 위한 머신러닝과 AI가 온통 바치는 검증 장치 전체를 물려받습니다. 무엇이 바뀌는지를 정확히 짚어둘 가치가 있습니다. 그 변화는 과소평가하기 쉽기 때문입니다.

첫째, 그것이 작동함을 어떻게 증명하는가 입니다. 농도 검정선은 크로마토그램 하나의 행을 무작위로 나누어 채점할 수 없습니다. 단일 용출 피크 위의 이웃한 시료들은 거의 동일하므로, 한 회분 안에서의 분할은 모델이 거의 중복된 점들 사이를 내삽(interpolate) 하게 하여, 새 배치에서는 살아남지 못할 우쭐한 정확도를 보고합니다. 정직한 검정은 배치 전체 를 떼어 둡니다 — 배치별 leave-one-batch-out 교차 검증(grouped cross-validation) — 그래서 점수는 모델이 본 적 없는 컬럼 회분에서 측정되며, 그것이 검토자가 믿어야 할 유일한 숫자입니다. 이것은 5권의 데이터 장모델과 검증 장이 소프트 센서 논증 전체를 세우는 바로 그 누출(leakage) 함정이자 그 배치별 분할 규율이며, 이 장의 저장소가 일부러 고정 임계값 버전을 싣는 이유입니다. 상수에는 누출될 폴드(fold)가 없습니다.

둘째, 언제 그것을 믿지 말아야 하는지 아는 것 입니다. 학습된 풀링 모델은 그것이 검정된 피크 모양, 완충액, 수지 연령의 영역 안에서만 유효합니다. 오염된 수지나 규격을 벗어난 적재는 회분을 그 엔벨로프 밖 으로 밀어낼 수 있고, 거기서 모델은 소리 없이 자신만만하게 외삽합니다. 그 방어 장치가 적용 범위(applicability domain) 점검 — Hotelling T²와 제곱 예측 오차(SPE) 검정, 출하 장의 그 관문 — 으로, 농도가 작동되기 전에 학습 엔벨로프 밖에 놓인 스펙트럼에 깃발을 들어, 분획 수집기 밸브가 분포 밖(out-of-distribution) 숫자에 결코 움직이지 않게 합니다. 고정 임계값에는 조잡한 대응물이 있지만(저장된 peak_UV_mAU가 검토자에게 잘못된 높이의 피크를 보여줍니다), 학습된 모델은 정식 관문이 필요합니다.

셋째, 그것이 늙어가는 것을 지켜보는 것 입니다. 이 모델이 읽는 피크는 순전히 물리적 이유로 드리프트합니다 — 수지가 수명에 걸쳐 오염되고, 새 원료 로트가 기준선을 옮기며, 규모 변경이 유체역학을 바꿉니다 — 이것이 분석 장이 이미 SPC로 차트화하는 진짜 공정 드리프트(process drift) 위에 얹힌 공변량 변화(covariate shift)(입력 분포의 이동)입니다. 둘은 별개이며 모두 중요합니다. 풀링된 풀 역가에 대한 관리도는 공정 의 방황을 잡고, 인라인 스펙트럼에 대한 모집단 안정성 지수(population-stability index)는 모델의 입력 의 방황을 잡으며, 흔히 더 일찍, 오프라인 검정을 기다리지 않고 잡습니다 — 5권 MLOps 장이 세우는 선행-대-후행 검출기 쌍입니다. 그리고 GMP 모델은 반드시 잠금(locked) 되어야 — 오늘의 풀링 결정이 검증된 바로 그것이 되도록 변경 관리 아래 고정되어야 — 하므로, 그 계보는 풀링 이벤트 곁의 기록에 속합니다. 어느 모델 버전이, 어느 데이터셋 해시로 학습되어, 이 윈도를 결정했는가. attributes jsonb가 그 고정의 자연스러운 거처입니다({"model": "pool-conc-pls@v2", "dataset_sha256": "…"}). 이는 풀링 행을 모델과 검증 장과 4권 인스턴스 그래프가 서술하는 감사 가능한 모델 계보 그래프 안의 노드로 바꿉니다. 고정 임계값에는 이 가운데 아무것도 필요 없고, 학습된 모델에는 이 모두가 필요합니다 — 그것이 대부분의 플랜트가 여전히 상수로 풀링하고, 정확도가 검증 부담을 진정으로 갚는 곳에서만 모델로 손을 뻗는 정직한 이유입니다.

강화된 변형: 다컬럼 연속 포획

지금까지 이 장 전체는 배치 컬럼을 설명합니다. 컬럼 하나, 사이클 하나, 사이클 사이에는 유휴 상태입니다. 현대적이고 강화된 대안은 다컬럼 연속 포획(multi-column continuous capture, 3MCC / PCC) 입니다 — 세 개 또는 네 개의 작은 단백질 A 컬럼을 배관으로 연결하여, 하나가 용출하는 동안 다음 컬럼이 적재하면서 첫 번째에서 나오는 돌파를 포획합니다. 이것이 관류(perfusion) 바이오리액터(끝에 한 번의 유가식 대신 제품을 꾸준히 수확하는 연속 바이오리액터 모드)가 공급하는 대상이며, 다운스트림 컬럼이 새어 나온 제품을 잡아준다는 것을 알기에 일부러 돌파 너머 까지 적재할 수 있으므로 수지 활용도 — 각 컬럼의 결합 용량을 실제로 얼마나 쓰는지 — 를 극적으로 개선합니다.

데이터 포착의 관점에서 3MCC는 한 가지를 근본적으로 바꿉니다. 깔끔한 페이즈 순서 하나 대신, 같은 순간에 서로 다른 페이즈에 있는 여러 컬럼을 갖게 되며, 제어기가 그들 사이의 전환(switch) 이벤트를 조율합니다. operation_event 모델은 이를 변경 없이 처리합니다. 모든 행이 자신의 unit_id를 지니므로, 세 개의 동시 페이즈 타임라인(컬럼당 하나, 예: PA01, PA02, PA03)은 같은 batch_id 아래의 세 개 행 스트림이 될 뿐입니다. (시드는 s88.unit에 단일 컬럼 유닛 PA01만 프로비저닝합니다. operation_event.unit_ids88.unit에 대한 외래 키(foreign key)이므로, 실제 3MCC 실행은 그 행들이 삽입되기 전에 먼저 PA02와 PA03을 거기에 추가해야 할 것입니다.) 풀링 로직은 한 단계 위로 올라갑니다. 피크 하나가 아니라 컬럼 전환을 가로지르는 누적 제품 스트림을 풀링합니다. 저장소의 유가식 경로는 위의 단일 컬럼 시뮬레이터를 사용하며, 3MCC 실행에서는 같은 단일 컬럼 검출기를 컬럼마다 그대로 적용하면 됩니다.

왜 중요한가

다운스트림은 분자가 약물이 되는 곳이자, 대부분의 수율 과 대부분의 위험 이 자리하는 곳입니다. 2 CV만큼 너무 늦게 내린 풀링 결정은 숙주 세포 단백질(host-cell-protein) 규격에서 탈락할 수 있고, 놓친 유지 시간은 25만 달러어치의 배치를 폐기시킬 수 있습니다. 우리가 추적선을 ISA-88 페이즈로 분할하고 구조화된 이벤트 행을 기록하는 수고를 들이는 이유는, 그 행들이 바로 배치 기록 검토, 규격 이탈(out-of-specification, OOS) 조사, 그리고 검사관 모두가 읽는 것이기 때문입니다. 원시 UV 시료의 벽은 그 자체로는 아무것도 증명하지 못합니다. "작업자는 검증된 14.5–17.5 CV 윈도 안인 15.0과 16.9 CV 사이에서 풀링했다" 라는 문장 — 추적선으로부터 자동으로 재구성된 — 이 바로 그 증거입니다.

실제 현장에서는

상업용 mAb 플랜트에서 크로마토그래피 스키드는 보통 Cytiva ÄKTA 프로세스 시스템이나 그에 준하는 것이며, 기록의 데이터는 벤더의 크로마토그래피 데이터 시스템(chromatography data system, CDS)과 MES(Manufacturing Execution System, 제조 실행 시스템) 배치 기록에 자리합니다. 우리의 OSS 스택은 그것들을 대체하지 않습니다 — 그 곁에서 맥락화(contextualize) 하고 히스토리화(historize) 합니다. 그 경계는 이 책에서 되풀이되는 정직함의 주제입니다.

실제 현장의 닻 몇 개.

  • 데이터 포맷: 벤더 CDS 익스포트는 크로마토그램을 독점 파일에 가둡니다. 대부분의 독자가 마주칠 대표적인 예는 ÄKTA 시스템을 구동하는 제어·CDS 소프트웨어인 Cytiva UNICORN(널리 보급된 여러 플랫폼 가운데 하나일 뿐)입니다. UNICORN 한 회분은 네이티브 .res 결과 파일로 떨어지며, 더 새로운 UNICORN 버전은 결과를 단일 바이너리 블롭(blob)이 아니라 XML의 zip 아카이브로 저장합니다. 어느 쪽이든 독점 컨테이너이므로, Benchling의 allotropy(또는 그 어떤 텍스트/CSV/Excel 정규화 도구)가 Allotrope ASM JSON으로 바꿀 수 있는 대상이 아닙니다 — 그 경로는 텍스트 익스포트를 위한 것이지, .res/XML-zip 기록 그 자체를 위한 것이 아닙니다. 크로마토그램을 교환 해야 할 때 — 데이터 레이크(data lake)로, 사이트 간에, 규제 제출(regulatory submission)로 — 벤더 중립적인 ASTM ANDI/NetCDF 크로마토그래피 포맷(ASTM E1947)이 오래도록 자리 잡은 표준이며, 벤더 블롭 대신 .cdf를 내보내는 것이 FAIR 친화적인 선택입니다 [9], [13].
  • 여기서 표준이 발목을 잡습니다. Annex 11(전산화 시스템에 대한 EU GMP)은 GMP 관련 결정과 변경 — 풀링 결정 같은 — 을 기록하는 시스템이 검토 가능하고 타임스탬프가 찍힌 감사 추적 을 생성하도록 요구합니다 [10]. PIC/S PI 041-1은 그 풀링 및 유지 시간 기록이 반드시 귀속 가능하고(attributable), 동시기적이며(contemporaneous), 완전해야(complete) 한다고(ALCOA+ 데이터 무결성 속성 — 귀속 가능, 가독, 동시기적, 원본, 정확, 더하여 완전, 일관, 영속, 가용), 그리고 결정 데이터를 어떻게 포착하는지를 좌우하는 것은 IT가 작동하느냐가 아니라 데이터 흐름과 위험 이라고 강조합니다 [11].
  • 규모 확대(scale-up)는 숫자를 바꾸지, 기록을 바꾸지 않습니다. 크로마토그래피의 CV 시계가 바로 공정을 문서상 규모 독립적 으로 만드는 것입니다. 1 mL 컬럼에서 0.5 CV/min으로 개발된 방법은, CV·체류 시간·선형 유속(분당 베드 높이)을 일정하게 유지한 채 베드 지름 만 키워, 수십 리터의 상용 규모 컬럼으로 전이됩니다. 그러나 물리는 완전히 협조하지 않습니다 — 벽 효과(wall effect), 충전 균일도, 압력-유량 한계가 모두 지름에 따라 달라지므로, 소규모에서 자격을 얻은 풀링 임계값과 DBC는 상용 규모에서 가정될 것이 아니라 확인 되어야 합니다. 그 확인이 기술 이전(tech transfer) 의 규제 대상 작업이며, 새 스키드는 IQ/OQ/PQ — 설치 적격성 평가(Installation Qualification, 장비가 규격대로 설치·연결됨), 운전 적격성 평가(Operational Qualification, 운영 범위 전반에서 작동함, 예: 기울기 펌프와 UV 검출기가 규격대로 거동함), 성능 적격성 평가(Performance Qualification, 실제 공정에서 규격 내 제품을 만듦) — 를 통과한 뒤에야 GMP 배치를 운전할 자격이 생깁니다. operation_event 행은 규모에 무관합니다 — PA01은 1 L 개발 컬럼일 수도, 50 L 상용 컬럼일 수도 있습니다 — 그러나 그 attributes가 강제하는 임계값과 한계 는 규모에 특정되며 자격 이력(qualification pedigree)을 지니므로, 검증된 14.5–17.5 CV 띠는 보편 상수가 아니라 특정 컬럼·수지 로트·자격을 갖춘 스키드에 묶인 숫자입니다.

이 계층에 대한 정직한 OSS 대 상업용 평결: 오픈 소스 스택은 포착과 맥락화를 멋지게 해냅니다. Python과 페이즈 검출기가 원시 추적선을 ISA-88 이벤트로 바꾸고, PostgreSQL이 그것을 완전한 구조와 함께 저장하며, 커뮤니티 유한 상태 기계(finite-state-machine) 노드를 갖춘 Node-RED 가 같은 분할을 엣지(edge)에서 라이브 흐름으로 실행하여, 풀과 유지 이벤트가 벌어지는 대로 트리거할 수 있습니다 [12]. 그것이 여러분을 대부분의 길까지 데려다줍니다. 순수 OSS가 즉시 제공하지 못하는 것은, 풀링 결정에 대한 검증된 Part 11(21 CFR Part 11 — 전자 기록과 서명에 관한 미국 FDA 규정) 수준의 전자 서명(e-signature), 풀링 임계값에 대한 잠금된 변경 관리(change control), 또는 분획 수집기(fraction-collector) 밸브를 물리적으로 명령한 CDS에 대한 벤더 책임입니다. 그것들이 GxP(규제되는 모범 운영 관행)의 마지막 한 마장(last mile) — 우리가 5부에서 (정직하게, 그리고 그 한계와 함께) 만들 감사 추적, 서명, 검증 — 입니다.

핵심 용어

  • 크로마토그래피 스키드(chromatography skid) — 충전된 컬럼을 그 페이즈들로 통과시키는 자동화 시스템(펌프, 밸브, UV/전도도/pH 검출기, 분획 수집기).
  • 단백질 A 포획(Protein A capture) — 항체의 Fc 영역(그 불변 줄기)을 선택적으로 결합하여 한 단계로 높은 순도를 주는 플랫폼 친화성 단계.
  • 컬럼 부피(column volume, CV) — 충전된 수지 베드의 부피. 크로마토그래피의 규모 독립적인 시계(여기서 1 CV = 1 L = 2 min).
  • 페이즈 / 오퍼레이션(phase / operation, ISA-88) — 프로시저 내의 이름 붙은 단계(평형화, 적재, 세척, 용출, 스트립, CIP). 분할의 단위.
  • 돌파(breakthrough) — 컬럼이 결합 용량에 다가가면 제품이 결합되지 않은 채 컬럼을 통과해 빠져나가는 현상. 적재 중 상승하는 UV 어깨로 보임.
  • 동적 결합 용량(dynamic binding capacity, DBC) — 흐름 하에서 돌파 전까지 수지 1리터가 결합할 수 있는 제품의 양. 여기서는 약 58 g/L.
  • 풀링 윈도(pooling window) — 용출액을 제품 풀로 거두는 시작/정지 부피. 이 장의 핵심적인 GMP 결정.
  • 유지 시간(hold time) — 풀이 한 단계에 머무는 검증된 최소(및 최대) 시간. 예: 낮은 pH 바이러스 불활화.
  • 무결성 시험(integrity test) — 사용 전/후 필터에 대한 합격/불합격 검사(예: 버블 포인트). 이산적 이벤트로 기록됨.
  • 3MCC / PCC — 다컬럼 / 주기적 역류(periodic counter-current) 연속 포획. 단일 배치 컬럼에 대한 강화된 대안.
  • PAT — 공정 분석 기술(Process Analytical Technology). 실시간 측정(여기서는 인라인 UV)을 사용해 공정 중 품질 결정을 내림.
  • operation_event — 시계열 스트림과 배치 기록을 잇는 PostgreSQL 테이블. ISA-88 페이즈당 한 행이며, 구조화된 열(batch_id, unit_id, phase, 시간 윈도)과 개방형 attributes jsonb 페이로드를 함께 가짐.
  • attributes (jsonb)operation_event 행의 이벤트별 JSON 페이로드. 장부 정리 행에서는 비어 있고, 용출 행에서는 풀링 윈도를, 유지 행에서는 유지 결과를 담는 식으로, 스키마 마이그레이션 없이 확장됨.
  • 트리플 / SHACL 형상(triple / SHACL shape) — RDF의 사실 단위(주어-술어-목적어), 그리고 그래프가 갖춰야 할 구조를 갖췄는지 검사하는 닫힌 세계 제약(여기서는 용출 이벤트가 풀링 윈도를 기록했는지). 행의 NOT NULL과 단위 시험을 그래프 방식으로 다시 진술한 것.
  • 역량 질문(competency question) — 온톨로지가 반드시 답할 수 있어야 하는 질의로, 인수 시험으로 쓰임. "어느 배치가 검증된 띠 밖에서 풀링했는가?"가 풀링 트리플에 대한 그런 질문 하나.
  • 배치별(leave-one-batch-out) 교차 검증 — 무작위 행이 아니라 배치 전체를 떼어 두어, 학습된 농도 기반 풀링 모델을 본 적 없는 컬럼 회분에서 채점하는 것. 한 회분 안에서의 분할은 거의 중복된 시료를 누출시켜 점수를 우쭐하게 만듦.
  • 적용 범위(applicability domain) — 학습된 모델이 유효한 피크 모양·완충액·수지 연령의 영역. Hotelling T²/SPE 관문이 농도가 밸브를 움직이기 전에 엔벨로프 밖 스펙트럼에 깃발을 듦.
  • 공변량 변화 대 공정 드리프트(covariate shift vs process drift) — 모델의 입력 이 움직이는 것(수지 오염, 새 로트의 기준선)과 공정 자체가 방황하는 것. 각각 스펙트럼에 대한 모집단 안정성 지수와 풀 역가에 대한 SPC 차트로 잡음.
  • IQ/OQ/PQ — 설치·운전·성능 적격성 평가(Installation, Operational, Performance Qualification). 크로마토그래피 스키드가 설치되고, 범위 전반에서 작동하며, 규격 내 제품을 만든다는 단계적 증명으로, GMP 배치를 운전하기 전에 거쳐야 함 — 규모에 특정된 풀링 띠 뒤의 기술 이전 관문.

다음 이야기

우리는 센서에서 스키드까지 공정을 포착했습니다 — 모든 인라인 태그, 모든 페이즈, 모든 풀링 결정을. 그러나 분자의 품질은 궁극적으로 오프라인에서, 기기로 판정됩니다. 순도를 위한 HPLC, 숙주 세포 단백질을 위한 분석법, 농도를 위한 저울입니다. 다음 장 분석 실험실: 기기, LIMS와 ELN(The Analytical Lab: Instruments, LIMS & ELN) 은 생산 현장을 떠나 QC 실험실로 향하며, 데이터 — 우리가 방금 기록한 결정이 실제로 출하할 가치가 있는 약물을 만들어냈는지를 확인해주는 오프라인 결과 — 를 어떻게 포착하는지 보여줍니다.