Re: Lahman, how ya doing?



Responding to Hansen...

But the user, meaning the person that is just using some classes and not programming their behavior, shouldn't have to know event IDs, how many different types of events an object has, or which order they should be in. If the thermometer in the real system doesn't have an event ID, the user shouldn't have to include an event ID when he instantiates a thermometer.

I am thoroughly disconnected here. This sounds like the "user" is the user of the simulation application. If so, I can't see why the user would be instantiating objects, much less knowing about events. Those are strictly software artifacts in the solution. The user might parameterize the processing (e.g., defining the number of samples to use for computing basic Thermometer statistics), but that mapping to simulation ticks is a pure software design issue.


I mean the programmer who only looks at the header files, who doesn't know anything about the internal workings of the classes.

OK. But that programmer needs to know what the object interface is. If one is using an object state machine, then the events /are/ the object interface. So one has the synchronous interface:


Thermometer.takeSample()
Thermometer.computeStatistics()

vs. the asynchronous interface:

E1:takeSample
E2:computeStatistics

Similarly, the programmer is going to have to know when to invoke those behaviors _in the course of the problem solution the programmer is providing_. One of my points here is that it is a lot easier and more robust to define when to invoke those behaviors via

Timer.add_event (&thermometer, E1, 1);
Timer.add_event (&thermometer, E2, 60);

than it is to provide an infrastructure for calling Thermometer.computeStatistics() after exactly 60 calls to Thermometer.takeSample(), especially if the context where one decides it is time to take a sample is different than the context that needs the statistics.


You do understand that I was proposing for Thermometer to register itself with Timer just once, during the initialization? That would give Timer the scheduling information it needs to generate the right events at the right time and in the right sequence. It's just an initialization, not an on-going process.

That's fine. The add_event calls are also a form of startup initialization for Timer. The issue is what each object needs to know about the other. Thermometer does not need to know Timer exists, much less register with it. Thanks to the decoupling provided by the event queue manager, Timer doesn't need to know that Thermometer exists either.


The developer knows the sequencing strategy because it is the developer's design. So the developer knows that to initialize Timer consistently with that overall strategy one needs certain data from Thermometer. But neither the Thermometer nor the Timer implementations need to know that strategy. In fact, it is highly desirable for maintainability that neither of them understand it. So the developer explicitly provides the mapping in the initialization of Timer. (Which could have been done through the constructor for Timer, BTW, though that would have been clumsier.)

You know, I've actually picked up a nice book on UML and OOA from the lab library and started reading it. I should be learning this there instead of bugging you about it. I've picked up other homework that has priority because it's more directly related to making a living, but I really should find time to just read about this stuff.

Hopefully it is a good book. B-) You might check out the Books section of my blog for some suggestions. Bruce Douglass has several books on the OOA/D-based use of event-based processing in R-T/E. One also has to be careful about books with 'UML' or a specific OOPL in the title; they are good at describing how to express your design in the UML or OOPL syntax but they tend to be short on advice about coming up with a good design in the first place.



I appreciate the help, though.

That's what these forums are for. Being retired after 40+ years in the business I have an oversupply of opinions and I need to get rid of them someplace.


It's not important in a simulation, but the approach you gave isn't robust against delays. If the interval is 10 ticks and 11 ticks pass, an event is skipped. And when it is processed at 20 ticks it will think that 10 ticks had passed. My implentation tests for current_time>=next_time so that the skipped event would be generated one tick late instead of ten, and it tracks last_time so that the actual interval current_time-last_time can be passed rather than an assumed nominal interval.

But you weren't planning to use real-time ticks. So there should never be a need to skip any processing when using simulation ticks because everything for each tick will be processed (i.e., time will be either scaled so that is true or Timer.tick() will not be invoked until the processing for the current tick is completed.


I want to understand it. It looks like you're generating dumb events-- the event generator can't adapt to changes and it doesn't pass context-dependent information in them.

That's correct because I abstracted Timer to a very restricted task: generating scheduled events. The "smarts" of the simulation is in the intervals assigned, the ordering rules, the destinations, and the state machines. That deals with the requirements as I understood them: your processing was defined in terms of relative position in a "hard-wired" time stream. All of the computations and sequencing you have described so far can be accommodated by the way the events are registered with Timer.


So far I don't see anything that is context-dependent or needs adaptation. When we talked about things like hardware propagation delays before you didn't seem to think they were relevant (i.e., all the controller processing could be completed in a base clock tick interval (100 ms) in the real controller). You can simulate that easily by scaling real time (to accommodate your simulation computations) or simply invoking Timer.tick() when all the rest of the processing for a tick is done. So...


