Skip to content

Using radiens-core from MATLAB

radiens-core works from MATLAB through MATLAB's built-in Python interface. You call AllegoClient and VidereClient from MATLAB code and get back the same objects a Python script gets; MATLAB converts numbers, strings and arrays at the boundary.

Requirements

  • MATLAB R2024b or later. radiens-core needs Python 3.12, and R2024b is the first MATLAB release that supports it. See MathWorks' Python version compatibility table for the versions your release accepts.
  • A Python 3.12+ installation that MATLAB supports, with radiens-core installed into it.
  • A running Radiens desktop application: Allego for AllegoClient, Videre for VidereClient.

Setup

  1. Install radiens-core into the Python that MATLAB will use. A virtual environment keeps it separate from other projects:

    python3.12 -m venv ~/radiens-matlab
    ~/radiens-matlab/bin/pip install radiens-core
    

    On Windows the interpreter is radiens-matlab\Scripts\python.exe.

  2. Point MATLAB at that interpreter, giving its full path. Run this once per MATLAB session, before any other py. call; MATLAB can't switch interpreters after it has loaded one.

    pyenv(Version="/path/to/radiens-matlab/bin/python");
    
  3. Check the install:

    client = py.radiens_core.AllegoClient();
    client.healthcheck()
    

Calling the clients

Python calls translate directly:

Python MATLAB
from radiens_core import AllegoClient
client = AllegoClient()
client = py.radiens_core.AllegoClient();
with AllegoClient() as client: cleanup = onCleanup(@() client.close()); inside a function
client.get_signals([2.0, 3.0]) client.get_signals([2 3])
client.downsample(meta, factor=4) client.downsample(meta, factor=int32(4))
status.streaming.stream_mode.name string(status.streaming.stream_mode.name)

MATLAB has no with statement. Put onCleanup inside a function rather than a script, so the client closes when the function exits, including on Ctrl-C or an error.

MATLAB numbers are doubles and reach Python as float. Where a parameter is typed int, such as factor above, wrap the value in int32(...). Numeric vectors such as time ranges [start end] and channel lists [0 1 2 3] can be passed as they are. Channel indices are zero-based native indices, as in Python.

Converting results

Returned Python type Example Convert with
numpy array sigs.amp double(sigs.amp)
tuple or list of numbers sigs.time_range.sec cellfun(@double, cell(sigs.time_range.sec))
float / int meta.time_range.fs double(meta.time_range.fs)
str meta.base_name string(meta.base_name)
enum member meta.file_type string(meta.file_type.name)
model object meta read fields with dot syntax: meta.time_range.dur_sec

Signal arrays are channels × samples, as in Python. Each conversion copies the data, so in a fast streaming loop the effective loop period can be noticeably longer than the poll interval.

A Python exception raised by radiens-core reaches MATLAB as a matlab.exception.PyException, an MException that try/catch handles like any other error. Its message reads Python Error: <exception class>: <text>, and its ExceptionObject property holds the Python exception for telling error types apart.

Examples

Closed-loop signal acquisition

The MATLAB version of the closed-loop example: continuously read only the samples that arrived since the last fetch.

function closed_loop_streaming()
% Closed-loop signal acquisition that continuously fetches only new data.
%
% Tracks the end time of the last successful fetch and requests only the
% samples that have arrived since, polling every pollIntervalSec seconds.
% Interrupt with Ctrl-C to stop.

pollIntervalSec = 0.5;

client = py.radiens_core.AllegoClient();
client.start_streaming();
cleanup = onCleanup(@() stopAndClose(client));

% Let the cache fill briefly before the first fetch
pause(1.0)

% Seed the cursor at the current cache head so we only fetch new data
cacheRange = primaryCacheRange(client);
cursor = cacheRange(2);
fprintf("Starting at cache head: %.3f s, polling every %g s\n", cursor, pollIntervalSec)
fprintf("Press Ctrl-C to stop.\n\n")

while true
    pause(pollIntervalSec)

    cacheRange = primaryCacheRange(client);
    if cacheRange(2) <= cursor
        continue  % no new data yet
    end

    % Clamp in case the rolling cache has advanced past our cursor
    fetchStart = max(cursor, cacheRange(1));

    try
        sigs = client.get_signals([fetchStart, cacheRange(2)]);
    catch err
        fprintf("Fetch error (skipping window): %s\n", err.message)
        cursor = cacheRange(2);
        continue
    end

    % --- Process the new chunk here ---
    served = toDouble(sigs.time_range.sec);
    amp = double(sigs.amp);  % channels x samples
    if ~isempty(amp)
        % get_signals returns wideband by default; remove each channel's DC offset
        ac = amp - mean(amp, 2);
        fprintf("[%.3f - %.3f s]  %d ch x %d samples  RMS = %.4f\n", ...
            served(1), served(2), size(amp, 1), size(amp, 2), sqrt(mean(ac.^2, "all")))
    end

    % Advance by what was served, which can be shorter than requested
    cursor = served(2);
end
end

function range = primaryCacheRange(client)
status = client.get_status();
range = toDouble(status.streaming.primary_cache_t_range);
end

function values = toDouble(pyTuple)
values = cellfun(@double, cell(pyTuple));
end

function stopAndClose(client)
client.stop_streaming();
client.close();
fprintf("\nStopped.\n")
end

Reading a recording

Link a recorded file in Videre, read one second of four channels and plot it.

function load_recording()
% Link a recorded file in Videre, read one second of four channels and plot it.

% --- CONFIGURATION (Change this to match your local setup) ---
dataPath = "path/to/your/recording_data.xdat";
% --------------------------------------------------------------

client = py.radiens_core.VidereClient();
cleanup = onCleanup(@() client.close());

meta = client.link_data_file(dataPath);
fs = double(meta.time_range.fs);
fprintf("Linked %s: %g Hz, %.2f s\n", string(meta.base_name), fs, double(meta.time_range.dur_sec))

% Seconds 2 to 3 of amplifier channels 0-3 (native indices, zero-based)
sigs = client.get_signals(meta, [2 3], [0 1 2 3]);

amp = double(sigs.amp);  % channels x samples
served = cellfun(@double, cell(sigs.time_range.sec));
t = served(1) + (0:size(amp, 2) - 1) / fs;

plot(t, amp')
xlabel("Time (s)")
ylabel("Amplitude")
legend("ch " + string(0:size(amp, 1) - 1))
end

Save each example as a .m file with the same name as its function, then run it from the MATLAB command window, for example closed_loop_streaming.

Troubleshooting

  • pyenv reports the wrong version, or the Version can't be changed. MATLAB loaded a Python interpreter earlier in the session. Restart MATLAB and run pyenv first.

  • MATLAB crashes or reports a library conflict when importing radiens_core. Run Python in a separate process instead, before any other py. call:

    pyenv(Version="/path/to/radiens-matlab/bin/python", ExecutionMode="OutOfProcess");
    

    Calls then carry some extra overhead. To restart that Python process, run terminate(pyenv).

  • Unable to resolve the name 'py.radiens_core.AllegoClient'. radiens-core isn't installed in the interpreter pyenv reports. Install it there, then restart MATLAB.