Skip to content

Algorithm: Process, Ports, and Scheduling

Overview

uprops::Algorithm (src/Core/uprops/Algorithm.h) is a zero-dependency mixin for a class that does work in a Process() step, reports its progress as a small state machine (Idle -> Running -> Finished/Failed), and can declare a device preference for callers that support offloading. uLib::Algorithm (src/Core/Algorithm.h) is its Object-facing bridge, following the same pattern as uLib::Property<T>: a zero-dependency uprops piece plus a thin Core-layer bridge that multiply-inherits Object and the uprops piece to gain signals, tree participation, and ObjectsContext visibility.

Because uLib::Algorithm is a uLib::Object, gcompose sees it exactly like any other domain object -- no dedicated GUI code was written for it:

  • Registering a concrete Algorithm with ObjectFactory (e.g. ULIB_REGISTER_OBJECT_CATEGORY(MyAlgorithm, "Algorithms")) makes it instantiable from gcompose's "Add" menu, journaled, named and added to the scene through the same generic path every other object type uses.
  • Its parameters are ordinary Property<T>/ULIB_PROPERTY members, so they show up as editable fields in the Properties panel.
  • Its inputs are ReferenceProperty<T> members (ULIB_ALGORITHM_INPUT), which render as the existing combo-box "pick another object from the live context" widget (ReferencePropertyWidget) -- the connection field the Properties panel already had, just put to use for algorithm wiring.
graph TD
    subgraph uprops ["uprops (zero-dependency)"]
        Alg["uprops::Algorithm\n(Status, DevicePreference, Process/Run)"]
    end

    subgraph Core ["mutomCore"]
        Object["uLib::Object\n(PropertyContainer, signals, tree)"]
        Bridge["uLib::Algorithm : Object, uprops::Algorithm\n(Status/PreferredDevice properties,\nStarted/Finished/Failed signals)"]
        Task["uLib::AlgorithmTask : Thread\n(Cyclic / Async scheduling)"]
    end

    subgraph GUI ["app/gcompose (unchanged)"]
        Add["Add menu (ObjectFactory category)"]
        Tree["Context tree"]
        Props["Properties panel\n(ReferencePropertyWidget = Input combo)"]
    end

    Object --> Bridge
    Alg --> Bridge
    Bridge --> Add
    Bridge --> Tree
    Bridge --> Props
    Task -- drives --> Bridge

uprops::Algorithm

class Algorithm {
public:
    enum class Status { Idle, Running, Finished, Failed };
    enum class DevicePreference { Host, Device };

    virtual void Process() = 0;
    void Run(); // Status transitions + exception containment around Process()

    Status GetStatus() const;
    const std::string& GetLastError() const;
    DevicePreference GetPreferredDevice() const;
    void SetPreferredDevice(DevicePreference device);

    boost::signals2::connection SubscribeStatusChanged(const StatusSlot& slot);

protected:
    virtual void OnStatusChanged(Status status) {} // override point, see below
};

Process() takes no arguments: inputs and outputs travel through whatever properties the concrete subclass declares, not through a value handed to Process(). That is what lets one non-template AlgorithmTask drive any Algorithm, and what lets chained algorithms wire up through the Properties panel instead of through code.

Run() wraps Process(): it transitions Status to Running, calls Process(), and transitions to Finished or (if Process() throws) Failed with GetLastError() populated. Exceptions are contained, not rethrown.

A bridging subclass that wants to react to status transitions overrides the protected OnStatusChanged(Status) virtual rather than subscribing a lambda in its constructor via SubscribeStatusChanged: a stored subscription is not replayed when the object is copy-constructed (Object::Clone()/Duplicate() copy-construct), while a virtual override keeps working correctly on a copy. This mirrors how PropertyBase itself handles the same problem for its own boost::signals2 change signal.

uLib::Algorithm

class Algorithm : public Object, public uprops::Algorithm {
public:
    // Process() inherited pure virtual; concrete subclasses implement it
    // and use uLibObjectMacro for Clone().
signals:
    virtual void Started();
    virtual void Finished();
    virtual void Failed();
};

The constructor registers two properties (visible in the Properties panel under group "Algorithm"):

  • Status -- read-only EnumProperty bound directly to uprops::Algorithm's protected m_statusInt, so there is a single source of truth (no duplicated state to keep in sync).
  • PreferredDevice -- editable EnumProperty (Host/Device) bound the same way, for algorithms whose subclasses branch on IsAccelerated()/GetPreferredDevice() to dispatch to an offloaded implementation.

