How the Publisher Works
Introduction
GMAT uses a publish-and-subscribe model to make propagation and optimization state data available for plotting, reporting, and output ephemeris files.
There are 3 components of the GMAT publish and subscribe model:
- The data provider: This is the GMAT class that pushes data to the Publisher. Currently, a data provider is either a PropagationEnabledCommand (typically the Propagate command), a SolverBranchCommand, or a Collocate* command, when not in the domain of the Nav code.**
Note: The data push is not restricted to Commands, but the base GmatCommand contains data and methods to facilitate the registration, collection, and push of the registered data to the Publisher. - The Publisher: This singleton component receives data from a registered data provider and pushes it to its known Subscribers.
- The Subscribers: These pieces receive data from the Publisher and output the data to users. Subscribers currently include graphics displays such as XY, OrbitView or GroundTrack plots, as well as Report Files and Ephemeris Files.
We’ll use the Propagate command as an example for this document.
Initialization
In order for the Publisher to know the plots and/or reports for which data are requested, each subscriber must register with the Publisher. This happens prior to initialization of the Data Providers. In GMAT, this step is performed when each Subscriber is cloned into the Sandbox, prior to object initialization. When a Subscriber registers, the Publisher adds it to a list of Subscriber pointers for later use. In other words, the Subscriber list is built for the Publisher before object initialization occurs in the Sandbox, so that the Publisher can pass data to all Subscribers during initialization.
As mentioned above, as part of pre-initialization, data providers register their data with the Publisher. This is done by calling the Publisher method:
Integer RegisterPublishedData(GmatBase *provider, Integer id,
const StringArray *ownerList,
const StringArray &elementList);
The arguments here are:
provider | A pointer to the data provider, supplied using its ‘this’ pointer. |
id | The ID for the data set that is pushed. Set to -1 if the Publisher needs to assign the id. If not -1, it needs to be an ID that the publisher assigned previously. |
ownerList | The names of the object(s) associated with the data. |
elementList | A text description of each element of the provided data. |
The is the Propagate code that sets up and makes this call:
// Prep the publisherStringArray owners, elements;owners.push_back("All");elements.push_back("All.epoch");for (UnsignedInt i = 0; i < propagators.size(); ++i){for (StringArray::iterator scName = satName[i]->begin();scName != satName[i]->end(); ++scName){SpaceObject *so = NULL;for (UnsignedInt i = 0; i < sats.size(); ++i)if (sats[i]->GetName() == (*scName))so = (SpaceObject*)sats[i];if (so == NULL)continue;if (so->GetType() == Gmat::FORMATION)FillFormation(so, owners, elements);else{// Add to the owners (spacecraft names) and elements arraysSetNames(so->GetName(), owners, elements);}}}streamID = publisher->RegisterPublishedData(this, streamID, owners, elements); |
The string “All” used for the first element indicates that the corresponding element (the epoch data) belongs to all identified owners.
The SetNames() method for this example (see PropagationEnabledCommand::SetNames()) pushes data onto the ‘owners’ and ‘elements’ string arrays, so that, for each spacecraft, the owner is set to the spacecraft name six times, and the element array receives the strings X, Y, Z, Vx, Vy, Vz (each with the spacecraft name prefix):
voidPropagationEnabledCommand::SetNames(conststd::string& name,StringArray& owners,StringArray& elements){// Add satellite labelsfor (Integer i = 0; i < 6; ++i)owners.push_back(name);// X, Y, Z, Vx, Vy, Vzelements.push_back(name+".X");elements.push_back(name+".Y");elements.push_back(name+".Z");elements.push_back(name+".Vx");elements.push_back(name+".Vy");elements.push_back(name+".Vz");} |
As an example, if the Propagate command has 2 spacecraft named Sat1 and Sat2, the owners array and elements array StringArrays look like this for the RegisterPublishedData() call:
owners | All | Sat1 | Sat1 | Sat1 | Sat1 | Sat1 | Sat1 | Sat2 | Sat2 | Sat2 | Sat2 | Sat2 | Sat2 |
elements | All.epoch | Sat1.X | Sat1.Y | Sat1.Z | Sat1.Vx | Sat1.Vy | Sat1.Vz | Sat2.X | Sat2.Y | Sat2.Z | Sat2.Vx | Sat2.Vy | Sat2.Vz |
This example is, of course, specific to the Propagate command. Each data provider will need to set up the data it will be providing to the subscribers.
When a provider registers, the Publisher assigns it a provider number. The Publisher then passes the data in the RegisterPublishedData call to the Subscribers, so they can build a map between data providers, objects, and data elements. The return from RegisterPublishedData() is the data provider ID, assigned by the Publisher and used by the provider when it pushes data to the Publisher.
Execution
Initialization tells the Subscribers what type of data they will receive, and in what order, during execution. During script execution, the data providers pass those data to the Publisher, which forwards them to each registered Subscriber. Each Subscriber pulls the data it needs from the forwarding call.
The data provider sends data to the publisher using a call to a Publish() method:
bool Publisher::Publish(GmatBase *provider, Integer id, Real *data,
Integer count, Real propDir = 1.0)
Note: The Publish() method is overloaded in the Publisher, but this is currently the only one used in the code.
The arguments here are:
provider | A pointer to the data provider, supplied using its ‘this’ pointer. |
id | The ID assigned to this data provider during initialization |
data | The data to be published; this data maps directly to the data provided during initialization (the “elements” StringArray in the earlier code). |
count | The size of the provided data array. |
propDir | Indicates the direction time is moving at the call (1.0 = forward, -1.0 = backward, 2.0 = forward, ephems only, -2.0 = backwards, ephems only) |
That call passes in the data as a double precision array. Here’s how the data are set up and passed to the Publisher in our example Propagate command:
pubdata = newReal[dim+1];// Publish the dataif (hasPrecisionTime)pubdata[0] = currEpochGT[0].GetMjd();elsepubdata[0] = currEpoch[0];// Walk the PropSetups to load the pubdata arrayInteger index = 1, size;Real *js;for (UnsignedInt i = 0; i < propagators.size(); ++i){if (p[i]->UsesODEModel()){js = fm[i]->GetJ2KState();size = fm[i]->GetDimension();}else{js = p[i]->GetJ2KState();size = p[i]->GetDimension();}memcpy(&pubdata[index], js, size*sizeof(Real));index += size;}publisher->Publish(this, streamID, pubdata, dim+1, direction); |
As mentioned above, streamID here is the data provider ID assigned during initialization. The code here fills in the dim+1 sized Real array, pubdata, with the epoch followed by the spacecraft state data retrieved from the propagation system. That data map directly to the data provided during initialization (the “elements” StringArray in the earlier code). The direction flag indicates the direction time is moving at the call.
Finalization
As described above, each data provider must register its data, and then publish its data when/where needed.
In addition, at the end of the mission, the data provider must also call the corresponding method to unregister the data. The UnregisterPublishedData method just takes the data provider pointer as input:
void Publisher::UnregisterPublishedData(GmatBase *provider);
For the example here, the Propagate command, the data are registered in its Initialize method, published by several of its methods – PrepareToPropagate, Execute, and TimeFinalStep, and unregistered in the destructor.
* the Collocate command is currently only available to persons with access to the internal plugins.
** there are currently other classes, mostly commands, that also interface with Publisher methods, though they don’t publish data themselves (i.e. they may set a flag, set a target or orbit color, or flush the data buffers)
Appendix A: Adding a Data Provider
A new data provider may need to be added occasionally in order to display data computed in a new class or subsystem. As mentioned above, a data provider does not have to derive from the GmatCommand class. However, the GmatCommand base class does provide data, e.g. object maps, and methods (e.g. FindObject) that are helpful in either registering or publishing the data.
For example, a new Collocate* command was recently added to GMAT. It was determined that the optimization data computed by this command should be displayed to the user, so a data provider needed to be created. The new command itself was selected as the data provider, due to the helpful inherited GmatCommand data and methods noted above. This command is a bit different than the Propagate command in several ways, primarily in that the to-be-published data are computed in a separate stand-alone utility with which GMAT interacts. To get the data to the command for publishing, we needed to provide a new interface class that would call a new Collocate::Publish method at appropriate points in the optimization process. This Publish method would then be able to retrieve the data from the utility and send it to the Publisher.
Other Considerations
Some Subscribers may need other information to handle the published data correctly and provide the data to the user in the requested format or appearance. For example:
- An XYPlot may need to know when to perform a pen-up or a pen-down to ensure that lines are not connected incorrectly (and that the plot is representative of the data and displayed neatly to the user); this could be done by a PenUpSubscribers method, like this one in SolverBranchCommand, which loops over its known subscribers and tells each one to perform a pen-up:
void SolverBranchCommand::PenUpSubscribers()
{
for (UnsignedInt i = 0; i < activeSubscribers.size(); ++i)
activeSubscribers[i]->TakeAction("PenUp");
}
- An OrbitView plot may need to display data differently depending on its user-set solverIterations flag in combination with the run state; for this reason, the data provider may need to pass the current run state to the Publisher.
publisher->SetRunState(currentRunState);
- The data provider may wish to set the color on the plot(s) differently for different segments of published data; this is done with a call to SetSegmentOrbitColor:
publisher->SetSegmentOrbitColor(this, overrideTheColor,
overrideOrbitColor, satNames);
- A data provider may want to cause the plot to be refreshed, particularly at the end of a block of data; this can be done by calling:
publisher->FlushBuffers();