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). A trend drawn as a second line on another chart names that chart in its scope.overlayOf, with axis
(left or right), name and unit: a pump wear finding's flow chart carries its current that way on a right axis,
and its flow-per-amp chart its twin's, so the finding page draws both tags the check compares (issue #315). 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).
- A Real issue on an anomaly that has no detector yet queues a proposal run of the same playbook (
trigger_kindverdict, run as that person, never held by the hourly cap);feedbackreturns itsproposalRunId. 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 recordsskipped: "already proposed as <id>". - 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
backtestDaysof history. Every time it would have fired becomes adetector-firefinding (abandordriftrule whoseaggregateisavg,minormaxover awindowMinuteslonger 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 aholdMinutesbeyond 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'sdetectorswith its score (times it would have fired, matched a real or known issue, false alerts, waiting for review) and the writer's explanation aswhy. A full asset (20) or collection (500) fails the run; a detector that run already wrote for another merged verdict stays. - 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.
- 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 expressionrunScript("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 isAboveValueorBelowValueat the line with an Absolutedeadbandof one normal spread (the rule'sdeadband), 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 isif({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/BelowValueinclude 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_maxfor 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 itspath) 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 onlyif,absandisGood(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. - Once imported, the detector reads
appliedand its fires come from the alarm journal: each screening run reads the journal for the alarm on the detector'spath(the model'salarms.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 adetector-firefinding withsource: 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 withsource: 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.