OnStatusChanged is overridden to fire the Started/Finished/Failed uLib signals and call Object::Updated(), so the Properties panel and the context tree refresh live when an Algorithm runs.

Inputs and outputs as connection fields

#define ULIB_ALGORITHM_INPUT(Type, name)   // SmartPointer<Type> m_##name +
                                            // ReferenceProperty<Type> name##Input
                                            // (group "Inputs", PortDirection::Input)

#define ULIB_ALGORITHM_OUTPUT(Type, name)  // same shape, group "Outputs",
                                            // PortDirection::Output

These declare a ReferenceProperty<Type> -- the same mechanism LogicalVolume already uses for its Material/Solid references (src/HEP/Geant/Solid.h) -- tagged with uprops::PortDirection so a future node-graph view can distinguish ports from plain structural references. PropertyEditor::setObject already special-cases any ReferencePropertyBase* as a combo box populated from the live ObjectsContext (app/gcompose/src/PropertyWidgets.cpp), so an Algorithm's Target/Input/ etc. field is pickable in the Properties panel with zero GUI changes.

Chaining pattern

An Algorithm that produces a new result object owns it and publishes it by adding it to its own child context:

void MyAlgorithm::Process() {
    m_Result = SmartPointer<MyData>(new MyData());
    // ... fill it in ...
    this->GetChildren()->AddObject(m_Result);
}

Object::GetChildren() auto-vivifies a private ObjectsContext for any object, and ReferencePropertyWidget already walks every context object's GetChildren()->GetObjects() recursively when building its combo box, so the published result becomes pickable by any other Algorithm's ULIB_ALGORITHM_INPUT field automatically. Algorithms that mutate their input in place (filter-style) do not need ULIB_ALGORITHM_OUTPUT at all -- downstream consumers just reference the same upstream input object again.

DuplicateObject's existing reference-relinking pass already re-points Input/Output fields that reference another node inside the duplicated subtree, since they are ReferencePropertyBase underneath like any other reference property.

AlgorithmTask

class AlgorithmTask : public Thread {
public:
    enum Mode { Cyclic, Async };
    void SetAlgorithm(Algorithm* alg);
    void SetMode(Mode mode);
    void SetCycleTime(int milliseconds); // Cyclic mode
    void Start();  // resets stop/trigger state, then starts the thread
    void Stop();
    void Notify(); // Async mode: trigger one iteration
    template <typename Func1>
    Connection ConnectTrigger(Sender* sender, Func1 signal); // Async: trigger from a uLib signal
signals:
    virtual void Stopped();
};

Drives any uLib::Algorithm* on a background thread, calling Run() (not Process() directly) so Status/signals stay correct. One non-template class suffices for every Algorithm subclass because inputs/outputs live on the Algorithm's own properties rather than being passed into Run().

  • Cyclic mode calls Run() on a fixed period.
  • Async mode blocks until Notify() (or a connected signal, via ConnectTrigger) wakes it, then calls Run() once per trigger.

gcompose integration

Running an Algorithm from the GUI reuses the existing Play-button precedent (app/gcompose/src/MainPanel.cpp, already used for Geant Scene/Recipe runs): selecting an Algorithm object enables the button; clicking it calls Run() synchronously and reports success/failure through the console. No other gcompose code needed to change -- ContextPanel, ContextModel, PropertiesPanel and PropertyWidgets all work unmodified because an Algorithm is, structurally, just another uLib::Object.

Examples

Both are registered under category "Algorithms" and are reasonable templates to copy when writing a real one.

  • src/Core/EchoAlgorithm.h/.cpp -- a minimal concrete Algorithm: a Message parameter, a Target input (any Object), and a read-only Result output string set by Process().
  • src/Core/DescribeAlgorithm.h/.cpp -- takes a Target input (any Object) and writes a human-readable dump of every property registered on it (GetQualifiedName() = GetValueAsString() [GetUnits()], one per line) into a read-only Description output. Works uniformly on any object, including another Algorithm -- pointing it at an Algorithm describes that Algorithm's own Status/PreferredDevice/input/output properties the same way it would any domain object's, which is what makes it a good second demonstration: it reads an input's whole property list generically rather than a single stored value.