But speaking of maintainability, if it were to be used in a real-time system with Timer reading the system clock rather than incrementing a counter, could it easily accomodate that?

You'd get rid of simulated hardware, then. But some things like the controller logic and the chart recorders should be reuseable.

I think Timer would need to be revised, but as long as it spits out events with EventElement.tick_count set appropriately, maybe that's it.

Let's say the real controller has a problem on "witching" ticks where there is so much processing that it will overflow the tick interval and screw up the synchronization. The controller is going to have to deal with that explicitly somehow. That means the design will have certain priorities for what gets done in the tick interval and it will have to work around them.


In your case the hardware sampling is probably more critical than some of the data processing steps. Thus whether the computation of the average in Thermometer includes 60 samples, 59, or 61 may not matter as long as one has a sample count for the standard deviation. One possible solution to that problem is to assign a different event queue to the statistic computation and put it in a lower priority thread than the temperature sampling itself. Then it doesn't matter if it bleeds over the tick interval because it won't affect other processing that must get done within the tick interval. (There is no problem with synchronization to the next statistic computation because that is 50+ ticks in the future.)

However, that may introduce another problem. The filtering and other computations in the feedback loop that are done after the statistics may need be deferred as well. So you may not want to trigger them from Timer directly. Instead the computeStatistics action of Thermometer can generate that event when it is done. That will ensure the subsequent processing in the loop is synchronized to the actual samples rather than the Timer's ticks.

This is just a simple example and I have no idea whether it would work in your controller. But hopefully the basic idea is clear. Not all the events need to be generated by Timer; other synchronizations in a daisy-chain may be done simply on the basis of relative processing step. To figure out the best way of solving the problem one would have to identify all the dependencies through the feedback loop and separate those that were directly dependent on clock ticks from those that were only indirectly dependent on them.

[Alas, that might get more complicated if dependencies were nastier. That might mean tracking value histories to ensure getting the right samples or something. However, I suspect your filters, etc. probably only care about the most recent N samples so if they are delayed in real time so that they never "see" some older samples it won't matter.]

The only situation that seems like a problem is if the total computation time on some "witching" ticks where the maximum number of events occur (e.g., a 1 tick sample, a 60 tick statistic, and a 7/60 adjustment all occur on the 420th tick) causes the total computation time to extend beyond the tick interval. That would screw up everything if one did all the processing. Is that what you had in mind?


Yep. In the real system, running on a ten year old computer, sometimes a scheduled time is missed. Whether it's because of witching ticks or delays when reading the GPIB or other, I'm not sure. But that happens on a small fraction of the total ticks, and the timing information is passed to the functions that need it.

OK. I described one general approach above, but in the end it depends on how the skip occurs and what significance it has.


Unrealistic as it may be, let's assume just for the sake of example that the statistics tick comes along but there are only 59 samples because one single tick event never got processed and we need exactly 60 samples in the statistic for consistency. Let's also assume that everything will be fine if you do two things: (1) don't invoke computeStatistics on the current tick and (2) do invoke computeStatistics on the next tick.

If that is all that needs to be done to keep synchronization, then your solution of having Timer defer the event generation to the next tick would work fine. There are lots of ways to do that, such has having Timer maintain an internal queue of deferred events (which could be a simple bitmap for events). You could also make Thermometer responsible for keeping track of how may samples it had since the last statistics computation. Then my code in Timer.tick for the event generation loop might look something like:

      int eventTickCount = eventList[i].getTickCount();
      bool isReady = eventList[i].getRecipient()->isReady();
      if ((tick_count MOD eventTickCount) == 0)
      {
         if (isReady())
            queueManager->push(&eventList[i]);
         else
            deferredBitmap |= (1 << i);  // set deferral of event
      }
      else
      {
         if ((deferredBitmap &= (1 << i))) && isReady)
         {
            queueManager->push(&eventList[i]);
            deferredBitmap ^= (1 << i);  // clear deferred event
         }
      }

[There is a drawback here because every receiver needs an isReady() method even if this notion of readiness is not relevant. (Worse, in C++ isReady needs to be a behavior of Object.) But I have a different point to make in the example, so ignore the details.]

I chose this implementation because it is logically almost identical to your solution where Thermometer decides when to do the computation. Indirectly Thermometer /is/ driving the decision here. There are, though, subtle differences.

