CIBC:Project:module development

From SCIRun Documentation Wiki
Jump to navigation Jump to search

Module Writing Tutorial (SCIRun 3.x and earlier only)

In your working copy of source code exists a few example modules designed to gradually introduce you to the structures and mechanisms used in all module writing.

modules in SCIRun are specified by Package::Category::ModuleName

Packages

Packages are the SCIRun plugin mechanism, and each Package gets its own menu item on the Network Editor. Core modules in SCIRun live in the SCIRun Package. In your working tree, the SCIRun Package corresponds to everything under the SCIRun/src/Dataflow/Modules directory. All other Packages live in SCIRun/src/Packages.

Categories

Categories are the directories under Modules. These are also reflected in the menu item, and should contain modules that share some common ground defined by the category.

Modules

Finally under each Category directory are the Modules contained within them. Each module has a .cc file with the same name as the module.


Examples

To help the novice module writer understand the basics of creating new modules, there is the Examples category. In it are actual modules that can execute.

"Hello World!" -- SCIRun style.

SCIRun::Examples::PrintHelloWorldToScreen

Files:


This is the most basic of modules. Each module must have an xml file that describes it. This xml file gets parsed at startup, and lets SCIRun know it is available. SCIRun then builds menu items based on available modules.

class PrintHelloWorldToScreen : public Module {
public:
  PrintHelloWorldToScreen(GuiContext*);

  virtual ~PrintHelloWorldToScreen();

  virtual void execute();
};

Our module inherits from Module base class, and provides an execute function. Note that it is a class like any other, and that everytime the module executes, the execute method will be called by the scheduler. Each module runs in its own thread, so be careful of static members.

DECLARE_MAKER(PrintHelloWorldToScreen)

The DECLARE_MAKER macro simply creates a maker function that creates an instance of this module. It gets called when you add an instance of the module to your Network Editor. This macro keeps the maker functions uniform and is a convenience for the module writer.

// Print Hello World! when we execute.
void
PrintHelloWorldToScreen::execute()
{
  std::cerr << "Hello World!" << std::endl;
}

Here we have the definition of our execute method. This is where interesting things happen -- usually. For now we just print "Hello World!" to the shell you ran from.

Pretty simple so far, but you now are familiar with a few of the files and how you get your code to execute. Your next question will be how do I get some data into this execute method...

Getting Input

SCIRun::Examples::GetInputField

Files:


This example lives in the source tree as GetInputField, but if you are writing a module as we go, you could simply add the new bits as you go to your module.

Now that we know how to execute, we would like to get some data passed into this module through its input port. First we need to let SCIRun know that we expect input and what type of input we expect. This happens in the XML file.

  <io>
    <inputs lastportdynamic="no">
      <port>
	<name>InField</name>
	<datatype>SCIRun::Field</datatype>
      </port>
    </inputs>
  </io>

The io section of our component contains port information. We give it a name (important to remember as we ask for this port by name from our C code) and we also give it a datatype. In this case we want to and only will accept SCIRun::Field as an input. Now when we instantiate this module, it will be created with a Field input port.

// Print the value of the input fields data pointer when we execute.
void
GetInputField::execute()
{
  FieldHandle field_handle;
  if (! get_input_handle("InField", field_handle, true)) {
    error("GetInputField must have a SCIRun::Field as input to continue.");
    return;
  }
  cerr << "GetInputField module got data :" << field_handle.get_rep() << endl;
}

All data that pass through ports in SCIRun are passed using Handles. Our handles are reference counted locking handles. They are essentially smart pointers that delete the object they point to when no one has a reference to it anymore. First we declare an empty FieldHandle, then we ask for it to be filled with data from the input port named "InField". As you recall this is the exact string used in the xml file to declare the port name.

get_input_handle does a number of things for you. If the last argument is true, then we require input data on this port (some ports are optional). get_input_handle then blocks waiting for data to arrive on the port, makes sure it is non NULL, and sets the handle appropriately.

get_rep() is a method on the handle class itself. It returns the value of the underlying pointer. handles overload the -> operator, so that any calls made that way work as if the handle were an actual pointer to the object type.

Using get_rep() we print out the value of the pointer to the shell, verifying that we indeed got some valid data.

Sending Output

Files:

In this example we simply add an output port, and send the same data we got as input along through the output port.

  <io>
    <inputs lastportdynamic="no">
      <port>
	<name>InField</name>
	<datatype>SCIRun::Field</datatype>
      </port>
    </inputs>
    <outputs>
      <port>
	<name>OutField</name>
	<datatype>SCIRun::Field</datatype>
      </port>
    </outputs>
  </io>

We add the output port and name it, very similar to adding the input port.

void
GetInputFieldAndSendAsOutput::execute()
{
  FieldHandle field_handle;
  if (! get_input_handle("InField", field_handle, true)) {
    error("GetInputFieldAndSendAsOutput must have a SCIRun::Field as input to continue.");
    return;
  }
  cerr << "GetInputFieldAndSendAsOutput module got data :" << field_handle.get_rep() << endl;


  send_output_handle("OutField", field_handle);
}

