본문으로 건너뛰기

상용 히스토리안 연동: AVEVA/OSIsoft PI

📍 현재 위치: Part IV · 현실과 마주하기 — 우리는 모든 것이 오픈소스인 안전지대를 떠나, 대부분의 공장이 결코 교체를 허락하지 않을 단 하나의 시스템, 즉 검증된 상용 히스토리안(historian)에 우리 스택을 연결합니다.

쉽게 말하면

공장의 PI 시스템(PI System)을 은행 금고 안에 있는 공식적이고 공증된 원장(ledger)이라고 생각해 보세요. 우리는 그것을 버릴 수 없고, 솔직히 버리고 싶어 해서도 안 됩니다 — 감사관(auditor)이 그것을 신뢰하고, 이미 검증을 마쳤으며, 현장 전체가 이미 거기서 데이터를 읽어 가기 때문입니다. 우리가 할 수 있는 일은 금고 옆에 빠르고 친절한 복사기를 만드는 것입니다. 이 복사기는 우리의 분석을 위해 원장의 충실한 사본을 만들어 내고, 우리가 무언가 새로운 것을 계산하면 그 사본을 적절한 창구를 통해 되돌려 줄 수 있습니다. 이 장에서는 바로 그 복사기 — 우리 오픈소스 스택과 상용 히스토리안 사이의 깔끔한 경계 — 를 만들고, 어느 쪽이 원본을 보유하는지에 대해 정직하게 다룹니다.

이 장에서 다루는 내용

지난 열여섯 개 장에 걸쳐 우리는 완전한 오픈소스 데이터 플랫폼을 구축했습니다. OPC UA 바이오리액터(bioreactor), 스파크플러그 버스(Sparkplug bus), TimescaleDB 히스토리안, PostgreSQL 안의 ISA-88/95 모델, 컨텍스트화(contextualization) 뷰, 그리고 지식 그래프(knowledge graph)까지. 이 모든 것이 노트북 한 대에서 돌아갑니다. 그렇다면 이 장은 왜 존재할까요?

실제 바이오의약품 공장에서 기록 원본(record of truth) 히스토리안은 거의 결코 여러분이 방금 만든 그것이 아니기 때문입니다. 그것은 검증된 AVEVA PI 시스템(예전에 OSIsoft PI로 브랜딩되었던 제품)이며, 수년간 쌓인 GMP 공정 데이터의 기록 원본(record-of-truth)입니다. (이 규제적 의미에서 "검증됨(validated)"이란, 시스템이 마땅히 해야 할 일을 수행하고 신뢰할 수 있는 기록을 유지함을 입증하는 공식적이고 문서화된 적격성 평가를 거쳤다는 뜻입니다 — 아래 왜 기록 원본은 상용으로 남는가를 참조하세요.) 이 책의 정직한 하이브리드(honest-hybrid) 이야기는 바로 여기서 시작됩니다. 이 장에서 다루는 내용은 다음과 같습니다.

  • GMP 히스토리안 기록이 왜 상용으로 남는지, 그리고 그것이 왜 오픈소스의 실패가 아니라 더 낮은 위험의 선택인지.
  • 여러분이 실제로 엔지니어링하는 두 경계: PI Web API(REST)와 OPC UA.
  • OSS 사본과 PI 원본이 일치하도록 하는 백필(backfill) 및 조정(reconciliation) 패턴.
  • 실제 PI 서버에 접근할 수 없을 때 PI 스텁(stub)을 상대로 브리지를 개발하고 테스트하는 방법.

참고로 cGMP는 current Good Manufacturing Practice(현행 우수 제조 관리 기준)로, 의약품 제조가 통제되고 문서화되며 재현 가능해야 한다는 FDA의 구속력 있는 기대를 말합니다. 그리고 이 책 전반에서 쓰이는 GxP는 모든 "우수 관리 기준" 규제(우수 제조·시험·임상·유통 관리 기준)를 아우르는 상위 개념이며, cGMP는 그중 제조에 해당하는 부분입니다. 이 단어를 기억해 두세요. 금고가 잠긴 채로 남아 있는 이유가 바로 그것입니다.

왜 기록 원본은 상용으로 남는가

PI 시스템은 단순한 데이터베이스가 아닙니다. 그것은 압축된 시계열 태그(tag)를 담은 PI 데이터 아카이브(PI Data Archive)에, 원시 태그를 계층 구조, 속성, 단위, 템플릿으로 컨텍스트화하는 자산 중심 계층인 PI 자산 프레임워크(PI Asset Framework, AF)를 더한 것입니다 [1]. 현장은 일반적으로 이를 검증하는 데 수년을 들입니다. 설치 적격성 평가(installation qualification), 운영 적격성 평가(operational qualification), 변경 관리(change control) SOP, 검토를 거친 감사 추적(audit trail)까지. 품질 위험 관리(quality risk management) 관점에서, 오픈소스 히스토리안을 좇겠다고 이것을 뜯어내는 것은 환자에게 거의 이득이 없으면서 노력과 위험이 큰 행동입니다. ICH Q9(R1)은 위험 관리의 노력, 형식성, 문서화가 위험 수준에 상응해야 한다고 명시합니다 [2] — 그리고 검증된 기록 시스템을 교체하는 위험은 큽니다.

FDA의 컴퓨터 소프트웨어 보증(Computer Software Assurance, CSA) 가이던스도 같은 방향을 가리킵니다. 이는 위험 기반의 최소 부담(least-burdensome) 접근법을 권장합니다. 보증 노력을 환자 위험이 있는 곳에 집중하고, 그 외에는 기존 통제에 의존하라는 것입니다 [3]. 이것이 우리에게 깔끔한 역할 분담을 줍니다. 검증된 PI 시스템은 높은 엄격성이 요구되는 GxP 기록을 담당합니다. 우리의 오픈소스 계층 — TimescaleDB, Grafana, 소프트 센서(soft sensor, 물리적 프로브로 읽는 것이 아니라 다른 신호로부터 계산되는 측정값) — 은 더 낮은 위험의 분석 및 엣지(edge) 보완재입니다(여기서 "엣지"란 계측기에 가까운 장비 쪽 계층을 뜻합니다). 그것은 계산하고, 시각화하고, 탐색합니다. 그것이 기록 원본일 필요는 없습니다.

이것이 바로 디지털 트윈(digital twin) 문헌이 기술하는 아키텍처입니다(디지털 트윈이란 공장에서 데이터를 공급받는, 물리적 자산의 살아 있는 데이터·모델 복제본입니다). 데이터 히스토리안은 기록의 통합 허브이며, 공정 데이터는 OPC와 TCP/IP 전송을 통해 거기서 클라우드와 분석 계층으로 복제됩니다 [4]. 우리는 새로운 패턴을 발명하는 것이 아닙니다. 우리는 누가 원본을 소유하는지를 명확히 인식한 채, 정립된 패턴을 오픈소스로 구현하고 있는 것입니다.

데이터 무결성(data integrity) 용어는 그 경계를 정밀하게 만듭니다. MHRA의 GXP 가이던스는 원본 기록(original record)과 진정 사본(true copy)을 구별합니다 [5]. 검증된 PI 시스템은 전체 감사 추적과 동적 데이터를 갖춘 원본 GxP 기록을 보유합니다. 우리의 OSS 계층은 진정 사본과 파생 데이터를 보유합니다. 설계 검토 회의에서 이 말을 소리 내어 하면 품질 부서(quality unit)는 안심할 것입니다. 여러분은 그들에게 원본을 오픈소스에 맡겨 달라고 요청하는 것이 아니기 때문입니다.

