diff --git a/physioqc/data/bootstrap.yml b/physioqc/data/bootstrap.yml new file mode 100644 index 0000000..8655e44 --- /dev/null +++ b/physioqc/data/bootstrap.yml @@ -0,0 +1,79 @@ +# 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: 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 + 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/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 d2d8b66..e8a9ab6 100644 --- a/physioqc/workflow.py +++ b/physioqc/workflow.py @@ -4,6 +4,8 @@ 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 from physioqc.interfaces.interfaces import generate_figures, run_metrics, save_metrics @@ -25,6 +27,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): """ @@ -74,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) @@ -88,10 +97,40 @@ 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"])} + metadata = { + "about-metadata": { + "Metrics": metric_dict, + "Version": {"version": "pre functional, Definitely does not work yet ;)"}, + } + } + + 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=out_folder, + bootstrap_file=os.path.join(BOOTSTRAP_PATH, "bootstrap.yml"), + metadata=metadata, + plugin_meta={}, + **filters, + ) + + robj.generate_report() def _main(argv=None): 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