Re: Lahman, how ya doing?
- From: "H. S. Lahman" <h.lahman@xxxxxxxxxxx>
- Date: Tue, 03 May 2005 20:17:00 GMT
Responding to Hansen...
However, I am concerned about that vector of "scheduling information" because...
Block has a single member function that accepts all events. I've defined event types by structs, including an EvolveEvent and an UpdateEvent, which contain whatever information is needed. EvolveEvent includes an integer member that indicates the type of action to be taken, and EvolveEvent and UpdateEvent are parsed by operator overloading, i.e.
void Block::trigger(EvolveEvent * ev) {...} void Block::trigger(UpdateEvent *) {...}
(UpdateEvent carries no information, so no variable name is needed. It just triggers the right function.)
Before, I told Timer to include a control block, and what the scheduling should be, e.g.
timer.add_task(&c1, 5); // five second intervals
I liked this (or at least a refinement where one registered the event associated the interval along with the recipient object who cares about it). That allowed one to define the scheduling in the way Timer::add_task() was invoked rather than embedding the scheduling rules in the implementation of Timer. (If you have gotten rid of Tasks, then you could just rename it to something like Timer::add_event(...), since that is what you are actually doing.)
The problem I had with it is that the number, types, and scheduling of events has to be known and declared separately from the object definition. E.g.
ThermalBlock target(10, 2); // heat capacity 10 J/K, initially 2K timer.add_task(&target, 1/60, 1); // You just have to know there's only one type of event to // generate, it is of type 1, and time information is declared // seperately from the initialization of target.
But Timer doesn't know anything about what responses go with those events.
In your problem Timer has to churn out events on a schedule that is based on counting time units (clock ticks or simulation ticks). To do that all it needs to know is what the event ID is, how often (the tick count interval) it should be generated, and who the event goes to. That's what registration provides. The registration completely decouples Timer from the responses and event the ordering decisions. More important, the responses are completely decoupled from the logic that determines when they should be executed. Best of all, everything about the sequencing of operations can be defined in a set of Timer.add_event calls.
ChartRecorder chart(&target); timer.add_task(&chart, 1, 1); timer.add_task(&chart, 60, 2); // You have to know that there are two types of events to // generate, they are of types 1 and 2, 1 should come before // 2, and the timing information of once per second and // once per 60 seconds is declared separately from the // initialization of chart.
IOW, somewhere in the design one needs to ensure correct sequencing when operations are invoked. You always have the problem; the issue is how to address it with minimal dependencies.
I wanted those classes, when programmed, to know for themselves how many, what types, and what order of events to generate, own their own timing information, and simply be told to register with timer in the way they see fit. E.g.
I assert that this is what you do not want. The individual objects do their individual responsibilities when it is time. They should not be concerned with sequencing their activities within the overall solution context (i.e., determining when it is time to do so). That is a context decision for collaboration.
Using Timer.add_event to define that sequencing is equivalent to drawing message lines in swimlanes in a UML Sequence Diagram. It defines the collaboration sequencing at a different level of abstraction than individual object responsibilities and implementations.
ChartRecorder chart(&target, 1, 60);
chart.declare(&timer);
// The user who just wants to define and use a ChartRecorder
// doesn't need to know the registration details. He needs
// to know what is being recorded, the two time intervals, // and what the timer is, and nothing else.
It is Timer that is generating the events so the relationship only needs to be navigated from Timer to ChartRecorder. So ChartRecorder has no need to know that Timer even exists; it just processes events it is given. More important, given your explanation below, is that ChartRecorder should not know about the rules and policies that determine the correct sequencing in the overall flow of control. ChartRecorder may reasonably know individual parameters (e.g., how many time ticks) that determine that sequencing, but it shouldn't know how events are defined or generated.
Also, who is the 'user' here? The simulation software user? If so, why would that user know anything about the sequencing mechanisms in either of our designs?
I am also concerned about how complicated that "scheduling information" is and the sorts of decision Timer will make. I think Timer should be rather simple-minded here. It just count ticks and generates events in an order defined by Timer::add_event. There shouldn't be any, "If I already generated the E47 event, then I should...," sort of decision making. Just have it enforce the simple rules that multiple events on the same tick are issued in the order that they were defined (or use a relative priority number that is defined when Timer::add_event is invoked).
A page of code is worth a hundred words, so I'll just show you how it looks right now.
struct EvolutionList { Block * bptr; int event_type; float wait_time; float next_time; float last_time; bool just_triggered; };
bptr points to the object that is to receive the event. bptr, event_type, and wait_time are parameters passed to Timer, and once defined they don't change. next_time, last_time, and just_triggered are variables that Timer manages, and they're initialized to next_time=wait_time, last_time=0, just_triggered=false.
That's what I was afraid of. B-)) I see no reason for Timer to have attributes like next_time, last_time, or just_triggered. All it needs to do is count ticks and generate the relevant event(s) when the count is right. So I see the entire <C++> implementation of Timer as:
class EventElement
{
private:
Object* recipient;
int event_id;
int tick_count;
public:
EventElement (Object* r, int e, int t)
{recipient = r; event_id = e; tick_count = t;};
int getTickCount() {return tick_count;};
}class Timer
{
private:
EventQueue* queueManager;
EventElement eventList[MAX_EVENT_COUNT];
int tick_count;
int next_event;
public:
Timer (EvetnQueue* e)
{queueManager = e; next_event = 0; tick_count = 0;};
void add_event (Object* o, int e, int t);
void tick()
void reset() {tick_count = 0;};
}void Timer::add_event (Object* o, int e, int t)
{
EventElement event = new [] (o, e, t);
eventList[next_event] = event;
next_event++;
if (next_event == MAX_EVENT_COUNT)
// signal exception.
};void Timer::tick()
{
tick_count++;
for (int i = 0; i < next_event; i++)
{
int eventTickCount = eventList[i].getTickCount();
if ((tick_count MOD eventTickCount) == 0)
queueManager->push(&eventList[i]);
}
}but in case not let me review a pretty typical way of implementing event-based processing. Any object that generates events talks only to the QueueManager by invoking
QueueManager::push (<event id>, <recipient handle>, <data packet handle>)
[This may be packaged in a more generic Event struct whose address is passed to push.] The QueueManager saves the event internally in a FIFO queue structure. At some point the QueueManager pops the event from the queue. When it does, it uses a table lookup on the <recipient class id> to find a static class method like
Class::dispatch (<recipient handle>, <event id>, <data packet handle>)
I think my Block::trigger() function must be taking this role. I suppose dispatch() would be a more standardized name?
Yes, that is what I assumed. Naming is largely style. At this level I see the method's primary function as dispatching to the right state action via the STT. I see triggering as really happening when the event was generated (the actual state change) or consumed by popping it from the queue. But one can also argue invoking dispatch is triggering the action.
[This assumes every relevant class has a static "dispatch" function. If it doesn't recognize the <event id> (i.e., you've got the wrong class), it will generate and error. Unfortunately statically typed languages may get upset trying to invoke a static method from an instance, so one may need to add a table lookup on a <recipient class id> argument passed with the event.]
Class::dispatch accesses the current_state attribute of the recipient object and does a table lookup for the state action in a static STT for the class. The STT is indexed by {current_state, event_id}. It then synchronously invokes the object action from the table lookup, passing it the data packet handle. Each action signature looks the same:
actionN (<recipient handle>, <data packet handle>)
[Note that the actions are implicitly static class actions so one can be independent of instance typing in the table lookup, hence the explicit "this" pointer so that the action can access instance attributes.]
It seems to me that your Block::trigger is doing this sort of dispatch, right? If so, then the thing that seems to be missing is the decoupling provided by the event queue manager. I think that decoupling is important despite the indirections of the table lookups. Also, the event infrastructure is highly reusable or at least amenable to template specification.
But I decided it doesn't make sense to separate timing information from the rest of the control block definition. The shutter cycle time is owned by the shutter, the controller sampling time is owned by the controller, etc. So, e.g. a beam shutter object is defined as
I am disconnected again. What's a "control block"?
A generic term for one of the shapes you'd find in a diagram of a control system. An amplifier, a thermal mass, a digital controller, etc.
OK, but they seem like different entities that would be uniquely abstracted as Amplifier, ThermalMass, DigitalController, etc., each with their own unique instances. I was hung up on the use of "control block" as a generic, implying something in common among all of them (e.g., a superclass).
I have no problem with the objects owning their own timing intervals. But that is just parametric data for the scheduling (i.e., an argument to Timer::add_event). My concern is with the coupling...
HeatInput beam(0, 10, 3); // low power 0 watts, high power 10 watts, cycle time 3 seconds
And now the control block informs timer of its scheduling needs,
beam.declare(&timer);
Why would a beam need to know anything about where the event came from? The beam just has an operation to perform when some condition arises. The event announces that condition. But that (that the condition prevails) is all Beam needs to know.
By employing true event-based processing you completely separate the concerns of determining when the Beam should do its thing (Timer's job to present events in a timely fashion) from the concerns of what it needs to do.
And beam.declare() internally uses a sequence of timer.add_tasks() to add as many schedulers as it needs. In this case one, but I've tried two.
I understand what you are trying to do and it is certainly plausible. The problem is that to do that Beam must understand the context of how its operations fit into the overall schedule of the problem solution. I applauded Timer::add_task above because it moved the rules and policies of sequencing out of the Timer's implementation. I am against putting those rules and policies in Beam's implementation for basically the same reasons.
I'm not sure I understand the objection. declare() tells beam to register itself with timer, it is called once during the initialization. beam doesn't actually keep the pointer, it's just passed so that beam can call as many timer->add_task()s as it needs, with the appropriate event types in the appropriate sequence.
As I indicated above, I think that the problem is that Beam should not know Timer exists. That is essentially navigating the relationship in the wrong direction. Nor should it know how to register events because that is a particular mechanism chosen for the sequence ordering of the overall solution. So if you change mechanism for defining sequencing, you have to tinker with the implementation of Beam and every other object triggered by that mechanism.
If you encapsulate the sequencing rules and the mechanism in a dedicated object, you only have to change that object. In this case the calls to Timer.add_event could be encapsulated in that single object. The rules and policies of sequencing are orthogonal to the rules and policies of collaboration. Complexity is better managed by separating those concerns. That's why we have lots of GoF patterns for instantiating objects and relationships; it separates and encapsulates instantiation away from collaboration.
I also argue that defining the events is a mechanism for defining the overall flow of control of the application execution. As such, that is at a higher level of abstraction that individual objects. That's why I like separately defining the Timer.add_event calls -- it can be done at the level of main() when initializing the application.
So now, with each tick, Timer updates the time and then goes through its list of schedules. For every one that's due it generates the indicated type of event, passes it to the target, and updates last_time and next_time. Then it goes through them again, and for every one that had just been triggered it sends an UpdateEvent to update the state of every triggered peice at the end of an iteration.
The first two sentences I'm fine with; that's just routine event generation. I am not sure that the last sentence represents, though.
That last sentence is a logistics issue, I want to make sure everyone is on the same tick. In a given iteration, or a tick, say the step from 10.0 seconds to 10.1 seconds, I don't want the controller to be updated to its output at 10.1 seconds, then the target to receive the heat from the controller at 10.1 seconds and the heat from the beam at 10.0 seconds.
But that should come for free from the event ordering. The granularity is 0.1 <simulation> seconds for a tick. All the events for the 10.0 tick are put on the event queue before the events for the 10.1 tick. Since there is a single event queue and it pops in FIFO order and it only processes one event at a time to completion, all of the actions for the 10.0 events will have executed before any of the actions for the 10.1 events.
Even the ordering for multiple events being generated on a single tick can be handled. (Note that eventList in my code example above is also essentially a FIFO queue, so one can ensure the right relative order for actions on the same tick through the order of the Timer.add_event calls.)
So I hold the new controller output in another variable, the target will receive heat from the controller and beam both at 10.0 seconds and the target will record T(10.1) seconds in a second variable, and the thermometer will receive T(10.0) from the target, etc. After every block has been managed, the new output values are transferred so they will be seen in the next iteration.
This seems like a different problem:
+-----------+ +------------+
10.0 heat | T1 | T2 10.0 heat | T3 | T4
---------->| Block1 |------------------->| Block2 |
| | | |
+-----------+ +------------+where you need to calculate new temperatures for each block on tick 10.0 in separate operations. But if you do Block1 first, the T2 computed for 10.0 will not the the right one to use to compute a new T3 for Block2. IOW, when computing 10.0 for Block2 one needs to use 9.9 value for Block1 because that is the value that is there at the computation of 10.0 for Block2. Is this correct?
If so, this sort of lead/lag thing is really a matter of concern for the individual blocks. One needs a data-based lag mechanism so that the computation for Block2 accesses the output of t-1 rather than the output of t. As you suggest, dual variables is probably easiest to do that. The trick is to synchronize.
If you "walk" the heat flows linearly through the blocks using event sequencing, then you can probably just do the migration of values (current [t] -> [t-1]) before the computation of the new [t] value.
************* 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
.
- Follow-Ups:
- Re: Lahman, how ya doing?
- From: Gregory L. Hansen
- Re: Lahman, how ya doing?
- From: Gregory L. Hansen
- Re: Lahman, how ya doing?
- References:
- 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
- Re: Lahman, how ya doing?
- Prev by Date: Re: How to pronouce "use-case"
- Next by Date: Re: Confusion about splitting classes to allow sharing of resources
- Previous by thread: Re: Lahman, how ya doing?
- Next by thread: Re: Lahman, how ya doing?
- Index(es):
Relevant Pages
|