깔끔한 경계 다이어그램: 왼쪽에는 더 낮은 위험의 분석으로 표시된 오픈소스 스택(OPC UA 바이오리액터, 스파크플러그 버스, TimescaleDB, Grafana, 소프트 센서)이 있고, 오른쪽에는 기록 원본으로 표시된 검증된 AVEVA PI 시스템(PI 데이터 아카이브와 자산 프레임워크)이 있다. 두 개의 화살표가 경계를 가로지른다: PI Web API에서 TimescaleDB로 태그 스트림을 끌어오는 읽기 경로, 그리고 계산된 소프트 센서 태그를 PI로 밀어 넣는 쓰기 경로다. 점선으로 된 조정 루프가 두 사본을 비교한다.

이데올로기가 아니라 엔지니어링으로서의 OSS↔PI 경계: PI는 원본 GxP 기록을 보관하고, 오픈소스 계층은 진정 사본을 보유하며 통제된 문을 통해 파생 값을 되돌려 쓴다. 저자가 AI의 도움을 받아 직접 제작한 그림입니다.

두 경계: PI Web API와 OPC UA

현대 PI 시스템으로 들어가는 문은 정확히 둘이고, 여러분은 둘 다 사용하게 될 것입니다.

첫 번째 문 — PI Web API. 이것은 클라이언트 애플리케이션에 PI 데이터 아카이브와 AF 데이터에 대한 HTTPS 기반의 읽기 및 쓰기 접근을 제공하는 RESTful 인터페이스입니다 [6]. JSON을 사용하고, 방화벽 친화적이며, 파이썬(Python) 클라이언트나 Apache NiFi 흐름에 자연스럽게 들어맞습니다. 시간 창(time window)에 걸친 스트림의 기록 값(recorded values)을 요청하고, 포인트(point)에 새 값을 POST합니다.

두 번째 문 — OPC UA. PI는 OPC UA용 PI 커넥터/어댑터(PI Connector/Adapter)를 통해 OPC UA 서버로부터 데이터를 수집하며, 이는 OPC UA를 PI와 엣지 사이의 일급(first-class) 읽기/쓰기 경계로 만듭니다 [7]. OPC UA는 플랫폼 독립적이고 안전하며 방화벽 친화적이고 [8], 우리 바이오리액터가 이미 그것을 사용하므로, 이 문은 우리에게 거의 비용이 들지 않습니다 — 기존의 opcua-server와 Telegraf의 OPC UA 입력 플러그인 [9]이 그대로 꽂힙니다. 미묘하지만 중요한 부분은 이력(history)입니다. OPC UA Part 11, 이력 접근(Historical Access)은 클라이언트가 단지 실시간 값뿐 아니라 이력 데이터와 이벤트를 읽고 쓰는 방법을 정의합니다 [10]. 바로 이것이 PI를 상대로 한 간격 채우기(gap-filling)를 가능하게 하며, 아래에 나오는 조정 패턴의 중추입니다.

현장에서 통용되는 경험칙: PI가 우리로부터 실시간 데이터를 끌어가야 할 때는 OPC UA를 사용하고(PI는 수집기로 남고, 검증된 경로는 변하지 않습니다), 우리가 PI에서 끌어와 분석하거나 계산된 값을 되돌려 쓸 때는 PI Web API를 사용하세요. 전자는 PI의 수집을 검증된 채 손대지 않은 상태로 유지하고, 후자는 우리 분석 루프를 빠르게 유지합니다.

PI Web API와 대화하기: 요청의 형태

AVEVA PI는 노트북에서 실행될 수 없고 공개 이미지를 제공하지 않으므로, 정직한 하이브리드 설계는 실물 대신 (mock)을 상대로 개발하는 것입니다 — 이 책이 SAP(전사적 자원 관리(enterprise resource planning) 시스템)와 DeltaV(분산 제어 시스템(distributed control system), 즉 DCS)에 대해 취하는 것과 동일한 접근입니다. 그 목은 이제 제공됩니다. 실제 PI Web API와 동일한 요청/응답 계약(contract)을 지키고, 우리의 골든 배치(golden batch)를 제공하며, 쓰기를 받아들이는, examples/services/pi-web-api-stub/에 있는 작은 FastAPI 서비스입니다. 동반 저장소의 compose.yaml은 이를 commercial 프로필 아래에서 실행합니다(core, capture, semantics, analytics/ops 프로필과 함께). 다음 장의 DeltaV dcs-mock은 이제 같은 commercial 프로필 아래에서 제공되어, sap-mock만이 로드맵에 남습니다. 따라서 아래의 PI 스니펫은 요청/응답 계약으로 — 명확히 표시된 예시적 형태로 — 간주하되, 그것을 제공하는 스텁과 그것을 파싱하는 브리지는 실제이며 테스트된 코드(examples/chapters/17-bridge-pi-historian/pi_bridge.py, examples/tests/test_bridges.py로 검증됨)임을 기억하세요. 경계 우리 쪽의 로더(loader) — 이 장 뒤에 나옵니다 — 역시 실제입니다.

PI Web API는 모든 것을 불투명한 WebId로 주소 지정합니다. 태그("PI 포인트(PI Point)")를 그 WebId로 한 번 해석(resolve)한 다음, 시간 창에 걸쳐 기록 값을 읽습니다. 기록 값 응답은 다음과 같은 모습일 것입니다(예시적인 PI Web API JSON으로, AVEVA의 문서화된 스키마와 일치합니다 — 여러분의 목이 제공하고 여러분의 브리지가 파싱할 형태입니다).

{
"Items": [
{ "Timestamp": "2026-01-18T23:58:00Z", "Value": 5.8214, "UnitsAbbreviation": "g/L",
"Good": true, "Questionable": false, "Substituted": false },
{ "Timestamp": "2026-01-18T23:59:00Z", "Value": 5.7589, "UnitsAbbreviation": "g/L",
"Good": true, "Questionable": false, "Substituted": false }
]
}

주목할 점이 두 가지 있습니다. 첫째, 저 역가(titer) 값들 — 5.82145.7589 g/L(역가 = 분비된 항체 산물의 농도로, 리터당 그램 단위) — 은 우리의 골든 배치 BATCH-2026-001에 대한 BR101.Titer.PV정확히 마지막 두 판독값으로, datasets/fedbatch_timeseries.parquet(아래 로더가 읽는 바로 그 파일)의 1분 간격 트레이스에서 가져온 것이며, 그 파일은 23:59에 끝납니다. (5.8214에서 5.7589 g/L로의 겉보기 하락은 실제 감소가 아니라 약 0.03 g/L 대역의 인라인 측정 노이즈입니다 — 유가식에서 누적 역가는 오직 상승할 뿐이며, 약 5.8 g/L는 14일 유가식 CHO(Chinese Hamster Ovary, 중국 햄스터 난소) mAb(monoclonal antibody, 단일클론항체) 런의 견실한 후기 수확 종점입니다. 조정은 그 노이즈가 섞인 판독값을 하락까지 포함해 정확히 재현해야 합니다.) PI와 우리 스택은 동일한 물리적 바이오리액터를 바라보고 있습니다. 둘은 소수점 자리까지 일치해야 마땅합니다. 둘째, PI Web API는 품질을 단일 숫자 OPC 품질 코드가 아니라 Good/Questionable/Substituted 플래그로 드러냅니다. 우리 히스토리안은 OPC 방식의 품질(Good은 192, Uncertain은 64)을 저장합니다. 둘을 잇는 것은 한 번 작성하고 검증해 두는 작은 매핑 테이블입니다 — 결코 나중에 생각할 일이 아닙니다.

