SmartCGMS developer zone

Discrete model

A discrete model interface is an extension of a filter interface. The SmartCGMS provides signal generator filter, which instantiates selected discrete model and manages its lifetime.

Discrete model is an entity, which has a state and is able to change this state in incremental, discrete steps. Unlike a signal model, a discrete model preserves its state, which renders it unable to retrieve past signal values. A typical use of discrete model is to implement a compartment model - it has a state (current compartment quantities) and performs discrete steps (usually as a single step of an ordinary differential equation solver).

Descriptor of a discrete model contains scgms::NModel_Flags::Discrete_Model in the flags field. The model is instantiated using the do_create_discrete_model call.

Interface

Every discrete model entity must implement both scgms::IFilter and scgms::IDiscrete_Model interface (as the former is a parent of the latter). The former is described in a filter subpage. The latter is defined as an abstract C++ class as follows:

class IDiscrete_Model : public virtual scgms::IFilter {
public:
	virtual HRESULT IfaceCalling Initialize(const double current_time, const uint64_t segment_id) = 0;
	virtual HRESULT IfaceCalling Step(const double time_advance_delta) = 0;
};

The scgms::IFilter method contracts remains the same - both are called upon the calls to the parent filter (i.e.; when the signal generator filter interface method is called, the filter calls the same method of the discrete model).

The Initialize is called by outer code typically at the start of a time segment. Its sole purpose is to initialize the state of the discrete model at a given time. The model should store the timestamp, as any further stepping is performed in relative increments. This method must be called exactly once, before any Step method call. Any further calls results in E_ILLEGAL_STATE_CHANGE return code. Otherwise, this method returns S_OK.

The Step method is called for two reasons. The first is to advance the model internal state by a fixed, discrete amount of time, when the time_advance_delta parameter is a positive number. The second is to emit current state, when the time_advance_delta equals zero. This parameter must never be negative, othwerise the call returns with E_INVALIDARG error code. This method should also never be called prior the Initialize call. Doing so will result in an E_ILLEGAL_METHOD_CALL error code.

Operation

The discrete model extends the filter state model :

  • Created - the discrete model just got instantiated and waits for the Configure call.
  • Configured - the discrete model is initialized and waits for the Initialize method call.
  • Operational - the discrete model is configured and properly initialized. It is able to process device events in Execute method and peform steps in Step method. The Step method is called exclusively in this state.
  • Terminated - the discrete model received the Shut_Down device event code, deallocated all its resources and waits for its deallocation by outer code. Any further Configure, Execute, Initialize and Step call on this discrete model instance is invalid.
The figure below illustrates the discrete model lifecycle.

Discrete model states

The behaviour is similar to a filter entity, with the exception of initialization step.

One important thing to note is, that when the discrete model is encapsulated by a filter (preferably a signal generator filter), the Execute method call is chained through the discrete model, as depicted in figure below.

Discrete model execute method call

Example implementation

At first, we usually create a header defines for our model to describe the purpose and basic idea. As an example, let us define a model, that generates a signal in a "saw" form (increasing until a certain point, in which it resets to a base value). Please note, that this is a very simple example of such a model and merely describes the interface and operation of discrete models.

The structures for a model usually falls to a descriptor file header:

namespace example_discrete_model {

    constexpr GUID model_id = { 0x1ac12918, 0x9a9c, 0x144f, { 0xaf, 0x12, 0x45, 0x8c, 0xf1, 0x53, 0x57, 0xe5 } };

    constexpr GUID saw_signal_id = { 0x9a7dd21a, 0x1744, 0xfe6a, { 0x89, 0x90, 0xa4, 0x91, 0x3b, 0xa, 0x11, 0x21 } };

    const size_t param_count = 3;

    // we represent model parameters by named fields and also as a vector
    struct TParameters {
        union {
            struct {
                double base, step_per_minute, threshold;
            };
            double vector[param_count];
        };
    };

    const TParameters lower_bounds = { { 0.0, 0.05, 5.0 } };
    const TParameters default_parameters = { { 5.0, 0.1, 10.0 } };
    const TParameters upper_bounds = { { 10.0, 0.5, 20.0 } };
}

Now, we can define the discrete model class (using the SmartCGMS SDK):

class CExample_Discrete_Model : public scgms::CBase_Filter, public scgms::IDiscrete_Model {

    private:
        example_discrete_model::TParameters mParameters;

    protected:
        uint64_t mSegment_Id = scgms::Invalid_Segment_Id;
        double mCurrent_Time = 0;
        double mCurrent_Value = 0;

    protected:
        // scgms::CBase_Filter iface implementation
        virtual HRESULT Do_Execute(scgms::UDevice_Event event) override final {

            // just pass the event further
            return mOutput.Send(event);
        }
        virtual HRESULT Do_Configure(scgms::SFilter_Configuration configuration, refcnt::Swstr_list& error_description) override final {

            mCurrent_Value = mParameters.base;

            return S_OK;
        }