What Thermometer knows is that it has enough samples, which is quite consistent with a responsibility for computing sample statistics. That semantic is quite different than knowing whether a sample has been skipped on some tick during the processing. So in Timer we combine reasonable knowledge from Thermometer for its semantics with a rule and infrastructure in Timer related exclusively to scheduling events. IOW, the developer is providing the mapping between the two independent semantic views through interpreting isReady() in the local context. Thus the separation of concerns has been preserved and the implementations of Timer and Thermometer remain independent of one another.

A second difference is that Timer is accessing a simple knowledge responsibility in Thermometer rather than a behavior. This is good because data is usually easier to manage and manipulate than sequencing of behaviors. Capturing the solution parametrically in data also reduces executable code, which improves reliability. However, the main reason this is good is because in OOA/D one thinks of knowledge and behavior responsibilities differently, so it is a form of separation of concerns and complexity management.

In OOA/D behavior communication is via an asynchronous communication model, as I think I have already mentioned. That's why events and state machines are ideally suited to OO development. OTOH, knowledge is accessed synchronously on an as-needed basis. This dual view tends to make things much simpler to manage in complex OOP implementations where stuff like concurrency may come into play. The asynchronous model effectively forces us to define behavior scope narrowly, essentially to the method level. That means that the scope of data integrity for knowledge access within those methods is very narrowly defined. One has to screw around with concurrent processing to have the importance of that burned into one's lobes, but it can avoid problems even in synchronous implementations.

One corollary of this separation of knowledge and behavior is manifested in two facts that are rarely mentioned in the OO literature but logically follows from encapsulation, separation of message and method, peer-to-peer collaboration, all the rest of that OO stuff. Behavior messages very rarely carry data in OO applications and behavior methods never return values in well-formed OO applications. The first is because knowledge access is synchronous so one accesses it directly from the source when it is needed. The second is because of the separation of message an method and encapsulation; returning a value that the caller uses would create an instant dependence on what the callee did.

What of my suggestion at the top,

timer.add_event(&chart, chart.event_list());?

I don't care for it because Chart owns the event_list, which implies it knows how to construct it. That is a matter for the overall simulation solution flow of control. Keep Chart simple by limiting its responsibilities to comptuing sums and averages on demand. So I would much rather see the events, the sequencing of events, and the addressing of events defined outside both Timer and Chart. So I much prefer


timer.add_event(&chart, COMPUTE_SUM, chart.sum_frequency);
timer.add_event(&chart, COMPUTE_AVE, chart.ave_frequency);

done outside both Timer and Chart. Note that Chart still owns the parametric data, but it is the developer that is providing the link between a state machine transition and that data. It may seem pretty much the same thing, but it is subtlely different enough to be much more robust.


But the two lines you've written are the only lines that can ever be used to register chart and produce sensible results. You need both lines, not just one. You need them in that order, they can't be reversed. You need those event IDs, inserting different IDs is not an option. You need those times, they cannot be altered or chart will produce non-sensible results.

Exactly my point! Those two lines provide an elegant solution to the entire simulation sequencing problem all in one place. Sure they have to be carefully crafted, but the solution flow of control /always/ has to be carefully crafted. You can do that crafting in one place with two lines or you can sprinkle it through several lines in multiple object implementations.


Okay, so there's no misunderstanding there.

I was coming from the perspective of hiding unnecessary information, the same sort of idea as the private: region of a class, or using local variables in a function. But you're saying it's important to make the lines of communication clear and visible, even if technically they don't have to be. The two add_event() lines should more or less just be considered as part of the parameter list.

Yes. The event-driven sequencing is probably one of the most critical parts of the design. Any change to the overall sequencing of the processing will require that those lines be modified. So I think it should be visible from the highest level of view of the solution.


Note that I mentioned above that we could have provided exactly the same information to the Timer constructor. But to do that conveniently we would have had to create an eventList much as you suggested in one of your examples. That construction would necessarily have hidden the event sequencing in the construction of that list somewhere in some method. One could put that in a GoF factory object, which would encapsulate it all in one place quite nicely. But I would still prefer to see the initialization done directly in main() at the highest level of control flow.

There's one and only one right way to register it. And I think you're spilling information into main() that shouldn't be there (the event IDs and the frequencies which chart already knows), and presenting the programmer with options (changing the sequence, changing the numbers, omissions) that should never be used.

I don't think it is necessary because all the registration statements are doing is parameterizing Timer as part of the overall application initialization and overall initialization is exactly what one does in main(). That is, they only get executed once during startup, just like assorted constructor invocations.


Note that the event-based solution is very heavy on statically defined rules rather than executable statements. That is all the registration is doing is defining that static structure at startup. The actual behavior is still in Timer and the other objects and they still collaborate with each other on a peer-to-peer basis to actually execute the simulation.

