The Subscriber subsystem schedules side effects during dynamics propagation. A concrete Subscriber writes selected simulation state when an attached CudaIntegrator reaches the subscriber's reporting interval. The interval is a positive, dimensionless number of propagated steps.
Use DcdSubscriber for coordinate trajectories and RestartSubscriber for restart state. Those two reporters are available through C++, the C ABI, and Python.
Subscribers are synchronous observers, not background tasks. An update runs on the propagation caller's thread after the corresponding dynamics step and may perform force evaluation, CUDA transfers, device-wide synchronization, and file I/O before propagation continues.
The Python example below writes two DCD frames while propagating four steps. It uses stable repository test data and unsubscribes before the context manager closes the DCD handle.
The equivalent direct C++ workflow uses shared ownership because native subscription obtains a shared self-reference from the integrator.
A concrete file-writing subscriber validates its positive report frequency, copies its std::filesystem::path, checks the nonempty parent component with std::filesystem::exists(), and creates or truncates the output during construction. DcdSubscriber opens binary output, while RestartSubscriber uses text output.
Establish state in this order:
Native CudaIntegrator::subscribe() requires the integrator itself to be owned by std::shared_ptr. It calls Subscriber::setCharmmContext() with the integrator's current context, then Subscriber::setIntegrator(), then appends the subscriber and a copy of its current frequency to parallel arrays. Set the integrator context first. Subscribing while that context is null can succeed, and a later context setter does not update the already attached subscriber.
The scheduling interval is a snapshot. Changing a subscriber's native frequency after subscription changes the property read by some file metadata, but it does not change the integrator's cached callback interval. Each separate propagate(num_steps) call numbers steps locally from one. A subscriber with frequency 100 is therefore called at local steps 100, 200, and so on in every call, rather than from an absolute lifetime-step modulus.
DcdSubscriber requires a live binary stream, a context with a positive atom count, exactly three positive box lengths, and a valid single-precision coordinate/charge device container. RestartSubscriber additionally requires the subscriber and integrator to retain the same context and supports only CudaNoseHooverIntegrator, CudaLangevinPistonIntegrator, and CudaLangevinThermostatIntegrator.
At the native layer, an integrator retains every subscribed object through std::shared_ptr<Subscriber>. The subscriber retains the context and integrator through shared pointers. This produces the following strong ownership graph:
CudaIntegrator::unsubscribe() erases the integrator-to-subscriber edge, but it does not clear the subscriber's context or integrator pointers. Unsubscribe before releasing final owners. The retained backlinks also mean that the same native subscriber cannot currently be resubscribed through the normal attachment path.
The public C ABI owns concrete handles such as apo_dcd_subscriber and apo_restart_subscriber. Each concrete handle embeds an apo_subscriber base view. Conversion returns a borrowed pointer into that concrete allocation; it is not a separately owned handle and has no destroy function. Destroying the concrete C handle invalidates the base pointer even when a native integrator continues to retain the underlying C++ object. Keep the concrete handle alive until after C ABI unsubscription.
Python DcdSubscriber and RestartSubscriber wrappers own their concrete C handles and store the borrowed base pointer. A Python CudaIntegrator appends a successfully subscribed wrapper to its _subscribers list, preserving the C handle and base-view lifetime. unsubscribe() removes that retained Python reference after native removal. Closing a subscriber clears its borrowed base view, so close only after unsubscription.
The subsystem performs no internal host-thread locking. Serialize propagation, updates, frequency changes, attachment, unsubscription, stream operations, and destruction involving shared objects.
The reporting interval and frame counters are dimensionless step counts. Integrator constructor time steps and public getTimeStep() values use picoseconds.
DcdSubscriber writes:
double unit-cell record per frame, with X, Y, and Z box lengths in elements 0, 2, and 5;float per coordinate component in atom order;The current DCD implementation writes native integer and floating-point representations and does not perform byte-order conversion.
RestartSubscriber writes a version-50 CHARMM-style text restart. Verified quantities include:
| Section or value | Shape and order | Unit |
|---|---|---|
| Box lengths | Three diagonal lengths in X, Y, Z order | Angstrom |
XOLD, YOLD, ZOLD | One double-precision XYZ row per atom | Angstrom |
VX, VY, VZ | One double-precision velocity row per atom | Angstrom per AKMA time unit |
X, Y, Z | One previous coordinate-delta XYZ row per atom | Stored integrator representation |
| Average temperature | One scalar | Kelvin |
| Atom, step, degree-of-freedom, and seed fields | Scalar integer fields | Dimensionless |
Restart Nose-Hoover and Langevin-piston state is serialized directly from the integrator. The current repository does not establish every one of those field-specific units clearly enough to make them a stable subscriber contract.
C++ constructors and base mutators use ApoCharmmError. Empty paths, nonexistent checked parent paths, and nonpositive frequencies use ApoCharmmErrorCode::InvalidArgument. Missing required update state uses ApoCharmmErrorCode::NotInitialized. Unsupported restart-integrator types use ApoCharmmErrorCode::NotImplemented. File open/write and state-consistency failures use ApoCharmmErrorCode::Runtime. CUDA transfers, kernels, and synchronization failures use ApoCharmmErrorCode::Cuda.
C ABI status functions clear the calling thread's previous diagnostic at entry. Native error categories map to the matching APO_STATUS_* value. Unexpected standard or nonstandard exceptions map to APO_STATUS_RUNTIME_ERROR. On failure, copy apo_last_error immediately on the same thread; its pointer is borrowed and changes after the next diagnostic-changing call on that thread. Concrete destroy functions are void, accept NULL, and do not normally clear a previous diagnostic.
Python configures each status-returning function with a shared error callback. Every nonzero status raises ApoCharmmError containing the numeric status, symbolic status name, operation context, and copied native diagnostic. Python also raises TypeError for wrapper/type or ctypes conversion failures, ValueError for values outside a signed 32-bit C int, and RuntimeError for closed wrappers or impossible successful-null-handle results.
A propagation failure is not transactional at any interface. The integrator increments its total requested-step count before the loop. Completed dynamics steps, changed host/device state, and file bytes written before a later callback failure remain observable.
cudaDeviceSynchronize() on the current device. They do not use a subscriber-specific CUDA stream.Subscriber::setFilePath() changes only the stored logical path. It does not reopen the stream. Mutable getFilePath() access bypasses all validation.NSTEP is derived from frame count times the subscriber's current frequency, not the integrator's absolute step.NSAVC and NSAVV values do not represent independent DCD coordinate and velocity saving frequencies.The native public layer begins at Subscriber. Its extension point is the pure virtual update() method. A concrete reporter normally delegates path, stream, frequency, context, and integrator storage to the base and adds only the format-specific state required to write one update. Override openFile() only when the format requires non-default stream flags or format state reset, as DcdSubscriber does for binary output.
The scheduling collaborator is CudaIntegrator. It stores parallel m_Subscribers and m_ReportFreqList arrays. Entry i in one must always correspond to entry i in the other. Subscription appends both; unsubscription erases both; reportIfNeeded() indexes both and invokes update(). Mutable accessors expose these arrays, so callers can violate the invariant. A future reorganization should replace the parallel representation with one attachment record and define explicit backlink clearing and resubscription semantics.
The native implementation layer under src/ performs DCD and restart format generation and CUDA transfers. DCD writes header, unit-cell, and coordinate records directly to std::fstream. Restart assembles one monolithic CHARMM-style file and reads integrator-specific containers through dynamic casts.
The C ABI exposes only the base operations, DCD writer, restart writer, and the integrator attachment/propagation operations. Private handle structs store a concrete std::shared_ptr and an embedded base struct that shares the same native owner. C functions validate handles, translate exceptions with apocharmm_c::guard, and never expose the private C++ layout as a stable public contract.
The Python layer mirrors that mapping. Concrete wrappers own their C handles, base methods operate on borrowed embedded views, and the integrator retains Python wrappers to preserve those views. Status translation is centralized in configure_status_function() rather than duplicated by subscriber wrappers.
Host/device data flow is explicit. DCD copies CudaContainer<float4> device coordinates to host before splitting components. Restart copies each required context or integrator container to host before text formatting. These transfers are blocking at the device level.
The error boundary is layered. Public C++ contracts name native categories. apocharmm_c::guard maps those categories and captures diagnostics. Python errcheck immediately copies the thread-local C diagnostic into an owned exception string. Destroy functions use a separate non-throwing boundary because they cannot return status.
Relevant tests are registered in test/unittests/CMakeLists.txt. Native coverage is concentrated in unittest-subscriber.cpp, unittest-dcdSubscriber.cpp, unittest-restartSubscriber.cpp, and unittest-cudaIntegrator.cpp. Direct C ABI coverage uses the corresponding unittest-capi*.cpp files. Python coverage uses the subscriber, DCD-subscriber, restart-subscriber, and CUDA-integrator pytest modules.
C++:
C ABI:
Python: