Overview
When several output ports of a SimObject — or its OdeFcn() and its ports — need the result of the same expensive calculation at the same (T, X), a common computation lets you compute it once and reuse the result.
A common computation is a const member function of your SimObject that you register in the constructor. The engine wraps it in an ICommonComputation and hands the wrapper back to you through an out-parameter. Every consumer inside the SimObject calls ComputeFunction() on that wrapper before reading the cached members; the wrapper runs the member function on the first call for a given model evaluation and does nothing on the following calls.
When to use
Use a common computation when:
- Two or more port functions (or a port function and
OdeFcn()) of the same SimObject need the same derived quantity.
- The computation is expensive relative to the port functions that consume it.
- The result depends on time
T and/or the state vector X. Values that do not depend on (T, X) belong in the constructor or in FinalSetup(); values only one consumer needs need no wrapper at all.
Interface
Two declarations matter. The callback type and its casting macro live in <fhsim/simobject/PortDefs.h>:
typedef void (SimObject::*CommonComputation)(const double time, const double* const stateVec) const;
#define COMMON_COMPUTATION_FUNCTION(functionPointer) static_cast<CommonComputation>(&functionPointer)
The registration function lives on ISimObjectCreator:
virtual void RegisterCommonCalculation(CommonComputation CalculationFunction,
ICommonComputation** computationAddress) = 0;
computationAddress is the address of an ICommonComputation* member of your SimObject. The engine fills that pointer while it assembles the model, so it is valid from FinalSetup() onwards — not inside the constructor.
The wrapper you get back implements <fhsim/simobject/ICommonComputation.h>:
class ICommonComputation
{
public:
virtual void ComputeFunction(const double T, const double* const X) const = 0;
virtual ~ICommonComputation() { }
};
Step-by-step usage
1. Declare the computation and its storage
The computation function is const, so every member it writes must be mutable.
{
public:
CLinearSpring(std::string sSimObjectName, ISimObjectCreator* pCreator);
const double*
ForceA(
const double dT,
const double*
const adX);
const double*
ForceB(
const double dT,
const double*
const adX);
protected:
void CalcOutput(
const double dT,
const double*
const adX)
const;
};
A linear spring in 3 degrees of freedom.
Definition CLinearSpring.h:25
const double * ForceA(const double dT, const double *const adX)
Calculates the end force A.
double m_adOutForceB[3]
Force vector on point B.
Definition CLinearSpring.h:61
ICommonComputation * m_CalcOutputs
Common computation for outputs.
Definition CLinearSpring.h:63
ISignalPort * m_pInPosB
Input for position B.
Definition CLinearSpring.h:59
const double * ForceB(const double dT, const double *const adX)
Calculates the end force B.
double m_adOutForceA[3]
Force vector on point A.
Definition CLinearSpring.h:60
ISignalPort * m_pInPosA
Input for position A.
Definition CLinearSpring.h:58
void CalcOutput(const double dT, const double *const adX) const
< Sets the parameters of the spring.
2. Register in the constructor
Registration is only accepted from the SimObject constructor; the engine reports an error if it is called later.
: SimObject(sSimObjectName)
{
pCreator->AddInport("PosA", 3, &m_pInPosA);
pCreator->AddInport("PosB", 3, &m_pInPosB);
}
CLinearSpring(std::string sSimObjectName, ISimObjectCreator *pCreator)
The constructor sets the pointer to the output object and the parser object.
3. Write the computation
{
const double* adPosA =
m_pInPosA->GetPortValue(dT, adX);
const double* adPosB =
m_pInPosB->GetPortValue(dT, adX);
}
4. Call it from every consumer
Each consumer calls ComputeFunction() first and then reads the cached members. The call is cheap once the value is already cached, so it must not be skipped: whichever consumer runs first is the one that triggers the computation.
The full source of this example is in CLinearSpring.h and CLinearSpring.cpp.
Caching and invalidation
The wrapper is a lazy cache, not a pre-pass:
- The engine does not call
ComputeFunction() on your behalf. Nothing runs until a consumer asks for it.
- The engine drops the cached flag together with the output-port caches at the start of every model evaluation —
OdeFcn(), PreOdeFcn() and, with visualization, RenderUpdate(). The next ComputeFunction() call therefore recomputes at the new (T, X).
- Because invalidation is tied to the model evaluation and not to the accepted step, a multi-stage method recomputes once per stage evaluation, and an implicit method recomputes for every trial state the solver tries.
- Warning
- The wrapper keys nothing on
T or X: it only remembers whether it has run since the last reset. Never call ComputeFunction() with a (T, X) other than the one your port function or OdeFcn() was handed, and never cache the result across calls yourself.
- Note
- When the model runs with a thread manager, the engine substitutes a mutex-protected wrapper, so the computation still runs exactly once per evaluation even when several SimObject threads reach it at the same time. This is transparent to your code.