User:Barre/Workflow

From KitwarePublic
< User:Barre
Revision as of 01:11, 25 October 2006 by Barre (talk | contribs)
Jump to navigationJump to search

Here is a first stab at a worfklow framework for the EM-Segmentation Wizard project. It's a first pass at a set of classes that are hopefully general enough to be subclassed into something more domain/application specific.

Since a wizard is pretty much a set of steps and some logic/controls to go from one to the other, a "decently robust" framework for this should map, at least, a state machine or a Petri net. I've talked to Luis, and we agreed that even a stab at a simple Petri net engine would not be a trivial task given the time frame. However, we do have some experience in state machines at Kitware, and we can leverage this effort. Such a framework is not going to be as fancy as the one found in IGSTK since we need to remain "wrappable" and we can't use ITK-style C++ templating and other compiler-checks, but this should do the trick and hopefully be high-level enough that people at NA-MIC can use it for more sophisticated workflows. After this section, I introduce a few more classes to constrain this framework to the wizard domain, and make our life a little easier.

  • vtkKWStateMachineState: a state
  • vtkKWStateMachineTransition: a transition between 2 states
  • vtkKWStateMachineInput: an input (which triggers transitions)
  • vtkKWStateMachine: a state machine

Nothing too complicated so far, you have your states, you specificy which transitions are triggered when the machine is in a particuliar state and receive particuliar inputs, etc.

vtkKWStateMachineState: a state

 - [Get] Id: auto-generated unique-id (int).
 - [Set/Get] Name (string): simple name (say, to put in a menu)
 - [Set/Get] Description (string): longer description (say, to display in a help section or bubble)
 - [Set/Get] EnterCommand (callback): (optional) callback fired when we enter that state, whatever the
          transition (this can be used, say, to bring the UI of a the wizard step associated to that state)
 - [Set/Get] LeaveCommand (callback): (optional) callback fired when we leave that state, whatever the
          transition (this can be used, say, to cleanup any resources used by the UI of the wizard step
          associated to that state, maybe a 3D renderwidget preview for example)

vtkKWStateMachineInput: an input

 - [Get] Id: auto-generated unique-id (int).

vtkKWStateMachineTransition: a transition between 2 states

 - [Set/Get] Input (vtkKWStateMachineInput*)
 - [Set/Get] PreviousState (vtkKWStateMachineState*)
 - [Set/Get] NextState (vtkKWStateMachineState*)
 - [Set/Get] Command (callback): (optional) callback fired by the transition
 

