Re: Lahman, how ya doing?
- From: "H. S. Lahman" <h.lahman@xxxxxxxxxxx>
- Date: Sat, 30 Apr 2005 18:32:17 GMT
Responding to Hansen...
There's also the matter of what resolution is needed. In the real system, sometimes an event like adjusting the control is more than 1/60 second late. But not 7/60 seconds late. And the thermal time constant of the target is around 14 seconds, much longer than any delays in the control loop. The data I've taken fits well to a model second-order control system, and in general I've seen no reason to believe that processing lags have any interesting effect, so I think the ideal computer approximation will do.
The actual system is a real-time hardware control system. As such, its software will be event-based and the software processing will mostly be done via interacting state machines. There will be <at least one> event queue manager program unit and a Timer program unit that gets ticks from the system clock and generates events to other program units. That's just basic R-T/E design.
My point in this context is that the closer your simulation models that actual software, the better and more robust it will be. In fact, if that controller is well-formed OO you should be able to reuse much of the software in the feedback loop with, at most, only cosmetic changes (e.g., the Thermometer object reads an attribute in the simulation rather than an instrument hardware register).
The actual software has a main loop that continually calls GetTicks(), a system-level call to the computer's internal timer, and then runs through a list of scheduling variables to see if it's time to call a function to read voltages, calculate a control action, move the shutter, etc. If the tick count doesn't change, it will just loop and loop, retesting the same number.
Whoever designed that software was definitely not an experienced R-T/E developer. It is also probably not an OO implementation. If you are using that as a model, I now understand where a some of our disconnects are coming from. B-)
There are only two things that make your simulation fundamentally different than the real system: (A) you must calculate the value of temperature that the thermometer "reads" and that computation may trash real-time absolute simulation because of its complexity; and (B) instead of writing to the heater, beam, shutter, and other hardware on the other end of the feedback loop you need to provide inputs for the calculation in (A) for the next cycle, which also may take time that would trash a real-time simulation. IOW, everything between reading the hardware and writing the hardware should be exactly the same; what you are actually simulating is the hardware itself.
The finite execution time for the simulation infrastructure computations probably means you can't do an absolute real-time simulation. Somehow you have to spread out the events to accommodate those computations. One way is to simply scale the time in the Timer (e.g., 50 clock ticks = 1 simulation (event) clock ticks). Another way is to not use the system clock at all and just trigger the next simulation clock tick with an event to the Timer that is generated when all the processing for a single <simulation> clock tick is done.
I plan to not use the system clock at all. The point of the simulation is that it isn't real-time. But the computational needs of the analog parts are pretty light.
I suspect you are predicating that on the poor R-T/E design of actual controller. Your controller /is/ a real-time system and it screams to be event-based. Your simulation should be faithful to that. (More precisely, it should be faithful to the structure of a properly designed controller.)
To repeat my point in the quoted text, the only thing you are simulating is the hardware itself. The simulation of the control feedback loop should be exactly the same as the software feedback loop in the <properly designed> controller. IOW, there is nothing special about the simulation of the sample processing compared to the processing in an actual controller so there is nothing in the sample processing that needs to be specially "simulated".
Whether you use the system clock to generate the events or not is a matter of the Timer object implementation. That implementation does not change the inherent nature of the processing, which is classic event-driven R-T. As far as the control feedback loop is concerned it /is/ a real time simulation that is inherently event-driven.
Bottom line: the poor design of the actual controller should not be transferred to your simulation software.
Either way, that is just a matter of how you implement the Timer for the simulation. That should be completely transparent to all the rest of the software. Thus you have to ensure is that the events will be generated by the Timer in _exactly the same order_ that they would be in the real-time controller timer. All of your calculations on temperature samples, statistics, filtering, smoothing, etc. should be completely indifferent to the way the events are generated. As far as they are concerned they are being done in real-time to the system clock. IOW, all of you simulation software -- except the hardware emulation itself -- /is/ a real-time simulation and it should work exactly like it does in the real controller. That's what I meant about the problem being a real-time simulation; it should work exactly the same way, down to being event-based.
I also need jobs that do task 1 at time interval 1, and task 2 at time interval 2. But it won't be hard to add a
time.add_task(&c3, 5, 20);
At the risk of being picky, doesn't this do the /same/ task twice?
No. In the system I want to simulate, there's a sampling time of, say, one second, and an averaging time of, say, 60 seconds. Once per second it takes a data point and calculates a running total and total^2, and once per 60 seconds it calculates and reports an average and standard deviation, and zeroes the totals.
OK, I thought the tasks would be in different objects. So the c3 object has a state machine like(?):
E2:sixty ticks +--------> [Sampled] --------------> [Statistics Computed] | | ^ | | | | | +------------+ +--------------------------+ E1:one tick E1:one tick
and all you are doing is registering it to receive both the E1 and E2 events from the Time object. If so, then it might be simpler to just register via
time.add_task(&c3, 1); time.add_task(&c3, 60);
since the Time object shouldn't care about duplicate handles; it just needs to know where to send the indicated event. Then you don't a special implementation in Time for this case.
Yes. Except it has to know that the first line refers to a sample and the second line refers to an averaging. I can think of not-so-clean ways around that, like a third parameter that specifies which task, or a counter internal to the object which then wouldn't need a trigger for the second event. Or maybe make every event a double even with a third parameter 0=off by default. I can't think of a satisfactorily clean way.
It doesn't matter. What you are telling the Timer is that it should generate the E1 event every 1 second and send it to &c3. It should also generate the E60 event every 60 seconds and send it <serendipitously> to &c3. The Timer does not and should not know anything about the semantics of how the events will be interpreted by the receivers, who the receivers are, or why the receivers care about the events.
I'm not sure I'm following you here. At one second, something would tell &c3 something like
c3->evolve(elapsed_time, E1);
and at 60 seconds it would tell &c3 something like
c3->evolve(elapsed_time, E60);
I am assuming proper event-based processing. The event queue manager would invoke the correct Thermometer action for the event, presumably take_sample() and compute_statistics() or some such. That selection would happen via a table lookup of the event id and the current state of the Thermometer state machine. So the action in Thermometer never "sees" the actual event. (The rules of FSMs say it can't because it can't know the previous state, which is implicit in the transition event.)
The Thermometer has a state machine something like the one in the quoted text above. When you register the Thermometer with the Timer, you tell the Timer you need two /different/ events sent to Thermometer: "E1:one tick" is sent on each time tick (real or simulated) and "E2:sixty ticks" is sent every 60 time ticks (real or simulated). If Timer does that properly, Thermometer will Do The Right Thing and Timer will know nothing about its semantics. In particular, Timer has no clue what state action will be invoked. That is all decoupled through the event queue manager.
In addition, all the sequencing decisions exist solely in the statements that register Thermometer with Timer.
? That is, pass both an elapsed time and an event designator to the block that is to record a data point, simulate the moving of the shutter, etc.? Or pass a current time to it, or whatever.
And then &c3 would have to be told, when it is initialized, that E1 means to perform action 1 and E60 means to perform action 2? Perhaps by a call like
ChartRecorder c3(&c1,1,60); // input from c1, nominal times 1 and 60 timer.add_task(&c3,1); // repeat timing info for timer's benefit timer.add_task(&c3,60);
As a practical matter you probably want better naming conventions for the events and you would parameterize that as well. B-) IOW, you register the receiver, the event to generate, AND the tick count interval.
I'm trying to solidify in some way what you're discussing.
ArbitraryControlBlock c1(parameter, another_parameter, etc);
This is unnecessary; it is defined in the object state machine transitions and the STD lookup the event queue manager does.
timer.add_task(&c1, time_to_wait, id_of_an_event); timer.add_task(&c1, second_time, id_of_another_event);
This is necessary to provide the event id when Timer pushes the event onto the event queue. Timer's only mission in life is to push specific events on the queue on certain time ticks. Mechanically, one needs to address the event (&c3) and provide the event id so that the event queue manager can dispatch it properly when the event is popped. But Timer should know nothing about c3; it is just a semantic-free handle.
If you have a timer at all it is easier to make it event-based. Then you don't have to write any conditional code or have history attributes. The serialization is essentially free, as in the 1, 60, and 7/60 event case above. At most you may have to have a prioritization scheme. The Timer will be a simpler and more generic as well. And the event queue manager will be entirely reusable (the target &cN is passed in the event).
I guess I'm not sure whether I'm event based. I gave a little more detail elsewhere, but as I have it set up now, when timer.add_task(&c1,5) is called, a Task internal to Timer (the user doesn't directly interact with it) is created, and is given the Block pointer and the timing information. Every time timer ticks, each Task is told the current time. The Task decides whether to trigger. When it triggers, it sends the Block the elapsed time since the last trigger, e.g. ptr->evolve(dt).
This is exactly what I am arguing against. The controller is /inherently/ event-based. Think of it as a major R-T/E design pattern.
I don't think I know what any design patterns look like.
The problem: a suite of operations occur with varying frequencies relative to one another but the data they operate on must be consistent in real time (i.e., there are sequencing dependencies in the operations).
The R-T/E pattern: Capture sequencing constraints in state machine transitions, separate concerns via interacting state machines, use event-based processing with an event queue manager, and use a timer to sequence events.
[Technically it is not an R-T/E pattern. That style of programming was just developed there first because it is a very unforgiving environment and developers needed every break they could get. It is actually a far more general pattern for development. For example, it is employed religiously in translation environments for IT and scientific programming as well as R-T/E.]
Decades of pain and suffering have determined that the best way to implement this sort of hardware controller is making it event-based. In that sort of design the sequencing of operations is determined by the order in which events are generated and who consumes them. Your simulation software should emulate that.
The recipient of an event does its thing immediately with no debate; consuming the event is the trigger. Doing that thing was properly placed in the overall processing sequence by the ordering of the events so there is no decision for the receiver to make.
Ordering the events? How do I order the events? I don't see any inherent order of events in the system I'm trying to simulate. Every block either executes at a particular time, or it does nothing. If it executes, it's assumed that everything executes at the same time. But if there were any sequence to it, the sequence just doesn't matter. No block depends on another block having completed an action before performing an action of its own.
One ordering is between operations. When Thermometer does its calculation of statistics you need to have exactly 60 samples available that have not been in previous samples. That synchronization between taking samples and computing statistics needs to be maintained in the event ordering (i.e., exactly 60 one-tick events must be consumed prior to consuming the special 60th tick event).
Since you have a single event queue manager that is a FIFO queue and you don't have any concurrent processing, the events will be consumed in exactly the same order that the Timer pushed them onto the queue. If Timer generates events on specific counts of single ticks, the ordering between operations Just Works.
But the ordering I was referring to here is on those ticks where multiple events are generated. For example, on the 60th tick you generate two events to Thermometer: one to take a temperature sample on the single tick and one to compute the statistics for the last 60 samples. Do you want the current tick's sample in the 60 or not? To do the standard deviation consistently, that order has to be consistent.
Now let's assume Timer generates events for the same tick in the order that you registered the events (i.e., the order in which Timer.add was called). You can change the order of the event generation by simply reversing the calls to Timer.add. (Alternatively -- and more robustly -- you can also register a relative priority with the event.)
In this particular case making it event-based actually makes the simulation a lot easier. You describe a significant infrastructure in each Task just to decide if it should do something. That goes away completely if Timer generates events and the Tasks just respond. All of the decision making is done in registering the events with the timer because that is where the ordering can be defined as well (i.e., Timer generates events for the same time tick in the order that they were registered). That also makes it trivially easy to change the order of processing -- you just reorganize the registration statements. So here the event-based view is much cleaner, simpler, and easier to maintain.
Each Task, recall, is generated by Timer in the first place when a control block is registered. The user defines a control block and adds it to Timer, then Timer defines a Task and attaches the control block to it, and manages a list of tasks. I did it that way because I wanted to bundle the scheduling data and logic for each block together. When I've finished testing it, I intend to make Task private to Timer because there's no reason for the user to make one externally.
And I am jumping up and down screaming: Don't Do That! B-))
Task and Timer are two different peer entities with their own unique responsibilities. You are effectively merging them and making it a Timer responsibility to understand how to construct a Task, what it relationships are, and how to invoke it. Cohesion is hemorrhaging and encapsulation has been trashed!
BTW, this "user" seems to have other abilities than simply defining simulation parameters like closing the shutter. This suggests the "user" can also design the hardware controller itself.
Off-hand, I can't think of another way to do it, except to unroll Task's functionality and parameter lists into Timer. E.g.
Let Timer generate events in the right order and make that its /only/ responsibility. One can greatly simplify even that responsibility through the registration process where the developer understands the context for generating events and can express it through simple event ordering.
Decouple Timer completely from the Tasks that respond to those events. Use a separate Factory pattern to construct Tasks and register them with Timer. That isolates the rules and policies of object instantiation and relationship participation from the rules and policies of collaboration. Design both Timer and Task so that they are as decoupled (know as little about each other) as possible.
That decoupling is exactly what an event-based structure does. The event is just a message that announces something has happened. Whoever cares about that consumes the event and provides an appropriate response. But neither sender nor receiver need to know anything about each other.
That sort of seems event based since each Task is really just sitting there waiting for a kick in the pants, and each Block is sitting there waiting for a kick. But I guess I don't know the language very well. The timer still calls each task, one by one. I suppose it's not really an event queue, anyway.
Event-based processing is a methodological mindset; its about the way one thinks of collaborations. One defines the overall solution sequence by determining where events should be generated and where they go. The receivers have no explicit decision making; they just execute their responsibility when triggered by an event.
The corollary is that events are announcements (I'm Done) and they are sent to whoever cares about that (i.e., has a response for the announcement). That decouples what the sender is doing from what the receiver does. That happens to be one of the key things that distinguishes OO methodology from procedural or functional methodologies where sequences are chained together by imperatives (Do This). It just so happens that the rules of finite state machines coincide very nicely with fundamental OO practice.
I can't say I have a queue for my events, but my end receivers, the control blocks, are given an elapsed time. That's all they're told, they don't make any timing decisions on their own. They just run when they're told to run, with the appropriate dt to stick into a delta_T=dQ/C/dt or something.
As I indicated in the following paragraph, one only needs an event queue if the actual processing is inherently asynchronous (e.g., the sampling events are driven off some sort of probabilistic mechanism where the order of events is not predetermined). But thinking about the OOA/D as-if it were asynchronous is a great talisman against allowing procedural techniques to sneak into the implementation.
Consider the other side of the coin. Suppose one has designed with object state machines and an event queue manager. If there is nothing inherently asynchronous in the problem (which there doesn't appear to be in your case), one could do a pure synchronous implementation at the 3GL level.
What's a 3GL level?
Third Generation Language level. C, FORTRAN, C++, etc. If one does an OOA model in UML with an abstract action language, one is using a true 4GL.
To do that all one needs to do is replace push calls to the event queue manager with direct method calls to the state action that would respond to the event. That eliminates the event queue manager and if one looks at the resulting 3GL code it might be difficult to tell the OOA/D had been done with state machines at all, much less that event-based processing had been used.
I think that's what I'm doing. In principle, a Task could be made to generate an event and stick it in a queue. But it just calls bptr->evolve(dt) directly.
You indicated that the Task polls the Timer's ticks to determine whether it needs to do something. That is not the way it should work at the conceptual level. The event is generated _when it is time for Task to do something_. So when Task consumes the event it should Just Do It. If you don't use events, then someone else directly invokes the behavior. [Hopefully, though, they will not depend in any way on what the behavior does. They won't if one thinks about the call as an announcement message (It's The Right Time) in the original design.]
To put it another way, Task has a set of responsibilities for doing stuff. It is a whole other responsibility to determine /when/ it should do that stuff. Deciding when requires understanding the /context/ of how Task fits into the overall solution. But Task should have intrinsic responsibilities that do not depend on context. (In theory, though almost never in practice, one could design all the objects and define all their responsibility implementation without a single method call in any method body; one could then backfill by determining the right places to insert method calls and the solution should Just Work.)
[One doesn't even want to encapsulate that understanding of context in an object directly. That would make it a god or controller object in a hierarchical decomposition. That's why we want to connect the dots of intrinsic, self-contained behaviors with peer-to-peer messages that can be defined independently of the individual behaviors.
My ardent advocacy of using Timer.add to define ordering of events is a reflection of this. I don't want some object to be saying, "You do This. Then You do That. Then..." That buries the sequencing rules in the implementation of the object. I want them expressed outside of object implementations. The use of Timer.add does a great job of providing exactly that sort of external sequence specification.]
The simple way to march those calculations is to just daisy-chain them.
Daisy-chaining makes me nervous. I'm not really sure what you mean by it.
Remember I am talking about the calculations you add that emulate the hardware, particularly the computation of the temperature value. Since they are mathematically defined by thermodynamics much more general than the context of this problem, I would be inclined to put them in realized code that I did procedurally or functionally outside of the OO development (e.g., in a function library or a reusable package). Procedural/functional techniques are more intuitive and tend to be more terse than OO solutions, especially for algorithmic processing. Since the algorithms don't change there is no maintainability issue once one gets them working. In that world literal, hierarchical daisy-chaining of imperatives is the normal operating mode. B-)
From a more philosophical perspective, all software solutions aredaisy-chained. The tricky part is to do it without creating implementation level dependencies, which is the OO goal. The tools for that are things like encapsulation, implementation hiding, peer-to-peer collaboration, and separation of message and method in OOA/D. So in OOA/D we daisy chain by sending messages from one object ot another on a peer-to-peer basis. If one looks at a UML Sequence Diagram, that is exactly what it describes -- connecting the behavior dots in time via passing messages. (Note that events are pure messages.) However, in OOA/D defining collaboration messages to connect the dots is usually the last thing we do (after we define objects, responsibilities, relationships, state machines, behaviors, etc.) while in procedural/functional development message and response are not separated at all.
If you feel the computations are complicated enough to warrant distributing over multiple objects and you want to do an OO solution for those computations, then I would suggest tucking it in a subsystem where you can address that problem alone. In that subsystem you may or may not choose to use events for communication. But that subsystem would still just return a temperature value when it was done and that would be encapsulated in a single method in your software the simulates the control system.
I imagine Blocks representing elements of the system, and they can be connected together in any sort of pattern, with loops and interconnections all over the place. Change any little thing and it's hard to maintain one iteration. Or maybe you mean the blocks are just arranged in a queue, and my code
for (ptr = list.begin(); ptr != list.end(); ptr++) (*ptr)->timeis(t);
would be replaced by
ptr = list.begin(); (*ptr)->timeis(t); // rely on first object to tickle second, etc.
This is close to the event-based approach. The most rigorous approach to state machine design is a variation on DbC.
What's DbC?
Design by Contract. Back in the Structured Programming Days the notion was that there is a contract between caller and callee that can be explicitly defined. That contract imposes constraints on the caller (preconditions) and the callee (postconditions and invariants). It has been highly influential in the application of formal specification methods.
It was adapted by the R-T/E people for use with state machines in the way I described. It was also adapted by Bertrand Meyer and others to be applied to OOPLs and OO development. However, the basic idea remains despite modifications to the formalism.
************* 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
.
- References:
- Lahman, how ya doing?
- From: Gregory L. Hansen
- Re: Lahman, how ya doing?
- From: H. S. Lahman
- Re: Lahman, how ya doing?
- From: Gregory L. Hansen
- Re: Lahman, how ya doing?
- From: H. S. Lahman
- Re: Lahman, how ya doing?
- From: Gregory L. Hansen
- Lahman, how ya doing?
- Prev by Date: Re: When and where to use Visitor Pattern?
- Next by Date: Re: inherited composition
- Previous by thread: Re: Lahman, how ya doing?
- Next by thread: Re: Lahman, how ya doing?
- Index(es):
Relevant Pages
|