되돌려 쓰는 것은 같은 문을 거꾸로 사용하는 것입니다. {Timestamp, Value} 항목으로 이루어진 JSON 본문을 포인트의 기록 값 엔드포인트에 POST하는 것입니다. 이것이 계산된 태그 — 가령 29장의 라만(Raman) 모델에서 나온 BR101.Titer.SoftSensor — 가 PI 세계로 다시 들어가, 운영자가 자신이 신뢰하는 계측기 태그 옆에서 그것을 보게 되는 방식입니다.

PI Web API 기록 값 Item의 해부

브리지 전체는 저 Items 배열 안에서 판독값마다 한 번씩 반복되는 하나의 작은 JSON 객체에 달려 있습니다. 그것을 빠짐없이 해부할 가치가 있습니다. 모든 필드는 우리 히스토리안으로 건너가며 살아남거나, 아니면 의도적으로 접혀 사라지기 때문입니다 — 그리고 어느 쪽인지 말하지 못하는 브리지 작성자는 조용히 품질을 잃게 됩니다. 제공된 스텁이 내보내는 객체는 services/pi-web-api-stub/app.py(recorded() 핸들러)에서 필드 하나하나 만들어지고, 브리지가 소비하는 객체는 pi_bridge.to_sensor_rows()에서 필드 하나하나 읽힙니다. 다음은 위 응답의 두 번째 Item — 골든 배치의 마지막 판독값 — 을 그것이 변하는 행과 나란히 놓은 것입니다.

  • 어떤 값보다 먼저, 주소 지정 체인이 옵니다. PI Web API는 핫 경로(hot path)에서 BR101.Titer.PV를 이름으로 요청하도록 결코 허락하지 않습니다. PI 포인트 이름 — 그 완전 정규화된 형태는 \\PISRV\BR101.Titer.PV — 을 불투명한 WebId로 한 번 해석한 다음, 그 핸들로 스트림을 읽습니다. 제공된 webid()는 블랙박스가 아닙니다. 그것은 base64.urlsafe_b64encode(point.encode()).decode().rstrip("=")이므로, BR101.Titer.PV는 정확히 QlIxMDEuVGl0ZXIuUFY로 해석됩니다 — 태그 바이트의 base64url에서 뒤따르는 = 패딩을 제거한 것입니다. 스텁의 _point()는 이를 역으로 되돌립니다(디코딩 전에 4의 배수로 다시 패딩을 채웁니다). 실제 PI WebId는 더 길고 서버가 발행한 토큰이지만, 계약은 동일합니다: 이름이 들어가고, 불투명한 핸들이 나오며, 핸들로 읽습니다.
  • Timestamp — ISO-8601 UTC 순간. 2026-01-18T23:59:00Z. 뒤따르는 Z가 핵심을 짊어집니다: 그것은 판독값을 UTC에 고정하여, timestamptz 열에 안착할 때 어떤 일광 절약 시간이나 현장 시간대 모호성도 끼어들 수 없게 합니다. 스텁은 그것을 strftime("%Y-%m-%dT%H:%M:%SZ")로 포맷하며, 그래서 우리의 분 단위 표시는 초 미만 부분이 없습니다.
  • Value — 측정값. 5.7589, BATCH-2026-001의 실제 마지막 역가입니다(직전 5.8214보다 낮은 것은 오직 인라인 측정 노이즈 때문이며, 실제 누적 역가는 결코 떨어지지 않습니다). 스텁은 소수점 넷째 자리로 반올림하며(round(float(v), 4)), 그래서 계약 테스트가 제공된 값이 parquet 꼬리를 넷째 자리로 반올림한 값과 같다고 단언하는 것입니다 — 일치는 와이어가 결코 지니지 않았던 정밀도가 아니라, 와이어가 실제로 운반하는 자릿수까지로 정의됩니다.
  • UnitsAbbreviation — 인라인 단위. g/L. PI는 모든 Item에 단위를 실어 나르므로, 이 와이어에서 값은 우리 히스토리안에서와 마찬가지로 결코 맨 숫자가 아닙니다.
  • Good / Questionable / Substituted — 세 개의 불리언으로 표현된 품질. 이것이 일대일로 매핑되지 않는 필드입니다. PI는 품질을 세 개의 독립적인 플래그로 드러내지만, 우리 히스토리안은 단일 OPC 방식의 smallint를 저장합니다. 다음 절이 그 접힘을 해부합니다.

PI Web API 기록 값 Item의 해부라는 제목의 라벨이 붙은 식별 카드: BR101.Titer.PV와 BATCH-2026-001에 대한 산출물을 명명하는 헤더, PI 포인트 이름이 base64url WebId로 해석되는 것을 보여 주는 인디고 주소 지정 블록, 그다음 Timestamp, Value 5.7589, UnitsAbbreviation g/L에 대한 Item 행들, Good·Questionable·Substituted 품질 불리언을 위한 녹색 블록, 그리고 각 필드를 이름으로 6열 ts.sensor_reading 행에 매핑하며 192 Good·64 Uncertain·0 Bad의 품질 크로스워크를 담은 보라색 블록. 완전히 펼쳐 본 하나의 PI Web API Item: 주소 지정 체인이 이름을 WebId로 해석한 다음, 브리지가 Timestamp, Value, 단위, 그리고 세 개의 품질 불리언을 ts.sensor_reading의 한 행으로 매핑한다. 저자가 AI의 도움을 받아 직접 제작한 그림입니다.

이것을 카드로 그리는 이유는 OPC UA 장이 노드를 카드로 그리는 이유와 같습니다: 브리지의 규율은 모든 필드를 빠짐없이 셈하고, 그것이 어디에 안착하는지 명명하며, 우리 여섯 개 열에 자리가 없는 한 필드(Substituted)에 대해 정직한 것입니다.

같은 행, 디지털 스레드가 걸을 수 있는 트리플로

저 여섯 열짜리 행은 또한 이전 장이 구축한 지식 그래프(knowledge graph) 안의 한 행이기도 합니다. 그래프의 데이터 모델은 RDF이며 — 모든 사실은 주어, 술어, 목적어의 트리플(triple)입니다 — 그 판독값은 작은 트리플 묶음으로 안착하는데, 이는 시맨틱스와 디지털 스레드가 출하 결과에 대해 내보내는 것과 같은 형태입니다. 터틀(Turtle, 사람이 읽기 좋은 RDF 구문)로 쓰면, 5.7589 g/L Item은 다음과 같이 됩니다.

@prefix bp: <https://example.org/bioproc#> .
@prefix qudt: <http://qudt.org/schema/qudt/> .
@prefix prov: <http://www.w3.org/ns/prov#> .

bp:reading-BR101-Titer-2026-01-18T2359
a bp:SensorReading ;
bp:ofTag "BR101.Titer.PV" ;
bp:value "5.7589"^^xsd:float ;
qudt:unit unit:GM-PER-L ; # g/L, 맨 문자열이 아니라 기계 판독 가능
bp:quality 192 ; # 크로스워크 이후의 OPC 코드
bp:fromBatch bp:BATCH-2026-001 ;
prov:wasDerivedFrom bp:pi-point-BR101-Titer-PV . # PI가 원본을 보유

