Another attempt at saner print time estimation

Seriously though, this topic is driving me nuts...

  * track origin of print time left estimate
  * less draconian threshold for estimation helper for all prints, not just
    sd prints
  * sanity check estimated/statistical total print time, if not sane force "dumb"
    estimate
  * force "dumb" estimate if no other estimate is available after set percentage or
    print time
This commit is contained in:
Gina Häußge 2016-07-26 11:17:16 +02:00
parent 75012472a2
commit 1fd776153c
2 changed files with 138 additions and 20 deletions

View file

@ -72,6 +72,10 @@ class Printer(PrinterInterface, comm.MachineComPrintCallback):
self._selectedFile = None
self._timeEstimationData = None
self._timeEstimationStatsWeighingUntil = settings().getFloat(["estimation", "printTime", "statsWeighingUntil"])
self._timeEstimationValidityRange = settings().getFloat(["estimation", "printTime", "validityRange"])
self._timeEstimationForceDumbFromPercent = settings().getFloat(["estimation", "printTime", "forceDumbFromPercent"])
self._timeEstimationForceDumbAfterMin = settings().getFloat(["estimation", "printTime", "forceDumbAfterMin"])
# comm
self._comm = None
@ -381,20 +385,21 @@ class Printer(PrinterInterface, comm.MachineComPrintCallback):
if self._selectedFile is None:
return
# we are happy if the average of the estimates stays within 60s of the prior one
threshold = settings().getFloat(["estimation", "printTime", "stableThreshold"])
rolling_window = None
threshold = None
countdown = None
if self._selectedFile["sd"]:
# we are interesting in a rolling window of roughly the last 15s, so the number of entries has to be derived
# by that divided by the sd status polling interval
rolling_window = 15 / settings().get(["serial", "timeout", "sdStatus"])
# we are happy if the average of the estimates stays within 60s of the prior one
threshold = 60
# we are happy when one rolling window has been stable
countdown = rolling_window
self._timeEstimationData = TimeEstimationHelper(rolling_window=rolling_window, threshold=threshold, countdown=countdown)
self._timeEstimationData = TimeEstimationHelper(rolling_window=rolling_window,
threshold=threshold,
countdown=countdown)
self._fileManager.delete_recovery_data()
@ -632,22 +637,12 @@ class Printer(PrinterInterface, comm.MachineComPrintCallback):
printTime = self._comm.getPrintTime()
cleanedPrintTime = self._comm.getCleanedPrintTime()
estimatedTotalPrintTime = self._estimateTotalPrintTime(progress, cleanedPrintTime)
totalPrintTime = estimatedTotalPrintTime
if self._selectedFile and "estimatedPrintTime" in self._selectedFile and self._selectedFile["estimatedPrintTime"]:
statisticalTotalPrintTime = None
if self._selectedFile and "estimatedPrintTime" in self._selectedFile \
and self._selectedFile["estimatedPrintTime"]:
statisticalTotalPrintTime = self._selectedFile["estimatedPrintTime"]
if progress and cleanedPrintTime:
if estimatedTotalPrintTime is None:
totalPrintTime = statisticalTotalPrintTime
else:
if progress < 0.5:
sub_progress = progress * 2
else:
sub_progress = 1.0
totalPrintTime = (1 - sub_progress) * statisticalTotalPrintTime + sub_progress * estimatedTotalPrintTime
printTimeLeft = totalPrintTime - cleanedPrintTime if (totalPrintTime is not None and cleanedPrintTime is not None) else None
printTimeLeft, printTimeLeftOrigin = self._estimatePrintTimeLeft(progress, printTime, cleanedPrintTime, statisticalTotalPrintTime)
if progress is not None:
progress_int = int(progress * 100)
@ -658,7 +653,121 @@ class Printer(PrinterInterface, comm.MachineComPrintCallback):
return dict(completion=progress * 100 if progress is not None else None,
filepos=filepos,
printTime=int(printTime) if printTime is not None else None,
printTimeLeft=int(printTimeLeft) if printTimeLeft is not None else None)
printTimeLeft=int(printTimeLeft) if printTimeLeft is not None else None,
printTimeLeftOrigin=printTimeLeftOrigin)
def _estimatePrintTimeLeft(self, progress, printTime, cleanedPrintTime, statisticalTotalPrintTime):
"""
Tries to estimate the print time left for the print job
This is somewhat horrible since accurate print time estimation is pretty much impossible to
achieve, considering that we basically have only two data points (current progress in file and
time needed for that so far - former prints or a file analysis might not have happened or simply
be completely impossible e.g. if the file is stored on the printer's SD card) and
hence can only do a linear estimation of a completely non-linear process. That's a recipe
for inaccurate predictions right there. Yay.
Anyhow, here's how this implementation works. This method gets the current progress in the
printed file (percentage based on bytes read vs total bytes), the print time that elapsed,
the same print time with the heat up times subtracted (if possible) and if available also
some statistical total print time (former prints or a result from the GCODE analysis).
1. First get an "intelligent" estimate based on the :class:`~octoprint.printer.estimation.TimeEstimationHelper`.
That thing tries to detect if the estimation based on our progress and time needed for that becomes
stable over time through a rolling window and only returns a result once that appears to be the
case.
2. If we have any statistical data (former prints or a result from the GCODE analysis)
but no intelligent estimate yet, we'll use that for the next step. Otherwise, up to a certain percentage
in the print we do a percentage based weighing of the statistical data and the intelligent
estimate - the closer to the beginning of the print, the more precedence for the statistical
data, the closer to the cut off point, the more precendence for the intelligent estimate. This
is our preliminary total print time.
3. If the total print time is set, we do a sanity check for it. Based on the total print time
estimate and the time we already spent printing, we calculate at what percentage we SHOULD be
and compare that to the percentage at which we actually ARE. If it's too far off, our total
can't be trusted and we fall back on the dumb estimate. Same if the time we spent printing is
already higher than our total estimate.
4. If we do NOT have a total print time estimate yet but we've been printing for longer than
a configured amount of minutes or are further in the file than a configured percentage, we
also use the dumb estimate for now.
Yes, all this still produces horribly inaccurate results. But we have to do this live during the print and
hence can't produce to much computational overhead, we do not have any insight into the firmware implementation
with regards to planner setup and acceleration settings, we might not even have access to the printed file's
contents and such we need to find something that works "mostly" all of the time without costing too many
resources. Feel free to propose a better solution within the above limitations (and I mean that, this solution
here makes me unhappy).
Args:
progress (float or None): Current percentage in the printed file
printTime (float or None): Print time elapsed so far
cleanedPrintTime (float or None): Print time elapsed minus the time needed for getting up to temperature
(if detectable).
statisticalTotalPrintTime (float or None): Total print time of past prints against same printer profile,
or estimated total print time from GCODE analysis.
Returns:
(float or None) estimated print time left or None if not proper estimate could be made at all
"""
if progress is None or printTime is None or cleanedPrintTime is None:
return None
dumbTotalPrintTime = printTime / progress
estimatedTotalPrintTime = self._estimateTotalPrintTime(progress, cleanedPrintTime)
totalPrintTime = estimatedTotalPrintTime
printTimeLeftOrigin = "estimate"
if statisticalTotalPrintTime is not None:
if estimatedTotalPrintTime is None:
# no estimate yet, we'll use the statistical total
totalPrintTime = statisticalTotalPrintTime
printTimeLeftOrigin = "stats"
else:
if progress < self._timeEstimationStatsWeighingUntil:
# still inside weighing range, use part stats, part current estimate
sub_progress = progress * (1 / self._timeEstimationStatsWeighingUntil)
if sub_progress > 1.0:
sub_progress = 1.0
printTimeLeftOrigin = "mixture"
else:
# use only the current estimate
sub_progress = 1.0
printTimeLeftOrigin = "estimate"
# combine
totalPrintTime = (1.0 - sub_progress) * statisticalTotalPrintTime \
+ sub_progress * estimatedTotalPrintTime
printTimeLeft = None
if totalPrintTime is not None:
# sanity check current total print time estimate
assumed_progress = cleanedPrintTime / totalPrintTime
min_progress = progress - self._timeEstimationValidityRange
max_progress = progress + self._timeEstimationValidityRange
if min_progress <= assumed_progress <= max_progress and totalPrintTime > cleanedPrintTime:
# appears sane, we'll use it
printTimeLeft = totalPrintTime - cleanedPrintTime
else:
# too far from the actual progress or negative,
# we use the dumb print time instead
printTimeLeft = dumbTotalPrintTime - cleanedPrintTime
printTimeLeftOrigin = "linear"
elif progress > self._timeEstimationForceDumbFromPercent or \
cleanedPrintTime * 60 >= self._timeEstimationForceDumbAfterMin:
# more than x% or y min printed and still no real estimate, ok, we'll use the dumb variant :/
printTimeLeft = dumbTotalPrintTime - cleanedPrintTime
printTimeLeftOrigin = "linear"
if printTimeLeft < 0:
# shouldn't actually happen, but let's make sure
return None, None
return printTimeLeft, printTimeLeftOrigin
def _addTemperatureData(self, temp, bedTemp):
currentTimeUtc = int(time.time())

View file

@ -283,6 +283,15 @@ default_settings = {
}
}
},
"estimation": {
"printTime": {
"statsWeighingUntil": 0.5,
"validityRange": 0.15,
"forceDumbFromPercent": 0.3,
"forceDumbAfterMin": 30,
"stableThreshold": 60
}
},
"devel": {
"stylesheet": "css",
"cache": {