The event IDs are the public interface to the objects. They have to be visible to access the object responsibilities.

The issue is not about providing the programmer options. There is only one way to correctly define the sequencing so the programmer doesn't have any options in that sense. The issue, as in all OO software, is how hard will it be to change the sequencing when the requirements change. Envision the various sorts of changes that might be made in the sequencing of Chart's responsibilities, especially relative to other object's responsibilities. I assert that for many such changes it will be a whole lot easier to do that by manipulating my statements than the implementations of Chart, Timer, and other objects. (In fact, I gave an example, as immediately below...)


In my implementation it would only be manipulating Chart, since by design Chart just internally takes on the task of doing what you did explicitly in main(). And if Chart's responsibilities change, you'd be mucking about with it anyway.

The problem is that proper sequencing is a broader issue than only one object. Consider:


Timer.add_event(&chart, E1, 1);
Timer.add_event(&thermometer, E2, 10);
Timer.add_event(&chart, E3, 20);

vs.

Timer.add_event(&chart, E1, 1);
Timer.add_event(&chart, E3, 20);
Timer.add_event(&thermometer, E2, 10);

In my solution this changes the ordering in which the E1, E2, and E3 responses are executed on the 20th tick, which could be significant to the results. How would you capture that change? I suspect you will have to change the implementations of at least two objects.

Actually, in my version I would use

 timer.set_ticks_per_second(100);

I think it wasn't clear, but all the times I'm using in my implementation
are in seconds.  Ticks are internal to Timer, and changing the number of
ticks per second should have no effect other than to change the time
resolution.  ChartRecorder and all the others only know seconds.

But that does bring up another point related to how timing events are
generated.  It would be perfectly possible, say, to define something like

 Timer timer(10);  // 10 ticks per second
 Object ob(0.15);  // trigger every 150 ms

That won't trigger at regular intervals.  But it should trigger at 0.20
seconds, 0.30, 0.50, 0.60..., and it should run perfectly fine.

My background is showing because an R-T/E person would never even consider this. B-)


I'm not sure I ever figured out what R-T/E means. Real-Time/Event?

Real-Time/Embedded.

The hardware only understands integers (unless it is an FPP in an ALU). Floating point is usually very expensive compared to integer arithmetic and performance is usually critical in device drivers and controllers. (I'm sure the data processing in your real controller needs floating point, but an R-T/E guy will be looking to minimize it.) And one has potential roundoff problems for things like clock ticks that could raise havoc with synchronization. So the interval would always be designed as an integer multiple of a base tick value.


Is floating point arithmetic still that much slower than integer in modern CPUs? I thought the floating point performance was getting quite fast.

It certainly is a lot faster than the Old Days where an FP addition took 300+ clock cycles compared to one cycle for an integer addition. It can also be very fast on RISC machines where more gate real estate is available for basic computation because the instruction set is a lot simpler. But on something like a Pentium the floating point isn't even done in the main ALU; it is farmed off to a special floating point processor (FPP). [That's why the RISC people only want to benchmark in FLOPs while the CISC people only want to benchmark "real" applications. In reality none of the benchmarks mean much because a reasonable comparison needs to consider stuff like bus architecture, register count, compiler optimization, pipelining, memory cache, etc., etc.]


Nonetheless, it can still take multiple clock cycles compared to integer arithmetic for multiplication and division.

But as each 10.0 object executes, it's left in the 10.1 state. Any 10.0 object getting data from it after that point will be getting data of the future, not the present.

That's what I addressed in the section immediately below. What did I miss?



That might be overcome by sequencing if the data only goes in one direction, and if there are no loops. But the data can go in two directions, e.g. from thermal mass 1 to mass 2 and from mass 2 to mass 1. And there are loops.

It's still just an array and each object just needs to know which index to use. A loop may make the array size > 2 so you may need a relative index for the loop, but its is still just about knowing which [t] to access.


You could even make it independent of the event ordering by having each object have an attribute for where the current t for the tick is in the array. That would allow "blocks" to process in arbitrary order using relative indexing from that index. But I would expect the computation ordering to be driven by the order of migration of heat flow (source -> sink). That, in turn, would drive the definition of event sequencing anyway. IOW, the event sequencing and order of heat flow computations are going to be deliberately tied together anyway so I don't see a need for an infrastructure to make them independent.


An array of size 2? That means you still have to manage the array. I'm not sure how that makes anything simpler.

