As I now understand your question, you're looking for a pair of functions. One that will be called by the model to simulate real-world conditions from the state of the model's parameters. This function will then make the calls to the actual timing function that is to be used in the real-world implementation.
My earlier code (retained at the bottom of this post), handles the second function, although it could be tweaked for efficiency if the function will be called repeatedly to monitor the state of the hardware. I will work on that if you confirm the need.
Here is a function that I think will suit your needs for the simulator. There is code within for robustness in the face of (possible, if perhaps unlikely) data from which a deterministic answer cannot be generated.
Please excuse the length, as I've littered it with comments.
switches = (0, 30) # the locations (in clockwise degrees from North)
# of the switches around the disk
def sim_trigger(angle, time):
"""sim_trigger(number, number) --> [(switch, time), ...]
angle is the current angle of the disk, in degrees
time is a scalar in any units. It is the current value of a
timer. The (adjusted) time when the switch would have been
triggered will be returned in the same units.
Three calls to this function are necessary to generate a useful
return, which will be a variable-length list of two-tuples,
whose first element is either the number 0 or the number 1,
indicating which of two switches was triggered, and whose second
element is an adjusted time value, reflecting the time when said
switch would have been triggered.
If no switch was triggered between the last input and this one,
then the return value will be an empty list.
If two consecutive inputs are non-deterministic (so that the current
speed and direction of rotation cannot be absolutely determined
because more than one solution exists), then this function will
ignore the second input and will resume processing once three
deterministic inputs have been received.
These tuples can be passed to the signal processing software.
This function doesn't directly call the signal processing software
to avoid coupling and so facilitate testing of mixed solutions.
Prior to three calls, or if given non-deterministic input (see above),
or if no switch was triggered, this function returns an empty list."""
if not 'hist' in sim_trigger.__dict__:
sim_trigger.hist = []
sim_trigger.hist = sim_trigger.hist[-2:]
hist = sim_trigger.hist # just to save some typing
hist.append((angle, time))
if len(hist) < 3:
return []
timedelta1 = hist[1][1] - hist[0][1]
timedelta2 = hist[2][1] - hist[1][1]
angledelta1 = hist[1][0] - hist[0][0]
angledelta2 = hist[2][0] - hist[1][0]
# For each angledelta / timedelta, we have two possible solutions,
# one clockwise, one counter-clockwise, each with a different speed.
#
# Having two sets of deltas, we can *usually* determine which of
# the solutions is correct, because one solution will be common
# to both sets (after accounting for rounding and timing errors).
#
# In some cases, however, the same two solutions will be found
# for both sets of deltas. This will occur if timedelta2 is an
# integer multiple of timedelta1 (or vice versa). In this case,
# this function will simply return an empty list and wait for
# deterministic inputs. This may mean that some translations from
# the model to the signal processor are skipped (the signal processor
# may miss some rotations of the wheel). The number of rotations
# missed depends on how many non-deterministic inputs are received.
#
# This scenario should be rare, but cannot be completely avoided
# unless input requirements can be further specified.
#
# An optimization can be added, however, to make an assumption
# once the first three deterministic inputs have been received
# that subsequent inputs are in the same direction of rotation.
# This avoids the possibility of missed signals, at the cost of
# complexity. I have not added this optimization in here, since
# it subtley alters this function's behavior.
arc1 = angledelta1 % 360, -angledelta1 % 360
arc2 = angledelta2 % 360, -angledelta2 % 360
# arc1 contains the two possible (cw, ccw) rotations for angledelta1,
# similarly for arc2. We now need to figure out if we're going
# clockwise or counter-clockwise. If our inputs are deterministic,
# then arc1[x] / timedelta1 will be equal (within some tolerance)
# to arc2[x] / timedelta2, where `x` is either 0 for clockwise, or
# 1 for counter-clockwise. If our inputs are non-deterministic,
# then both values of `x` will result in equality.
#
# Rather than doing arc1[x] / timedelta1 == arc2[x] / timedelta2,
# we've rearranged the equation to use multiplication instead
# (faster, less chance of rounding error), and to move both products
# to one side of the equals (taking their difference instead of
# comparing them for equality). We can then take the clockwise
# and counter-clockwise distances from zero to see if one of them
# is much closer to zero (a much smaller absolute value) than the
# the other. This should account for any rounding or timing errors.
diffs = [abs(arc1[x] * timedelta2 - arc2[x] * timedelta1) for x in (0, 1)]
if diffs[0] < diffs[1] / 100: # Seems like a reasonable test for
# closer to zero, yes?
# This might need some tweaking.
# so we're going clockwise
arc = arc2[0]
elif diffs[1] < diffs[0] / 100: # Tweak this, too.
# so we're going counter-clockwise
# use a negative arc to reflect counter-clockwise rotation
arc = -arc2[1]
else: # we got non-deterministic inputs
return []
# We now know what exact arc was covered since the previous call,
# and in what direction. All that's left to do is to determine
# whether or not this arc crossed either of the switches. To
# handle edge cases where this function was called exactly at the
# intersection of one of the switches, we'll define that the switch
# is triggered if the arc ends on a switch, but not if it starts on one.
start = angle - arc
if arc > 0: # clockwise
return [(i, time - (angle - x) % 360 * timedelta2 / arc)
for (i, x) in enumerate(switches)
if start < x <= angle or
start + 360 < x <= angle + 360
]
# else, counter-clockwise
return [(1 - i, time - (x - angle) % 360 * timedelta2 / -arc)
for (i, x) in enumerate(reversed(switches))
if angle <= x < start or
angle - 360 <= x <= start - 360
]
A quick test:
inputs = [
(0, 0), (20, 40.1), (26, 52.08), (41, 82.01), (62, 124), (81, 162.07),
(105, 210.05), (170, 340.06), (240, 480.1), (301, 602.09),
(334, 668.04), (5, 730.03), (49, 818.01), (40, 1520)
]
print tuple(sim_trigger(angle % 360, time) for (angle, time) in inputs)
Outputs:
([], [], [], [(1, 60.061333333333337)], [], [], [], [], [], [], [], [(0, 720.03161290322578)], [(1, 780.01863636363635)], [(0, 1440.0011396011396), (1, 1500.0002849002849)])
The random decimals in the inputs (and reflected in the outputs) are to simulate timing inaccuracies, which are inevitable.
Similarly, if you change sim_trigger(angle % 360...
to sim_trigger(-angle % 360...
(note the minus), then you get counter-clockwise rotation:
([], [], [], [], [], [], [], [], [], [], [(1, 660.04606060606056)], [(0, 720.03161290322578)], [], [(1, 1380.0019943019943), (0, 1440.0011396011396)])
Notice that several inputs came in before the first switch was encountered.
Now test hooking it up to the signal processor function (at the end of this post):
tuple(wheel_vector(*x) for (angle, time) in
inputs for x in sim_trigger(angle % 360, time))
Outputs:
('insufficient data', 'insufficient data', ('clockwise', 719.95730303030302), ('clockwise', 719.96952669791381), ('clockwise', 719.98164853664855))
Notice again the random decimals, reflecting the simulated timing inaccuracies.
The signal processor (wheel_vector
) will only be called each time a switch is encountered, and like sim_trigger
, three initial inputs are required before any useful data can be generated. So in real world use such as:
timer.tick = lambda:
for (switch, time) in sim_trigger(model.angle, timer.time):
output = wheel_vector(switch, time)
if isinstance(output, tuple):
print output
There may be quite a few samples before any output is generated (depending on how much of a rotation the average sample covers).
Following is the second function, preceded by my original commentary, based on the understanding of your needs and parameters that I had at the time.
Given the current definition, it's impossible to correctly answer this question, due to the wagon wheel effect.
For a simple example, consider the following inputs:
Angle Time
10 0
20 1
30 2
There are two possibilities here:
- the wheel is rotating clockwise at 36 time units per rotation, or
- the wheel is rotating counter-clockwise at 36/35 (1.0285714) time units per rotation.
It's impossible to know which.
To answer the question correctly, you need two things:
- the time at which 3 consecutive switch signals come in, and from which switches, and
- the clockwise angle between the switches (a fixed constant).
Your function should look something like this:
switch_angle = 30 # the fixed angle in degrees between the switches
def wheel_vector(switch, time):
"""wheel_vector(int, number) -> tuple or string
switch is either 0 or 1. Switch 1 is defined to be switch_angle
clockwise from switch 0.
time is a scalar in any number of units. It is the value of a
timer when the switch was triggered. The return value will be in
the same units.
Three calls to this function are necessary to generate a useful
return, which will be a two-tuple, whose first element is either
the string 'clockwise' or the string 'counter-clockwise',
representing the wheel's direction of rotation, and whose second
element is a number, representing the number of time units required
for each rotation.
Prior to three calls, this function returns the string 'insufficent
data'."""
if not 'hist' in wheel_vector.__dict__:
wheel_vector.hist = []
wheel_vector.hist = wheel_vector.hist[-2:]
hist = wheel_vector.hist # just to save us some typing
hist.append((switch, time))
if len(hist) < 3:
return 'insufficient data'
delta1 = hist[1][1] - hist[0][1]
delta2 = hist[2][1] - hist[1][1]
if switch_angle < 180:
delta = min(delta1, delta2)
else:
delta = max(delta1, delta2)
first_switch = hist[delta != delta1][0]
return (
('clockwise', 'counter-clockwise')[first_switch],
hist[2][1] - hist[0][1]
)
This assumes that the wheel speed is constant, as you said, that the event timing is consistent (i.e. the lag time between the wheel triggering the switch and the timer value being read is always the same), and that the function is called with correct data for three consecutive switch events. Garbage in, garbage out.