브리지가 이미 하는 세 가지가 형식 시맨틱(formal semantics)에 깔끔하게 대응합니다. 첫째, bp:fromBatch는 계보(genealogy)가 사용하는 바로 그 엣지이므로, 이 판독값은 bp:derivedFrom에 뿌리내린 배치 노드에 매달리며, 단 하나의 (bp:derivedFrom)+ SPARQL 프로퍼티 경로(property path)가 거기서 작업 세포 은행까지 거슬러 걸어 올라갑니다 — Book 4가 구축하는 derivedFrom 전이적 척추입니다. 둘째, qudt:unit이야말로 브리지가 값을 결코 맨 숫자로 이동시키지 않는 이유입니다: 단위는 타입이 지정된 사실이며, 정확히 식별자와 단위의 규율입니다. 셋째 — 그리고 이것이 관계형 행이 실어 나를 수 없는 필드인데 — PROV-Oprov:wasDerivedFromOSS 사본이 PI 원본에서 파생되었다는 것을 기록하여, MHRA의 "진정 사본 대 원본 기록" 구분을 산문에서 기계로 점검 가능한 출처(provenance) 엣지로 바꿉니다.

조정 계약에는 더욱 빈틈없는 온톨로지적 독해가 있습니다: "모든 판독값은 정확히 하나의 값, 하나의 단위, 하나의 품질 코드를 실어야 하며, 품질은 {192, 64, 0}에서 추출되어야 한다"는 규칙은 SHACL sh:NodeShapesh:path bp:quality ; sh:in ( 192 64 0 ) ; sh:minCount 1 ; sh:maxCount 1 — 이며, 배치의 CQA를 게이팅하는 것과 같은 릴리스 게이트와 SHACL 셰이프 언어입니다. 그리고 "OSS 사본이 PI 원본을 소수점 자리까지 재현하는가?"는 교과서적인 역량 질문(competency question, 모델이 답할 수 있어야 하는 질문을 그 기대 결과와 짝지은 것)입니다: 태그 T에 대해 창 W에 걸쳐, OSS와 PI 값이 허용 오차 내에서 일치하는가? — 아래의 0개 행 조정 쿼리가 바로 그 CQ이며, PASS/FAIL 테스트로 실행됩니다. Book 4가 쿼리로서의 역량 질문에서 목록화하는 규율입니다. 브리지가 올바르기 위해 트리플스토어(triplestore)가 필요한 것은 아니지만, 그것이 강제하는 의미 — 타입이 지정된 단위, 계보, 출처, 셰이프 — 는 정확히 그 형식 어휘이며, 그래서 같은 행이 번역 없이 그래프 안으로 떨어집니다.

이 행은 어디서 오는가 — 삼부작의 척추

5.7589 g/L 역가는 물리적 사건으로 시작되었습니다. 1권의 생산 바이오리액터에서 뽑힌 시료로, 거기서 CHO 배양이 이 숫자가 측정하는 항체를 실제로 분비합니다. 2권은 같은 판독값을 이동해야 하는 데이터 포인트로 틀 짓습니다 — 그것을 현장에서 실어 나르는 연결성 표준(OPC UA, PI Web API), 그리고 그것이 히스토리안에 충실하게 안착하는 자동화 및 제어 데이터의 열린 과제입니다. 이 장은 그 고리를 닫는 코드입니다: 브리지와 ts.sensor_reading 행은 물리적 측정과 데이터 포인트가 테스트되고 조정된 산출물이 되는 곳입니다.

PI 자산 프레임워크 요소의 해부: WebId는 어디서 오는가

WebId는 임의적으로 보이지만, 실제 공장에서 그것은 풍부하게 컨텍스트화된 무언가의 주소입니다. PI는 단지 원시 태그의 PI 데이터 아카이브가 아닙니다. 그것은 그 태그들을 계층 구조, 속성, 단위, 템플릿으로 감싸는 자산 중심 계층인 PI 자산 프레임워크(PI Asset Framework, AF)입니다 [1]. 단일 WebId 뒤의 체인은 세 단계 깊이입니다.

  • 요소(Element) — 공장의 한 부분을 모델링하는 AF 객체. 우리에게 그것은 바이오리액터 BR101로, AF 요소 템플릿의 인스턴스이므로 현장의 모든 바이오리액터가 같은 이름의 같은 속성을 드러냅니다. 이것이 AF의 임무입니다: 원시 태그를 사람이 탐색할 수 있는 계층 구조로 컨텍스트화하는 것 — 정확히 우리 자신의 ISA-88/95 모델이 오픈소스 쪽에서 수행하는 역할입니다.
  • 속성(Attribute) — 요소에 매달린, 이름이 붙고 타입이 지정된 프로퍼티: Titer, 측정 단위(g/L)와 메타데이터를 가집니다. 속성은 분석 클라이언트가 탐색해 들어가는 대상입니다. 그것은 OPC UA Variable의 BrowseName에 대응하는 AF 쪽 유사물입니다.
  • PI 포인트(PI Point) — 속성이 가리키는 원시 아카이브 태그: 데이터 아카이브 안의 BR101.Titer.PV. 속성은 컨텍스트화된 얼굴이고, PI 포인트는 그 아래의 압축된 시계열입니다. 여러분이 해석하는 WebId는 셋 중 어느 것이든 — 요소, 속성, 또는 기저 포인트의 스트림 — 주소 지정할 수 있으며, 그래서 실제 PI는 포인트, 스트림, AF에 대한 엔드포인트를 노출합니다. 제공된 스텁은 우리 골든 배치를 실어 나르는 둘(points 해석과 streams/.../recorded 읽기·쓰기)을 제공하며, AF 계층은 여기서 제공되는 것이 아니라 충실성을 위해 기술됩니다.

그 대가: 맨 PI 포인트가 아니라 속성을 상대로 브리지를 작성하면, PI 쪽의 단위나 태그 이름 변경이 여러분의 읽기를 조용히 깨뜨리지 않습니다. AF가 디지털 트윈 문헌이 기록 원본 히스토리안에 기대하는 컨텍스트화를 보유하기 때문입니다 [4]. 제공된 스텁은 이를 더 가볍게 유지하지만 — PI 포인트 이름을 직접 해석하고 PIPoint 해석과 Stream 기록 값 엔드포인트를 제공합니다(AF는 제공되지 않고 기술됩니다) — 그것들은 실제 PI가 노출하는 바로 그 형태이므로, 여러분이 상대로 테스트하는 계약이 곧 여러분이 마주하는 계약입니다.

태그 해석: PI 포인트 이름에서 WebId로

어떤 값이 움직이기 전에, 정확히 한 번의 왕복이 식별자를 해석합니다. 브리지는 스텁의 GET /piwebapi/points?path=\\PISRV\BR101.Titer.PV를 호출하고, 핸들러는 마지막 \로 구분된 세그먼트를 태그로 취하여 _webid(tag)를 계산하고 {WebId, Name, Path}를 반환합니다. 그때부터 모든 읽기는 GET /piwebapi/streams/{webId}/recorded입니다. 이것의 세 가지 속성이 목을 상대로 한 개발을 안전하게 만듭니다.

  1. 목에서 WebId는 결정론적입니다. webid()가 태그 문자열의 순수 함수이므로, 브리지와 스텁은 등록 단계 없이 핸들에 대해 합의합니다 — to_sensor_rows()는 스텁에 "이것이 무슨 태그였나?"를 결코 물을 필요가 없는데, 호출자가 이미 자신이 해석한 태그를 알기 때문입니다.
  2. 역방향 매핑이 존재합니다. 스텁의 _point()는 WebId를 태그로 다시 디코딩하여 그 recorded() 핸들러가 골든 트레이스를 필터링할 수 있게 합니다 — df[df.tag == tag]. 실제 PI를 상대로는 WebId가 불투명하므로 여러분은 그것을 디코딩하지 않을 것입니다. 여러분은 태그 이름을 곁들여 운반할 것이며, 정확히 to_sensor_rows(items, tag, batch_id)가 이미 하는 그대로입니다. 브리지는 WebId가 디코딩 가능하다는 데 의존하지 않습니다.
  3. 누락된 태그는 조용한 빈 결과가 아니라 404입니다. 해석된 태그에 행이 없으면 스텁은 HTTPException(404, f"no PI Point for {tag}")를 일으킵니다. 그것을 빈 백필로 삼켜 버린 브리지는 조용히 간격을 남길 것입니다. 그것을 드러내는 것이 계약입니다.