If all one has to worry about is using the [t-1] values from the last tick versus the <recently computed> [t] values from the current tick, all one needs are old/new values. Loops potentially add an iteration count to the number of values to be tracked.



Speaking of dependencies, if the array size would have to be increased depending on the loops and interconnections, doesn't that introduce unwanted dependencies? I had imagined defining any number of objects and connecting them in any way that pleases you, changing parts at a whim, and you just wouldn't have to worry about things like registering the thermometer before the controller, or adjusting which index to use.

The array is probably hidden entirely in the implementation of the object. The interface is something like getCurrentTemperature() and getPreviousTemperature(). Alternatively on could use getTemperature(UNCOMPUTED) vs. getTemperature(COMPUTED), depending on whether the object accessed is before or after the object being computed in the computation order. So in...



        +-----------+                    +------------+
10.0 heat | T1        | T2     10.0 heat   | T3         | T4
---------->|   Block1  |------------------->|  Block2    |
        |           |                    |            |
        +-----------+                    +------------+

if one is computing Block2, one asks asks for getPreviousTemperature

from Block1 to get T2 but if one is computing Block1, one asks for

getCurrentTemperature to get T3 from BLock2.


I don't like having to decide which temperature to get based on the order of processing. E.g. if I had a single loop with the target connected to a constant temperature sink, then decided to simulate the system more closely by connecting the target through a thermal link to another controlled block. Or just decide to throw in a low-pass filter, or a second power supply to solve a problem with dynamic range while not sacrificing resolution near an error of zero. I wouldn't want to have to reevaluate which temperatures the peices should get. The code might be easy maintain, but the implementation of the problem to be simulated isn't.

I don't think you would have to re-evalute which temperatures. Consider:

        0..1 leads
[Block] <---------------+
   | 0..1               |
   | follows            |
   |                    |
   +--------------------+

where the a given block follows a block that has already been computed and leads a block that has not yet been computed for the current tick. Now you insert a new block in the series seamlessly. When doing the computation for a given block, you still ask for getPreviousTemperature from /whoever/ is on the end of the 'follows' relationship and ask for getCurrentTemperature from /whoever/ is on the end of the 'leads' relationship.

The ordering is defined when the relationships are instantiated. Of course, that ordering has to be in synch with the event ordering (i.e., the events to each block are generated in the same sequence as the relationships are instantiated. However, there are tricks for defining both the event ordering and the relationship instantiation from the same external configuration data, which guarantees synchronization. That is due in no small part to the fact that the sequencing has been expressed in terms of parametric initialization data rather than behavior. But that's a whole other topic... B-)


************* There is nothing wrong with me that could not be cured by a capful of Drano.

H. S. Lahman
hsl@xxxxxxxxxxxxxxxxx
Pathfinder Solutions  -- Put MDA to Work
http://www.pathfindermda.com
blog: http://pathfinderpeople.blogs.com/hslahman
(888)OOA-PATH



.



Relevant Pages

  • Re: Lahman, how ya doing?
    ... But some of what I want to do involves time-dependent evolution, like to calculate a change in temperature, or to calculate a control action. ... You can't just skip a tick and do an average. ... but at least it's a slope. ... The Thermometer state machine was essentially the same as the one I originally proposed a few messages ago; it just no longer did the actual hardware sample and now counted ticks. ...
    (comp.object)
  • Re: Lahman, how ya doing?
    ... You can't just skip a tick and do an average. ... >> It's not quite on schedule, but at least it's a slope. ... float temperature; ... The thermometer records ...
    (comp.object)
  • Re: Lahman, how ya doing?
    ... The statistic computation on the 60th tick will then be computing on 59 samples rather than 60 whenever a skip occurred and that would affect the standard deviation. ... So the easy way to deal with that is for Thermometer to record an actual sample count which is incremented when a sample is taken, regardless of what tick it is in. ... A classic example is Normal Form in UML Class Diagrams. ...
    (comp.object)
  • Re: Lahman, how ya doing?
    ... A time tick in the simulation can occur at arbitrary intervals in real time to accommodate software processing. ... the rules and policies for what /must/ happen and the order in which it must happen on a given tick are exactly the same for a simulation tick as for a real-time tick. ... Heat goes from the heater to thermal block 1, heat goes from thermal block 1 to the heat link and to block 2, the temperature and resistance of the thermometer change as the heat load changes, all at the same time. ...
    (comp.object)
  • Re: Lahman, how ya doing?
    ... Then control passes to the low priority queue. ... In a simulation, you have to tick the clock some how. ... One delays the results selectively so that they emulate the delays in the real controller that led to the skipped processing in a tick. ...
    (comp.object)