|
Programmer's Notebook |
All computer source code presented on this page, unless it includes attribution to another author, is provided by Ed Halley under the Artistic License. Use such code freely and without any expectation of support. I would like to know if you make anything cool with the code, or need questions answered.
python/ bindings.py boards.py buzz.py cache.py cards.py constraints.py english.py getopts.py gizmos.py goals.py improv.py interpolations.py namespaces.py nihongo.py nodes.py octalplus.py patterns.py persist.py physics.py pieces.py quizzes.py recipes.py relays.py romaji.py ropen.py sheets.py strokes.py subscriptions.py svgbuild.py testing.py things.py timing.py ucsv.py useful.py uuid.py vectors.py weighted.py java/ GlobFilenameFilter.java RegexFilenameFilter.java StringBufferOutputStream.java ThreadSet.java TracingThread.java Utf8ConsoleTest.java perl/ CVQM.pm Kana.pm Typo.pm cxx/ CCache.h equalish.cpp |
# goals - generic monitors of task progress ''' A goal is a monitor of some task to be completed. It offers a callback hook to automatically perform when the goal has been achieved. ABSTRACT This module includes simple goals that watch variables, driving goals that actually modify variables for you, and compound goals that can be performed and monitored in series or parallel. All goals have a progress() method which returns a value from 0.0 to 1.0. The caller should check the goal progress occasionally to see if the goal has been achieved. (Goals by themselves do not represent or employ thread execution.) Once the goal has been achieved, the progress() method will return 1.0 and trigger a callback function exactly once. Most goal types return a numerical estimation of the overall progress, suitable for progress bars. An alternative to calling progress() is to call done(), which just returns True if the progress() has reached 1.0. Example def-style callback: def task(g): print 'done!' ... goal = Goal(todo=task) AUTHOR Ed Halley (ed@halley.cc) 10 November 2007 ''' #---------------------------------------------------------------------------- import time import interpolations #---------------------------------------------------------------------------- class Goal (object): '''A generic goal class, serving as the basis for several goal types. This goal is done immediately and triggers its callback as soon as the goal is first checked with progress() or done(). ''' def __init__(self, todo=None): self.todo = todo def __str__(self): return self.__class__.__name__ + "()" def progress(self): '''May be called anytime to evaluate or effect progress. Should return 0.0 for no progress made, to 1.0 if goal achieved. Should call self.trigger() if goal achieved. ''' self.trigger() return 1.0 def trigger(self): '''Called once the goal has been met. Calls the supplied callback todo function once if specified. ''' if self.todo: fun = self.todo self.todo = None fun(self) def done(self): '''May be called anytime to evaluate or effect goal achievement. Returns False if not completely achieved, or True if achieved. ''' return self.progress() >= 1.0 #---------------------------------------------------------------------------- def __time__(): return time.time() class RangeGoal (Goal): '''A RangeGoal fires when a given variable reaches a range limit. The variable must be a mutable object which supports certain operators. The limit values are exclusive by default, meaning that a variable value that equals the limit value will complete the goal. A goal cannot monitor a scalar variable directly, but it can monitor a scalar value wrapped in an object that supports the __cmp__() method. A simple way to achieve this for scalar variables is to put it in a list reference. Since lists can be compared by their elements, the minimum and maximum values are lists too. Example: var = [ 5.0 ] goal = RangeGoal(v, [0.0], [10.0]) while not goal.done(): print var[0] var[0] += random.random()-0.5 print var[0] ''' def __init__(self, variable=None, minimum=None, maximum=None, exclusive=True, todo=None): '''Construct a new TimeGoal. Specify a variable object reference to be monitored. Optionally specify a "minimum" limit value to compare against. Optionally specify a "maximum" limit value to compare against. If "exclusive" is set True, value must exceed the given range before the goal is considered achieved or finished. If "exclusive" is set False, value must meet or exceed the range. Optionally specify a callable hook function with a "todo" argument. ''' super(RangeGoal, self).__init__(todo=todo) if isinstance(variable, (type(None), type(int), type(float))): raise ValueError, 'cannot monitor a scalar variable type' self.variable = variable self.minimum = minimum self.maximum = maximum self.exclusive = exclusive self.progress() def progress(self): '''Fractional progress is hard to measure in a general way since the RangeGoal cannot guess when the variable will go out of range. This class returns 1.0 for complete (a limit value hit), else 0.0. ''' var = self.variable min = self.minimum max = self.maximum if self.exclusive: if min is not None and cmp(var, min) <= 0: self.trigger() return 1.0 if max is not None and cmp(var, max) >= 0: self.trigger() return 1.0 else: if min is not None and cmp(var, min) < 0: self.trigger() return 1.0 if max is not None and cmp(var, max) > 0: self.trigger() return 1.0 return 0.0 #---------------------------------------------------------------------------- def __time__(*args): return time.time() class TimeGoal (Goal): '''A TimeGoal will trigger its callback when a given time passes, or when a given timespan into the future has elapsed. If an absolute time is not given, time is measured from the first call to check progress, not from the construction of the goal. ''' def __init__(self, when=None, dt=0., todo=None): '''Construct a new TimeGoal. Optionally specify an absolute time to finish with a "when" value. Optionally specify a relative time to finish with a "dt" value. (If both are given, when+dt is the finish target.) Optionally specify a callable hook function with a "todo" argument. ''' super(TimeGoal, self).__init__(todo=todo) self.start = None self.when = when self.dt = dt self.now = __time__ if when is not None: self.begin() def begin(self): '''This is called to realize state on the first progress check. If the absolute completion time is not specified at construction, then the completion time is measured relative to the current time. ''' self.start = self.now() if self.when is None: self.when = self.start self.when += self.dt self.progress() def progress(self): '''Monitor the progress of the goal. Returns a value between 0.0 on the first call, and 1.0 when the completion time has arrived. ''' if self.start is None: self.begin() now = self.now() if now < self.start: return 0.0 if now >= self.when: self.trigger() return 1.0 return interpolations.linear(self.start, self.when, now, 0.0, 1.0) #---------------------------------------------------------------------------- class ChangeGoal (TimeGoal): '''Changes a specified object variable, interpolated over time. Each time the goal is checked for progress or completion, the value of the given variable is updated to an interpolated value towards a curve. The variable must be a mutable object which supports certain operators. The value of the variable is interpolated along a linear or bezier curve. If just a delta timespan is given, the curve starts at the variable value measured from the first call to check progress, not from the construction of the goal. ''' def __init__(self, variable=None, controls=None, relative=False, when=None, dt=0., todo=None): '''Construct a new ChangeGoal. Specify a single value or list of bezier points for "controls". Optionally specify an absolute time to finish with a "when" value. Optionally specify a relative time to finish with a "dt" value. (If both are given, when+dt is the finish target.) Optionally specify a callable hook function with a "todo" argument. ''' if isinstance(variable, (type(None), type(int), type(float))): raise ValueError, 'cannot monitor a scalar variable type' self.variable = variable self.relative = relative if controls is None: controls = [] if not isinstance(controls, (list, tuple)): controls = [ None, controls ] self.controls = list(controls) super(ChangeGoal, self).__init__(when=when, dt=dt, todo=todo) def __str__(self): string = self.__class__.__name__ + "(" string += str(self.variable) string += "->" string += str(self.controls[-1]) string += ")" return string def begin(self): '''This is called to realize state on the first progress check. If the curve is specified as being relative, control points are calculated from the current value of our monitored variable. If the first curve control is None, we use the current value of the monitored variable. This allows goals to be sequenced neatly. ''' if self.relative: for i in range(len(self.controls)): if self.controls[i] is not None: self.controls[i] = self.variable + self.controls[i] if self.controls[0] is None: self.controls[0] = 1*self.variable super(ChangeGoal, self).begin() def update(self, value): '''Internal hook to actually set the monitored variable value as the goal time target approaches. This class assumes that the variable type offers support for __imul__() and __iadd__() methods, so as to allow for in-place adjustment of the variable value. If this is not the case, override this method with appropriate logic. ''' self.variable *= 0.0 self.variable += value def trigger(self): '''The monitored variable will be set to the final control value upon completion, to ensure smooth transitions if goals are stacked. ''' self.update(self.controls[-1]) super(ChangeGoal, self).trigger() def progress(self): '''As time passes toward the desired completion target time, the monitored variable value will be interpolated along a bezier curve. At progress 0.0, the variable is at the value of controls[0]. At progress 1.0, the variable is at the value of controls[-1]. ''' i = super(ChangeGoal, self).progress() if i >= 1.0: return 1.0 if i < 0.0: value = self.controls[0] else: value = interpolations.bezier(i, *self.controls) self.update(value) return i #---------------------------------------------------------------------------- class CompoundGoal (Goal): '''Internal type to make compounds like SerialGoal and ParallelGoal.''' def __init__(self, goals=None, todo=None): if goals is None: goals = [] if isinstance(goals, Goal): goals = [ goals ] self.goals = goals super(CompoundGoal, self).__init__(todo) def __str__(self): string = self.__class__.__name__ return string + "(" + ', '.join([str(x) for x in self.goals]) + ")" class SerialGoal (CompoundGoal): '''A SerialGoal is an ordered series of subgoals. Later goals are not monitored for progress until earlier goals report their completion. Those subgoals that finish will be removed from the series. ''' def __init__(self, goals=None, todo=None): '''Creates a goal to accomplish a series of goals in order. Optionally give a list of sub-"goals" to be active immediately. Goals may be added at any time, even after construction. ''' super(SerialGoal, self).__init__(goals, todo) def __str__(self): string = self.__class__.__name__ return string + "(" + ', '.join([str(x) for x in self.goals]) + ")" def progress(self): '''May be called anytime to evaluate or effect progress. Calls the first subgoal for progress(). If it is now done, it calls the next one, and so on. Our progress is the ratio of done versus total goals added. The ratio counts all goals added, including the ones which have been completed and removed. ''' count = len(self.goals) if count > 0: done = 0 for i in range(count): goal = self.goals[i] if not isinstance(goal, Goal) or goal.done(): self.goals[i] = None done += 1 else: break if done < count: return float(done) / count self.trigger() return 1.0 def append(self, goal): '''Adds another subgoal to the end of the chain at any time. Adding more goals will cause progress ratio to drop numerically, so callers should not assume the progress() value always increases. ''' self.goals.append(goal) def prepend(self, goal): '''Adds another subgoal to the beginning of the chain at any time. The current partially-done subgoal will not be checked for further progress until this new subgoal has been done. Will cause our progress() ratio to drop, of course. ''' self.goals.insert(0, goal) #---------------------------------------------------------------------------- class ParallelGoal (CompoundGoal): '''A ParallelGoal is an unordered set of subgoals. Goals are not monitored for progress in any particular order; each one is checked on every request for the progress of the set. Those subgoals that finish will be removed from the set. ''' def __init__(self, goals=None, todo=None): '''Creates a goal to accomplish a set of goals in any order. Optionally give a list of sub-"goals" to be active immediately. Goals may be added at any time, even after construction. ''' super(ParallelGoal, self).__init__(goals, todo) def progress(self): '''May be called anytime to evaluate or effect progress. Calls each undone subgoal progress(). Do not assume any given ordering in how each subgoal is checked. Our progress is the ratio of total subgoal progress versus total goals added. The ratio counts all goals added, including the ones which have been completed and removed. ''' count = len(self.goals) if count > 0: done = 0.0 for i in range(count): goal = self.goals[i] amount = 1.0 if goal is not None: amount = max(0.0, min(goal.progress(), 1.0)) if amount == 1.0: self.goals[i] = None done += amount if done < count: return done / count self.goals = [ ] self.trigger() return 1.0 def add(self, goal): '''Adds another subgoal at any time. All existing subgoals will continue to be called for further progress in parallel. Will cause our progress() ratio to drop, of course. ''' self.goals.append(goal) #---------------------------------------------------------------------------- __test_t = 0 def __test_now(): global __test_t now = __test_t __test_t += 1 return now def __test__(): from testing import __ok__, __report__ print 'Testing goals...' if __name__ == '__main__': raise Exception, \ 'This module is not a stand-alone script. Import it in a program.' |
|
Contact Ed Halley by email at
ed@halley.cc. Text, code, layout and artwork are Copyright © 1996-2008 Ed Halley. Copying in whole or in part, with author attribution, is expressly allowed. Any references to trademarks are illustrative and are controlled by their respective owners. |
|