OSS 사본이 안착하는 곳: 다른 모든 소스처럼 적재하라

경계 우리 쪽에서, PI에서 끌어온 값은 그저 또 하나의 시계열 행일 뿐입니다. 그것은 모든 수집 장이 기록하는 동일한 ts.sensor_reading 하이퍼테이블(hypertable)에, 동일한 여섯 개의 열로 안착합니다. 동반 저장소의 로더가 정확한 형태와 정확한 COPY 경로를 보여 줍니다 — 다음은 examples/tools/load_datasets.py에서 가져온 것입니다.

def load_timeseries(conn) -> int:
df = pd.read_parquet(DATA / "fedbatch_timeseries.parquet")
buf = io.StringIO()
df[["ts", "tag", "value", "unit", "quality", "batch_id"]].to_csv(buf, index=False, header=False)
buf.seek(0)
with conn.cursor() as cur:
cur.execute("TRUNCATE ts.sensor_reading")
with cur.copy("COPY ts.sensor_reading (ts, tag, value, unit, quality, batch_id) "
"FROM STDIN WITH (FORMAT csv)") as copy:
copy.write(buf.read())
return len(df)

이 열들이 계약입니다: ts, tag, value, unit, quality, batch_id. PI 브리지가 하는 일은 이보다 더 이국적이지 않습니다 — 그것은 PI Web API JSON을 정확히 이 형태의 행으로 바꾸어 COPY해 넣을 뿐입니다. 같은 파일의 헤더가 그 설계를 명확하게 말합니다.

This is the path Chapters 7–16 build up piece by piece; here it is one script so
the contextualization and ALCOA+ chapters have data to query. Idempotent: it
truncates the loaded tables first.

멱등성(idempotent)이라는 단어가 브리지에서는 모든 것의 핵심입니다. 로더를 다시 실행해도 테이블은 같은 상태로 남는데, 먼저 truncate하기 때문입니다. 운영 환경의 PI 브리지는 매 실행마다 세상을 truncate할 수 없으므로, 다른 방식으로 멱등성을 얻어 냅니다. 제공된 스키마가 부과하는 한 가지 제약에 주목하세요. platform/db/20-historian.sqlts.sensor_reading(tag, ts DESC)(batch_id, ts DESC)에 대한 비고유(non-unique) 인덱스만 가진 TimescaleDB 하이퍼테이블로 만듭니다 — (tag, ts)에 대한 기본 키(primary key)나 고유 제약(unique constraint)은 없습니다. 따라서 순진한 INSERT ... ON CONFLICT (tag, ts)는 구축된 그대로의 저장소에서는 실패합니다. 현재 스키마가 실제로 지원하는 메커니즘은 윈도우 삭제 후 삽입(delete-window-then-insert)입니다. 하나의 트랜잭션 안에서, 곧 백필하려는 태그와 시간 창의 행들을 DELETE한 다음, 새로 읽은 값들을 INSERT합니다. 그러면 겹치는 창을 다시 읽어도 결코 행이 중복되지 않습니다. (진정한 업서트(upsert)를 원한다면 먼저 UNIQUE (tag, ts) 인덱스를 추가해야 하며 — 하이퍼테이블에서는 그 인덱스가 파티셔닝 열인 ts를 반드시 포함해야 합니다.) 같은 목표, 다른 메커니즘입니다. 로더가 소비하는 CSV는 시뮬레이터가 내보내는 것을 본 것과 동일한 롱 포맷(long format)입니다(다음은 골든 배치의 마지막 두 개의 1분 간격 행, 즉 fedbatch_timeseries.parquet의 꼬리 부분입니다).

ts,tag,value,unit,quality,batch_id
2026-01-18 23:58:00+00:00,BR101.Titer.PV,5.8214,g/L,192,BATCH-2026-001
2026-01-18 23:59:00+00:00,BR101.Titer.PV,5.7589,g/L,192,BATCH-2026-001

왕복(round-trip)이 닫히는 것에 주목하세요. 위의 PI Web API JSON과 이 CSV 행은 같은 측정값을 기술합니다. 브리지의 유일한 임무는 그것이 참으로 유지되도록 하는 것입니다.

백필과 조정: 두 사본을 일치시키기

실시간 스트리밍은 쉬운 90%입니다. 어렵고 GxP와 관련된 10%는 네트워크가 두 시간 동안 다운된 후 무슨 일이 일어나는가입니다. 이제 OSS 사본에는 PI 원본에는 없는 간격이 생겼습니다. PI의 검증된 수집기가 정전 동안 버퍼링을 했기 때문입니다. 바로 이것이 OPC UA 이력 접근이 중요한 이유입니다 [10]. 브리지는 누락된 창에 걸친 이력 값을 PI에 요청하여 구멍을 메울 수 있습니다.

네 가지 동작의 백필 루프

견고한 백필 루프에는 네 가지 동작이 있습니다.

  1. 간격을 찾으세요. 우리 히스토리안에서 태그별 최신 ts를 조회합니다. PI에 그보다 더 새로운 것이 있다면 여기서는 누락된 것입니다.
  2. 이력을 읽으세요. [last_ours, now]에 대한 PI Web API 기록 값(또는 OPC UA HA 읽기)을 끌어옵니다.
  3. 창을 교체하세요, 무턱대고 덧붙이지 마세요. 하나의 트랜잭션 안에서, 해당 태그와 [last_ours, now] 창의 행을 DELETE한 다음, 새로 읽은 값을 INSERT합니다 — 그래야 겹치는 창을 행 중복 없이 안전하게 다시 끌어올 수 있습니다. (제공된 하이퍼테이블이 아직 갖고 있지 않은 UNIQUE (tag, ts) 인덱스를 추가한 후에만 INSERT ... ON CONFLICT로 전환하세요.)
  4. 조정하세요. 겹치는 점들의 표본을 다시 비교하여 허용 오차(tolerance) 내에서 일치하는지 확인하고, 어떤 불일치든 조용한 덮어쓰기가 아니라 데이터 무결성 이벤트로 로깅합니다.

품질 매핑: PI의 세 불리언을 하나의 OPC 코드로

2단계는 브리지의 유일하게 진짜 손실이 있는 이음새를 숨기고 있으며, 그것은 자체 그림을 가질 자격이 있습니다 — 왕복이 무손실이 아닌 유일한 곳이기 때문입니다. PI는 모든 Item에서 품질을 세 개의 독립적인 불리언으로 기술합니다 — Good, Questionable, Substituted. 우리의 ts.sensor_reading.quality192를 기본값으로 하는 단일 smallint로, 플랫폼의 나머지가 이미 사용하는 OPC 방식 코드를 담습니다(192 Good, 64 Uncertain, 0 Bad). 제공된 quality_code()가 크로스워크 전부이며, 그 순서가 중요합니다: 그것은 Questionable먼저 확인하므로(불확실한 점이 결코 Good으로 올림될 수 없습니다), 그다음 Good에는 192를, 그 외에는 0을 반환합니다.

