Skip to content

Anomaly Watch

Template anomaly-watch@1, writer role: screens a collection's process measurements against their own history for the machine's current operating state, checks that the historian is recording them properly, and turns an anomaly a person confirms into a proposed alarm. The arithmetic decides what is an anomaly; the model only explains it. No model is trained, and the writer's call is the run's only model call.

Setting it up. The easiest route is the Setup view (section 10): it lists each asset's tags that keep history, suggests a run signal, and on Save writes the assets' state into the context model (section 7.5) and the playbook anomaly-watch-<collection> with a cron trigger (every 1, 2, 3, 4, 6, 8 or 12 hours, or daily at a time you type as HH:mm) over a 24-hour window plus a script trigger; Save and run now also queues one run. Ask can fill the form in: the form's Ask Artekis to fill this in button asks for you, or a question such as "set up anomaly watch on this area" calls the anomaly_setup_draft tool, and the answer carries a Review the setup button that opens the Setup view pre-filled. Nothing is written until a person at the admin level presses Save. A playbook written by hand works the same way. No example playbook ships. The Setup form offers every n hours or daily at a chosen time; any cron works in the Playbooks editor, but a later Save in the Setup form replaces a cron it does not offer (the form says so).

An area already watched. The Setup view finds the project's own anomaly-watch@1 playbooks whose collections name the area, whatever they are called and wherever they sit (Whitfield/AnomalyWatch-cell3 as well as anomaly-watch-cell3), and says in plain words which machines each covers (params.assets, else every machine), what it checks, when it runs and whether it is turned off. Save changes only the playbook the form creates itself, anomaly-watch-<collection>. While only other playbooks watch the area, Save and Save and run now are off, the page says to change those playbooks on the Playbooks page (with a button to it), and anomaly.saveSetup refuses with 409 before writing anything, the run signals included, so the form never starts a second watch over the same machines. The Drift check also runs the slow-trend, setpoint and per-run checks (section 8.5, checks), and the form says so.

