FhSim  3.1.0
Marine systems simulation
Loading...
Searching...
No Matches
Common computations

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.

class CLinearSpring : public SimObject
{
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;
ISignalPort* m_pInPosA;
ISignalPort* m_pInPosB;
mutable double m_adOutForceA[3];
mutable double m_adOutForceB[3];
ICommonComputation* m_CalcOutputs;
};
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.

CLinearSpring::CLinearSpring(std::string sSimObjectName, ISimObjectCreator* pCreator)
: SimObject(sSimObjectName)
{
pCreator->AddInport("PosA", 3, &m_pInPosA);
pCreator->AddInport("PosB", 3, &m_pInPosB);
pCreator->RegisterCommonCalculation(COMMON_COMPUTATION_FUNCTION(CLinearSpring::CalcOutput), &m_CalcOutputs);
pCreator->AddOutport("ForceA", 3, PORT_FUNCTION(CLinearSpring::ForceA));
pCreator->AddOutport("ForceB", 3, PORT_FUNCTION(CLinearSpring::ForceB));
}
CLinearSpring(std::string sSimObjectName, ISimObjectCreator *pCreator)
The constructor sets the pointer to the output object and the parser object.

3. Write the computation

void CLinearSpring::CalcOutput(const double dT, const double* const adX) const
{
const double* adPosA = m_pInPosA->GetPortValue(dT, adX);
const double* adPosB = m_pInPosB->GetPortValue(dT, adX);
// ... fill m_adOutForceA and m_adOutForceB ...
}

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.

const double* CLinearSpring::ForceA(const double dT, const double* const adX)
{
m_CalcOutputs->ComputeFunction(dT, adX);
return m_adOutForceA;
}
const double* CLinearSpring::ForceB(const double dT, const double* const adX)
{
m_CalcOutputs->ComputeFunction(dT, adX);
return m_adOutForceB;
}

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.