send_output_handle takes the exact string we declared the output port with in our xml file, and we send the handle to our field along through the output port.


Now we know how to get data and send data, lets do something with this Field.

Changing Data Values

Files:

In this example we get our first look at interfacing with a Field. The Field base class doesn't say much about what a field really is. Fields in SCIRun are designed somewhat like STL classes. There is a Field Concept, Mesh Concept, and Basis Concept. These guarantee certain interface to eachother such that they can be combined in a GenericField<> class via template arguments. This gives SCIRun great flexibility in Field creation. It also poses some logistical problems.

For now we will assume the input to this module is a specific fully instantiated type. Later we will show how to deal with the vast array of possible types that 'could' come in through this Field port.

  typedef TetVolMesh<TetLinearLgn<Point> >    TVMesh;
  typedef TetLinearLgn<double>                DataBasis;
  typedef GenericField<TVMesh, DataBasis, vector<double> > TVField;  

This set of typedefs we create mostly for readablity. This is a Tetrahedral mesh with double data, with both data and mesh having a linear basis.

  // Must detach since we will be altering the input field.
  field_handle.detach();

The detach call here is required for valid Dataflow. We fully intend on altering the data in this Field. The detach call on the handle tells the handle to clone the memory and become the only reference to that new memory. We have no idea how many other modules may get the same input handle, so to modify it underneath them is a no no.

  TVField *in = dynamic_cast<TVField*>(field_handle.get_rep());

  if (in == 0) {
    error("This Module only accepts Linear TetVol Fields with double data.");
    return;
  }

Here we assure that the type of field we get is actually the one specific type we can handle. If we don't have input of this exact type, we call error with a message. This turns the status box on the module icon to red, and when you click it and open it, you will see the message. Then we return, since we do not know how to work on anything but that specific type.

  TVField::mesh_handle_type mh = in->get_typed_mesh();
  TVMesh::Node::iterator iter;
  TVMesh::Node::iterator end;
  mh->begin(iter);
  mh->end(end);

  while (iter != end) {
    TVMesh::Node::index_type ni = *iter;
    Point node;
    mh->get_center(node, ni);
    TVField::value_type val;
    in->value(val, ni);
    cerr << "at point: " << node << "the input value is: " << val << endl;

    // Set the value to be 0.0;
    in->set_value(0.0, ni);
    ++iter;
  }

Yay some meat. We have a valid pointer, so we can now iterate over all the nodes in this mesh. For example purposes, we print out the x,y,z point of each node, as well as the value stored at the node. We can do this because we know we have a linear basis, so we are guaranteed to have at least one data value at each node. After we print out what we got as input, we go ahead and set each node value to 0.0 as the name of our module suggests. The data is then sent downstream as before.

User Input

Files:

Setting values to 0.0 may not be what we always want to do. Lets suppose the user would like to decide what value to set from a dialog. This example introduces a new file type. A tcl file, that follows the module ui convention, added to the sub.mk makefile fragment, will now trigger a UI button to show up on your instantiated module icon. If it does not, you forgot to add it to the makefile, or you mistyped the name of the class which follows a Package_Category_ModuleName convention.

    method ui {} {
        set w .ui[modname]
        if {[winfo exists $w]} {
            return
        }
        toplevel $w

        frame $w.f
	pack $w.f -padx 2 -pady 2 -side top -expand yes
	
	frame $w.f.options
	pack $w.f.options -side top -expand yes

        iwidgets::entryfield $w.f.options.format -labeltext "New Value:" \
	    -textvariable $this-newval
        pack $w.f.options.format -side top -expand yes -fill x

	makeSciButtonPanel $w.f $w $this
	moveToCursor $w

	pack $w.f -expand 1 -fill x
    }

The ui method in the .tcl file is what gets called when you click on the UI button. In this case we have an entryfield whose input is bound to a variable $this-newval which is important when we hook this up on the 'C' side.

class SetTetVolFieldDataValues : public Module 
{
public:
  SetTetVolFieldDataValues(GuiContext*);
  virtual ~SetTetVolFieldDataValues();

  virtual void execute();
private:
  GuiDouble       newval_;
};

Added to our class declaration is a GuiDouble. When we initialize this at construction time, we marry it to the tcl side variable of the specified name. We will be interpreting the input value as a double.

SetTetVolFieldDataValues::SetTetVolFieldDataValues(GuiContext* ctx) : 
  Module("SetTetVolFieldDataValues", ctx, Source, "Examples", "SCIRun"),
  newval_(get_ctx()->subVar("newval"), 1.0)
{
}