Sizing. A run writes about its candidates in batches of budget.candidatesPerCall (12) per writer call (section 8), so a busy area needs no splitting. If writer calls still run long, raise the provider's timeoutsSeconds for the role (section 6.1), point roles.writer at a faster provider role ({"writer": "triage"}), raise budget.maxOutputTokens, or smooth the drift check with thresholds.drift.windowSamples (36, i.e. 3 hours of 5-minute steps). Counters are state, not measurements: a per-shift counter or an OEE figure that resets every shift jumps each reset and files an anomaly every time: leave it out of the screened tags, or use it as the asset's product/state signal. Manual runs: system.artekis.trigger with at (the window's end) and window re-runs a past stretch (at most 31 days, refused before any model call); every playbook whose event trigger matches runs unless the event names one with playbook (section 11).

params key Meaning (default)
checks which screenings run: drift, band, stuck, rate, quality, dataHealth (all six); trend, runs and setpoint follow drift
runs the per-run pump check (see Pump runs), true or false (on wherever drift is)
setpoint the setpoint check (see Setpoint vs actual), true or false (on wherever drift is)
sensitivity fewer, standard or more: a shipped threshold set per check (standard)
thresholds per-check overrides by name, e.g. {"drift": {"sigma": 4}}; an unknown check or name is refused
baselineDays whole days of history before the window that build the baseline, 7–365 (30)
excludePastAnomalies leave the spans of this playbook's resolved and known anomalies on a tag out of its baseline, as live ones always are, so a repeating drift is not absorbed by the last wear cycle; a dismissed or "wrong" anomaly stays in (the plant said it was normal); true or false (true)
baselineGapDays end the baseline this many days before the window, so a slow drift younger than the gap is compared with history it has not reached; with no gap a daily run's baseline takes a slow drift in as it grows and never flags it. 7 to 14 suits a daily run looking for drifts that build over one to three weeks; whole days, 0 to 60 (0)
backtestDays whole days a proposed detector is replayed over, 30–730 (180)
assets the asset ids to screen (every asset of the collection)

A wrong value is refused at save and at scan, naming its field (params.baselineDays).

Baselines. Each run builds, from the baselineDays before its window, one baseline (mean and standard deviation, σ) per measurement tag and operating state: running, stopped, changeover, or one undivided baseline for an asset with no run signal: and per product where a product tag is set and the pair has at least a day of samples (288); fewer than 36 samples gives no baseline and the tag is not screened in that state. A product that ran too little in the baselineDays to have its own baseline is looked up in its own older runs, up to two more baselineDays back; a product still without one is not screened at all (never against all products, which reads a different product as a drift), and the run's KPIs list it under partsWithoutBaseline. A baseline value more than 6 σ out is left out once a day of samples is in, so an old anomaly does not widen its own baseline, and the time of every anomaly the playbook still has open (open, confirmed, hidden or Known) is left out of its tag's baseline: a drift from its start onward , so a slow drift never becomes the normal it is measured against. The baselines are written into the run's KPIs.

Changes a person made (audit log). For a drift, trend, band, stuck or rate finding, the writes and edits in the asset's own tag folder from 12 hours before its start to 2 hours after it become an evidence item ev:aw:change:N (kind event, up to 3, closest first) and the closest is named in the text. Since 1.5.15 each write also carries the value the tag held just before it (its last stored value) and the change ("from 165 to 150 (down 15)"), and the item carries the finding's own reading over the 6 hours before and after the closest change, which way it moved and whether that is the way the finding says: "Around when it began: 2nd shift lead set fast shot velocity setpoint from 165 to 150 (down 15) at Oct 26, 10:14 PM (audit log); DCM-203 fill time went from about 40 to about 44 ms across it." A drift or learned-band finding whose span starts at the screened window's start began before the window, so its real onset can be days older: with nothing in the usual hours, the newest 5 writes to its own tag or to the setpoint the context model pairs with it, up to the baseline's length back (at most 31 days), are looked at newest first, and the first one across which the reading's 6-hour median moved the finding's way by at least half the finding's shift (score × baseline spread) is named, as "It began before the screened window; the latest earlier change it moved with: …". Newer writes to it that were not named are listed in the item (newerChangesNotNamed), each with why in notNamedBecause: the reading did not move across it, moved against the finding across it, moved the finding's way by less than half its shift, or could not be read on each side: and the text says they are there. A trend or a band past a fixed limit has no shift from normal to compare, so it is not looked back for. That audit read keeps at most 5,000 records; when it reaches the cap the run logs that older changes were not looked at. The value before a write is the tag's last stored value before the audit record's time; when that is already the value written (the historian stamped it a little early), the value 2 minutes earlier is taken if it differs, unless another write to the same tag falls in those 2 minutes (a quick repeat then reads as no change). The text names what was done and when, never that it caused the change.

The checks. The window is screened in 5-minute steps, and only steps where the asset runs (or has no run signal) and the step holds at least one real sample; the four value checks also skip a step whose quality is not Good, while quality counts every such step, Good or not. A stopped or changeover period never raises an anomaly, and neither does a value the historian filled in across a gap. Values are read as SimpleAverage, sample counts as Count, and quality raw over the window.

Check Fires when Threshold by sensitivity (fewer / standard / more) Fixed
drift the 3-hour rolling median moves sigma σ from the baseline sigma 5.5 / 3.2 / 1.85 windowSamples 36, returnSigma 1.0
band several values in a row far from the baseline, on one side, with a median of sigma σ; and minSamples values in a row past the tag's fixed range in the context model (normal min/max, else the alarm limits l/h, then ll/hh) sigma 8.0 / 5.5 / 4.0 minSamples 3
stuck samples repeats of exactly the same value samples 36 / 18 / 12 :
rate several large sample-to-sample swings in a row, with a median of sigma σ sigma 6.4 / 5.0 / 3.4 minSwings 4
quality flips Good ↔ not-Good changes within 2 hours flips 18 / 9 / 4 windowSamples 24

A run past the fixed range is a band finding whatever the learned baseline says, so a slow drift that has widened the baseline is still caught once it leaves the range the engineers set; its metric carries limit {min, max, source} and its text names the value passed ("Press 1 force above its normal max 255 kN for 2 h"). Where a learned-band run overlaps it, the fixed-range finding stands. A detector proposed from such a finding alarms at that fixed value (above the max, or below the min, on the side the finding crossed), held band.minSamples buckets like any band detector.

Where two kinds overlap on one tag, one finding stands, in the order stuck, quality, rate, trend, drift, band, except that a trend and a stuck, quality or rate run inside it both stand. A fixed name can be overridden through thresholds like the others.

Per-part results. A tag that holds one stored value for each part tested (a leak test's rate, a gauge's dimension) is not a continuous signal, so it is not screened by the 5-minute drift, band, stuck, rate or trend checks above (they read its parts as readings over time: a "rate" row for individual parts swinging, a "trend" of 0.06 sccm a day while the daily means sat flat). Artekis counts parts instead, over the stored values taken while the asset's run signal is on (all of them when it has none; the signal's last value before the window is used, so an on-change signal that has not moved for days still counts), Good quality only. Mark such a tag in the context model with "kind": "perPart" on the measurement tag; "kind": "continuous" says it is not one. With no kind, Artekis infers it when the asset holds a part counter (a state tag whose name is a count, such as PartCounter or PartCount, not a reject, scrap, fault, alarm or target count (a shift's or cycle's count is fine: the ratio decides); one with part, piece or unit in its name is preferred, and the run record names the counter used) and the tag's name has a whole name part from leak, torque, gauge, gage, weight, thickness, hardness, flatness, porosity, dimension, result, fit or seal.

The counter decides. Over the baseline period the stored in-run values are compared with the counter's rises (a fall is a reset, so the new value is counted from 0; a step over 100 is unknown, and its span is left out of both the rises and the stored values). One value a part reads as 0.8 to 1.25 stored values for each counter rise. An inferred tag outside that ratio, or on a counter that rose fewer than 50 times, stays continuous, and the run record's perPart list says why. A tag marked perPart outside the ratio is judged anyway with the mismatch noted (mismatch); an inferred tag already judged per part (the last screening run's record lists it under perPart: the newest window that ends no later than this run's, ignoring proposal, test and past-window runs, or it has an open per-part row) stays per part until the ratio leaves 0.7 to 1.4, and becomes per part only inside 0.8 to 1.25, so an edge ratio does not flip it between runs; when it is stored several times a part (the ratio above 1.25) the window and the counts are in parts, not stored values, so one part is not counted several times. A tag stored on change cannot show a stuck value (two equal parts in a row store once); stuck here is for a tag stored on every part. The last partWindow (200) parts of the window are judged against the baseline period's parts (at least partMinReference 200 of them, the newest 20,000; a window with under partMinCurrent 50 parts is not judged and the run record says so):

