views:

282

answers:

4

This is a major re-write of the original question, I tried to clarify those points that were evidently confusing for some in my first version of the question. Thanks for the input in helping formulate the problem better!

CONTEXT

The challenge comes from a real-life coding problem. The first thing that responders should be aware of, is that there are two distinct layers, which cannot be mixed. These are:

  1. The simulator (SM), which is a part of the software that replicates the physical description and behaviour of the objects existing in the simulated world. [Curiosity: the simulation engine I used for the project is based on OpenGL]
  2. The signal processor (SP) which is the software that will be installed on the electronic devices in the real world. This layer is the one that gives the title to the challenge. [Curiosity: in real life this software will be running on PIC microchips]

If you decide to participate to the challenge you should submit two distinct piece of codes that do the following two things:

  1. SM: Process the data provided by the simulator in order to replicate the behaviour of real life objects (namely triggering the switches, see below).
  2. SP: Process the switch signals to obtain the needed information (namely speed and direction of rotation, see below).

REASONS AND FOCUS OF THE CHALLENGE

Both problems do not require complex logic to be solved: what I particularly found difficult was to write concise, elegant code that handles edge cases. So, entries for the challenge should provide an example of smart coding, and possibly not just "the rough idea of the algorithm". [An example of "smart code" is the one from P Daddy]. Any language is welcome.

(SM) THE PHYSICAL WORLD BEING SIMULATED

alt text

  1. The world is 2D (so everything happens on a plane)
  2. A disk D is rotating at constant speed around its centre
  3. Around the circumference of the disk, but fixed on the plane, there are two switches (sw0 and sw1) which position is expressed in terms of degrees relative to the north (in the image above they are located at ~0° and ~30°). The angle between sw0 and sw1 is always != 180°.
  4. On the edge of the disk (i.e. attached to the disk), there is a switch trigger t, that momentarily close the circuit when passing in front of a switch (so, the switch sends a single almost-zero long event to the controller attached to it).

(SM) CODE FOR THE SIMULATOR LAYER

Your code will be executed repeatedly at short intervals of time dt which will be irregular in length. At any time your code is executed, you have the following information available:

  • the position of the switches (always the same)
  • the present position of the disk trigger (degrees relative to the north, normalised to 0° ≤ value < 360°)
  • the present time (number of [micro]seconds since the simulation has started)

You can store any values you might wish between calls and even spawn new processes if you so wish, but the position of the disk will be available only when the simulator invokes your code.

It is safe to assume that the function will be called at time intervals dt < time_needed _for_a_full_disk_rotation. But it is NOT safe to assume that dt will be shorter than the time needed to cover the distance between two switches. (Also remember: time intervals are not regular).

(SP) THE SIGNAL PROCESSING SOFTWARE

Using the two distinct signals sent by sw0 and sw1 (the microprocessor will know which of the two switches have been triggered) and the ticks from the microprocessor (the absolute time of the event), we want to find the rotation speed (degrees per second) and the direction of rotation (clockwise or counter-clockwise) of D.

In other words: any data that you might know from the simulator engine is not accessible. You can only use the trig events and the time to do your calculations.

The code can either return the speed and direction of the disk rotation, either a "not_yet_enough_data_to_know_the_result" value.

THE DEVIL LIES IN THE DETAILS...

While some has already provided elegant solutions for the part #2 of the problem, the trickiest part of the problem is - IMO - part #1. I shall reiterate that the reason for the whole question is managing effectively edge cases. In other words: the fact that a switch can be considered "triggered" when its location is comprised in the arc between p(t1) and p(t2) is obvious, but there are less obvious questions such as:

  • Writing an efficient algorithm that will work correctly for arcs spanning across the 0° mark
  • What to do if the arc encompass both switches
  • What to do when dt are such that the arc they create extends for a large part of the whole circumference (When should you trigger the switch then?)
  • ...anything else that you might think of...

QUALITY CRITERIA (in order of importance)

  1. Handling edge cases (this is a must)
  2. Code elegance and smartness
  3. Accuracy of the output (SM: triggering the switch at the right moment, SP: correct speed and direction)
  4. Fastest response (less cycles [SM: simulator calls, SP: trig events] to provide the answers)

