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:
parent
75012472a2
commit
1fd776153c
2 changed files with 138 additions and 20 deletions
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
Loading…
Reference in a new issue