Finding Fires when Threshold by sensitivity Fixed
band (share outside) at least partMinOutside parts are past the tag's alarm limits (else its normal range), the share is at least partRise times the baseline's (floored at 0.5 % and at one part in the baseline's size) and the exact one-sided binomial tail of that many outside, at the baseline share, is no larger than the normal tail of band.partZ band.partZ 4 / 3 / 2.5 partRise 2, partMinOutside 5
drift (shift in the mean) the mean of those parts is at least partShift baseline spreads from the baseline's mean and the test of a mean of that many parts reaches drift.partZ drift.partZ 5 / 4 / 3 partShift 0.5, partWindow 200
stuck stuck.parts parts in a row with exactly the same value (not a value the baseline mostly reads) parts 60 / 30 / 20 :

A tag with no alarm limits and no normal range gets only the mean and stuck checks. When the share and the mean both moved, one share finding stands and carries the mean's numbers. The three findings are their own families, part-share ("Per part: share outside limits"), part-mean ("Per part: shift in the mean") and part-stuck ("Per part: same value"), so a per-part row never takes over an old continuous row's identity and a share row and a stuck row on one tag are two findings. They are screened under drift, band and stuck (a playbook that unticks all three does not screen per part) and a run's found counts list them under their own families. When a tag is first judged per part, its open rate, trend, drift, band and stuck rows close at once with the reason "now judged per part tested". A confirmed per-part finding does not propose a detector: detectors are for the continuous families. The metric has a perPart object (parts, referenceParts, outside, share, referenceShare, mean, referenceMean, referenceSpread, z, from, to, inferred) and the text counts parts: "Of the last 200 parts tested, 18 (9 %) read over its alarm limit 4 sccm, against 3.8 % of the 14,451 parts tested in the baseline period." The tag's quality check and its data-health check still run on the 5-minute screen. Reading costs one raw history query per tag per day of baseline up to 20,000 parts (plus the counter's and the run signal's), so mark only real result tags.

Slow trends (trend, family trend; runs wherever drift is checked, params.trend: false turns it off). A slow climb drags the baseline along with it, so this check reads no baseline: it takes the median of each 12 hours of screened steps over the 28 days before the window's end (each product's offset from the older days taken out), and on the last 10 days it is a trend when the Theil–Sen slope is steady (Mann–Kendall z at least 2.5), the change over those days is at least change (8 / 6 / 4 by sensitivity) times the tag's normal 12-hour-to-12-hour spread, the last third of the days sits past the first by half the change, and the shape is a ramp, not a step (a step is a level change, band's job). A trend found again continues its finding; one not found is held 36 hours, so a daily run resolves it on the second morning without it. A trend whose climb has been undone: the latest three 12-hour medians all back within a quarter of the climb of the level it started from (a worn part replaced, a filter swapped) , is not reported, and a finding still open on it resolves on that run, judged whether or not the readings still look like a trend. The resolved finding carries the evidence item ev:aw:reset in the tag's own readings and unit (for example, rounded: "Resolved: the reading fell back to about 12 ms, its level before the climb (12 ms), by Nov 3, 11:00 PM; the climb had reached 24 ms."), and the run's KPIs list it under trendResets (tag, by, base, peak, level). Not a repair: one low median inside a climb that goes on, readings well below the old level (idle) or part of the way down (light load), a return on a tag whose readings already went back to the old level once the climb was half way and climbed on (a light part), or a part new to the 28 days, whose level reads as the old level by construction. A day and a half that reads exactly the old level, on a tag that never went back there during the climb, and then climbs again resolves the finding, and the climb is filed again. Wear after a repair (issue #276): when the 10-day test fails, the tested days are searched for a repair, latest first (an older one would date a later climb to itself): a step (the median of three 12-hour blocks against the three before it) of at least change times the spread, with at least six present blocks after it. The blocks from the step must pass the trend test on their own (the same z, change and last-third rules) while climbing away from the step, and the old part must have worn: the level just before the step sits past the reading's usual low level earlier in the 28 days (the lower quartile of every earlier block, so one dip or light stretch cannot set it) by at least change times the spread and by at least half the step: the sawtooth of a part that wore (or wore and then held, a worn plateau; or wears in back-to-back cycles), was replaced and wears again. Known limit: slow wear that began before the 28 days, whose new part reads below anything in them, is left to the 10-day test (as before), which files the new climb once it fills the tested days. A step where a product new to the 28 days starts running (its blocks read at the base by construction) is a changeover, not a repair; a product that started days before a real replacement does not hide it. Known limits: a replacement within about a day and a half of a new product starting is skipped (its size cannot be read from this series), and once a product's start falls into the reference days its offset is taken from the old part's worn readings, which can hide the new part's climb; the 10-day test still files it once the climb fills the tested days. The finding's trend evidence then carries afterRepair, repairAt and repairAtPlant, and its why says the reading stepped on that day "as when a worn part is replaced". That climb is the trend: it starts at the step (told as "since about", never "for at least"), at its own rate, and its text speaks of the days since the step. A dip that recovers (to a flat level, after a mild drift, or on a reading that dipped or ran light before) had not worn before the step, so it is not this. Without this the most common wear pattern (worn, replaced, wearing again) went unseen for about a week after each replacement, until the new climb outgrew the old one in the tested days: on Whitfield DCM-304's new sleeve (12.5 → 15.2 ms, Nov 4–7, after the old one had worn to 23 ms) nothing was filed. A replaced part that holds its level, or moves on the step's own way, is not a trend.

A trend that has levelled off: the latest eight 12-hour medians with data move at no more than a quarter of the ramp's rate in either direction, by Theil–Sen, so a day-and-night alternation in the medians does not count as movement and a tail falling back is not flat: is still a finding (the reading did change and has not come back) but is told as past: "HTST-2 holding tube temperature rose about 0.7 °F from Oct 11, level since about Nov 2", with where it holds, and with no "at this rate it reaches" forecast. For such a trend the ramp is fitted over the whole 28 days as flat, a straight ramp, flat again, so its start is the true start of the climb (the 20-day window used to cut it to a later day), its rate is the ramp's own and its total is what the ramp added. Artekis allows for the reading's normal day-to-day wobble, so a flat stretch with noise still counts as level: the tail counts as level while it moves by no more than a quarter of the ramp's rate, or no more than twice what a truly flat tail's measured slope would wander by given that wobble, whichever is larger. A levelled trend is judged on the whole lookback rather than the last 10 days (which a plateau empties of change): it stays a finding while its level is still a real change from before the ramp (at least change times the spread, over a steady ramp that is not a step), and ends when the reading comes back toward its old level (the three-median test above) or the change falls below that. The metric's trend item carries levelledOff, levelledSince, levelledSincePlant and rampStartPlant. Every trend finding also says which direction is the dangerous one (trend.worseWay): from the tag's alarm limits, else its normal range, in the context model (the dairy demo's model now carries them: HTST-1/2 low 161 °F, RS-4 high 42 °F, wastewater pH 5.5–9.5). With only a high limit rising is the dangerous way and a fall is the safe way, but "a reading that falls with nothing else changing can also be an instrument reading low, hiding a real value higher than shown, so check the sensor" (and the reverse for a low limit); with both, either way matters and the one it heads for is named; with neither, it says no limit or normal range is set rather than guessing.