Happy coding and good luck!

PS: I am using python for this, but any language is welcome for the answers!

A: 

Every time a signal is sent, call a function. The first three calls of this function record two time intervals and two corresponding angle intervals that add up to 360 degrees. Match the longer time interval to the longer angle interval, and find change in angle/change in time to find velocity, which gives both speed and direction.

For example, in the picture the two angle intervals are 30 and 330. If the shorter time interval is first, the disk rotated through 30 degrees clockwise in the first interval and 330 degrees clockwise in the second interval.

If the difference in angle between the two switches is 0, the direction cannot be determined.

murgatroid99
A third call will allow you to identify which direction it is going also (as the triggers won't be at 180 degrees)
ck
Actually, now that you point that out, if the calls are at 180 degree increments forever (a possibility), then finding the direction is theoretically impossible.
murgatroid99
I edited the answer, so I think it will work better now.
murgatroid99
@murgatroid99 - Yes, as I mentioned in the original question, the problem seems straightforward. The whole point is "managing edge cases 'efficiently'". Such edge cases are to be found in the other half of the problem, BTW, i.e. on the part that simulates the switch triggering....
mac
@mac - Considering the single edge case you describe (2 switches trigger within 1 dt), your description seems inconsistent because either dt is the time between the triggers, in which case your edge case makes no sense, or you have not provided enough information about how the signals, switch triggering, and dt relate.
murgatroid99
@murgatroid99 - `dt` is not the time between the trigger events (please tell me which part of the description gives that impression, so I can edit it). **`dt` is the time between two calls to your function**. Input = disk position + clock time --> you find out if any switch has been triggered --> you use trigger signals and time of triggering to compute the answer... Hope this clarify! :)
mac
What I did not understand was what caused the functions to call if not the triggering of the switches. Now that I see in your latest comments on the question that the function calls are completely separate in time from the switch triggers, I realize that I do not understand your simulation. Your switches must be executing some code when triggered, to change the last triggered time. Why not also call the function at that time and get rid of the problems you describe?
murgatroid99
A: 

I think the point is to find the total # of rotations and hence the total rotation angle (not just the truncated 0-360 value). This discribes an encoder wheel function. Give the truncated angle is given you establish the sence of direction with Δθ/Δt and then each time you wrap your angle around 360° (or -360° for negative rotation) increment the # of turns

void event(time, angle)
{
    last_time = time;
    last_angle = angle;
    if(cw_rotation && angle<previous_angle) //clockwise rotation
    {
        n_turns++;
    }
    if(cww_rotation && angle>previous_angle) //counter-clockwise
    {
        n_turns++;
    }
}

double TotalAngle()
{
    if(cw_rotation) 
    { 
       return last_angle + n_turns*360;
    }
    if(ccw_rotation)
    {
       return last_angle - n_turns*360;
    }
}
jalexiou
The point is to find the direction of rotation and the (constant) speed at which the wheel is turning, not the total angle rotated through.
murgatroid99
+3  A: 

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:

  1. the wheel is rotating clockwise at 36 time units per rotation, or
  2. 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:

  1. the time at which 3 consecutive switch signals come in, and from which switches, and
  2. 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.