    public:
        CExample_Discrete_Model(scgms::IModel_Parameter_Vector *parameters, scgms::IFilter *output);
        virtual ~CExample_Discrete_Model() = default;

        // scgms::IDiscrete_Model iface
        virtual HRESULT IfaceCalling Initialize(const double current_time, const uint64_t segment_id) override final {
            
            if (mSegment_Id != scgms::Invalid_Segment_Id) {
                return E_ILLEGAL_STATE_CHANGE;
            }

            mSegment_Id = segment_id;
            mCurrent_Time = current_time;

            return S_OK;
        }

        virtual HRESULT IfaceCalling Step(const double time_advance_delta) override final {
            
            if (mSegment_Id == scgms::Invalid_Segment_Id) {
                return E_ILLEGAL_METHOD_CALL;
            }

            if (time_advance_delta < 0.0) {
                return E_INVALIDARG;
            }

            if (time_advance_delta > 0.0) {

                mCurrent_Time += time_advance_delta;
                mCurrent_Value += (time_advance_delta / scgms::One_Minute) * mParameters.step_per_minute;

                if (mCurrent_Value > mParameters.threshold) {
                    mCurrent_Value = mParameters.base;
                }
            }

            scgms::UDevice_Event evt{ scgms::NDevice_Event_Code::Level };

            evt.device_id() = example_discrete_model::model_id;
            evt.device_time() = mCurrent_Time;
            evt.level() = mCurrent_Value;
            evt.signal_id() = example_discrete_model::saw_signal_id;
            evt.segment_id() = mSegment_Id;

            return mOutput.Send(evt);
        }
};

Then, we define a descriptor block:

// we will sure use the following headers
#include <utils/descriptor_utils.h>
#include <iface/DeviceIface.h>
#include <lang/dstrings.h>
#include <rtl/manufactory.h>

namespace example_discrete_model {

    const wchar_t *model_param_ui_names[model_param_count] = {
        L"Saw base value",
        L"Increment per minute",
        L"Upper threshold"
    };

    const scgms::NModel_Parameter_Value model_param_types[model_param_count] = {
        scgms::NModel_Parameter_Value::mptDouble,
        scgms::NModel_Parameter_Value::mptDouble,
        scgms::NModel_Parameter_Value::mptDouble
    };

    constexpr size_t number_of_calculated_signals = 1;

    const GUID calculated_signal_ids[number_of_calculated_signals] = {
        saw_signal_id
    };

    const wchar_t* calculated_signal_names[number_of_calculated_signals] = {
        L"Saw signal"
    };

    const GUID reference_signal_ids[number_of_calculated_signals] = {
        Invalid_GUID
    };

    scgms::TModel_Descriptor desc = {
        model_id,
        scgms::NModel_Flags::Discrete_Model,
        L"Example discrete model (saw)",
        nullptr,
        model_param_count,
        0,
        model_param_types,
        model_param_ui_names,
        nullptr,
        lower_bounds.vector,
        default_parameters.vector,
        upper_bounds.vector,

        number_of_calculated_signals,
        calculated_signal_ids,		
        reference_signal_ids,
    };

    // for more info, see signal subpage
    const scgms::TSignal_Descriptor saw_signal_desc = {
        saw_signal_id,
        L"Saw signal",
        L"",
        scgms::NSignal_Unit::Unitless,
        0xFFFF0000,
        0xFFFF0000,
        scgms::NSignal_Visualization::smooth,
        scgms::NSignal_Mark::none,
        nullptr
    };
}

The block above contains a definition of signal (its descriptor). For more details about the descriptor contents, see signal subpage.

Then we have to define and export do_get_model_descriptors and do_get_signal_descriptors functions:

const std::array model_descriptors = { { example_discrete_model::desc } };

const std::array signal_descriptors = { { example_discrete_model::saw_signal_desc } };

HRESULT IfaceCalling do_get_model_descriptors(scgms::TModel_Descriptor **begin, scgms::TModel_Descriptor **end) {
    return do_get_descriptors(model_descriptors, begin, end);
}

HRESULT IfaceCalling do_get_signal_descriptors(scgms::TSignal_Descriptor **begin, scgms::TSignal_Descriptor **end) {
    return do_get_descriptors(signal_descriptors, begin, end);
}

The last step is to implement the factory method do_create_discrete_model:

HRESULT IfaceCalling do_create_discrete_model(const GUID *model_id, scgms::IModel_Parameter_Vector *parameters, scgms::IFilter *output, scgms::IDiscrete_Model **model) {
    if (*model_id == example_discrete_model::model_id) {
        return Manufacture_Object(model, parameters, output);
    }

    return E_NOTIMPL;
}