본문으로 건너뛰기

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

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

쉽게 말하면

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

이 장에서 다루는 내용

바이오리액터를 거친 뒤에도 수확된 배양액(broth)은 여전히 대부분 물, 세포 잔해, 그리고 약간의 항체가 녹아 있는 숙주 세포(host-cell) 찌꺼기입니다. 다운스트림 정제(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에 결정적인 두 가지 결정 — 풀링 윈도(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)까지 치솟으며, 정치 세정(clean-in-place) 중에는 수산화나트륨을 흘려보내기 때문에 전도도가 뛰어오릅니다.

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

신호 자체는 스키드의 PLC에서 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 단계가 항체를 날카롭고 약간 끌리는 피크(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)이 좋아하는 모양입니다.

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 로 정점에 이릅니다. 바로 그 하나의 숫자가, 이 장의 나머지가 작동시키고자 하는 대상입니다.

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

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

Original diagram by the authors, created with AI assistance.

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

실제 플랜트에서는 스키드 제어기가 자신이 어느 단계에 있는지 이미 알고 있으며, 각 시료에 단계/페이즈 라벨을 찍어둡니다. 저장소의 시뮬레이터도 똑같이 합니다. 추적선의 모든 행은 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) — 컬럼이 포화에 다가가고 있다는 조용한 조기 경보 — 입니다. 운영 환경에서는 이것에 알람을 걸 것입니다. (이 장의 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 스펙트럼 디컨볼루션(deconvolution)을 사용하여 원시 흡광도가 아니라 농도 (심지어 불순물 함량까지)에 맞춰 풀링하며, 오프라인 피크 면적 적분보다 단계 수율을 훨씬 정확하게 계산합니다 [6]. 저장소는 단순하고 방어 가능한 임계값 버전을 구현합니다.

# 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리터의 용출액 — 을 보관합니다. 바로 그 단 하나의 기록이 검사관의 질문에 답합니다. 시뮬레이터가 기록하는 요약(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를 넘는지를 단언합니다.

기록하기: 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_start | phase_end | 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)이 실제로 신경 쓰는 해석되고 결정을 담은 몇 안 되는 기록을 담습니다.

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

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

  • 바이러스 불활화 는 낮은 pH 유지입니다 — 용출액이 검증된 최소 시간 동안 pH 약 3.5에 머뭅니다. 그 유지에는 다음 단계로 넘어가기 전 정해진 최대 유지 시간 이 있으며, 최소와 최대 모두가 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부에서 만듭니다.

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

지금까지 이 장 전체는 배치 컬럼을 설명합니다. 컬럼 하나, 사이클 하나, 사이클 사이에는 유휴 상태입니다. 현대적이고 강화된 대안은 다컬럼 연속 포획(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을 거기에 추가해야 할 것입니다.) 풀링 로직은 한 단계 위로 올라갑니다. 피크 하나가 아니라 컬럼 전환을 가로지르는 누적 제품 스트림을 풀링합니다. 저장소의 유가식 경로는 위의 단일 컬럼 시뮬레이터를 사용하고, 관류 사이드바(sidebar)는 컬럼마다 동일한 검출기를 재사용합니다.

왜 중요한가

다운스트림은 분자가 약물이 되는 곳이자, 대부분의 수율 과 대부분의 위험 이 자리하는 곳입니다. 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 배치 기록에 자리합니다. 우리의 OSS 스택은 그것들을 대체하지 않습니다 — 그 곁에서 맥락화(contextualize) 하고 히스토리화(historize) 합니다. 그 경계는 이 책에서 되풀이되는 정직함의 주제입니다.

실제 현장의 닻 몇 개.

  • NIIMBL — 바이오의약품 혁신을 위한 미국의 민관 합작 Manufacturing USA 연구소 — 은 바로 이런 종류의 연속 및 강화 공정 작업에 자금을 댑니다. 그 SABRE 시설(NIIMBL / 델라웨어 대학교(University of Delaware)의 파일럿 규모 cGMP — current Good Manufacturing Practice — 시설로, 2024년 4월에 착공)은 연속 포획을 포함한 차세대 공정을 시연하기 위해 지어지고 있습니다. SABRE는 데이터 프로그램이 아니라 시설 이지만, 이 장이 그려내는 3MCC 방식의 공정이 파일럿 규모에서 운전되기로 되어 있는 곳입니다.
  • 데이터 포맷: 벤더 CDS 익스포트는 크로마토그램을 독점 파일에 가둡니다. 크로마토그램을 교환 해야 할 때 — 데이터 레이크(data lake)로, 사이트 간에, 규제 제출(regulatory submission)로 — 벤더 중립적인 ASTM ANDI/NetCDF 크로마토그래피 포맷(ASTM E1947)이 오래도록 자리 잡은 표준이며, 벤더 블롭(blob) 대신 .cdf를 내보내는 것이 FAIR 친화적인 선택입니다 [9].
  • 여기서 표준이 발목을 잡습니다. Annex 11(전산화 시스템에 대한 EU GMP)은 GMP 관련 결정과 변경 — 풀링 결정 같은 — 을 기록하는 시스템이 검토 가능하고 타임스탬프가 찍힌 감사 추적 을 생성하도록 요구합니다 [10]. PIC/S PI 041-1은 그 풀링 및 유지 시간 기록이 반드시 귀속 가능하고(attributable), 동시기적이며(contemporaneous), 완전해야(complete) 한다고(ALCOA+ 속성), 그리고 결정 데이터를 어떻게 포착하는지를 좌우하는 것은 IT가 작동하느냐가 아니라 데이터 흐름과 위험 이라고 강조합니다 [11].

이 계층에 대한 정직한 OSS 대 상업용 평결: 오픈 소스 스택은 포착과 맥락화를 멋지게 해냅니다. Python과 페이즈 검출기가 원시 추적선을 ISA-88 이벤트로 바꾸고, PostgreSQL이 그것을 완전한 구조와 함께 저장하며, 커뮤니티 유한 상태 기계(finite-state-machine) 노드를 갖춘 Node-RED 가 같은 분할을 엣지(edge)에서 라이브 흐름으로 실행하여, 풀과 유지 이벤트가 벌어지는 대로 트리거할 수 있습니다 [12]. 그것이 여러분을 대부분의 길까지 데려다줍니다. 순수 OSS가 즉시 제공하지 못하는 것은, 풀링 결정에 대한 검증된 Part 11 수준의 전자 서명(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)을 사용해 공정 중 품질 결정을 내림.

다음 이야기

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