품질 매핑이라는 제목의 3행 매핑 표로, 왼쪽의 PI Web API Item 플래그가 화살표로 오른쪽의 ts.sensor_reading 품질 코드에 매핑된다: Questionable false인 Good true는 녹색 192 Good에, (Good을 우선 덮어쓰며 먼저 확인되는) Questionable true는 호박색 64 Uncertain에, Questionable false인 Good false는 장미색 0 Bad에 매핑되고, 하단 주석은 Substituted 플래그가 안착할 열이 없으며 조정이 값 일치를 1e-6까지 단언함을 알린다.

그 표에서 두 가지 정직한 귀결이 떨어져 나옵니다. 첫째, Substituted는 안착할 곳이 없습니다 — 우리 여섯 개 열에는 "이 값은 수집기가 채워 넣은 것"이라는 자리가 없습니다 — 그래서 브리지는 그것을 버립니다. 그것은 사고가 아니라 문서화된 손실이며, 품질 검토자는 그것이 OSS 사본이 보유하지 않는 유일한 PI 사실임을 알아야 합니다. 둘째, 계약 테스트가 골든 배치의 Good 판독값에 대해 rows[0][4] == 192를 단언하므로, 그 크로스워크는 설계 문서 안의 주석이 아닙니다. 그것은 브리지와 함께 제공되는 실행 가능한 단언입니다.

출처(provenance)를 갖춘 시각적이고 재생 가능한 흐름을 원할 때는 Apache NiFi가 이를 위한 자연스러운 거처입니다. 그것의 InvokeHTTP 프로세서는 구성 가능한 엔드포인트를 호출하고 FlowFile 본문을 요청으로 보내는 HTTP 클라이언트로, 읽기에는 GET을, 쓰기에는 PUT/POST/PATCH를 지원합니다 [11] — 정확히 PI Web API가 사용하는 동사들입니다. 그러면 NiFi의 출처 기능이 누가 무엇을 언제 어디서 끌어왔는지를 기록하는데, 이는 조사 중에 금처럼 귀중합니다. 조정 단계 자체는 두 사본을 상대로 한 평범한 SQL입니다(예시 — stage.pi_recorded는 실제 브리지가 PI 읽기 결과를 안착시킬 스테이징 테이블이며, 제공된 테스트는 이에 상응하는 값 일치 단언을 파이썬으로 수행합니다, tests/test_bridges.py).

-- Reconcile the OSS copy against PI for one tag/window.
-- Flags any point where the copies disagree by more than tolerance.
SELECT o.ts, o.value AS oss_value, p.value AS pi_value,
abs(o.value - p.value) AS delta
FROM ts.sensor_reading o
JOIN stage.pi_recorded p ON p.tag = o.tag AND p.ts = o.ts
WHERE o.tag = 'BR101.Titer.PV'
AND o.batch_id = 'BATCH-2026-001'
AND abs(o.value - p.value) > 1e-6; -- expect zero rows

그 쿼리가 행을 반환한다면, 여러분의 사본이 어긋난 것입니다 — 어쩌면 단위 변환 버그, 어쩌면 시계 편차(clock skew) 때문입니다. 0개의 행을 반환하는 것이 이 쿼리가 증명하려는 속성이며, 제공된 테스트(tests/test_bridges.py)는 이에 상응하는 단언을 파이썬으로 수행합니다 — 제공된 PI 값이 parquet 꼬리와 소수점 자리까지 같고 Good이 192로 매핑된다는 것 — 그것이 진정 사본이 원본에 충실하다는 엔지니어링적 증명입니다.

PI에 접근할 수 없는 상태에서 브리지 테스트하기

여러분은 이 코드의 거의 전부를 근처에 PI 서버 하나 없이 작성하게 될 것이고, 그래도 괜찮습니다 — 단, 분위기(vibe)가 아니라 계약을 상대로 테스트한다면 말입니다. 이제 저장소에 제공된 정직한 하이브리드 설계는, 실제 PI가 노출하는 PIPoint 해석과 Stream 기록 값 엔드포인트를 제공하고(AF 엔드포인트는 제공되지 않고 기술됩니다), 골든 데이터를 제공하며, 쓰기를 받아들이는 pi-web-api-stub입니다. 이것은 FastAPI이므로 실행될 때 /openapi.json을 자동으로 노출합니다. 그 스펙에 계약 테스트를 고정(pin)하세요 — 그러면 schemathesis 같은 퍼징(fuzzing) 도구가 실제 PI가 지키는 것과 동일한 스키마를 상대로 스텁과 브리지를 모두 행사(exercise)할 수 있습니다. 이 책이 명확하게 강조하는 요점은 이것입니다: 목표는 PI 측이 목 처리된 채로, 계약을 상대로 실제이며 테스트된 브리지를 갖는 것입니다. 실제 PI가 있는 현장에 도달하면, 베이스 URL과 자격 증명(credential) 하나를 바꾸고, 실제 서버를 상대로 동일한 계약 테스트를 실행하면, 끝입니다. 이 장에서 오늘 실행되는 산출물은 브리지(examples/chapters/17-bridge-pi-historian/pi_bridge.py)와 그 스텁을 상대로 한 테스트(examples/tests/test_bridges.py), 그리고 위의 로더(examples/tools/load_datasets.py)입니다.

읽기 경로를 위한 최소 계약 — 예시적이며, FastAPI가 제공된 스텁의 읽기 엔드포인트에 대해 생성하는 형태입니다.

# pi-web-api-stub — read path contract (illustrative)
paths:
/piwebapi/streams/{webId}/recorded:
get:
parameters:
- { name: startTime, in: query, schema: { type: string } } # e.g. "*-2h"
- { name: endTime, in: query, schema: { type: string } } # e.g. "*"
responses:
"200": { description: Recorded values, content: { application/json: {} } }

이것이 이 장 전체의 정직한 핵심입니다: 우리는 AVEVA PI를 노트북에서 실행할 수 없으므로, 그런 척하지 않습니다. 우리는 계약을 고정하고, 그것을 상대로 가차 없이 테스트하며, 현실로 바꾸는 한 줄짜리 교체를 문서화합니다.

되돌려 쓰는 소프트 센서: 브리지가 모델에 빚지는 것

쓰기 경로 — 29장의 라만(Raman) 모델에서 나온 BR101.Titer.SoftSensor를 PI로 다시 POST하는 것 — 는 단방향 파이프처럼 보이지만, 그것은 또한 머신러닝(machine learning) 모델이 검증된 기록과 만나는 이음새이기도 하며, 몇 가지 규율이 그 위에 올라타 있어 브리지가 그것을 존중하지 않으면 조용히 깨뜨리게 됩니다.

첫째는 정직한 검증(honest validation)입니다. 소프트 센서(다른 신호로부터 계산되는 값으로, 여기서는 라만 스펙트럼으로부터 추론된 역가)는 한 번도 본 적 없는 배치에서 검증되었을 때만 신뢰할 수 있으며 — 그리고 "본 적 없음"의 단위는 행이 아니라 배치입니다. 같은 작업 세포 은행에서 나온 형제 배치는 배지 로트와 시드 트레인(seed train)을 공유하므로 거의 쌍둥이입니다. 판독값을 행 단위로 무작위 분할하면 거의 쌍둥이가 학습/시험 경계의 양쪽에 모두 떨어지고, 모델이 사실상 답을 보게 되며, 보고된 점수는 환상이 됩니다. 해법은 그룹화된 leave-one-batch-out 분할(grouped, leave-one-batch-out split) — 한 배치의 모든 판독값이 학습이나 시험 중 한쪽으로 통째로 가는 것 — 이며, 브리지가 모든 행에 충실하게 실어 나르는 batch_id 열이 바로 그 그룹화 키입니다. 일탈을 추적하는 계보와 정직한 검증 폴드를 정의하는 계보는 같은 스레드입니다. Book 5는 모델과 검증에서 이를 GroupKFold와 중첩 교차검증(nested cross-validation)으로 바꾸고, 데이터, 연료에서 배치 그룹화 분할을 기본값으로 삼습니다.

