A toner rate needs days behind it
This commit is contained in:
@@ -37,6 +37,18 @@ NEW_CARTRIDGE_LEVEL = 80
|
|||||||
# through a coarse gauge can "prove" any rate at all.
|
# through a coarse gauge can "prove" any rate at all.
|
||||||
MIN_POINTS_FOR_ESTIMATE = 4
|
MIN_POINTS_FOR_ESTIMATE = 4
|
||||||
|
|
||||||
|
# A rate needs TIME behind it, not just readings. Supply items are often polled
|
||||||
|
# every few minutes, so four readings can span a quarter of an hour - and a 2
|
||||||
|
# point drop across fifteen minutes extrapolates to nearly 200 percent a day,
|
||||||
|
# which is how a cartridge sitting at 82 percent gets forecast to run out in a
|
||||||
|
# fortnight. The history window can be short for the same reason: the fetch
|
||||||
|
# keeps the newest rows up to a per-item cap, so a fast-polled item returns days
|
||||||
|
# rather than the ninety asked for.
|
||||||
|
#
|
||||||
|
# Two days is the smallest span that survives a printer's daily rhythm - a
|
||||||
|
# single heavy morning does not become the whole picture.
|
||||||
|
MIN_DAYS_FOR_ESTIMATE = 2
|
||||||
|
|
||||||
# Many printers report in 10% steps, so a fortnight can pass on one plateau.
|
# Many printers report in 10% steps, so a fortnight can pass on one plateau.
|
||||||
# Without a minimum observed drop the slope reads as zero and the forecast
|
# Without a minimum observed drop the slope reads as zero and the forecast
|
||||||
# says "never", which is worse than saying nothing.
|
# says "never", which is worse than saying nothing.
|
||||||
@@ -201,7 +213,7 @@ def burn_rate(points):
|
|||||||
if len(points) < MIN_POINTS_FOR_ESTIMATE:
|
if len(points) < MIN_POINTS_FOR_ESTIMATE:
|
||||||
return None
|
return None
|
||||||
total_days = (points[-1][0] - points[0][0]).total_seconds() / 86400
|
total_days = (points[-1][0] - points[0][0]).total_seconds() / 86400
|
||||||
if total_days <= 0:
|
if total_days < MIN_DAYS_FOR_ESTIMATE:
|
||||||
return None
|
return None
|
||||||
# The overall drop still gates the estimate: a gauge sitting on one plateau
|
# The overall drop still gates the estimate: a gauge sitting on one plateau
|
||||||
# has not proved anything yet, whatever the intervals say.
|
# has not proved anything yet, whatever the intervals say.
|
||||||
@@ -290,7 +302,8 @@ def analyse(points, rise=REPLACEMENT_RISE, currentlevel=None):
|
|||||||
|
|
||||||
if rate is None:
|
if rate is None:
|
||||||
# Say which of the three it is; "no estimate" alone invites a bug report.
|
# Say which of the three it is; "no estimate" alone invites a bug report.
|
||||||
if len(run) < MIN_POINTS_FOR_ESTIMATE:
|
basis = result.get('basisdays') or 0
|
||||||
|
if len(run) < MIN_POINTS_FOR_ESTIMATE or basis < MIN_DAYS_FOR_ESTIMATE:
|
||||||
result['reason'] = ('replaced recently' if replacements
|
result['reason'] = ('replaced recently' if replacements
|
||||||
else 'not enough history yet')
|
else 'not enough history yet')
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -339,3 +339,26 @@ def test_a_real_near_empty_reading_before_a_swap_is_kept():
|
|||||||
points = normalise(series([40, 30, 5, 100, 95, 90]))
|
points = normalise(series([40, 30, 5, 100, 95, 90]))
|
||||||
assert 5.0 in [level for _, level in points]
|
assert 5.0 in [level for _, level in points]
|
||||||
assert len(find_replacements(points)) == 1
|
assert len(find_replacements(points)) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_minutes_of_readings_do_not_forecast_weeks():
|
||||||
|
"""Supply items are often polled every few minutes.
|
||||||
|
|
||||||
|
Four readings a quarter of an hour apart, with a 2 point drop between the
|
||||||
|
ends, used to extrapolate to nearly 200 percent a day - so a cartridge
|
||||||
|
sitting at 82 percent was forecast to run out in a fortnight. A rate needs
|
||||||
|
time behind it, and without it the honest answer is no estimate.
|
||||||
|
"""
|
||||||
|
minutes = 5
|
||||||
|
detail = analyse(series([84, 83, 83, 82], hours=minutes / 60))
|
||||||
|
assert detail['burnrateperday'] is None
|
||||||
|
assert detail['daysleft'] is None
|
||||||
|
assert detail['reason'] == 'not enough history yet'
|
||||||
|
assert band(detail['daysleft']) is None # lands in "No estimate yet"
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_run_with_enough_days_still_estimates():
|
||||||
|
"""The guard must not silence a genuinely slow, genuinely long run."""
|
||||||
|
detail = analyse(series([84, 83, 82, 81, 80, 79]))
|
||||||
|
assert detail['burnrateperday'] == 1.0
|
||||||
|
assert detail['daysleft'] == 79
|
||||||
|
|||||||
Reference in New Issue
Block a user