Fix what the last round of device fixes broke, and two dips it missed
This commit is contained in:
@@ -90,7 +90,7 @@
|
||||
<th class="collevel">Level</th>
|
||||
<th>Runs out</th>
|
||||
<th>Rate</th>
|
||||
<th title="Cartridge changes detected, over the days of history behind this row">Replacements</th>
|
||||
<th title="Cartridge changes seen anywhere in the history window, and how long the CURRENT cartridge has been in">Replacements</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -131,10 +131,15 @@
|
||||
<template v-else>-</template>
|
||||
</td>
|
||||
<td class="muted small">
|
||||
<!-- "2 / 90d" read as a date, a ratio, or a version to
|
||||
everyone who saw it. Say the unit. -->
|
||||
<!-- Two DIFFERENT spans, so they must not be joined by a
|
||||
word that implies one contains the other. Replacements are
|
||||
counted across the whole history window; basisdays is only
|
||||
how long the CURRENT cartridge has been in. "2 in 4.0d"
|
||||
claimed two changes within four days, which is the shape
|
||||
that reads as broken data - and here it would be the
|
||||
label lying, not the data. -->
|
||||
<template v-if="c.basisdays">
|
||||
{{ c.replacements || 0 }} in {{ c.basisdays }}d
|
||||
{{ c.replacements || 0 }}, this one {{ c.basisdays }}d
|
||||
</template>
|
||||
<template v-else>{{ c.replacements || 0 }}</template>
|
||||
</td>
|
||||
|
||||
@@ -33,6 +33,20 @@ REPLACEMENT_RISE = 10
|
||||
# while a phantom swap resets the run and throws the estimate away entirely.
|
||||
NEW_CARTRIDGE_LEVEL = 80
|
||||
|
||||
# How far ABOVE its previous level a reading may come back and still count as a
|
||||
# recovery rather than a new cartridge. Small, because it exists for gauge
|
||||
# noise: a real swap returns near full, well past this.
|
||||
RECOVERY_TOLERANCE = 2
|
||||
|
||||
# How long a dip may last and still be a bad reading rather than real use.
|
||||
# Levels alone cannot separate the two: a swap to full, ordinary consumption,
|
||||
# then another swap has the same SHAPE as an outage that recovers. What differs
|
||||
# is elapsed time. A supply out of the machine, a door open or a bad poll spans
|
||||
# minutes to an hour or two at the few-minute polling these items use; a level
|
||||
# that stays down for days is genuinely down, and deleting those readings would
|
||||
# hide a real empty period.
|
||||
MAX_DIP_HOURS = 6
|
||||
|
||||
# Below this many readings a slope is arithmetic, not evidence. Two points
|
||||
# through a coarse gauge can "prove" any rate at all.
|
||||
MIN_POINTS_FOR_ESTIMATE = 4
|
||||
@@ -92,8 +106,23 @@ def normalise(points):
|
||||
return drop_spikes(out)
|
||||
|
||||
|
||||
def _recovered(previous, following, tolerance=RECOVERY_TOLERANCE):
|
||||
"""Did the level come BACK to where it was, rather than up to a new one?
|
||||
|
||||
Toner only falls, so a dip that recovers returns to at or below the level it
|
||||
left - consumption carried on while the reading was junk. A cartridge that
|
||||
was CHANGED comes back HIGHER than the level before the dip.
|
||||
|
||||
That asymmetry is the whole test. Comparing the absolute difference instead
|
||||
treated a genuine swap of a nearly-full cartridge (95, then 5, then 100) as
|
||||
a recovery and deleted the evidence, because 100 and 95 are close. The small
|
||||
upward tolerance is for gauge noise, not for swaps.
|
||||
"""
|
||||
return following <= previous + tolerance
|
||||
|
||||
|
||||
def drop_spikes(points, rise=REPLACEMENT_RISE):
|
||||
"""Remove one-reading dips that RECOVER to where they came from.
|
||||
"""Remove dips that RECOVER to where they came from, however long they run.
|
||||
|
||||
A single reading far below both neighbours, then a recovery, is a big
|
||||
upward step that scores as a cartridge change. That is how a cartridge
|
||||
@@ -108,22 +137,45 @@ def drop_spikes(points, rise=REPLACEMENT_RISE):
|
||||
The filter keys on SHAPE rather than cause, which is why it holds for all of
|
||||
them: a level that comes back to where it was did not get a new cartridge.
|
||||
|
||||
Only a dip that comes BACK to roughly its previous level is removed. A
|
||||
genuine near-empty reading before a swap (30, 5, 100) does not recover - it
|
||||
jumps to full - so it is kept, and the swap after it still counts.
|
||||
ANY LENGTH, not just one reading. At the few-minute polling these items
|
||||
often use, a door left open or a supply out of the machine spans several
|
||||
polls, and a filter that only removed single readings left the original
|
||||
failure in place for every cartridge above NEW_CARTRIDGE_LEVEL.
|
||||
|
||||
A genuine near-empty reading before a swap (30, 5, 100) does not recover -
|
||||
it comes back HIGHER than 30 - so it is kept and the swap still counts.
|
||||
"""
|
||||
if len(points) < 3:
|
||||
return points
|
||||
out = [points[0]]
|
||||
for index in range(1, len(points) - 1):
|
||||
index = 1
|
||||
while index < len(points) - 1:
|
||||
previous = points[index - 1][1]
|
||||
current = points[index][1]
|
||||
following = points[index + 1][1]
|
||||
dipped = (previous - current) >= rise and (following - current) >= rise
|
||||
recovered = abs(following - previous) <= rise
|
||||
if dipped and recovered:
|
||||
if (previous - points[index][1]) < rise:
|
||||
out.append(points[index])
|
||||
index += 1
|
||||
continue
|
||||
out.append(points[index])
|
||||
# A dip starts here. Take every consecutive reading that stays down.
|
||||
end = index
|
||||
while end < len(points) - 1 and (previous - points[end][1]) >= rise:
|
||||
end += 1
|
||||
following = points[end][1]
|
||||
# ONE bad sample is a candidate whatever the cadence: the reading is an
|
||||
# instant, and the gap to its neighbours says nothing about how long the
|
||||
# supply was actually out. SEVERAL consecutive low readings only count
|
||||
# as one outage if they are close together - spread over days they are a
|
||||
# real absence, and they also have the same shape as a swap, ordinary
|
||||
# consumption, then another swap.
|
||||
spanhours = (points[end][0] - points[index - 1][0]).total_seconds() / 3600
|
||||
brief = (end - index) == 1 or spanhours <= MAX_DIP_HOURS
|
||||
if (brief
|
||||
and (following - points[end - 1][1]) >= rise
|
||||
and _recovered(previous, following)):
|
||||
index = end # drop the whole stretch
|
||||
continue
|
||||
for keep in range(index, end):
|
||||
out.append(points[keep])
|
||||
index = end
|
||||
out.append(points[-1])
|
||||
return out
|
||||
|
||||
|
||||
Reference in New Issue
Block a user