Home  >  User guide  >  Implementations

Home
Screenshots
Examples
Background
Changes
Contact
User guide
Implementations

Defining component behaviors

Component behaviors are defined by including fragments of java source code for the structureScript or itemScript fields in the component definition. Model behaviors can also be defined in a similar way if model-specific programmed logic is needed in a particular model.

In many cases, the same model could be implemented with either a model-level script or a component-level script. A simple way to decide between them is to decide whether you are likely to want to use the same code but with different parameter values elsewhere. If so, it should probably be built into the component. If not, it should probably be built into a model. Another approach is to assess whether the intended function describes something fairly general about a particular type of thing, or whether it is a one-off behavior that just needs coding up without taking the time to develop a reusable declarative parameterization.

Two types of implementation

Catacomb3 supports two classes of component implementation corresponding loosely to cases where the component logic can be self contained, and where it needs access to other components.

Self-contained components

In the self-contained case it is often sufficient to fill out the auto-generated method stubs in the itemScript. Stubs are generate for each of the possible events that can happen to the component and methods are provided for optional outputs. The supplied methods define the behavior of instances of the component completely. The objective here is that most methods should only be a few lines long. There is nothing wrong with including long sections of code within an itemScript, but it may be easier to develop the implementation independently, import it at the top of the script and then just call methods from the external implementation from within the generated method stubs.

The self-contained case is ideal for single components and handles some cases of compound components. The difficulty arises where the bet way to implement a component is different from the best way to describe it. A good example of this is an ion channel. The description naturally takes a form like "a channel has a number of states; there are transitions between the states which can be parameterized on one of these ways...". This leads to an object tree in the specification where the channel object has some properties of its own, and also set of states, and a set of transitions where each transition makes reference to a couple of state. however, when implementing a channel, it is not very efficient to provide an implementation for each channel type and then one for the channel as a whole. It is much better to take the collection of states and transitions and convert them into a single transition matrix which allows updating the state of the channel by simple matrix operations.

[Aside - Interestingly, however it is possible to implement a channel, at least in the stochastic case, by defining a behavior for each state and each transition. A transition might, for example, first look to see if one of its end states is live; if so, compute the probability of transitioning to the other; and use a random number to decide whether to actually change its end states. This is also much closer to what is going on in the underlying biology, much easier to parallelize and much easier to implement in hardware. So there are lots of good reasons for considering whether the self-contained implementation style might be appropriate even if it doesn't much look like it at first. More on this elsewhere.]

Multi-component ("refactored") implementations

The second case can be called "refactored" implementations (as in factorization, or the breaking of a model down into smaller bits in different ways). When setting up a refactored implementation, you an use as much or as little of the internal component instantiation as you like. If you let it go all the way it will give you one java object tree containing parameters with a structure paralleling that of the declarative model and then for each use of the component it will generate another parallel tree of state objects each with a reference back to the corresponding object in the parameter tree. These state objects will contain any code provided in the itemScript. The reason for this is that typically the parameter objects may be big, but the state objects are very small, so the former are not duplicated and instead every state object of a given type shares a reference to the same set of fixed parameters.

In a refactored implementation, you typically want to break into this process at some stage and replace the tree of default state objects with your own smart state object. This can be done by supplying structure scripts that do their own thing as the model is instantiated and then an item script that uses the result of whatever the structure scripts have done in order to get an instance of the new clever state object. The biggest difficulty is avoiding any dependence on the classes generated by Catacomb. That is, in making the dependency only one-way, so the generated classes depend on the refactored implementation but not vice-versa. In particular, this rules out a natural design pattern for a channel which would involve getting the Catacomb object tree form and then iterating over the transitions to build up the compact representation. A good resolution is to use a "Constructor" pattern where the refactored implementation supports incremental definition of complex components along the lines of "add a state to channel X; add another state; add a transition between them with these parameters;" etc. These statements are just what is needed to go in the structureScript for states and transitions. Typically you don't need anything in the itemScripts of the substates since they won't be used. The itemScript of the top component then just calls on the constructor class to spawn a new state instance.

Probably the best way to make sense of the above is from examples such as the KSChannel model. This style of implementation does impose some tight constraints on what type of interface is required for a refactored implementation, but it leaves the internals of the implementation completely free. And the detailed structure of the resulting interface is not Catacomb-specific so it may well prove useful in other contexts (like use from an interpreted scripting environment).

Historical note

There is, in fact, a third form of implementation that has also been developed. This uses special declarative implementation blocks in the components hierarchy that point to external code that is capable of handling a particular type of component. If present, they cause the normal instantiation to be interrupted and instead the parameter tree is handed off to an instance of the new class which must then spawn suitable instances. The weakness of this design is that the implementation code must know about the structure of the parameter object it will receive and must implement certain catacomb methods, which puts it in a rather tightly coupled part of the dependency tree. One benefit is that it can support a variety of different implementations for the same component (as displayed in the run-controller). There may be some mileage in pursuing this benefit at some stage but right now its use is deprecated.