P Daddy
This does not apply because the time intervals are not constant: Signals are sent by switches not separated by 180 degrees.
murgatroid99
@mrgatroid99: There is no guarantee that intervals are not constant (just no guarantee that they are). And inconsistent intervals really do nothing to help the problem, anyway.
P Daddy
@P Daddy - Thanks for your answer. I am plusone-ing it as it offers an elegant solution to *half* of the problem. [Misses the trigger simulation algorithm, which is trickier than this part, IMO]. The initial comment about the wheel wagon effect does not apply here. In the problem specification it is said: "It is safe to assume that the function will be called at time intervals dt < time_needed _or_a_full_disk_rotation", which rules out the 37/36 RPM scenario.
mac
@mac: No, dt < one_rotation doesn't rule out the problem. In either direction, clockwise or counter-clockwise, the wheel hasn't completed a full rotation at the second sample. It has completed either 1/36 of a rotation clockwise, or 35/36 of a rotation counter-clockwise, both less than one. The trigger simulation algorithm is flawed, and the solution cannot be correctly given unless it is fixed. At best, a solution could offer two possible answers. Note that the scenario I outlined is not unique. For any found rotation, an opposing rotation can be found for the same inputs.
P Daddy
Now if you could say that dt < one_half_rotation_time, then that's another story. In that case, the solution can be absolutely given, requiring only two inputs and code even simpler than what I gave for a proper simulator.
P Daddy
@P Daddy - can you explain why my example/solution does not eliminate the wagon wheel problem?
murgatroid99
@P Daddy - Ok, I got what you mean. However it seems to me that inconsistent time intervals do get rid of the problem, as pointed out by mrgatroid99. Why are you saying that they don't? For a speed of +1/36 or -1/35, the trajectories of the trigger in space are different, so intermediate should tell you if the motion is cw or ccw and eliminate any ambiguity... Can you clarify further what you are trying to explain? Thanks! [BTW: I am fixing an inconsistency in the original question, where I said on dt "not necessarily regular" and further down "they are not regular"]
mac
@murgatroid99: I just read your solution, and it's essentially a summary of the solution I coded. Both assume to be called when the switches are hit, instead of at random times within the rotation.
P Daddy
@mac: Inconsistent intervals add nothing, because the disk rotates at consistent speed. You can take samples at any two intervals, and extrapolate every other point. For instance, in my example, if you skipped the sample at time 2 and added a sample at angle 40 and time 3, you'd have inconsistent samples, but the same math applies. Between time 0 and 3, it has moved either 30° clockwise (at 36 time units per rotation), or 330° counter-clockwise (at 36/35 time units per rotation). You would get the same result comparing any other pair of samples (try it).
P Daddy
@P Daddy - I am afraid to say it... but I still don't get it. Here's a brief of what I am trying to say. Alternate speeds to consider: +1/4 RPM (A) or -3/4 RPM (B). Times of reading: 0s, 20s, 30s. Wheel readings for (A): [0°, 30°, 45°]. For (B): [0°, 270°, 225°]. If you contrarily wanted to start reasoning from readings and deduce speeds (using readings from case A): speeds(cw) in °/s:(?, 1.5, 1.5); speeds(ccw): (?, -16.5, -10.5). But since we know the motion is uniform (speed = K), then we know the wheel is turning cw. What am I not getting yet?
mac
I guess the math would help. If it's turning clockwise at 36 time units per rotation, then the angle at time *t* is `360 / 36 * t`, whereas if it's going counter-clockwise at 36/35 time units per roation, then the angle at time *t* is `-360 / (35 / 36) * t`. After normalization, you'll find that the equations are equal for any value of *t*.
P Daddy
Sorry, make that `-360 / (36 / 35) * t`.
P Daddy
@P Daddy - Math always helps... but after normalisation values are **not** the same for the two equations. **(CW)** `arc = (10°/s * Xs) % 360 = 10*X° % 360` **(CCW)** `arc = -350*X° % 360`. **(TESTING)**. For values of `t` (0.0, 0.5, 1.0, 1.7, 4.1, 4.5, 5.0) the results are the tuples [int((arc(cw)), int(arc(ccw))] : `[(0, 0), (5, 185), (10, 10), (17, 125), (41, 5), (45, 225), (50, 50)]`. Here's the pastebin of the code: http://pastebin.com/d1R7kiBS
mac
Notice that they are they same for t in (0, 1, 5), as they would be for any other integer. Integer values of `t` represent correlations of the pattern (because `t` is the quantum from which the initial computation is performed). Your time values, both in your 1/4 vs. 3/4 RPM example as well as your values for `t` above, have a special property: The deltas between them are not integer multiples of one another, therefore not allowing the pattern to repeat. This is a fairly specific requirement, and irregularity alone does not suggest it.
P Daddy
Unless this requirement can be met, a deterministic answer cannot be given. This property of the simulator can be added, but it would be simpler to either (a) make the simulator function as the real-world device (as spec'ed in my code above), or to (b) meet the requirement I stated earlier that the simulator will fire no less than once per half rotation. If that is a known, then a deterministic function is simple.
P Daddy
Ok, so we are on the same page finally! For the sake of knowing it: the "irregular dt's come from OpenGL: the polygon representing the disk gets updated irregularly, based on the amount of work the GPU is subject to. Indeed - seen in its context (which I did not share before) - "fairly specific" would have been to have `dt` as integer multiples of one another... Anyhow - as I stated in the first comment - I highly appreciated the code you wrote for this half of the problem. [From the various answers so far, I understand it is the only half that gets people interested... interesting discovery!]
mac
Good to have the background. Now I know that the simulator isn't arbitrary. What you're saying is that one `dt` will *likely* not be a factor of the next. With this information, one can write a function that will *likely* work, but cannot be deterministic, since the input condition cannot be met with certainty. Depending on your level of uncertainty and comfort with indeterminism, additional complexity can be added to mitigate indeterministic output. I should have time to work on this after dinner.
P Daddy
@P Daddy - Excellent, I am looking forward for it! Meanwhile I took the time to make a major edit of the original question. Thanks for all your inputs!
mac
@P Daddy - This looks all very nice! :) It will take me a couple of days to get an occasion to test it against my own code... but as it stands, it look yours is definitively neater, and you also accounted for the "non deterministic answer", that I neither realised it was a possible edge case! :)
mac
@P Daddy - I have began porting your code to the actual program (hope it works out faster, as I mistakenly forgot to commit my own version of it!). It is taking time to give you a feedback because the real-life problem has a notable difference from the "simplified" version I gave as a challenge: on the disk there are multiple triggers... Just thought of mentioning it so you don't think I am not going to come back to you! :) BTW: the way I implemented the code was - at its core - the way AShelly did in his answer.
mac
@mac: How many triggers? Are they evenly spaced? If so, is the angle between them greater than the angle between the switches?
P Daddy
@P Daddy - the number of triggers is variable, and the angle between them is not necessarily greater than the angle between the switches. Most probably the angle between switches will be 90<x<180, while the ones between triggers might be as little as 10°. Anyhow: unless you are enjoying challenging yourself with this problem, don't bother think on how to do it... I'm happy with your answer as it is! :)
mac
+1  A: 

