Add A New Recorder
A Recorder in the interpreted OpenSees applications is used to obtain information from the model during the analysis. To add a new Recorder option into the interpreted applications, the developer must provide a new C++ subclass of the Recorder class and an interface function that will be used to parse the input and create the new recorder.
Recorder Class
The Recorder class itself is an abstract base class. It inherits from both the TaggedObject class and the MovableObject class. The class has a minimal interface, which is as shown below:
The Recorder Class:
class Recorder: public MovableObject, public TaggedObject
{
public:
Recorder(int classTag);
virtual ~Recorder();
virtual int record(int commitTag, double timeStamp) =0;
virtual int restart(void);
virtual int domainChanged(void);
virtual int setDomain(Domain &theDomain);
virtual int sendSelf(int commitTag, Channel &theChannel);
virtual int recvSelf(int commitTag, Channel &theChannel,
FEM_ObjectBroker &theBroker);
virtual void Print(OPS_Stream &s, int flag);
protected:
protected:
private:
static int lastRecorderTag;
};
The most important methods in the interface are:
- setDomain() - this is the method that is called when the new recorder object is first added to the domain. It is inside this method that all data, typically memory and pointer values, need to be initialized for subsequent record commands.
- record() - this is the method that is called when the recorder is called upon to record/save information. The method is called with a tag that will be unique and the current time in the domain.
Other Important methods are:
- domainChanged() - this is a method called when something major has happened in the Domain, ie. a new element, node, constraint and/or load pattern has been added to the domain or removed from the domain. It is necessasry for the Recorder to check in this call if it's pointers are still valid (i.e. if an element it was recording info for has been removed from the domain, it wuill have been deleted and it's old pointer information will no longer be valid.)
- send/recvSelf() - are two methods called in parallel applications. When invoked the recorders send/recv information about what they are recording.
- restart() - this method is called if restart() is invoked on the Domain. What the recorder does is up to you the developer.
Example - SumElementForcesRecorder
In the following section we will provide all necessary code to add a new recorder. The purpose of this recorder will be to sum the forces obtained from the list of inputted elements. The recorder will use the getResistingForce() method in the elements to obtain these forces. A similar class exists in the framework, which uses the setResponse()/getResponse() methods in the element interface. To demonstrate some of the output file options, the result will go to either the screen, a text file, or a binary file. More output options are of course available and the developer should look at existing recorder options.
Header
The header for thew new class, which we will call SumElementForcesRecorder is as follows:
#ifndef SumElementForcesRecorder_h
#define SumElementForcesRecorder_h
#include <Recorder.h>
#include <Information.h>
#include <ID.h>
class Domain;
class Vector;
class Matrix;
class Element;
class Response;
class FE_Datastore;
class SumElementForcesRecorder: public Recorder
{
public:
// constructors
SumElementForcesRecorder();
SumElementForcesRecorder(const ID eleID,
bool echoTime,
OPS_Stream *theOutputHandler);
// destructors
~SumElementForcesRecorder();
// public methods
int record(int commitTag, double timeStamp);
int restart(void);
int domainChanged(void);
int setDomain(Domain &theDomain);
const char *getClassType(void) const;
int sendSelf(int commitTag, Channel &theChannel);
int recvSelf(int commitTag, Channel &theChannel, FEM_ObjectBroker &theBroker);
protected:
private:
int numEle; // the number of elements
Element **theElements;// pointer to array of element pointers
ID eleID; // ID (integer list) of element tags to record
Domain *theDomain; // pointer to domain holding elements
OPS_Stream *theOutput;// pointer to output location
bool echoTimeFlag; // flag indicating if pseudo time to be printed
Vector *data; // Vector (double array) to store sum of element forces
};
#endif
The header file defines the interface and variables for the class SumElementForceRecorder. It defines the new class to be a sublass of the Recorder class. In the public interface are 2 constructors and 1 destructor in addition to all the methods defined for the Recorder class. There are no protected data or methods as we do not expect this class to be subclassed. In the private section we store data that will be used by the SumElementForceRecorder objects. The header has a number of #include directives, one is needed for the base class and every class used as a variable in the list of data (except those that are used as pointers). For those classes that only appear as pointers in the header file (Domain, Vector, Element, OPS_Stream) a forward declaration is all that is needed (the include could also have been used, but using the forward declaration simplifies dependencies and reduces the amount of code that ha to be recompiled later if changes are made).
Implementation
It another file, SumElementForcesRecorder.cpp, we place the code that details what the constructors, destructor and methods do. In addition we provide one additional procedure OPS_SumElementForcesRecorder() (NOTE it has the same name as the class with an OPS_ prefix). We will go through each part of the file.
Include Directives
The first part of the file contains the list of includes. It is necessary to have an #include directive for each class and api file that is used within the .cpp file and is not included in the header.
#include "SumElementForcesRecorder.h"
#include <elementAPI.h>
#include <Domain.h>
#include <Element.h>
#include <ElementIter.h>
#include <Matrix.h>
#include <Vector.h>
#include <ID.h>
#include <string.h>
#include <Response.h>
#include <Message.h>
#include <Channel.h>
#include <FEM_ObjectBroker.h>
#include <StandardStream.h>
#include <BinaryFileStream.h>
#include <DataFileStream.h>
#include <elementAPI.h>
Constructors
After the list of includes, we provide the 2 constructors. The constructors are rather simple. They just initialize all the data variables defined in the header. Note it is very important to set all pointer values to 0.
SumElementForcesRecorder::SumElementForcesRecorder()
:Recorder(-1),
numEle(0), theElements(0), eleID(0),
theDomain(0), theOutput(0),
echoTimeFlag(true), data(0)
{
}
SumElementForcesRecorder::SumElementForcesRecorder(const ID ele,
bool echoTime,
OPS_Stream *theoutput)
:Recorder(-1),
numEle(0), theElements(0), eleID(ele),
theDomain(0), theOutput(theoutput),
echoTimeFlag(echoTime), data(0)
{
// set numEle
numEle = eleID.Size();
if (numEle == 0) {
opserr << "WARNING SumElementForcesRecorder::SumElementForcesRecorder() - no elements tags passed in input!\n";
}
}
Destructor
The we provide the destructor. In the destructor all memory that the Recorder created or was passed to it in the constructor must be destroyed. Failing to delete this memory, will result in memory leaks.
SumElementForcesRecorder::~SumElementForcesRecorder()
{
if (theElements != 0)
delete [] theElements;
if (data != 0)
delete data;
if (theOutput != 0)
delete theOutput;
}
record() Method
After the destructor, we provide the code for the record() method. It does the following operations:
- Zeros the vector which will contain the final sum
- If the time stamp is needed, it places it at the first location in the vector.
- Loops over all valid elements adding their resting force to the vector.
- Send the vector to the output handler to be written.
- Returns success.
int
SumElementForcesRecorder::record(int commitTag, double timeStamp)
{
// check for initialization
if (data == 0) {
opserr << "SumElementForcesRecorder::record() - setDomain() has not been called\n";
return -1;
}
// zero the data vector
data->Zero();
int forceSize = data->Size();
int startLoc = 0;
// write the time if echTimeFlag set
if (echoTimeFlag == true) {
(*data)(0) = timeStamp;
forceSize -= 1;
startLoc = 1;
}
//
// for each element that has been added to theElements add force contribution
//
for (int i=0; i< numEle; i++) {
if (theElements[i] != 0) {
int loc = startLoc;
const Vector &force = theElements[i]->getResistingForce();
int forceSize = force.Size();
for (int j=0; j<forceSize; j++, loc++)
(*data)(loc) += force(j);
}
}
//
// send the response vector to the output handler for o/p
//
if (theOutput != 0)
theOutput->write(*data);
// succesfull completion - return 0
return 0;
}
restart() and domainChanged() methods
Afte the record() method, we have the two simple short methods restart() and domainChanged(). restart does nothing and domainChanged simply calls the objects own setDomain() method.
int
SumElementForcesRecorder::restart(void)
{
return 0;
}
int
SumElementForcesRecorder::domainChanged(void)
{
if (theDomain != 0)
this->setDomain(*theDomain);
}
setDomain() Method
The setDomain() method follows. In this method we perform the following:
- set the pointer for the enclosing domain object.
- allocate space from memoory for our array of ponters and our data vector.
- initialize the array components to be 0 or point to an element given by the eleID.
- determine the size of the vector that will be used to store the sum of the forces.
- allocate space for the vector.
int
SumElementForcesRecorder::setDomain(Domain &theDom)
{
theDomain = &theDom;
// set numEle
if (numEle == 0) {
opserr << "WARNING SumElementForcesRecorder::initialize() - no elements tags passed in input!\n";
return 0;
}
// create theElements, an array of pointers to elements
theElements = new Element *[numEle];
if (theElements == 0) {
opserr << "WARNING SumElementForcesRecorder::initialize() - out of memory\n";
numEle = 0; // set numEle = 0, in case record() still called
return -1;
}
//
// loop over the list of elements,
// if element exists add it's pointer o the array
// get its resisting force, check size to determine compatable with others
//
int sizeArray = -1;
for (int i=0; i<numEle; i++) {
int eleTag = eleID(i);
Element *theEle = theDomain->getElement(eleTag);
if (theEle != 0) {
const Vector &force = theEle->getResistingForce();
int forceSize = force.Size();
if (sizeArray == -1) {
sizeArray = forceSize;
theElements[i] = theEle;
} else if (sizeArray != forceSize) {
opserr << "WARNING: forces mismatch - element: " << eleTag << " will not be included\n";
theElements[i] = 0;
} else {
theElements[i] = theEle;
}
} else {
theElements[i] = 0;
}
}
// if echTimeFlag is set, add room for the time to be output
if (echoTimeFlag == true)
sizeArray++;
// create the vector to hold the data
data = new Vector(sizeArray);
if (data == 0 || data->Size() != sizeArray) {
opserr << "SumElementForcesRecorder::initialize() - out of memory\n";
delete [] theElements;
theElements = 0;
numEle = 0;
}
return 0;
}
sendSelf() and recvSelf() methods
These methods only need be provided if the object will be used in a parallel program. We provide their implementation for completeness, though typicall developers are interested in running the code in a sequential application and should just return -1.
static char myClassType[] = {"SumElementForcesRecorder"};
const char *
SumElementForcesRecorder::getClassType(void) const
{
return myClassType;
}
int
SumElementForcesRecorder::sendSelf(int commitTag, Channel &theChannel)
{
// send in an ID (integar array) to the receiving object the following:
// recorder tag
// size of eleID
// class tag of handler
// echoTimeFlag
static ID idData(5);
idData(0) = this->getTag();;
idData(1) = eleID.Size();
idData(2) = theOutput->getClassTag();
if (echoTimeFlag == true)
idData(3) = 1;
else
idData(3) = 0;
if (theChannel.sendID(0, commitTag, idData) < 0) {
opserr << "SumElementForcesRecorder::recvSelf() - failed to recv idData\n";
return -1;
}
// send eleID to receiving object
if (theChannel.sendID(0, commitTag, eleID) < 0) {
opserr << "SumElementForcesRecorder::sendSelf() - failed to send idData\n";
return -1;
}
// send theOutput to receiving object
if (theOutput->sendSelf(commitTag, theChannel) < 0) {
opserr << "SumElementForcesRecorder::sendSelf() - failed to send theOutput\n";
return -1;
}
return 0;
}
int
SumElementForcesRecorder::recvSelf(int commitTag, Channel &theChannel, FEM_ObjectBroker &theBroker)
{
// receive from the sending object the ID
static ID idData(5);
if (theChannel.recvID(0, commitTag, idData) < 0) {
opserr << "SumElementForcesRecorder::recvSelf() - failed to recv idData\n";
return -1;
}
// with the data received
// setTag
// resize the eleID array
// set echoTimeFlag
// get an outputHandler
this->setTag(idData(0));
eleID.resize(idData(1));
idData(2) = theOutput->getClassTag();
if (idData(3) == 0)
echoTimeFlag = true;
else
echoTimeFlag = false;
if (theOutput != 0 && theOutput->getClassTag() != idData(4))
delete theOutput;
theOutput = theBroker.getPtrNewStream(idData(4));
if (theOutput == 0) {
opserr << "SumElementForcesRecorder::recvSelf() - failed to get Output of correct type\n";
return -1;
}
// receive eleID
if (theChannel.recvID(0, commitTag, eleID) < 0) {
opserr << "SumElementForcesRecorder::recvSelf() - failed to recv eleID\n";
return -1;
}
// get theOutput to receive data
if (theOutput->recvSelf(commitTag, theChannel, theBroker) < 0) {
opserr << "SumElementForcesRecorder::sendSelf() - failed to send theOutput\n";
return -1;
}
return 0;
}
Interface Function
At the end of the implementation file is the interface function. This function is required by all new classes. It is a function which will use the api to
- parse the input
- based on the input create objects
- create a recorder object of the correct type, and return it to the calling function.
The interface function is the function that is called when the interpreter comes across the command telling it to create a SumElementForcesRecorder.
#ifdef _USRDLL
#include <windows.h>
#define OPS_Export extern "C" _declspec(dllexport)
#elif _MACOSX
#define OPS_Export extern "C" __attribute__((visibility("default")))
#else
#define OPS_Export extern "C"
#endif
static int numSumElementForcesREcorder = 0;
OPS_Export void *
OPS_SumElementForcesRecorder()
{
Recorder *theRecorder = 0;
int numRemainingArgs = OPS_GetNumRemainingInputArgs();
// check for quick return, possibly parallel case
if (numRemainingArgs == 0) {
Recorder *theRecorder = new SumElementForcesRecorder();
}
//
// parse args
//
int numEle = 0, eleTag;
ID eleID(0);
OPS_Stream *theOutputStream = 0;
int outMode = 0; // standard stream
bool echoTime = false;
bool doneParsingArgs = false;
char data[100];
char outputName[200];
char **eleArgs = 0;
int numEleArgs = 0;
while (numRemainingArgs > 0) {
if (OPS_GetString(data,100) < 0)
return 0;
// output to standard file
if (strcmp(data,"-file") == 0) {
outMode = 1;
if (OPS_GetString(outputName,200) < 0)
return 0;
numRemainingArgs -= 2;
}
// output to binary file
else if (strcmp(data,"-binary") == 0) {
outMode = 2;
if (OPS_GetString(outputName,200) < 0)
return 0;
numRemainingArgs -= 2;
}
// echo domain time stamp in output
else if (strcmp(data,"-time") == 0) {
echoTime = true;
numRemainingArgs -= 1;
}
// read the list of elements & place in an ID
else if ((strcmp(data,"-ele") == 0) ||
(strcmp(data,"-eles") == 0) ||
(strcmp(data,"-element") == 0)) {
numRemainingArgs --;
int one = 1;
while (numRemainingArgs > 0 && OPS_GetIntInput(&one, &eleTag) == 0) {
eleID[numEle] = eleTag;
numEle++;
numRemainingArgs--;
}
doneParsingArgs = true;
}
//
// create the output handler
//
if (outMode == 0)
theOutputStream = new StandardStream();
if (outMode == 1)
theOutputStream = new DataFileStream(outputName);
else if (outMode == 2)
theOutputStream = new BinaryFileStream(outputName);
//
// create the recorder
//
theRecorder = new SumElementForcesRecorder(eleID,
echoTime,
theOutputStream);
// return it
return theRecorder;
}
Example Script
(OpenSeesDeveloper/recorder/example1.tcl)
An example OpenSees tcl input file for this new recorder is:
#create the model
model basic -ndm 2 -ndf 2
node 1 0.0 0.0
node 2 144.0 0.0
node 3 168.0 0.0
node 4 72.0 96.0
uniaxialMaterial Elastic 1 3000
element truss 1 1 4 10.0 1
element truss 2 2 4 5.0 1
element truss 3 3 4 5.0 1
fix 1 1 1
fix 2 1 1
fix 3 1 1
pattern Plain 1 Linear {
# apply the load - command: load nodeID xForce yForce
load 4 100 -50
}
# Create the analysis
system ProfileSPD
constraints Plain
integrator LoadControl 1.0
algorithm Linear
numberer RCM
analysis Static
recorder Element -file a.out -time -ele 1 2 3 forces
recorder SumElementForcesRecorder -file b.out -time -ele 1 2 3
# perform the analysis
analyze 10
Example Output
The output shows that the model is in equilibrium, and that at node 4 the node the element resisting forces are equal to the applied forces.
1 -100 50 100 -50
2 -200 100 200 -100
3 -300 150 300 -150
4 -400 200 400 -200
5 -500 250 500 -250
6 -600 300 600 -300
7 -700 350 700 -350
8 -800 400 800 -400
9 -900 450 900 -450
10 -1000 500 1000 -500