정비 계획 담당자는 지난달 P-101 펌프가 412시간 돌았다고 했다. 신뢰성 대시보드는 511시간이라고 했다. 두 숫자 모두 같은 히스토리언에서 나왔다. 1년 반 간격으로, 서로 이야기한 적 없는 두 사람이 만든 calculated tag 두 개였다. 하나는 motor current 4 A 이상을 running으로 봤고, 다른 하나는 run feedback 접점을 봤다. Current 기준 태그는 jog와 순환 시험 때도 계속 시간을 쌓았고, feedback 기준 태그는 드라이브가 fault로 잡지 않은 저부하 운전 90시간을 통째로 버렸다.
누가 틀린 게 아니다. "runtime"이 뭔지 아무도 적어두지 않았을 뿐이다.
Calculated tag 문제는 결국 이거다. 시작은 사소하다. Trend 화면이 빨리 뜨라고 만든 1시간 평균, CSV export를 그만하려고 만든 일일 합계. 그런데 1년쯤 지나면 이 태그가 shift report와 에너지 리뷰, 그리고 아무도 손대기 싫어하는 MES interface로 들어가 있다. 정의 없는 calculated tag는 없는 것보다 나쁘다. 공식 숫자처럼 보이기 때문이다.
수식보다 먼저 공정 언어로 규칙을 적는다
Expression editor를 열기 전에, 운전팀이 읽고 반박할 수 있는 문장으로 규칙을 적는다. 위 펌프라면 이렇게 된다.
- Running은
P101_StatusCode = 30이다. Motor current도 아니고 VFD speed feedback도 아니다. - Source quality가 Bad인 구간은 누적하지 않는다. 대신
P101_RuntimeUnknown태그에 따로 쌓는다. - 결과는 UTC 달력일이 아니라 현장 production day(06:00~06:00) 기준 시간 단위로 저장한다.
- 늦게 들어온 데이터가 있으면 최근 48시간을 다시 계산한다.
네 줄이다. 10분이면 쓴다. 그리고 412 대 511 논쟁을 영구히 끝낸다. 다음 사람이 expression을 역추적하지 않아도 되기 때문이다.
나는 이 문장을 공유 폴더의 Word 문서가 아니라 tag description 필드에 넣는다. Description은 백업과 마이그레이션을 따라다니지만 Word 문서는 그렇지 않다.
계산 태그 다섯 종류는 각각 다르게 깨진다
Trend 화면 하나로 다섯 개를 다 검증할 수는 없다.
| 종류 | 예 | 실제로 깨지는 부분 |
|---|---|---|
| 순간 expression | Density 보정 flow | Bad input이 눈에 띄는 스파이크가 아니라 그럴듯한 값을 만든다 |
| Time-weighted average | 1시간 tank temperature | 불규칙 sampling과 deadband 압축이 평균을 치우치게 한다 |
| Event duration | Running / faulted / blocked 상태 시간 | Stale 값과 놓친 전환 |
| Totalizer rollup | 일일 water, gas 사용량 | Counter reset과 rollover |
| Shift, batch summary | OEE, downtime, 생산량 | 경계 timestamp와 late data |
이 중 순간 expression이 제일 교활하다. Density 전송기가 Bad로 안 가고 그럴듯한 998 kg/m³로 고장 나면, 보정된 flow는 계속 trend의 정상 범위 안에 머문다. 알람도 안 뜬다.
Bad quality를 어떻게 처리할지 명시적으로 정한다
한 시간 중 10분 동안 입력 하나가 Bad였다면 그 시간 평균은 얼마인가. 방어할 수 있는 답이 다섯 가지, 방어할 수 없는 답이 하나 있다.
방어 가능: 결과 전체를 Bad로 둔다. Partial 값에 ValidPercent 동반 태그를 같이 저장한다. Last good hold는 운전화면 표시용으로만 쓴다. Bad 구간을 평균에서 제외하고 제외된 시간을 따로 보여준다. 아예 누적을 멈춘다.
방어 불가: expression이 Bad를 0으로 바꾸고 계속 계산한다. 계량기 통신이 끊긴 날 에너지가 12% 절감된 것으로 나오는 경로가 정확히 이거다.
이건 제대로 해둘 값어치가 있다. 표준에 이미 어휘가 있기 때문이다. IEC 62541-13(OPC UA Part 13, Aggregates)은 AggregateConfiguration에 TreatUncertainAsBad, PercentDataBad, PercentDataGood를 정의한다. 구간에서 쓸 만한 데이터가 얼마나 있었는지에 따라 aggregate 결과를 Uncertain이나 Bad로 돌려준다. 들어온 값만 조용히 평균 내지 않는다. 히스토리언이 이 설정을 노출한다면 expression에 직접 quality 로직을 짜지 말고 그걸 쓴다. 없으면 최소한 개념은 흉내 낸다. 값 옆에 coverage 퍼센트를 같이 저장하는 것이다.
특히 Uncertain_LastUsableValue를 조심한다. 어떤 클라이언트는 이걸 Good과 똑같이 그려준다. 4시간 동안 평평하게 유지된 hold 값은 아주 안정적인 공정처럼 보인다.
산수보다 시간 경계에서 더 많이 깨진다
경험상 calculated tag 결함의 대부분은 수식 오류가 아니다. Commissioning 전에 아래를 훑는다.
- Rollup timestamp가 기간 시작인가 종료인가. 00:00에 찍힌 일일 합계와 23:59:59에 찍힌 일일 합계는 1년에 한 번 서로 다른 달에 들어간다.
- 하루 경계가 자정인가, shift change인가, production day인가. 리포트마다 하나를 고르고 그걸 명시한다.
- UTC로 저장하고 local time으로 보여주는가. 그러면 1년에 이틀은 23시간과 25시간이다. 25시간짜리 날을 24로 나눈 "일 평균"은 4% 틀린다.
- 계산 기준이 source event time인가 히스토리언 arrival time인가. Store-and-forward collector가 있으면 이 둘은 몇 분에서 며칠까지 벌어진다.
- 늦은 sample이 이미 닫힌 기간을 다시 열 수 있는가. 열 수 있다면, 이미 리포트를 뽑아 간 사람에게 알려주는 장치가 있는가.
숫자가 맞아도 시간이 한 칸 밀리면 그 리포트는 틀린 것이고, 대놓고 이상한 숫자보다 훨씬 찾기 어렵다.
구간을 골라서 손으로 계산해 본다
한 달치부터 보면 안 된다. 아래가 가능하면 다 들어가는 2시간짜리 구간을 고른다.
- 정상 운전.
- 상태 전환이나 정지 한 번.
- Bad quality 또는 통신 끊김 구간.
- Hour, shift, day 경계 통과.
- Counter 기반이면 reset이나 rollover.
그 구간의 raw source sample과 calculated sample을 export하고, spreadsheet나 Python 20줄로 결과를 재현한다. 열에 아홉은 가장자리에서 어긋난다. 구간의 첫 sample, 마지막 sample, 그리고 구간이 [start, end)인지 (start, end]인지.
Export 자체에도 함정이 하나 있다. 검색 모드가 raw가 아니라 interpolated면, 히스토리언의 보간값을 히스토리언의 aggregate와 비교하는 셈이라 아무것도 증명하지 못한다. Raw로 뽑는다.
Counter reset이 단일 결함으로는 제일 흔하다
일일 합계는 대개 누적 counter에서 온다. kWh, m³, cycle count, kg. 이 counter는 PLC download, 계기 교체, power cycle, rollover에서 값이 튄다. 16-bit Modbus register는 65,535에서, 32-bit DINT는 4,294,967,295에서 넘어간다. 기계식 계량기 스타일 register는 데이터 타입과 무관하게 999,999에서 넘어가기도 한다.
Rollup이 그냥 마지막 − 처음이면 reset 한 번에 큰 음수 일일 합계가 나오고, rollover 한 번에 웃기는 숫자가 나온다. 믿을 만한 rollup은 아래를 구분한다.
- 정상적인 증가.
- 설비 정지로 인한 평탄 구간.
- 0으로 reset.
- 알려진 최대값에서 rollover(modulus를 더한다).
- 물리적으로 불가능한 jump. 최대 유량 × 구간 시간으로 상한을 잡고 버린다.
- 운전팀이 입력한 manual correction.
무엇을 버렸든 기록으로 남긴다. WaterTotal_Discarded 같은 태그나 히스토리언 event 하나면 된다. 반년 뒤에 3월 사용량이 왜 적게 나왔는지 알려줄 유일한 단서다.
계속 반복해서 보는 실패들
단위를 화면에서 가져온다. PLC 태그는 L/min, HMI는 m³/h, 리포트는 gallon으로 가정한다. 단위를 태그 이름이나 description에 넣는다. FIT201_Daily라는 이름은 아무것도 알려주지 않는다.
평균을 다시 평균 낸다. 24개 hourly average로 만든 daily average는 매 시간의 valid coverage가 같을 때만 맞다. 어느 한 시간이 40% Bad가 되는 순간, hourly mean의 산술평균은 time-weighted daily mean이 아니다. Raw에서 다시 rollup하거나 가중치를 같이 들고 다녀야 한다.
Backfill 후 재계산이 없다. Raw trend는 복구됐는데 calculated tag는 그대로다. 리포트는 영원히 틀린 채로 남는다. 우리 히스토리언이 late arrival에서 파생 데이터를 자동 재계산하는지, 수동 trigger만 되는지 콕 집어 확인한다. 제품마다, 때로는 태그 설정마다 다르다.
공학 경계로 업무 질문에 답한다. 압축기 효율은 calendar hour가 맞다. 생산 손실은 shift 경계가 맞다. 이미 있다는 이유로 기존 rollup을 재사용하면 downtime이 엉뚱한 조에 붙는다.
수식 변경에 owner가 없다. Expression 한 줄 수정은, 히스토리언이 재계산하면 몇 년치 리포트 이력을 조용히 바꾸고, 재계산하지 않으면 불연속을 만든다. Formula 변경은 PLC download와 같은 취급으로 change control에 넣고 적용일을 남긴다.
리포팅에 넘기기 전
Formula, source tag, 단위, 계산 주기가 tag description에 있을 것. Bad quality 동작은 머리로 따져본 게 아니라 실제로 케이블을 뽑아서 시험했을 것. Rollup timestamp 기준이 리포트 작성자가 가정하는 것과 일치할 것. Backfill 동작을 알고 있을 것. Counter가 있으면 reset을 시험했을 것. 경계가 들어간 구간에서 손계산과 값이 맞을 것. 그리고 이름은 계속 쓸 각오가 된 이름일 것. MES가 이미 조회 중인 태그의 이름을 바꾸는 건 편집이 아니라 change control 작업이다.
시간이 딱 하나만 있다면 이 시험을 권한다. Source 태그 하나를 Bad로 만들어 놓고 내일 리포트에 뭐가 찍히는지 본다. 어차피 대부분의 현장은 그렇게 quality 처리를 알게 된다. 보통은 감사받는 중에.