Using the GuiContext object, we marry our GuiDouble to the tcl variable newval. $this- on the tcl side scopes our variable under our unique module id. The string we pass to subVar must match exactly the string after $this- on the tcl side. The second argument is the initializer.

  while (iter != end) {
    TVMesh::Node::index_type ni = *iter;
    Point node;
    mh->get_center(node, ni);
    TVField::value_type val;
    in->value(val, ni);
    cerr << "at point: " << node << " the input value is: " << val << endl;

    // Set the value to be the value from the gui;
    in->set_value(newval_.get(), ni);
    ++iter;
  }

Now in our loop, we ask for the value from the UI entry box that is coupled to our GuiDouble newval_. We call .get() on the GuiVar expecting a double since we know it is a GuiDouble. This value is now used to set the field variable value at each node.

Generic Algorithms

Files:

This example shows how we can generically handle any and all Fields as input. We have augmented the RTTI from C++ such that we can compile algorithms at run time that are exactly typed on the input field. The algorithms use the Concepts involved to interface with the objects.

We introduce a new file, a .h file to contain the algorithm to be compiled.

class SetFieldDataValuesAlgoBase : public DynamicAlgoBase
{
public:
  virtual FieldHandle execute(ProgressReporter *, FieldHandle,
			      double) = 0;

  //! support the dynamically compiled algorithm concept
  static CompileInfoHandle get_compile_info(const TypeDescription *td);
};

template <class Fld>
class SetFieldDataValuesT : public SetFieldDataValuesAlgoBase
{
public:
  //! virtual interface.
  virtual FieldHandle execute(ProgressReporter *, 
			      FieldHandle,
			      double);
};

The base class inherits from DynamicAlgoBase. It is this type we will get a handle to in our module execute. The virtual method execute is our entry into our dynamically compiled algorithm. We call it execute by convention but you can call it whatever you wish. This is a class like any other, have as many methods, or data as you wish in the class.

The second declaration is the templated version which inherits from our base class and provides the overloaded virtual. It has one template parameter, in this case which we expect to be a Field.


template <class Fld>
FieldHandle
SetFieldDataValuesT<Fld>::execute(ProgressReporter *reporter, 
			     FieldHandle ifh, 
			     double newval)
{

  //Must detach since we will be altering the input field.
  ifh.detach();
  
  Fld* in = dynamic_cast<Fld*>(ifh.get_rep());
  if (! in) {
    cerr << "Input field type does not match algorithm paramter type." 
	 << endl;
    return 0;
  }

  typedef typename Fld::mesh_type Msh;

  typename Fld::mesh_handle_type mh = in->get_typed_mesh();
  typename Msh::Node::iterator iter;
  typename Msh::Node::iterator end;

  mh->synchronize(Mesh::NODES_E);

  mh->begin(iter);
  mh->end(end);
  while (iter != end) {
    typename Msh::Node::index_type ni = *iter;
    Point node;
    mh->get_center(node, ni);
    typename Fld::value_type val;
    in->value(val, ni);
    cerr << "at point: " << node << " the input value is: " << val << endl;

    // Set the value to be the value from the gui;
    in->set_value(newval, ni);
    ++iter;
  }

  return ifh;

}

This should look pretty familiar at this point, except that no where do we know the exact type of the field. We pass in the new value we want to set as a method parameter, but otherwise it is roughly the same code as before.

Now we want to instantiate a specific instance of this algorithm based upon the input field type, so lets go to the .cc file.

  const TypeDescription *ftd = field_handle->get_type_description();
  CompileInfoHandle ci = SetFieldDataValuesAlgoBase::get_compile_info(ftd);
  Handle<SetFieldDataValuesAlgoBase> algo;
  if (!DynamicCompilation::compile(ci, algo, this)) return;

  FieldHandle out_field_handle(algo->execute(this, field_handle, newval_.get()));

Here we use our augmented RTTI to get all the strings required to write out a .cc file with the algorithm instantiation in it. We do this with get_type_decription() on the input field.

When we call compile we get our handle algo filled up. A .cc instantiation file is created, compiled into a small library, loaded into the system, and instantiated. It is then set in the handle algo. If you have compile errors the function will fail, and you will see the errors from your status box.

Finally we call our newly compiled algorithm, which has been instantiated for the input field type.


CompileInfoHandle
SetFieldDataValuesAlgoBase::get_compile_info(const TypeDescription *td) 
{

  // use cc_to_h if this is in the .cc file, otherwise just __FILE__
  static const string include_path(TypeDescription::cc_to_h(__FILE__));
  static const string template_class_name("SetFieldDataValuesT");
  static const string base_class_name("SetFieldDataValuesAlgoBase");


  CompileInfo *rval = scinew CompileInfo(template_class_name + "." +
					 td->get_filename() + ".",
					 base_class_name, 
					 template_class_name, 
					 td->get_name());
  rval->add_include(include_path);
  td->fill_compile_info(rval);
  return rval;
}

get_compile_info we provide in this file as well. It sets up some more strings, that let us know where files are etc. and fills up the CompileInfo object with all the strings needed to write an instantiation file.