Workflows#
A workflow file is a YAML or JSON file with a list of steps. M3Resp runs the steps in order. Each step reads results by name, does one calculation and writes its results under a name. Before the run, M3Resp checks that each input of a step comes from an earlier step.
This page describes all parts of a workflow file. For the tasks, see How to write a workflow file, How to check a workflow file and How to add a step of your own. A preset (session.run_preset("eit")) is a short, built-in alternative to a workflow file (see Preset contracts).
Run a workflow#
From the command line:
m3resp run examples/multimodal_example/multimodal.workflow.yaml
uv run m3resp run examples/multimodal_example/multimodal.workflow.yaml
From Python:
from m3resp import run_spec, run_workflow
result = run_spec("examples/multimodal_example/multimodal.workflow.yaml")
print(result.status) # succeeded
print(result.resolved_output_dir) # the folder with the result files
Function |
What it runs |
|---|---|
|
The steps, then the |
|
Only the steps. |
Both return a WorkflowResult:
Field or method |
Contents |
|---|---|
|
|
|
One result of the run, by name, for example |
|
All results of the run, by name |
|
The session with all recordings and results |
|
The Python warnings of the steps |
|
One record per step, with its status and time |
|
The results folder and the run manifest |
|
The identifiers and the times of the run |
An example workflow file#
name: my-workflow
inputs:
eit_file: ../data/subject.bin # use it in a step as "@eit_file"
vendor: draeger
outputs:
dir: ../output/results
mode: automatic
timestamped: true
steps:
- uses: eit.load
with: { file_path: "@eit_file", vendor: "@vendor" }
- uses: eit.detect_rates
in: { signal: raw_eit }
with: { subject_type: adult }
- uses: eit.mdn_filter
in: { signal: raw_eit }
- uses: eit.global_impedance
in: { signal: filtered_eit }
- uses: eit.detect_breaths
in: { signal: global_impedance }
- uses: metric.interval_cv
in: { intervals: breath_intervals }
out: { cv: breath_duration_cv }
The keys of a step#
Key |
Contents |
|---|---|
|
The name of the step, for example |
|
An optional fixed name of this step in the file (see Step identifiers) |
|
Which result each input of the step reads: |
|
The settings of the step |
|
New names for the results of the step: |
In with, a text that starts with @ is a value from inputs, also inside lists and dictionaries: with: {paths: ["@a", "@b"]}. Write @@text for the text @text.
M3Resp reads a relative file path in a step (for example file_path of eit.load) and in outputs.dir from the folder of the workflow file.
The sections of a workflow file#
Section |
Contents |
|---|---|
|
The name of the workflow, for the log |
|
|
|
A short text about the workflow |
|
Values that the steps use with |
|
The results folder and the result files (see The outputs section) |
|
Study details for the file names of |
|
Free notes about the study, the site or the session, as a dictionary |
|
|
|
The list of steps |
The outputs section#
Setting |
Default |
Meaning |
|---|---|---|
|
None |
The results folder. Without |
|
Set by M3Resp |
What happens after the steps (see the next table) |
|
|
Each run writes to its own subfolder |
|
|
Write |
|
|
Write one CSV file per list of events |
|
|
Write |
|
|
Add the EMG results of |
|
|
Write the other result files (see How to export the results of a session) |
|
|
Save the figures |
|
|
Add a SHA-256 checksum of each input file to the run manifest |
|
What happens after the steps |
|---|---|
|
M3Resp writes the result files after a successful run. Use it for a workflow without an |
|
The |
|
M3Resp writes no result files, figures or run manifest. The |
With
schema_version: 1, setmodewhen you setdir. Withoutschema_version, M3Resp selectsexplicitif the file has anexport.*step, elseautomatic, and gives aFutureWarning.M3Resp saves the figures only after a successful run. After a failed or cancelled run, the files of the finished steps stay.
All export paths of one run use the same results folder, also with
timestamped: true. A step of your own can read this folder from_resolved_output_dir(see How to add a step of your own).
Strict and lenient files#
Without |
|
|
|---|---|---|
Unknown key |
Ignored |
Error ( |
A setting that must be true or false, given as text ( |
Converted, with a |
Error |
|
Optional |
Required |
With schema_version: 1, true and false are the YAML words true and false (also yes, no, on and off without quotes), or true and false in JSON. Both kinds of files give the same steps and the same results. Use schema_version: 1 for new files.
Step identifiers#
Each step has an id. If you do not give one, M3Resp makes step_<position>_<name>, for example step_003_eit.detect_breaths. This identifier changes when you add or move steps. Give an id to a step that other tools must find again, for example in run_manifest.json:
steps:
- id: detect_breaths
uses: eit.detect_breaths
in: { signal: global_impedance }
The id of each step must be different from the others.
The command line#
Command |
What it does |
|---|---|
|
Runs the workflow. |
|
Checks the file (see How to check a workflow file). |
|
Lists all steps. A step that needs a package that is not installed is marked |
|
Shows all details of one step, for example |
If you installed M3Resp with uv, put uv run in front of each command, for example uv run m3resp steps.
Exit code |
Meaning |
|---|---|
0 |
Success |
1 |
A wrong command or option |
2 |
The workflow file has an error, or M3Resp cannot read it |
3 |
Readiness error, for example a missing input file |
4 |
A step failed during the run |
5 |
The run was cancelled |
Built-in steps#
Run m3resp steps for the list of all steps. The groups are:
Group |
Steps for |
|---|---|
|
Load and slice EIT data, filter, find the rates and the breaths, calculate TIV, EELI, pixel TIV and lung regions |
|
Load and slice EMG data, filter, remove the ECG, find the baseline and the breaths, calculate the parameters and the quality checks |
|
Load and slice ventilator data, select the channels, find the breaths and the occlusions (Pocc) |
|
Put the recordings on the shared clock: |
|
Calculations on lists, for example the coefficient of variation of the breath durations |
|
Write results to files: values, JSON, the session summary and the ROTARC result |
The steps that slice data (see Slicing data to a time window):
Step |
What it slices |
|---|---|
|
The loaded recording, with all data in the same file. The start time of the recording moves, so it stays in line with the other recordings. Times in seconds from the first sample. |
|
Each separate ventilator recording (loaded with |
|
One signal in the workflow. The loaded recording does not change. |
The older names emg.slice and eit.slice still work.
Load EIT data#
Two steps load EIT data:
Step |
Reads |
|---|---|
|
A Dräger, Sentec or Timpel file, with |
|
Any other file, with a loader that you write in Python. M3Resp calls it as |
Both steps give the same results (raw_eit, raw_global_impedance_signal, raw_pixel_impedance_signal, …), so the next steps work with both. Give the loader to the EIT adapter of the session before the run:
from m3resp.adapters import EITProcessingAdapter
from m3resp.workflows import run_workflow
result = run_workflow(
"my-workflow.yaml", eit_adapter=EITProcessingAdapter(loader=my_loader)
)
Without a loader, eit.load_custom stops with an error.
A device can split one recording into several files. The file_path of both steps also takes a list of these files, in the order of the recording:
- uses: eit.load
with: { file_path: [part_1.bin, part_2.bin], vendor: draeger }
eit.loadreads each file and joins them into one recording, in the order of the list.Each file must start after the end of the file before it. If the files are in the wrong order or overlap, the workflow stops with an error that names the two files.
A pause between two files stays in the time axis and gives a warning with its length, because the filters and the breath detection run across the pause.
The files must come from the same device, with the same image size and sampling rate.
first_frameandmax_frameswork only with one file, becauseeitprocessingwould apply them to each file. To slice the joined recording, useeit.slice_recording.eit.load_customgives the list to the loader as it is.
Ventilator channels#
ventilator.channels selects airway_pressure, flow and volume by default. For a recording with only pressure, set channels: [airway_pressure].
M3Resp selects the channels by their names. If no name is known, it uses the positions 0, 1 and 2. An unknown name gives a warning.
A channel number (for example
airway_pressure_channel: 0, counted from 0) replaces the selection by name.The channels keep their recorded units. The Pocc pressure limits use the pressure unit, and the pressure time products use the pressure unit × s.
With more than one pressure channel, the steps use the main airway pressure channel, also with names such as
airway_pressure__pod.
EMG and ventilator steps#
The EMG and ventilator steps need the package resurfemg (python -m pip install -e ".[emg]", or uv sync --extra emg). Without it, these steps raise OptionalDependencyError. import m3resp and m3resp steps work also without it.
Each step also writes its result in the form that resurfemg gives, beside the M3Resp results in the table. In the last column, “resurfemg” means that the calculation comes from resurfemg, and “M3Resp” that it is part of M3Resp.
Step |
Reads (besides the session) |
Main settings |
M3Resp results |
Calculation |
|---|---|---|---|---|
|
|
|
resurfemg |
|
|
|
|
M3Resp. Also slices the ventilator channels from the same file. |
|
|
|
|
M3Resp. Refuses ventilator data from the EIT or EMG file. |
|
|
|
|
|
|
|
|
|
M3Resp |
|
|
|
|
|
M3Resp |
|
|
|
|
resurfemg |
|
|
|
|
M3Resp |
|
|
|
|
resurfemg |
|
|
|
|
resurfemg |
|
|
|
|
resurfemg |
|
|
|
|
M3Resp |
|
|
|
|
resurfemg |
|
|
|
|
resurfemg |
|
|
|
|
M3Resp |
|
|
|
|
M3Resp |
|
|
|
|
M3Resp |
|
|
|
|
resurfemg |
|
|
|
|
resurfemg |
|
|
|
|
resurfemg |
|
|
|
|
resurfemg |
|
|
|
resurfemg |
|
|
|
|
resurfemg |
|
|
|
|
resurfemg |
|
|
|
|
|
resurfemg |
|
|
|
|
resurfemg |
|
|
|
|
resurfemg |
m3resp steps also lists the parameter steps (emg.time_to_peak, emg.pseudo_slope, emg.amplitude, emg.time_product, emg.area_under_baseline, emg.respiratory_rate) and the detection steps (emg.onoffpeak_baseline_crossing, emg.onoffpeak_slope_extrapolation, ventilator.detect_breaths, ventilator.find_occluded_breaths).
Channels and sampling rates#
The
channelofemg.preprocessandemg.select_channelis a channel number of the loaded file, counted from 0. It is not a channel name.In
emg.ecg_detect_peaks,sourceselects a signal ofprocessed_emg:"raw_channel","filtered"or"envelope".ecg_channelselects a raw channel instead. Useecg_channelfor a separate ECG channel, andsourcewhen the ECG is inside the EMG channel.The EMG and the ventilator data can have different sampling rates. The Pocc and timing steps change each side to seconds with its own sampling rate before they compare.
The default
min_breath_width_secondsofemg.detect_breathsis 0.5 s. It also finds the breaths of fast breathing, for example 22 breaths per minute. For noisy recordings, changemin_breath_width_secondsorprominence_factor.
The order of the EMG steps and the envelope#
The EMG steps run in this order:
emg.select_channel -> emg.notch_filter (optional) -> emg.bandpass_filter
-> ECG removal (emg.ecg_gating or emg.ecg_wavelet_denoising)
-> emg.envelope -> baseline -> emg.detect_breaths
The notch filter can also come after the band-pass filter.
emg.preprocessruns the first three steps as one step, with all their settings. To change the order, or to add a step of your own between them, use the separate steps.Each step gives the processed EMG to the next step as a dictionary (
processed_emg). It holds the selected channel ("raw_channel"), the filtered signal ("filtered"), the signal after the ECG removal ("ecg_cleaned"), the envelope ("envelope"), the sampling rate in Hz ("fs") and the settings that the steps used ("filter").
Only emg.envelope calculates the envelope. It uses the signal after the ECG removal, if there is one. The filter and ECG removal steps give the processed EMG without an envelope, because an envelope of an earlier signal does not match the new signal. The steps that need the envelope (baselines, breath detection, parameters per breath and quality checks) stop with an error that names emg.envelope if there is no envelope.
Each step writes a result name of its own, so connect the steps with in and out. The last step before the baseline writes to processed_emg, which the next steps read:
- uses: emg.select_channel
with: { channel: 1 }
out: { processed_emg: selected_emg }
- uses: emg.notch_filter
in: { processed_emg: selected_emg }
with: { base_frequency: 50.0 }
- uses: emg.bandpass_filter
in: { processed_emg: processed_emg_after_notch }
with: { high_pass_hz: 20.0, low_pass_hz: 500.0 }
- uses: emg.ecg_detect_peaks
in: { processed_emg: processed_emg_after_bandpass }
with: { source: filtered }
- uses: emg.ecg_wavelet_denoising
in: { processed_emg: processed_emg_after_bandpass }
- uses: emg.envelope
in: { processed_emg: processed_emg_after_ecg }
with: { window_seconds: 0.25, method: rms }
out: { processed_emg_with_envelope: processed_emg }
Remove the ECG: gating or wavelet denoising#
emg.ecg_gating and emg.ecg_wavelet_denoising both write processed_emg_after_ecg. Use one of them, then calculate the envelope from its result:
- uses: emg.preprocess
out: { processed_emg: processed_emg_before_ecg }
- uses: emg.ecg_detect_peaks
in: { processed_emg: processed_emg_before_ecg }
- uses: emg.ecg_gating # or emg.ecg_wavelet_denoising
in: { processed_emg: processed_emg_before_ecg }
- uses: emg.envelope
in: { processed_emg: processed_emg_after_ecg }
out: { processed_emg_with_envelope: processed_emg }
The
fill_methodof gating: 0 fills the gap with zeros, 1 interpolates between its edges (default), 2 uses the mean of a segment next to the gap, 3 uses a running RMS.Wavelet denoising makes the signal longer with zeros, to a multiple of
2**levels, for its calculation. It cuts the cleaned signal, the thresholds and the gate mask back to the original length.wavelet_decomposition_resultkeeps the longer length. Its metadata has both lengths (original_length,padded_length).
Baseline: moving or slope sum#
emg.moving_baseline and emg.slopesum_baseline both write baseline. Use one of them. slopesum_baseline also writes the pandas.Series of resurfemg (slopesum_baseline_detail), and the same values as NumPy arrays (slopesum_baseline_native_detail).
Measurements and pass or fail#
A quality step gives a pass or a fail only for a real criterion:
A result that is true or false is a criterion. It gives a quality flag that passed or failed.
A number (for example the SNR of each peak of
emg.snr_pseudo) is a measurement. It gives a pass or a fail only when you set a limit, for exampleminimum_snr. Without a limit, the flag haspassed=Falseandmetadata["measurement_only"] == True.A check that could not run because an input is missing has
passed=Falseandmetadata["skipped"] == True.
Pocc steps#
ventilator.pocc_quality needs the end of each occlusion and the pressure time products. Run these steps first:
ventilator.find_occluded_breaths -> pocc_indices
ventilator.pocc_intervals -> pocc_start_indices, pocc_end_indices
ventilator.pocc_time_product -> pocc_time_products
ventilator.pocc_quality (needs all of the above)
The default limits of ventilator.pocc_quality (dp_up_10_threshold=0.0, dp_up_90_threshold=2.0, dp_up_90_norm_threshold=0.8) are the limits of resurfemg for the pressure rise after the release of the occlusion (Warnaar et al. 2024).
Missing and unmatched events#
A quality step with no inputs (no ventilator data, no breaths) gives no results and does not stop the workflow.
emg.evaluate_event_timingpairs the EMG and the ventilator events by their position. If the two lists have different lengths, it pairs as many as it can, givesevaluate_event_timing_unmatched_count, and adds a warning flagevaluate_event_timing_unmatched.The results per breath or per occlusion have
breath_id= the position in the list, counted from 0. Their metadata has the sample of the extremum inextremum_sample_index, and its time in seconds inextremum_timewhen the sampling rate is known.emg.remove_invalid_breathsfinds the breaths of the quality flags by this sample. It also reads the older keypeak_sample_index, with aUserWarning.
Array results#
Results with an array as value (gate masks, wavelet results, bell curve fits) go to parameter_result_arrays.npz. Their row in parameter_results.csv gives the key, the shape and the data type of the array (see How to export the results of a session).
Example workflow files#
The examples folder has these workflow files. Run them from the M3Resp source folder.
File |
What it does |
|---|---|
|
A short EIT, EMG and ventilator workflow, easy to read from start to end |
|
All EIT steps: loading, rates, MDN filter, breaths, TIV, EELI, pixel breaths, pixel TIV and the four lung region steps |
|
All EMG and ventilator steps: loading, ECG removal, baseline, breaths, start and end of breaths, parameters, Pocc and the ten quality checks |
|
All steps of the three modalities, with |
|
The coefficient of variation of the EIT breath durations, written as a ROTARC result file |
|
The EMG analysis of one time window of a real diaphragm recording: notch filter, band-pass filter, wavelet ECG removal, median envelope and breaths. It needs |
All examples except the last one use the synthetic recordings in tests/data. The examples eit-full, emg-full, multimodal-full and breath-duration use schema_version: 1, a fixed id for each step and an explicit outputs.mode.
m3resp run examples/eit_full_preprocessing/eit-full.workflow.yaml
uv run m3resp run examples/eit_full_preprocessing/eit-full.workflow.yaml
Run events, cancellation and errors#
run_spec and run_workflow accept two optional arguments, event_sink and cancellation_token:
event_sinkis a function that M3Resp calls at each event of the run:workflow_started,step_started,step_completed,step_failed,step_warning,workflow_completed,workflow_failedandworkflow_cancelled. Use it, for example, for a progress bar.A
CancellationTokenstops the run: aftertoken.cancel(), the current step finishes and the next step does not start. The status is then"cancelled", and the finished results and the manifest stay. Ctrl-C inm3resp rundoes the same. A step that hangs does not stop with Ctrl-C. Stop it with Ctrl-\ orkill.M3Resp collects the Python warnings of each step in its step record and in
WorkflowResult.warnings.If a step fails, M3Resp raises
WorkflowExecutionError. It has the step (step_id,position,operation_id), the run (run_id,started_at), the step records up to the error, and the original error as its cause.
The run manifest#
With outputs.dir and a mode other than none, run_spec writes run_manifest.json to the results folder. It writes the file with status: "running" before the first step, and replaces it with the final status at the end, also after a failure or a cancellation. So a stopped run always leaves a manifest with its status.
The manifest has the run identifier, the status, the name of the workflow, the times, the inputs, each step record, the warnings, and an error block after a failure. M3Resp hides the values of inputs with password, secret, token, credential or api_key in their name. With outputs.checksums: true, it also has a SHA-256 checksum of each input file.
Results in two forms#
Some steps write the same result in two forms: an M3Resp object (Signal, Event, ParameterResult or QualityFlag) with its method and source, and the form of the outside package, for example a pandas.Series. Use the M3Resp object in new code. m3resp steps --details and m3resp describe <step> show the results of each step.
For applications: WorkflowService#
m3resp.workflows.WorkflowService is for an application, for example a graphical interface, that uses M3Resp. Each method takes a workflow (a path, a dictionary or a WorkflowSpec) and returns only JSON data:
from m3resp.workflows import WorkflowService
service = WorkflowService()
service.list_capabilities(prefix="eit.") # all eit.* steps
service.describe_capability("eit.detect_rates") # one step
service.validate_workflow(spec, readiness=True) # the check report
service.compile_workflow(spec) # the steps that will run
service.run_workflow(spec, event_sink=..., cancellation_token=...)
For each step, describe_capability gives "available", "missing_optional_dependency" or "deprecated", without an import of the outside package. So an application can show the steps and their settings, and disable a step whose package is not installed.
Where the code is#
File |
Contents |
|---|---|
|
|
|
Reads and checks a YAML or JSON workflow file |
|
|
|
|
|
The built-in steps |
|
Shared functions, for example |
|
The short summary of a session in the log after a run |
Continue with How to write a workflow file.