A trend small against its setpoint band is not filed. When the reading declares a setpoint and a tolerance in the context model (abs, or pct of the setpoint), a slow trend is filed only when it matters against that band: it moved at least bandShare (default 0.5, so half) of the tolerance; or the reading is already outside the band (setpoint ± tolerance, using the latest setpoint); or, still moving, at its present rate it reaches the edge of the band it is heading for within the days it was measured over (recentDays, 10). Otherwise the change is real but too small to act on: on Whitfield, DCM-306's metal temperature fell 1.8 °F over five days on a bath held to ±8 °F of its setpoint, and the falling side's edge was weeks away. A finding still open on such a trend resolves on that run with a note saying why ("Resolved: still falling about 0.36 °F a day, but only 1.8 °F against its ±8 °F setpoint band around 1245 °F; at this rate it would take about 18 days to reach the band's edge, so it is no longer listed."; the digest's reason reads "closed: small against its setpoint band"), and the run's KPIs list the tag under trendsWithinBand with the numbers. bandShare is set like the other trend values (thresholds.trend.bandShare, above 0 and at most 1): lower it to file smaller moves, raise it toward 1 to file only moves that use most of the band. A reading with no tolerance is judged exactly as before, as is one whose setpoint cannot be read. The writer also follows the asset's rules (a rule about a reading that is high or low) for the direction. The check's own direction sentence is added to the end of the writer's text for a trend when the finding is stored, unless that text already holds the sentence's key phrase word for word ("rising is the safe direction", "either direction can matter"); a long text is shortened so the sentence is never cut (issue #274: on the dairy both HTST-2 and the wastewater pH lost it to the writer). A writer that said it in other words gets a short repeat of the same fact; the writer's wording is not guessed at. The "no alarm limit or normal range is set" sentence is never added, since the writer may know the direction from the asset's rules.

Pump runs (runs, family runs; runs wherever drift is checked, params.runs: false turns it off). A pump that runs a few minutes per cycle blends running and idle samples in a 5-minute step, so this check judges it run by run instead, and only assets that are pumps: a run signal, a liquid-flow tag (gpm, l/s, m3/h, …; not an air unit) and a current tag (A), or with no unit a tag whose path ends in a name with the whole word flow and amps or current (the tag's name is not read); totals, switches, alarms and setpoints are left out, and so is an asset named a blower, fan or compressor. Its twins are the pumps with the same parent. It reads the run signal and both tags raw, one day per call (finished days are kept in memory, so a repeated run reads only its new days), cuts runs at the signal's edges, drops 15 s at each end, and uses only runs with no twin running. The last recentDays (3) are compared with the pump's runs from lookbackDays (28) to recentDays before the run (25 days by default): ragging is burstRuns (3) runs in a row with current at least ampRise (4) % up and flow at least flowDrop (3) % down; wear is flow per amp down at least change % (7 / 4 / 2.5) and z (3) run-to-run spreads, unless every twin that can be compared fell at least twinShare (0.5) of that too. While a wear finding is open it is measured against the reference it was first filed against. A pump the run could not judge (deadline, failed read, too few runs) keeps its open findings. One finding per pump per condition, in the check's own words (no model text).

Pressure falling back while every pump is off (a third condition of the same check: family runs, no new family, no model marker). A check valve that leaks back lets the force main drain through the pump while it is off; the pressure falls in every stop and the pump has to move the same water again. It judges a station: the parent asset of pumps (as above) that carries a discharge pressure tag of its own (a unit of pressure, or pressure / psi in the name; with several, the one named discharge, force, main, header or outlet; a tag named suction, intake, inlet, seal, oil or tank, or with a name part of total, switch, overflow, alarm, fault, trip, setpoint, status, command or limit (the pump check's own list of non-measurements) is not a discharge pressure; with several left and none so named the station is not judged, and a station whose only pressure tag was left out by its name is not either. Both are listed with the reason under skipped in the Anomaly Watch run record). A level tag on the station (a length unit, or level) is read for the fill rate. A stop is the stretch from the last running pump's falling edge to the next start of any pump, at least stopMinutes (4) long; its drop is the median of its first two pressure samples minus the median of its last two (30 s after the stop and 10 s before the next start left out), as a share of the first. The last recentDays (3) are compared with the stops from lookbackDays (28) to recentDays before the run (25 days by default), as for wear: a finding needs minRecentRuns (10) and minReferenceRuns (30) stops, the recent median drop at least backflowDrop % (15 / 10 / 6) and z (3) spreads above the reference median, and backflowShare (0.7) of the recent stops each at that level. While the finding is open it is measured against the reference it was first filed against, so a leak that goes on never becomes its own normal. The pressure is judged, not the wet-well fill rate: closed check valves hold the force main whatever the rain or the inflow, while rain also fills the well faster; the fill rate is only carried in the evidence and the text ("also refilled about 12 % faster") when it rose. Which pump's valve leaks is not told: the action says to run each pump alone and watch the pressure. The stops are read like the pump runs (one day per call, finished days kept in memory; the run record lists rawQueries, chunks and cachedDays), and a pump whose run signal has no sample in the window starts from its A station the run could not read or judge (deadline, failed read, too few stops) holds its open finding (marked seen, unchanged, with the reference it was filed against) for 72 hours after the finding's last span; past that it is not marked seen, the usual closing rules apply and the row closes with the reason not-checked ("could not be checked for more than three days"), listed under letGo in the run record. A pump with no run-signal sample in the window is seeded from its last value, found by reading back one day at a time. A station without a pressure tag is not checked.

Two more conditions look at a station (the parent asset of the pumps) from the runs already read, with nothing more read and no model marker. A station is judged when any of its pumps is screened (its other pumps are read as twins, and a station whose pumps could not all be read shows as unchecked); the comparison with neighbours uses the stations read, so a playbook that screens one station's pumps has no neighbours. A station row is scoped to the station asset; the playbook's screened list gets the station of every screened pump added, so a playbook that names pumps but not their station does not close the station's rows as "no longer screened".

Duty share: at a station with two or more pumps, one pump that carried at least dutyShare (0.85 / 0.75 / 0.65) of the starts and of the run hours in the last recentDays files one finding, if the pumps used to take turns: the pump carried at least dutyRise (0.2) less of the hours in the oldest nine days of the lookback (three 3-day stretches), and the station was not always run that way. "Always run that way" means that both the oldest stretches and most of the stretches overall (at least dutyEarlier, 0.5, of each) had one pump doing dutyShare of the hours: a duty and standby pump, or a lead that changes weekly, is left alone. In plain words: a pump that took over all the work less than about 19 days ago is found; one that has done it for longer has become the station's normal in the 28 days the check reads, unless its finding is already open, which keeps the reference it was filed against. Duty needs about four weeks of pump history, and about 30 pump starts (minReferenceRuns, roughly 3 a day) in the oldest nine days of it, before it can judge a station; a station that pumps less often than that is not judged. The finding reads "LS-2 Pump 1 does 100 % of the pumping at LS-2 (the last 3 days), where the pumps used to share it". A pump that is only worn runs longer but starts as often, so it is not this.

Storm response: each station's volume per plant-local day is its pumps' runs (flow times minutes), and its extra over its own dry level (the lowest quarter of its own weekdays, or weekend days) is its extra a day. A storm is a stretch of days on which the typical station is up at least stormRise (0.15); no rain gauge is needed. Two comparisons, one finding per station: against the neighbours (three or more stations read), when stormMinStorms (2) of the last three storms the others were up in each had at least stormRatio (2 / 1.6 / 1.4) times their extra and the three together did ("LS-6 pumps about 2.0 times as much extra water after rain as the other stations"); and against the station's own earlier storms, when each of its last stormMinStorms storms had at least stormRatio times the median extra of its earlier storms in the lookback (at least two of them) and stormRise of a dry day more. Rain is not measured, so a much bigger storm also raises the own-history comparison (the text says so and asks for the rainfall); the neighbour comparison is immune to that. While a finding is open it stays down to half way to the ratio. A wet spell every station shares files nothing by the neighbour comparison; with three or more stations read the own-history comparison files only when the station's own ratio is at least stormRatio times the median of the other stations' own ratios (below three stations it runs as described). Limit: a leak that most stations of a large plant share is not found without a rain gauge. A station whose pumps could not all be read keeps its open row for three days after its last sighting, then lets it go with the reason not-checked; the note says what is needed to file it again (for duty: the pumps still not taking turns and the change within the last 19 days). The run record lists the stations under runs.stations (checked, hits, liveRowsKept, unchecked, letGo).

Setpoint vs actual (setpoint, family setpoint; runs wherever drift is checked, params.setpoint: false turns it off). A die caster's setup sheet gives metal temperature, fast-shot velocity and intensifier pressure a target per part and a band around it; the checks above compare a reading with its own history, so a setpoint changed for a new part reads as a drift, and a reading that slips off a setpoint that did not move can hide in its own scatter. This check compares the reading with its setpoint instead, on every measurement tag paired with a setpoint tag (section 7: named by setpoint, or paired by name on the same asset). On the same running 5-minute steps the other checks use, it reads the setpoint's last value per step (carried over steps with no sample; across the baseline gap the setpoint's last sample in the gap is the window's starting value) and leaves out the settleMinutes (30) after a setpoint change, while the reading catches up, so a setpoint that changes more often than that is never judged. The gap is reading minus setpoint, and the band is the tag's tolerance, or by default spreadFactor (3) times the gap's normal scatter in the baseline or setpointPct (1) % of the setpoint's median over the baseline, whichever is wider; the finding says which band it used, with the factors in force. A band of no width (a pct tolerance at a setpoint of 0) is not judged. Per part (all running steps when the asset has no product tag), the window is compared with the same part's baseline steps (all parts' when that part ran fewer than minBuckets, 36, steps), and a finding is filed when the share of running time outside the band is at least minShare % (20 / 15 / 10) and rise (4 / 3 / 2) times the share before, or the average gap sits past biasTolerance (half) of the band with biasShare (half) of the steps on that side, where the baseline was not already offset the same way (an offset the plant always runs with is its normal). A setpoint that moves with the reading following it files nothing. The finding names the part and reads, for example, "DCM-203 metal temperature outside its setpoint band (±10 °F) 31 % of running time, up from 4 %"; its text adds the average gap ("on average 14 °F under setpoint"), the window and the baseline dates, and its evidence holds the numbers, the gap per step and a per-part table. One finding per tag pair, continued like a drift (a span within 72 hours of it continues it) and resolved by the first run that reads the pair back in band; a run that cannot judge the pair (too few running steps, no baseline, the setpoint not read, or the part the finding was filed on did not run) keeps it open. An open finding's time is left out of the pair's baseline; when that empties the baseline (a finding older than baselineDays, however old), the steps before the finding began are read instead: only that pair's reading, state signals and setpoint, up to two baseline lengths back from the finding's start, stopping once a day of running steps is found or the history runs out: so the finding still resolves once the reading is back. Where a setpoint finding is stored, a drift on the same tag running the same way, on the same part and within the setpoint finding's flagged steps (an hour either side), or a trend whose part in the window overlaps them, is the same movement: it is not filed again, and an open one is kept as it is. A band finding always stands (it is about the reading's own range). Setpoint findings take up to 25 of the run's 50 places first, strongest first, and the value findings get the rest; a setpoint finding past those 25 is not stored (its open row is kept), so nothing is ever held for a setpoint finding that was not stored. Pairing by name leaves out a tag ending in a controller or statistics word (Out, CV, CVO, Err, Max, Min, Avg), and where either tag has no unit it pairs only a tag whose role is measurement and whose name ends in nothing or a unit-like token (F, C, Ips, Psi, PV, Pct); a setpoint whose name matches more than one tag is not paired and is listed in the run's KPIs (setpoint.unpaired), so name its setpoint in the context model. A setpoint historized only on change should use the Discrete deadband style, so the historian holds its value between changes. The run's KPIs carry setpoint (pairs, how many were judged, hits, stored, kept, value findings held, steps read from before an old finding, unpaired setpoints, and the pairs it could not judge with why). Shift breakdowns are not in this version.

A controller that hides a sensor (inside the setpoint check, family setpoint, no new family; needs a tag's control in the context model, section 7). A reading a controller holds on its setpoint looks perfect even when its sensor drifts: Millbrook's aeration probe fouls and reads high, the controller backs the blowers off until the screen reads 2.0 mg/L again, and the real oxygen falls. What gives it away is what the controller does. The check reads hourly means of the reading, its setpoint, the output (manipulated) and the load, over the last ctlLookbackDays (28) complete plant days, folds them into plant-local days (a day needs 18 hours) and asks, for the last ctlRecentDays (3) days: was the reading within ctlPinPct (10) % of its setpoint (or the tag's tolerance) on at least ctlShare (60 %, and at least two) of them, and was the output off its normal on at least that many, all the same way, by at least ctlChange % (20 / 10 / 7) and ctlSpreads (3) times its own day-to-day spread? The normal is taken from the oldest ctlRefDays (14) days of the look-back, not the days just before, so a fault younger than that is read against what the plant did before it: the median of those days, or, when a load tag is declared and spread at least 5 % over them, the line of output against load, used only when it cuts the reference days' scatter by a quarter (a recent day with a load outside that range by more than a quarter of it is not judged). A row that is open keeps the normal it was filed with, so a fault that goes on never becomes its own normal. With lab declared, the gap lab minus reading is compared with the same gap in the reference days (ctlMinLab, 5 lab days, and two recent ones): a shift of at least ctlLabPct (5) % of the setpoint and ctlSpreads times the gap's own spread, the way acts says the sensor must be off, confirms it and the finding says so; no shift means the lab agrees with the probe, so nothing is filed; a shift the wrong way is contradictory and is not judged. With no lab, too few lab days or an unreadable source the finding is filed as unchecked and says exactly that. The text reads, for example, "Aeration dissolved oxygen holds its setpoint, but total airflow is 14.9 % lower than usual ... The lab values also moved away from the reading: the lab value is 1.3 mg/L below the reading now ... the sensor may be reading high". A loop the run cannot judge (too few days, a tag unread, lab contradicting) keeps its open row for 72 hours after its last span; after that the row resolves with the reason not-checked. The reading itself is not screened for drift by this check, and the 5-minute screen is unchanged; the extra read is a few hundred hourly points per tag. A setpoint moved during the recent days (by more than the band from the reference days' median) is not a sensor fault: the loop is not judged, and it stays unjudged until the reference days' median setpoint has changed over (about 21 days), so a drift that starts in that time can land inside the reference and be read as normal. Limits: a fault older than the reference days when the check first runs is read as normal; the daily resolution finds a change over days, not one that comes and goes within a day; a controller with several outputs declares one; and an output can shift for reasons that are no fault (season, water temperature, a process change), so declare lab where there is one: it is the guard that tells a drifting probe from a plant that simply needs less air.

Data health (dataHealth) counts the samples the historian holds for each screened tag and reports a gap (no sample for gapMinutes, 60, or six times the tag's usual spacing if longer, between two samples inside the window , a tag silent from the window's start, for the whole window, or still silent at its end files no gap; one that stopped at least rateMinHours into a window at least twice that long, and stayed silent for at least rateMinHours, shows as a sample-rate change instead, and otherwise nothing is filed), duplicate samples (at least duplicateFactor, 2, times the usual count in duplicateMinBuckets, 3, or more steps in a row) and a sample-rate change (the hourly count off by rateFactor, 2, for at least rateMinHours, 6, up to the window's end). Gaps on one asset that begin and end together are one finding naming every tag; so are gaps on different assets that begin and end within a tenth of their length (at least one bucket) of each other when each lasts 30 minutes or more (a gateway, historian or network stop, not a device). These are configuration findings: the Gateway Health email lists every collection's open ones in a Data health section, re-listed after the repeat window, and sends a Gateway Health email for them alone when its own run has nothing new.

Titles in plant words. Anomaly Watch's own title names the asset, the tag's own name in words, how far from normal in the tag's units and for how long: DCM-304 int response 4 ms above normal for 7.5 h (the asset's name, the tag's name when it has one, and the tag's unit from the context model; section 7); σ and the tag path stay in the text and evidence, and the writer is asked for the same shape.

Anomalies are events. Every span a check passes is stored as a finding with the check's own numbers (confidence 0.6, open) whether or not the writer writes about it; a writer finding on the same span replaces that text. At most 50 are stored per run, strongest first. A finding is keyed by where its span started (<family>:tag:<path>@<start>#<asset>). A span on the same tag, check and asset that overlaps an open finding's span, whichever starts first, continues that finding (its seen_count and last_seen move, verdicts and notes stay on it). Each open finding is continued by at most one span a run, and a span continues at most one finding: one stored with exactly its start keeps it, and the others pair by the nearest start, so two data-health problems on one tag stay two findings. A span after a gap is a new finding, and a run that finds an already-closed span again reopens that same finding rather than filing a new one. A drift is one finding per episode: drift spans of one tag less than 24 hours apart (a product change or a stop ends a span, not the drift) are one finding, whose metric counts its spans, and the finding keeps the baseline it was first filed against. While the drift lasts, each run compares the last windowSamples of the tag (same state and product) with that filed baseline: still returnSigma σ or more on the drift's side and the finding is seen again, even when the regular check no longer fires; back within it and the finding closes under the rule below; with too few samples to tell (the machine stopped, another product ran) it is left as it is. A run closes only findings of the assets and checks it screened, and only those it actually read past: its window must start no later than the end of the window the finding was last seen in and end no earlier than the finding's start, and no span it found, including those past the 50 cap, may overlap the finding. So a manual run over an old stretch (the at event below) files findings that the regular schedule leaves open, and running that stretch again updates them. Every stored anomaly carries the window it was last screened in (evidence ev:aw:screened), and its trend evidence the tag's context-model limits and normal range in scope. A trend starts before its span: 6 hours before, or a third of the span when that is longer: and runs an hour past it, so a drift that began in an earlier run still shows the normal running before it and where it began (the history before the window is read for this, at most 31 days back). Past detector fires and confirmed anomalies are never closed for not being seen again. One run screens at most 31 days, and a history read that runs past the run's deadline fails the run naming the deadline.

From a verdict to an alarm. In the review queue a person marks an anomaly Real issue (confirm), Known (known) or Not an issue (dismiss).

  1. A Real issue on an anomaly that has no detector yet queues a proposal run of the same playbook (trigger_kind verdict, run as that person, never held by the hourly cap); feedback returns its proposalRunId. A later Real issue retries a proposal that wrote nothing; once a detector names the anomaly, further verdicts queue nothing, and two at once still write one detector. A proposal whose rule is the same as a detector the asset already has on that tag (same kind, state and rule, not rejected: a Real issue on a second row of one anomaly proposes exactly that) writes nothing and records skipped: "already proposed as <id>".
  2. The run proposes a detector: a rule at the playbook's sensitivity, armed only while the asset runs and not in a changeover (always, for an asset with no run signal): and replays it over backtestDays of history. Every time it would have fired becomes a detector-fire finding (a band or drift rule whose aggregate is avg, min or max over a windowMinutes longer than 5 minutes is judged on that window's average, minimum or maximum over a full window of armed Good readings, as its words say: "the 180-min average of the value is above X"; the window is its hold, and a holdMinutes beyond the window must hold too), the newest 50 per run (the score still counts them all; since a screening run files fires from their own text and never sends them to the writer: the detector is the saved rule); a fire overlapping an anomaly or fire on the same tag that a person already judged takes that verdict. Fires of several detectors on the same tag and asset starting in the same minute are one finding ("Detectors fired: …") whose evidence holds each detector's fire; one verdict on it counts for every detector it names, and each detector's live counts include it. The detector is written into the asset's detectors with its score (times it would have fired, matched a real or known issue, false alerts, waiting for review) and the writer's explanation as why. A full asset (20) or collection (500) fails the run; a detector that run already wrote for another merged verdict stays.
  3. Review the proposed detector opens the Detector view: the trend with the threshold and past fires, the rule in words, the score, and Download the tag import.
  4. Import it yourself. The download is Ignition's tag import holding one expression tag and its alarm. A detector judged over a window (step 2) carries a second tag first: a helper, <Tag>_<kind>_<aggregate> beside the detector tag, holding the window's average (or minimum, maximum) of the watched tag. Ignition's expression language reads no history, so the helper is the one fixed expression runScript("system.tag.queryTagCalculations(['<tag>'], ['Average'], rangeMinutes=<window>).getValueAt(0, 1)", 300000) at a fixed 5-minute execution rate (no tag group needed); the watched tag must be historized. The detector tag compares the helper with the line, its alarm's on-delay is only the hold beyond the window (0 for a proposal; an on-delay a person edited is kept), and the alarm's notes name the helper tag. The helper averages every reading, running or not; the detector tag stays armed only while the asset runs. A drift detector proposed by 1.5.2 or later (one drift, one alarm) carries the helper's value instead of true/false, and its alarm is AboveValue or BelowValue at the line with an Absolute deadband of one normal spread (the rule's deadband), so it clears only once the average is back past the line by that much (the notes say where). With a run signal its expression is if({run} >= 0.5 [&& {changeover} < 0.5], {helper}, <line ± half the deadband>): while the asset is not running the tag reads a value inside the deadband, where Ignition's alarm neither starts nor clears (AboveValue/BelowValue include the setpoint itself), so a wash or a stop pauses the alarm and one drift stays one alarm; the backtest pauses its fires the same way. From 1.5.2 the armed test covers the whole window, as Artekis's own check does: the import adds an arm helper per signal, <Tag>_<kind>_run_min (the run signal's minimum over the window; _run_max for a detector armed while stopped) and <Tag>_<kind>_changeover_max, in the same helper template, and the detector tag tests those instead of the signals' current values. So a wash or changeover anywhere in the window keeps the average's wash readings away from the alarm (the run and changeover signals must be historized too). A detector imported before 1.5.2 still lists as applied; re-import it to get the arm helpers. Import all of them into the detector's parent folder (the folder in its path) with the collision policy Abort. Artekis never imports the detector or writes it into your tag providers. Before it is offered, the tag is checked: only the allowed keys, alarm modes, priorities and delays; an expression that parses, references tags by absolute path and calls only if, abs and isGood (a helper is accepted only as the exact expression above for its own watched tag and window, beside the detector tag, its watched tag existing, readable and free of quotes and backslashes; arm helpers the same way, a Minimum or Maximum over the helper's own window, at most two); a parent folder that exists and is not inside a UDT instance; a path that is free or already holds this exact expression; every referenced tag readable under the policy.
  5. Once imported, the detector reads applied and its fires come from the alarm journal: each screening run reads the journal for the alarm on the detector's path (the model's alarms.journal, else the gateway's first journal profile), from a day before its window so a continuing alarm keeps its start, and files each activation as a detector-fire finding with source: journal. The module stops evaluating that rule. Where no journal can be read (no journal profile on the gateway, and none named by the model), Artekis keeps evaluating the applied detector's rule itself, as before the import, and files its fires with source: module, so the detector never goes silent; the run's KPIs say why (journalFires.skipped) and list those detectors (journalFires.ruleFallback), and the Detector view shows "No alarm journal: fires come from Artekis's own check". Until it is applied, every screening run evaluates a proposed detector itself and files its fires the same way.

band, drift and quality detectors carry an Ignition expression and alarm. stuck and rate detectors cannot (an expression cannot see the previous value) so Artekis proposes them with no expression, refuses their download for that reason and keeps evaluating them. (The refusal is on the missing expression: do not add one to a stuck or rate detector by hand.) Reject (admin level) sets a detector rejected; it leaves the queue's links.