둘째는 적용 범위(applicability domain) — 새 판독값이 모델이 학습한 데이터를 닮았는지 묻는 게이트로, 소프트 센서가 자신 있게 틀린 숫자를 내놓는 대신 능력 밖에서 추측을 거부할 수 있게 합니다. 아래 실제 현장에서는의 관류(perfusion) 변형이 바로 그런 도메인 밖 사례입니다: 14일 유가식으로 보정된 모델이 한 번도 학습한 적 없는 높은 정상 상태 세포 밀도와 연속 수확 태그(PBR201.Harvest.Titer)를 마주합니다. 학습된 범위 밖의 영역에 대해 역가를 되돌려 쓰는 소프트 센서는 검증된 기록에 허구를 써 넣는 것입니다. 학습 문제 장이 이 게이트를 틀 짓고, 브리지는 모델이 도메인 밖으로 표시한 값의 POST를 거부함으로써 그것을 강제합니다.

셋째는 조정 루프가 똑바로 유지하려 존재하는 구분입니다: 공정 드리프트 대 모델 드리프트(process drift versus model drift). 살아 있는 세포는 배치마다 실제로 떠돕니다 — 그것은 공정 드리프트로, 디지털 스레드가 충실히 보존해야 하는 실제 제조 신호이며 결코 매끄럽게 지워서는 안 됩니다. 그 움직이는 공정에 대해 소프트 센서가 낡아 가는 것은 모델 드리프트로, 탐지하고 조치해야 하는 결함입니다. 둘을 혼동하는 것이 모니터가 거짓 경보를 울리거나 실제 변화를 놓치는 방식이며, Book 5가 구축하는 MLOps와 라이프사이클 거버넌스입니다. 브리지는 둘 다를 떠받칩니다: 그것은 SPC, 다변량 모델, 소프트 센서가 읽어 가는 거버넌스 데이터 소스이며, 모델의 계통(lineage) — 어떤 데이터셋 해시, 어떤 모델 버전이 BR101.Titer.SoftSensor를 만들었는지 — 이 일급 출처로 기록되어야 하는 곳이라, 나중의 감사가 출하된 로트에서 그것을 건드린 정확히 동결된 모델까지 거슬러 걸어갈 수 있습니다. 메커니즘적 물질 수지가 데이터 기반 추정을 제약하는 같은 하이브리드·물리 추론은 하이브리드 모델과 디지털 트윈 이야기입니다. batch_id를 잃거나, 출처를 떨어뜨리거나, 도메인 밖 추측을 되돌려 쓰는 브리지는 데이터만 잃는 것이 아닙니다 — 그것은 OSS 계층이 먹여 살리려 존재하는 바로 그 모델들을 오염시킵니다.

왜 중요한가

이 경계를 잘못 다루면 두 가지 실패 중 하나를 마주하게 됩니다. 오픈소스 히스토리안을 GMP 기록으로 만들려다 — 어떤 OSS 히스토리안도 기본 제공하지 않는 검증, 감사 추적, 21 CFR Part 11(FDA의 전자 기록 및 전자 서명 규정) 부담을 환자에게 아무런 이득도 없이 떠안거나. 아니면 두 사본이 조용히 어긋나게 두어, 여러분의 번지르르한 Grafana 대시보드가 감사관이 실제로 읽을 기록과 슬그머니 어긋나게 되거나.

검사관이 실제로 발견하는 것

이것은 가설적 위험이 아닙니다. 그것은 공개된 점검 기록에서 가장 많이 지적되는 결함 중 하나입니다. 2010년부터 2020년까지 제약 회사에 발행된 FDA 경고 서한(Warning Letter)을 회고적으로 분석한 연구는, 문서화 및 데이터 무결성 문제가 지배적인 cGMP 결함 범주였음을 발견했습니다 — 평균적으로 약 20–25%의 서한에서 주요 결함으로 지적되었고, 연구 기간 동안 cGMP 경고 서한의 약 21%를 차지했습니다 [13]. 반복되는 실패 양상은 정확히 이 장이 대비하여 엔지니어링하는 그것입니다: 원본의 충실하고 귀속 가능한 반영임을 보여 줄 수 없는 사본 또는 파생 기록 — 검사관이 적용하는 ALCOA+ 데이터 무결성 속성(귀속성(Attributable), 가독성(Legible), 동시성(Contemporaneous), 원본성(Original), 정확성(Accurate)에, 완전성(Complete), 일관성(Consistent), 지속성(Enduring), 가용성(Available)을 더한 것). 바로 그것이 원본 기록진정 사본이라는 MHRA 어휘가 존재하는 이유이며 [5], 위의 조정 쿼리 — 0개의 행을 반환해야 하는 그것 — 가 형식적 점검이 아니라 OSS 진정 사본이 여전히 PI 원본과 같다는 문서화된 증명인 이유입니다.

현장의 교훈은 단호합니다: 여러분의 사본이 경보 없이 원본에서 어긋날 수 있게 되는 순간, 여러분은 검사관이 적발해 적는 바로 그 결함을 만든 것입니다. 네 가지 동작의 루프, 윈도우 삭제 후 삽입 멱등성, 품질 크로스워크, 그리고 0개 행 조정 단언은 각각 그 21%에 들어가지 않기 위한 작은 보험 증서입니다.

올바르게 다루면 그것은 해방적입니다. 검증된 PI 시스템은 계속 자기 일을 합니다 — 원본 기록, 전체 감사 추적, 품질 부서가 승인하는 바로 그것 [5]. 여러분의 오픈소스 계층은 SPC, 다변량 모델, 소프트 센서를 위한 진정 사본에 빠르고 저렴하며 제약 없는 접근을 얻고, 계산된 통찰을 통제된 문을 통해 되돌려 줄 수 있습니다. GAMP 5 제2판은 오픈소스가 — 검증된 라이프사이클 안에서, 용도에 비례하는 공급자 및 위험 평가와 함께 — GxP에 속한다고 명시합니다 [12]. 브리지는 그 라이프사이클이 그어지는 이음새입니다.

실제 현장에서는

승인된 제품을 만드는 거의 어떤 mAb 공장에 들어가든, 중심에 PI 시스템이 있고 그것에 데이터를 공급하는 DCS(분산 제어 시스템(distributed control system)) 수집기와 그것에서 데이터를 읽는 분석 도구가 그것을 둘러싸고 있는 모습을 보게 될 것입니다. 이 장의 패턴은 교육용 단순화가 아닙니다. 그것은 통합 팀이 실제로 살아가는 방식입니다. 우리가 모델링하는 유가식(fed-batch) CHO + Protein A 라인은 승인된 단일클론항체(monoclonal antibody, mAb)의 지배적 양식이며, 그 데이터는 20년 동안 PI 히스토리안에 안착해 왔습니다.