I have a partial elegant answer for the simulation part (ruby implementation). It returns an array of events, just like P Daddy's answer.

class Sim
  def initialize( sw0,sw1 )
    @sw[0]=sw0;
    @sw[1]=sw1;
    @sw[2]=sw0+360;
    @sw[3]=sw0+360;
    @lastTime = @lastPos = 0.0
  end
  def step( pos, time )
    events=[]
    curPos = pos;
    curPos+=360 if (curPos<@lastPos)
    speed = (curPos-@lastPos)/(time-@lastTime);
    @sw.each_with_index{|s,i|
      if (s>lastPos && s<=curPos) 
        events<<[i%2, time-(curPos-@sw[i])/speed]
    }
    @lastPos=pos;
    @lastTime=time;
    return events
  end
end

It works by keeping track of 720 degrees of arc, and forcing the current position to always be greater than the last position. The "less than one full rotation" clause prevents double triggers.

However, I realized after testing it that it only works if the disk is rotating in the positive direction. I think it's fairly simple (but I haven't tried) to create a compliment function that works for the other direction by reversing signs and comparisons.

You mention that the disk has a constant velocity. Is it possible for that value (or at least it's sign) to be passed to the sim layer at initialization? Or does the sim layer need code to detect which direction it is simulating?

Either way, once the direction is known it should be possible to call either this function or it's compliment for an efficient simulation of the switch events.

AShelly
@AShelly - At its core, your implementation is very practically identical to the one I came up myself in my own attempts to solve the problem efficiently. Yes, the sim "knows" the versus of the rotation, somewhere in it's belly, but - in order to keeps things as de-couples as possible, I chose not to use that information (although - in relation to the real world problem - that would not be a problem, as it is only the _devices_ that must not know about it). Also, both mine and your solution do not account for the non-deterministic answers that P Daddy illustrated in his answer.
mac
The non-determinisim on the sim side is only in finding the direction, not in generating events once it is known, right?
AShelly