From f588db7954f859647978c91abfdef23e9d8bcc8d Mon Sep 17 00:00:00 2001 From: SRSteinkamp Date: Thu, 20 Jun 2024 08:22:41 +0200 Subject: [PATCH 1/3] adding nireports as dependency --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index 7d5ae4a..98f5016 100644 --- a/setup.cfg +++ b/setup.cfg @@ -28,6 +28,7 @@ install_requires = pandas peakdet duecredit + nireports tests_require = pytest >=5.3 test_suite = pytest From 3ab02769837735453ba6aea2ef47431128d5016b Mon Sep 17 00:00:00 2001 From: SRSteinkamp Date: Thu, 20 Jun 2024 08:26:43 +0200 Subject: [PATCH 2/3] adding very basicreporting objects (too many things still hardcoded) --- physioqc/data/bootstrap.yml | 73 +++++++++++++++++++++++++++++++++++++ physioqc/workflow.py | 28 ++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 physioqc/data/bootstrap.yml diff --git a/physioqc/data/bootstrap.yml b/physioqc/data/bootstrap.yml new file mode 100644 index 0000000..a861a1b --- /dev/null +++ b/physioqc/data/bootstrap.yml @@ -0,0 +1,73 @@ +# Copyright 2023 The NiPreps Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# We support and encourage derived works from this project, please read +# about our expectations at +# +# https://www.nipreps.org/community/licensing/ +# +########################################################################### +# Reports bootstrap file +# ====================== +# This is a YAML-formatted file specifying how the NiReports assembler +# will search for "reportlets" and compose them into a report file, +# typically in HTML format. +########################################################################### + +packagename: physioqc +title: '{filename} :: Respiration QC Report (rawdata)' +sections: +- name: Rawdata + reportlets: + - bids: {datatype: figures, desc: raw} + caption: Here we see the rawdata over time. + style: + max-height: 700px +- name: Average peak + reportlets: + - bids: {datatype: figures, desc: average} + caption: This panel shows the average peak, and its errobars (or traces). + style: + max-height: 700px +- name: Power spectrum + reportlets: + - bids: {datatype: figures, desc: power} + caption: Plot of the power spectrum for the raw signal. + style: + max-height: 700px +- name: Histogram of peak amplitudes + reportlets: + - bids: {datatype: figures, desc: histogram} + caption: Histogram of peak amplitudes. + style: + max-height: 700px + +- name: Others + nested: true + reportlets: + - metadata: "input" + settings: + # By default, only the first dictionary will be expanded. + # If folded is true, all will be folded. If false all expanded. + # If an ID is not provided, one should be generated automatically + id: 'about-metadata' + - custom: errors + path: '{reportlets_dir}/{run_uuid}' + captions: PhysioQC may have recorded failure conditions. + title: Errors + +# Rating widget +plugins: +- module: nireports.assembler + path: data/rating-widget/bootstrap.yml diff --git a/physioqc/workflow.py b/physioqc/workflow.py index d2d8b66..7a7bd65 100644 --- a/physioqc/workflow.py +++ b/physioqc/workflow.py @@ -4,6 +4,7 @@ import numpy as np import peakdet as pk +from nireports.assembler.report import Report from physioqc.cli.run import _get_parser from physioqc.interfaces.interfaces import generate_figures, run_metrics, save_metrics @@ -25,6 +26,10 @@ std, ) +BOOTSTRAP_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "data")) + +print("BOOTSTRAP path", BOOTSTRAP_PATH) + def save_bash_call(outdir): """ @@ -93,6 +98,29 @@ def physioqc( # Save the metrics in the output folder save_metrics(metrics_df, outdir) + metric_dict = metrics_df.to_dict(orient="list") + metric_dict = {i: j for i, j in zip(metric_dict["Metric"], metric_dict["Value"])} + metadata = { + "about-metadata": { + "Metrics": metric_dict, + "Version": {"version": "pre functional, Definitely does not work yet ;)"}, + } + } + + filters = {"subject": "01"} + + robj = Report( + outdir, + "test", + reportlets_dir=outdir + "/figures/", + bootstrap_file=os.path.join(BOOTSTRAP_PATH, "bootstrap.yml"), + metadata=metadata, + plugin_meta={}, + **filters, + ) + + robj.generate_report() + def _main(argv=None): options = _get_parser().parse_args(argv) From b0f7f92edc8d5ab9fafa5eaf9f32ae02051a00c6 Mon Sep 17 00:00:00 2001 From: SRSteinkamp Date: Fri, 21 Jun 2024 09:58:16 +0200 Subject: [PATCH 3/3] Adding some very basic pybids support (i.e. creating file names and folders) --- physioqc/data/bootstrap.yml | 8 +++- physioqc/interfaces/interfaces.py | 75 ++++++++++++++++++++++++++----- physioqc/workflow.py | 19 ++++++-- 3 files changed, 85 insertions(+), 17 deletions(-) diff --git a/physioqc/data/bootstrap.yml b/physioqc/data/bootstrap.yml index a861a1b..8655e44 100644 --- a/physioqc/data/bootstrap.yml +++ b/physioqc/data/bootstrap.yml @@ -48,10 +48,16 @@ sections: max-height: 700px - name: Histogram of peak amplitudes reportlets: - - bids: {datatype: figures, desc: histogram} + - bids: {datatype: figures, desc: histpeakamp} caption: Histogram of peak amplitudes. style: max-height: 700px +- name: Histogram of peak distances + reportlets: + - bids: {datatype: figures, desc: histpeakdist} + caption: Histogram of peak distanes. + style: + max-height: 700px - name: Others nested: true diff --git a/physioqc/interfaces/interfaces.py b/physioqc/interfaces/interfaces.py index 4c5d587..8342479 100644 --- a/physioqc/interfaces/interfaces.py +++ b/physioqc/interfaces/interfaces.py @@ -2,11 +2,19 @@ import matplotlib.pyplot as plt import pandas as pd +from bids import layout from physioqc.metrics.multimodal import peak_amplitude, peak_detection, peak_distance +# Save pattern as global +pattern = ( + "sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}]" + + "[_rec-{reconstruction}][_run-{run}][_echo-{echo}]" + + "[_desc-{description}]_{suffix}.{extension}" +) -def generate_figures(figures, data, outdir): + +def generate_figures(figures, data, outdir, entities): """ Generates all the figure needed to populate the visual report. Save the figures in the 'figures' folder @@ -17,9 +25,22 @@ def generate_figures(figures, data, outdir): A list of functions to run to generate all the figures. data : np.array or peakdet Physio object Physiological data + outdir: str + The path to the output directory. + entities: dictionary + A dictionary of bids entities used to write out the files. """ # Create the output directory if not existing - os.makedirs(os.path.join(outdir, "figures"), exist_ok=True) + sub_folders = [] + for k in ["subject", "session"]: + if k in entities: + sub_folders.append("-".join([k[:3], entities[k]])) + + out_folder = os.path.join(outdir, *sub_folders, "figures") + os.makedirs(out_folder, exist_ok=True) + + out_entities = {k: v for k, v in entities.items()} + out_entities.update({"description": "", "extension": ".svg"}) for figure in figures: # Get the plot name from the name of the function that was ran @@ -35,21 +56,35 @@ def generate_figures(figures, data, outdir): peak_dist = peak_distance(data) fig, _ = figure(peak_dist) - plot_name = "histogram_peak_distance" + out_entities["description"] = "histpeakdist" # TO IMPLEMENT the subject name should be automatically read when the data are loaded - fig.savefig(os.path.join(outdir, "figures", f"sub-01_desc-{plot_name}.svg")) + fig.savefig( + os.path.join( + out_folder, layout.writing.build_path(out_entities, pattern) + ) + ) # Plot histogram of peak amplitude peak_ampl = peak_amplitude(data) fig, _ = figure(peak_ampl) - plot_name = "histogram_peak_distance" - fig.savefig(os.path.join(outdir, "figures", f"sub-01_desc-{plot_name}.svg")) - + out_entities["description"] = "histpeakamp" + # TO IMPLEMENT the subject name should be automatically read when the data are loaded + fig.savefig( + os.path.join( + out_folder, layout.writing.build_path(out_entities, pattern) + ) + ) else: fig, _ = figure(data) # Save the figure - fig.savefig(os.path.join(outdir, "figures", f"sub-01_desc-{plot_name}.svg")) + out_entities["description"] = plot_name + # TO IMPLEMENT the subject name should be automatically read when the data are loaded + fig.savefig( + os.path.join( + out_folder, layout.writing.build_path(out_entities, pattern) + ) + ) def run_metrics(metrics_dict, data): @@ -90,7 +125,7 @@ def run_metrics(metrics_dict, data): return metrics_df -def save_metrics(metrics_df, outdir, to_csv=False): +def save_metrics(metrics_df, outdir, entities, to_csv=False): """ Save the metrics in the defined output path @@ -109,8 +144,24 @@ def save_metrics(metrics_df, outdir, to_csv=False): A dataframe containing the value of each metric """ # TO IMPLEMENT : there may be a bug associated to the next line IsDirectoryError - os.makedirs(outdir, exist_ok=True) + sub_folders = [] + for k in ["subject", "session"]: + if k in entities: + sub_folders.append("-".join([k[:3], entities[k]])) + + out_folder = os.path.join(outdir, *sub_folders) + os.makedirs(out_folder, exist_ok=True) + + out_entities = {k: v for k, v in entities.items()} + if to_csv: - metrics_df.to_csv(os.path.join(outdir, "metrics.csv"), index=False) + out_entities.update({"description": "metrics", "extension": ".csv"}) + metrics_df.to_csv( + os.path.join(out_folder, layout.writing.build_path(out_entities, pattern)), + index=False, + ) else: - metrics_df.to_json(os.path.join(outdir, "metrics.json")) + out_entities.update({"description": "metrics", "extension": ".json"}) + metrics_df.to_json( + os.path.join(out_folder, layout.writing.build_path(out_entities, pattern)) + ) diff --git a/physioqc/workflow.py b/physioqc/workflow.py index 7a7bd65..e8a9ab6 100644 --- a/physioqc/workflow.py +++ b/physioqc/workflow.py @@ -4,6 +4,7 @@ import numpy as np import peakdet as pk +from bids import layout from nireports.assembler.report import Report from physioqc.cli.run import _get_parser @@ -79,6 +80,9 @@ def physioqc( figures = [plot_average_peak, plot_histogram, plot_power_spectrum, plot_raw_data] + # Stand in for further BIDS support: + bids_entities = layout.parse_file_entities(filename=filename) + # Load the data d = np.genfromtxt(filename) @@ -93,10 +97,10 @@ def physioqc( metrics_df = run_metrics(metrics, data) # Generate figures - generate_figures(figures, data, outdir) + generate_figures(figures, data, outdir, bids_entities) # Save the metrics in the output folder - save_metrics(metrics_df, outdir) + save_metrics(metrics_df, outdir, bids_entities) metric_dict = metrics_df.to_dict(orient="list") metric_dict = {i: j for i, j in zip(metric_dict["Metric"], metric_dict["Value"])} @@ -107,12 +111,19 @@ def physioqc( } } - filters = {"subject": "01"} + filters = {k: bids_entities[k] for k in ["subject"]} + + sub_folders = [] + for k in ["subject", "session"]: + if k in bids_entities: + sub_folders.append("-".join([k[:3], bids_entities[k]])) + + out_folder = os.path.join(outdir, *sub_folders, "figures") robj = Report( outdir, "test", - reportlets_dir=outdir + "/figures/", + reportlets_dir=out_folder, bootstrap_file=os.path.join(BOOTSTRAP_PATH, "bootstrap.yml"), metadata=metadata, plugin_meta={},