우리 공정의 강화/연속(intensified/continuous) 변형 — 다중 컬럼 포착(multi-column capture)을 갖춘 관류(perfusion) — 은 이 요점을 더욱 날카롭게 할 뿐입니다. 관류 런은 높은 정상 상태 생존 세포 밀도를 유지하며, 배치에는 결코 없는 연속 태그를 추가합니다 — PBR201.CSPR.PV, PBR201.Perfusion.Rate, PBR201.CellBleed.Rate, PBR201.Harvest.VCD, PBR201.Harvest.Titer(모두 datasets/perfusion_timeseries.parquet에 있습니다). 더 많은 센서, 더 많은 태그, 저렴한 오픈소스 분석을 원할 더 많은 이유, 그리고 검증된 히스토리안을 그대로 두어야 할 정확히 같은 이유입니다.

이 계층에 대한 정직한 오픈소스 대 상용 평결: 오픈소스는 여러분에게 탁월하고 확장 가능한 히스토리안(TimescaleDB), 세계 수준의 대시보드(Grafana), 그리고 자유롭고 유연한 브리지(Telegraf [9], NiFi [11], 파이썬 PI Web API 클라이언트)를 줍니다. 그것이 주지 않는 것은 검사관이 전화했을 때 책임질 공급업체가 있는, 턴키(turnkey)이며 검증되고 Part-11 준비가 된 기록 시스템입니다. PI는 그것을 줍니다. 따라서 더 낮은 위험의, 규정을 준수하는 행보는 하이브리드입니다: PI는 원본을 보관하고, OSS는 분석과 사고를 담당합니다 [3][2]. 순수 오픈소스는 여러분에게 플랫폼의 약 80%를 줍니다. 이것이, 솔직히 말해, 마지막 GxP 1마일이 하이브리드인 이음새 중 하나입니다.

핵심 용어

  • 히스토리안 / 기록 원본(Historian / record-of-truth) — 공식적이고 감사 추적이 갖춰진 공정 기록을 보유하는 시스템. 대부분의 공장에서 이것은 OSS 히스토리안이 아니라 검증된 AVEVA/OSIsoft PI 시스템이다.
  • PI Web API — PI 데이터 아카이브와 자산 프레임워크 데이터에 대한 AVEVA의 RESTful HTTPS 읽기/쓰기 인터페이스 [6].
  • PI 자산 프레임워크(PI Asset Framework, AF) — 원시 PI 태그를 계층 구조, 속성, 단위로 컨텍스트화하는 자산 중심 계층 [1].
  • WebId — PI Web API가 포인트, 스트림, AF 요소를 주소 지정하는 데 사용하는 불투명한 식별자. 제공된 스텁에서는 패딩을 제거한 base64url(tag)이므로 BR101.Titer.PVQlIxMDEuVGl0ZXIuUFY로 해석된다. 실제 PI는 더 긴 서버 토큰을 발행하지만, 해석 후 읽기라는 계약은 동일하다.
  • PI 포인트(PI Point) — PI 데이터 아카이브 안의 원시, 압축된 시계열 태그(예: BR101.Titer.PV). AF 속성이 그것을 컨텍스트화한다(요소속성 → PI 포인트). 속성을 상대로 한 브리지는 태그나 단위 이름 변경에서 살아남지만, 맨 포인트를 상대로 한 브리지는 그렇지 못하다.
  • 기록 값 Item(Recorded-values Item) — PI Web API 기록 값 읽기가 반환하는 Items 배열의 한 요소: Timestamp, Value, UnitsAbbreviation, 그리고 세 개의 품질 불리언 Good/Questionable/Substituted. 단일 ts.sensor_reading 행으로 매핑되는 단위.
  • OPC UA 이력 접근(OPC UA Historical Access, HA) — OPC UA Part 11. 이력 값과 이벤트를 읽고 쓰는 것으로, 백필의 기반 [10].
  • 백필(Backfill) — 정전 후 PI 원본에서 누락된 창을 읽어 OSS 사본의 간격을 채우는 것.
  • 조정(Reconciliation) — 겹치는 창에 걸쳐 두 사본을 비교하여, 조용히 덮어쓰는 대신 불일치를 표시하는 것.
  • 멱등성(Idempotent) — 반복해도 안전한 연산. 제공된 히스토리안((tag, ts)에 고유 제약이 없는)을 상대로, 브리지는 해당 창을 삭제하고 다시 삽입하여 이를 달성하므로, 다시 읽어도 결코 행이 중복되지 않는다.
  • 진정 사본 대 원본 기록(True copy vs original record) — MHRA 용어. OSS 계층은 진정 사본/파생 데이터를 보유하고, PI는 원본을 보유한다 [5].
  • CSA(Computer Software Assurance) — OSS 분석 계층에 더 가벼운 엄격성을 적용할 수 있게 하는 FDA의 위험 기반, 최소 부담 보증 접근법 [3].
  • cGMP — current Good Manufacturing Practice. 통제되고 문서화되며 재현 가능한 의약품 제조에 대한 구속력 있는 기대.
  • GxP — "Good x Practice"(우수 x 관리 기준). 규제 대상 우수 관리 기준 규칙(GMP, GLP, GDP, …)을 아우르는 상위 개념이며, cGMP는 그중 제조에 해당하는 부분이다. "GxP 기록"이란 그 규칙들이 규율하는 기록을 말한다.
  • 21 CFR Part 11 — 신뢰할 수 있는 전자 기록과 전자 서명에 대한 FDA의 규정. OSS 히스토리안이 기본 제공하지 않는 검증, 감사 추적, 보증 부담. 위의 (이력 접근) OPC UA Part 11과는 무관하며 별개다.
  • SPC — 통계적 공정 관리(Statistical Process Control). 공정 변수를 시간에 따라 차트로 그려 드리프트를 잡아내는 것.
  • ALCOA+ — 검사관이 기록에 적용하는 데이터 무결성 속성: 귀속성(Attributable), 가독성(Legible), 동시성(Contemporaneous), 원본성(Original), 정확성(Accurate)에, 완전성(Complete), 일관성(Consistent), 지속성(Enduring), 가용성(Available)을 더한 것.
  • PROV-O / 출처 엣지(provenance edge) — W3C 출처 온톨로지. prov:wasDerivedFrom 트리플은 OSS 사본이 PI 원본에서 파생되었다는 을 기록하여, MHRA의 "진정 사본 대 원본 기록" 구분을 산문이 아니라 기계로 점검 가능한 사실로 바꾼다.
  • 그룹화된 leave-one-batch-out 분할(Grouped, leave-one-batch-out split) — 한 배치의 모든 판독값을 학습/시험 경계의 한쪽에 통째로 두어, 소프트 센서가 진정으로 본 적 없는 로트에서 채점되게 하는 검증 규율. 브리지의 batch_id 열이 바로 그 그룹화 키이며, 행 단위 무작위 분할은 환상 점수를 보고한다.
  • 적용 범위(Applicability domain) — 새 판독값이 모델의 학습 데이터를 닮았는지 묻는 게이트로, 소프트 센서가 자신 있게 틀린 값을 기록에 되돌려 쓰는 대신(예: 유가식 모델이 관류 영역을 마주할 때) 능력 밖에서 추측을 거부할 수 있게 한다.

다음 이야기

히스토리안은 연동하기에 가장 친근한 상용 시스템이었습니다. 그것은 개방형 프로토콜을 사용하고 우리가 이미 이해하는 시계열을 다루기 때문입니다. 다음 장 DCS, MES & ERP 연동: DeltaV, Siemens, SAP는 더 험한 영역으로 걸어 들어갑니다 — OPC UA를 통한 제어 시스템, 신뢰할 만한 오픈소스 GxP MES가 없다는 단호한 평결, 그리고 B2MML/ISA-95 메시지를 통한 자재, 로트(lot), 작업 지시(work order)의 ERP 교환 — 여기서는 정직한 하이브리드 경계가 선호가 아니라 필요에 의해 그어집니다.