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_PROPERTYmembers, 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-onlyEnumPropertybound directly touprops::Algorithm's protectedm_statusInt, so there is a single source of truth (no duplicated state to keep in sync).PreferredDevice-- editableEnumProperty(Host/Device) bound the same way, for algorithms whose subclasses branch onIsAccelerated()/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, viaConnectTrigger) wakes it, then callsRun()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: aMessageparameter, aTargetinput (anyObject), and a read-onlyResultoutput string set byProcess().src/Core/DescribeAlgorithm.h/.cpp-- takes aTargetinput (anyObject) and writes a human-readable dump of every property registered on it (GetQualifiedName() = GetValueAsString() [GetUnits()], one per line) into a read-onlyDescriptionoutput. Works uniformly on any object, including another Algorithm -- pointing it at an Algorithm describes that Algorithm's ownStatus/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.