vtkKWStateMachine: a state machine

 - [Add] State (vtkKWStateMachineState*): add a state to the state machine
 - [Add] Input (vtkKWStateMachineInput*): add an input to the state machine
 - [Add] Transition (vtkKWStateMachineTransition*): add a transition to the state machine
 - [Add] Transition (vtkKWStateMachineState*, vtkKWStateMachineInput*, vtkKWStateMachineState*, callback):
       convenience method to create a transition and add it to the state machine
 - [Set/Get[ CurrentState (vtkKWStateMachineState*): initial/current state
 - PushInput(vtkKWStateMachineInput*): push a new input in the queue of inputs to be processed
 - ProcessInputs(): perform the state transition and invoke the corresponding action for every 
                    pending input stored in the input queue.

A class that needs a state machine would declare states and inputs as class members, initialize the machine, add the states, add the inputs, then add the transitions (transitions objects don't really need to be created explicitly by the user). This would probably look like the example, for the first few steps of the EMseg project:

  • "specify tree",
  • "specify atlas",
  • "specify target images"

In this specific example, the vtkKWEMSegmentationWorkflow is a class them embed a state machine instance, create states and inputs as member variables, and have states and/or transitions callbacks point to its own methods. This provides a decent encapsulation of the whole wizard behavior inside a single class, but nothing would prevent people from splitting the wizard into several classes and have the callbacks point to different helper objects.

vtkKWEMSegmentationWorkflow.h:

class vtkKWEMSegmentationWorkflow:

  vtkKWStateMachineState *TreeStep;
  vtkKWStateMachineState *AtlasStep;
  vtkKWStateMachineState *TargetImagesStep;

  vtkKWStateMachineInput *InvalidTreeStepInput;
  vtkKWStateMachineInput *GoToTreeStepInput;
  vtkKWStateMachineInput *GoToAtlasStepInput;
  vtkKWStateMachineInput *GoToTargetImagesStepInput;

  vtkKWStateMachine      *StateMachine;

  virtual void ShowTreeStep();
  virtual void HideTreeStep();

  virtual void ShowAtlasStep();
  virtual void HideAtlasStep();

  virtual void ShowTargetImagesStep();
  virtual void HideTargetImagesStep();

  virtual void ReportTreeStepInvalidError();

vtkKWEMSegmentationWorkflow.cxx:

  // Steps. The callbacks as used to bring up or hide the UIs

  this->TreeStep = vtkKWStateMachineState::New();
  this->TreeStep->SetName("Specification of the tree");
  this->TreeStep->SetEnterCommand(this, "ShowTreeStep");
  this->TreeStep->SetLeaveCommand(this, "HideTreeStep");

  this->AtlasStep = vtkKWStateMachineState::New();
  this->AtlasStep->SetName("Specification of the atlas");
  this->AtlasStep->SetEnterCommand(this, "ShowAtlasStep");
  this->AtlasStep->SetLeaveCommand(this, "HideAtlasStep");

  this->TargetImagesStep = vtkKWStateMachineState::New();
  this->TargetImagesStep->SetName("Specification of the target images");
  this->TargetImagesStep->SetEnterCommand(this, "ShowTargetImagesStep");
  this->TargetImagesStep->SetLeaveCommand(this, "HideTargetImagesStep");

  // Inputs

  this->InvalidTreeStepInput = vtkKWStateMachineInput::New();
  this->GoToTreeStepInput = vtkKWStateMachineInput::New();
  this->GoToAtlasStepInput = vtkKWStateMachineInput::New();
  this->GoToTargetImagesStepInput = vtkKWStateMachineInput::New();

  // State Machine

  this->StateMachine = vtkKWStateMachine::New();;

  // Add the states

  this->StateMachine->AddState(this->TreeStep);
  this->StateMachine->AddState(this->AtlasStep);
  this->StateMachine->AddState(this->TargetImagesStep);

  // Add the inputs

  this->StateMachine->AddInput(this->InvalidTreeStepInput);
  this->StateMachine->AddInput(this->GoToTreeStepInput);
  this->StateMachine->AddInput(this->GoToAtlasStepInput);
  this->StateMachine->AddInput(this->GoToTargetImagesStepInput);

  // Add the transitions

  // Error control: loop over the 'Tree' step each time it is found
  // to be invalid, and fire a method that could be used to display an
  // error message.
  // Say, pressing the "Next" button in a wizard from the 'Tree' step would 
  // first validate this step, and if it fails push the 'InvalidTreeStepInput'
  // input to the state machine; the transition would invoke the
  // 'ReportTreeStepInvalidError' method on 'this' object)

  this->StateMachine->AddTransition(this->TreeStep,
                                    this->InvalidTreeStepInput,
                                    this->TreeStep,
                                    this, "ReportTreeStepInvalidError")

  // Go to the next step (no callback needed on the transition)
  // Say, pressing the "Next" button in a wizard from the 'Tree' step would 
  // first validate this step, and if it succeeds push the 'GoToAtlasStepInput'
  // input to the state machine, leading us to the 'Atlas' step.
                                    
  this->StateMachine->AddTransition(this->TreeStep,
                                    this->GoToAtlasStepInput,
                                    this->AtlasStep)

  // Go back the previous step (no callback needed on the transition)
  // Say, pressing the "Back" button in a wizard from the 'Atlas' step would 
  // push the GoToTreeStepInput' input to the state machine, leading us back to
  // the 'Tree' step.

  this->StateMachine->AddTransition(this->AtlasStep,
                                    this->GoToTreeStepInput,
                                    this->TreeStep)

  // Skip a step (no callback needed for the transition)
  // Say, pressing the "Finish" button in a wizard from the 'Tree' step (or
  // selecting a step directly from a pulldown menu listing all steps) would 
  // first validate this step, and if it succeeds push the GoToAtlasStepInput 
  // input to the state machine, leading us to the 'Target Images' step
  // (it is considered to be the last one in this example). Now of course
  // more logic is required here, for if you skip steps, you probably
  // want to estimate good default values for the steps in between, etc)

  this->StateMachine->AddTransition(this->TreeStep,
                                    this->GoToTargetImagesStepInput,
                                    this->TargetImagesStep)

How can we make things a little easier for the specific case of a simple Wizard. Here are a couple of thoughts:

  • a wizard is a most of the time a linear succession of steps (this will be our major assumption so far),
  • you can navigate from one step to the next one other using a 'Next' and 'Back' button,
  • you can navigate from one step to the previoous one using a 'Back' button,
  • a 'Finish' may allow you to skip to the last step,
  • a menu or a list of steps may allow you to go to a specific step explicitly,
  • one need to make sure a step is valid before going to the next one,
  • one probably do *not* need to make sure a step is valid before going to the previous one,
  • one probably want to be able to guess a reasonable set of defaults values for a given step (ideally, values that make that step already 'valid'),
  • one could be interested in knowing how much of a step is 'completed' to make it 'valid' (this could be used in the UI to display step names as 'green' or 'red' and locate visually which steps require to be completed in priority).

Let's introduce the following subclasses:

vtkKWWizardStep: a wizard step, subclass of vtkKWStateMachineState

 - [Set/Get] ValidateCommand (callback): callback that will validate the step and push the 
                                         corresponding error inputs if needed (returns int)
 - [Set/Get] EstimateDefaultsCommand (callback): callback that will attempt to estimate reasonable 
                                                 defaults value for this step
 - [Set/Get] EstimateCompletionCommand (callback): callback that can be used to estimate the completion
                                                   of that step (returns normalized double)
 - [Get] GoToSelfInput (vtkKWStateMachineInput*): member variable that acts as an input specifying 
                                                  "I want to go to that instance step"

vtkKWWizardWorkflow: a wizard workflow, subclass of vtkKWStateMachine

 - AddNavigationTransitions(): assuming the states/steps were added as instance of vtkKWWizardStep, in 
                               the right order, create transitions from one step to the next and previous
                               one (if any), and from one step to any other step, using each step's
                               vtkKWWizardStep::GoToSelfInput member variable as input.
 - vtkKWStateMachineState* GetNextStep(): given the current step, return the next one (assuming they 
                                          were added in order and as instance of vtkKWWizardStep)
 - vtkKWStateMachineState* GetPreviousStep(): given the current step, return the previous one (assuming 
                                              they were added in order and as instance of vtkKWWizardStep)
 - GoToNextStep(): given the current step 'current_step':
   invoke 'current_step->ValidateCommand on the current step/state to check if we can leave the step 
   (i.e. its contents is valid), or if an error should be reported (its contents is not valid):
   - if ValidateCommand fails (returns o), do nothing, as it is up to the ValidateCommand
     to push error inputs that will in turn trigger transitions that can display error messgages and lead back
     to the same 'current_step' for example.
   - if ValidateCommand succeeds (returns 1), get the next step 'next_step' using GetNextStep(), push a 
     next_step->GoToSelfInput to the state machine to go to 'next_step'.
 - GoToPreviousStep(): given the current step 'current_step':
   - get the previous step 'prev_step' using GetPreviousStep(), push a prev_step->GoToSelfInput to the
      state machine to go to 'prev_step'
 - GoToStep(vtkKWWizardStep *step): given the current step 'current_step'
   'while' we have not reached 'step', validate current step, 
     - if it fails break the loop, 
     - if it succeeds get the next step, invoke EstimateDefaultsCommand and
       push a next_step->GoToSelfInput
   the loop will try to go as far as it can, as long as the step could be
   validated with the defaults that were estimated.    
 - GoToLastStep(): self-explanatory
 - GetCurrentStep(): convenience method to get the current step as a safe-downcast instance of a vtkKWWizardStep

Note that the "linearity" of transition between the states is from a "step" point of view, you can add more states to the machine as instances of vtkKWStateMachineState, as long as only instances of vtkKWWizardStep are used to specificy a linear "path" among the states. This is a constraint/assumption.

From a UI point of view, we probably also need something that can tell us *if* we can go to a given step, so that we can disable or grey out the corresponding "Next"/"Back" button, I'll leave that for another time.

Here is the new code.

vtkKWEMSegmentationWorkflow.h:

class vtkKWEMSegmentationWorkflow:

  vtkKWWizardStep *TreeStep;
  vtkKWWizardStep *AtlasStep;
  vtkKWWizardStep *TargetImagesStep;

  vtkKWStateMachineInput *InvalidTreeStepInput;

  vtkKWWizardWorkflow      *WizardWorkflow;

  virtual void ShowTreeStep();
  virtual void HideTreeStep();
  virtual int  ValidateTreeStep();
  virtual void EstimateTreeStepDefaults();

  virtual void ShowAtlasStep();
  virtual void HideAtlasStep();
  virtual int  ValidateAtlasStep();
  virtual void EstimateAtlasStepDefaults();

  virtual void ShowTargetImagesStep();
  virtual void HideTargetImagesStep();
  virtual int  ValidateTargetImagesStep();
  virtual void EstimateTargetImagesStepDefaults();

  virtual void ReportTreeStepInvalidError();

vtkKWEMSegmentationWorkflow.cxx:

  this->TreeStep = vtkKWWizardStep::New();
  this->TreeStep->SetName("Specification of the tree");
  this->TreeStep->SetEnterCommand(this, "ShowTreeStep");
  this->TreeStep->SetLeaveCommand(this, "HideTreeStep");
  this->TreeStep->SetValidateCommand(this, "ValidateTreeStep");
  this->TreeStep->SetEstimateDefaultsCommand(this, "EstimateTreeStepDefaults");

  this->AtlasStep = vtkKWWizardStep::New();
  this->AtlasStep->SetName("Specification of the atlas");
  this->AtlasStep->SetEnterCommand(this, "ShowAtlasStep");
  this->AtlasStep->SetLeaveCommand(this, "HideAtlasStep");
  this->AtlasStep->SetValidateCommand(this, "ValidateAtlasStep");
  this->AtlasStep->SetEstimateDefaultsCommand(this, "EstimateAtlasStepDefaults");

  this->TargetImagesStep = vtkKWWizardStep::New();
  this->TargetImagesStep->SetName("Specification of the target images");
  this->TargetImagesStep->SetEnterCommand(this, "ShowTargetImagesStep");
  this->TargetImagesStep->SetLeaveCommand(this, "HideTargetImagesStep");
  this->TargetImagesStep->SetValidateCommand(this, "ValidateTargetImagesStep");
  this->TargetImagesStep->SetEstimateDefaultsCommand(this, "EstimateTargetImagesStepDefaults");

  // Inputs

  this->InvalidTreeStepInput = vtkKWStateMachineInput::New();

  // State Machine

  this->WizardWorkflow = vtkKWWizardWorkflow::New();;

  // Add the states

  this->WizardWorkflow->AddState(this->TreeStep);
  this->WizardWorkflow->AddState(this->AtlasStep);
  this->WizardWorkflow->AddState(this->TargetImagesStep);

  // Add the inputs

  this->WizardWorkflow->AddInput(this->InvalidTreeStepInput);

  // Add the transitions

  this->WizardWorkflow->AddNavigationTransitions();

  // Error control: loop over the 'Tree' step each time it is found
  // to be invalid, and fire a method that could be used to display an
  // error message.
  // Say, pressing the "Next" button in a wizard from the 'Tree' step would 
  // first validate this step, and if it fails push the 'InvalidTreeStepInput'
  // input to the state machine; the transition would invoke the
  // 'ReportTreeStepInvalidError' method on 'this' object)

  this->WizardWorkflow->AddTransition(this->TreeStep,
                                    this->InvalidTreeStepInput,
                                    this->TreeStep,
                                    this, "ReportTreeStepInvalidError")

Now at this point this vtkKWEMSegmentationWorkflow class could be a subclass of a vtkKWWizardWidget that presents some UI real-estate and "Next" / "Back" / "Finish" buttons at the bottom of the page associated to the GoToNextStep, GoToPreviousStep and GoToLastStep methods of the workflow instance...

Note that this code:

 this->TreeStep = vtkKWWizardStep::New();
 this->TreeStep->SetEnterCommand(this, "ShowTreeStep");
 this->TreeStep->SetEstimateDefaultsCommand(this, "EstimateTreeStepDefaults");

...means that when the 'Tree' step/state is reached (whatever the transition), the ShowTreeStep callback is triggered, which we have set to:

 virtual void ShowTreeStep();

In this ShowTreeStep callback, you could reuse the validation callback by doing something like:

 this->WizardWorkflow->GetCurrentStep()->InvokeEstimateDefaultsCommand();

which would invoke:

 virtual void EstimateTreeStepDefaults();