diff --git a/.gitignore b/.gitignore index 206289357..7a235f4e0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +ramutils/test/test_data/*.png out.html .cache/ ramutils/test/plotting/sample_plots/ @@ -9,7 +10,6 @@ scratch/ *.pdf *.synctex.gz *.swp -*.png *.zip *.ipynb_checkpoints/ diff --git a/demos/report_generation.py b/demos/report_generation.py index 90d67851d..c0eccb842 100644 --- a/demos/report_generation.py +++ b/demos/report_generation.py @@ -1,21 +1,16 @@ -from __future__ import print_function - -import os.path -from ramutils.parameters import FilePaths, FRParameters, PS5Parameters +from ramutils.parameters import FilePaths, FRParameters from ramutils.pipelines.report import make_report from ramutils.tasks import memory -memory.cachedir = "~" - - +memory.cachedir = "scratch/ramutils_test" paths = FilePaths( - root='/', - dest='/scratch/zduey/', - data_db='/scratch/zduey/' + root="~/mnt/rhino", + dest="scratch/ramutils_test/", + data_db="/scratch/report_database", ) params = FRParameters() -make_report("R1001P", "FR1", paths, exp_params=params, stim_params=None, - joint_report=False, rerun=False, sessions=None) +make_report("R1384J", "FR5", paths, exp_params=params, stim_params=None, + joint_report=False, rerun=False, sessions=[0], clinical=True) diff --git a/ramutils/pipelines/report.py b/ramutils/pipelines/report.py index e765cea6e..f1d970ad4 100644 --- a/ramutils/pipelines/report.py +++ b/ramutils/pipelines/report.py @@ -19,7 +19,7 @@ def make_report(subject, experiment, paths, joint_report=False, retrain=False, stim_params=None, exp_params=None, sessions=None, vispath=None, rerun=False, trigger_electrode=None, use_classifier_excluded_leads=False, - pipeline_name="report"): + pipeline_name="report", clinical=False): """ Constructs a report and saves out all the necessary data to re-construct the report This pipeline should be used for generating single session reports for both record-only and @@ -59,6 +59,9 @@ def make_report(subject, experiment, paths, joint_report=False, classifier training pipeline_name : str Name to use for status updates. + clinical: bool + If True, builds a demo clinical report instead of the internal RAM + report Returns ------- @@ -117,7 +120,8 @@ def make_report(subject, experiment, paths, joint_report=False, pre_built_results['target_selection_table'], pre_built_results['classifier_evaluation_results'], paths.dest, - hmm_results=pre_built_results['hmm_results']) + hmm_results=pre_built_results['hmm_results'], + clinical=clinical) return report.compute() final_pairs = generate_pairs_for_classifier(ec_pairs, excluded_pairs) @@ -182,7 +186,8 @@ def make_report(subject, experiment, paths, joint_report=False, report = build_static_report(subject, experiment, data.session_summaries, data.math_summaries, data.target_selection_table, data.classifier_evaluation_results, - hmm_results=output, dest=paths.dest) + hmm_results=output, dest=paths.dest, + clinical=clinical) if vispath is not None: report.visualize(filename=vispath) diff --git a/ramutils/reports/generate.py b/ramutils/reports/generate.py index 8c8b6633e..10bfebf4b 100644 --- a/ramutils/reports/generate.py +++ b/ramutils/reports/generate.py @@ -4,16 +4,15 @@ import json import os.path as osp import random -import itertools from itertools import compress from jinja2 import Environment, PackageLoader import numpy as np -from pkg_resources import resource_listdir, resource_string +from pkg_resources import resource_listdir, resource_filename, resource_string from ramutils import __version__ from ramutils.reports.summary import FRSessionSummary, MathSummary, FRStimSessionSummary -from ramutils.events import extract_experiment_from_events, extract_subject +from ramutils.events import extract_experiment_from_events from ramutils.utils import extract_experiment_series @@ -68,9 +67,12 @@ class ReportGenerator(object): """ def __init__(self, subject, experiment, session_summaries, math_summaries, - target_selection_table, classifier_summaries, hmm_results=None, dest='.'): + target_selection_table, classifier_summaries, hmm_results=None, + dest='.', clinical=False): self.subject = subject self.experiment = experiment + self.clinical = clinical + self.session_summaries = session_summaries self.math_summaries = math_summaries self.target_selection_table = target_selection_table @@ -301,7 +303,7 @@ def generate(self): raise NotImplementedError("Unsupported report type") def _render(self, experiment, **kwargs): - """Convenience method to wrap common keyword arguments passed to the + """ Convenience method to wrap common keyword arguments passed to the template renderer. Parameters @@ -311,7 +313,20 @@ def _render(self, experiment, **kwargs): Additional keyword arguments that are passed to the render method. """ - template = self._env.get_template(experiment.lower() + '.html') + if self.clinical: + from base64 import b64encode + + template = self._env.get_template('clinical_target_selection.html') + + for hemi in ["left", "right"]: + with open(resource_filename("ramutils.reports.static", + "r1384j_{}.png".format(hemi)), "rb") as infile: + encoded = b64encode(infile.read()) + img_string = "data:image/png;base64,{}".format(encoded.decode()) + kwargs["{}_hemisphere_image".format(hemi)] = img_string + else: + template = self._env.get_template(experiment.lower() + '.html') + return template.render( version=self.version, subject=self.subject, @@ -364,20 +379,26 @@ def generate_open_loop_fr_report(self): ) def generate_closed_loop_fr_report(self, experiment): - """ Generate an FR5 report + """ Generate a closed loop stimulation report Returns ------- - Rendered FR5 report as a string. + Rendered stimulation report as a string """ + stim_params = FRStimSessionSummary.stim_parameters(self.session_summaries) + multistim = False + if len(stim_params) > 1: + multistim = True + return self._render( experiment, stim=True, + multistim=multistim, + date=self.session_summaries[0].session_datetime, combined_summary=self._make_combined_summary(), classifiers=self._make_classifier_data(), - stim_params=FRStimSessionSummary.stim_parameters( - self.session_summaries), + stim_params=stim_params, recall_tests=FRStimSessionSummary.recall_test_results( self.session_summaries, experiment), feature_data=self._make_feature_plots(), diff --git a/ramutils/reports/static/plots.js b/ramutils/reports/static/plots.js index 107605528..8db482f85 100644 --- a/ramutils/reports/static/plots.js +++ b/ramutils/reports/static/plots.js @@ -9,12 +9,18 @@ var ramutils = (function (mod, Plotly) { * @param {Array} serialPos - Serial positions (x-axis) * @param {Object} overallProbs - Overall probabilities per serial position * @param {Object} firstProbs - Probability of first recall per serial position + * @param {Object} plot_first -- If true, plot probability of first recall */ - plotSerialpos: function (serialPos, overallProbs, firstProbs) { + plotSerialpos: function (serialPos, overallProbs, firstProbs, plot_first = true) { const mode = "lines+markers"; let data = []; + max_prob = 0 for (let name in overallProbs) { + current_max = Math.max(...overallProbs[name]) + if (current_max > max_prob) { + max_prob = current_max + }; data.push({ x: serialPos, y: overallProbs[name], @@ -23,14 +29,20 @@ var ramutils = (function (mod, Plotly) { }); } - for (let name in firstProbs) { - data.push({ - x: serialPos, - y: firstProbs[name], - mode: mode, - name: name - }); - } + if (plot_first) { + for (let name in firstProbs) { + current_max = Math.max(...firstProbs[name]) + if (current_max > max_prob) { + max_prob = current_max + }; + data.push({ + x: serialPos, + y: firstProbs[name], + mode: mode, + name: name + }); + } + }; const layout = { title: "Probability of Recall as a Function of Serial Position", @@ -40,7 +52,7 @@ var ramutils = (function (mod, Plotly) { }, yaxis: { title: 'Probability', - range: [0, 1] + range: [0, max_prob + 0.01] } }; @@ -103,9 +115,10 @@ var ramutils = (function (mod, Plotly) { * @param {Object} nonStimRecalls * @param {Object} stimRecalls * @param {Object} stimEvents + * @param {bool} plot_stim_events */ - plotRecallSummary: function (nonStimRecalls, stimRecalls, stimEvents) { - const data = [ + plotRecallSummary: function (nonStimRecalls, stimRecalls, stimEvents, plot_stim_events = true) { + let data = [ { x: nonStimRecalls.listno, y: nonStimRecalls.recalled, @@ -122,13 +135,15 @@ var ramutils = (function (mod, Plotly) { marker: {size: 12}, name: 'Stim recalls' }, - { + ] + if (plot_stim_events == true) { + data.push({ x: stimEvents.listno, y: stimEvents.count, type: 'bar', name: 'Stim events' - } - ]; + }) + }; const layout = { title: 'Number of Items Stimulated and Number of Items Recalled', diff --git a/ramutils/reports/static/r1384j_left.png b/ramutils/reports/static/r1384j_left.png new file mode 100644 index 000000000..9ece5c459 Binary files /dev/null and b/ramutils/reports/static/r1384j_left.png differ diff --git a/ramutils/reports/static/r1384j_right.png b/ramutils/reports/static/r1384j_right.png new file mode 100644 index 000000000..b0aa96494 Binary files /dev/null and b/ramutils/reports/static/r1384j_right.png differ diff --git a/ramutils/reports/summary.py b/ramutils/reports/summary.py index 02bb13d7d..cec5d613e 100644 --- a/ramutils/reports/summary.py +++ b/ramutils/reports/summary.py @@ -1029,9 +1029,16 @@ def recall_test_results(summaries, experiment): df = df[df.list > -1] results = [] + n_correct_recalls = df.recalled.sum() + n_items = len(df) for name, group in df.groupby(['stimAnodeTag', 'stimCathodeTag', 'amplitude', 'stim_duration', 'pulse_freq']): + if name[0].find(",") != -1: + target_name = "Multi-Site" + else: + target_name = "-".join([name[0], name[1]]) + single_target_results = {"target": target_name} parameters = "/".join([str(n) for n in name]) # Stim lists vs. non-stim lists @@ -1047,13 +1054,16 @@ def recall_test_results(summaries, experiment): n_correct_stim_list_recalls, n_correct_nonstim_list_recalls], [n_stim_list_words, n_nonstim_list_words]) - results.append({"parameters": parameters, - "comparison": "Stim Lists vs. Non-stim Lists", - "stim": (n_correct_stim_list_recalls, - n_stim_list_words), - "non-stim": (n_correct_nonstim_list_recalls, n_nonstim_list_words), - "t-stat": tstat_list, - "p-value": pval_list}) + single_target_results['list'] = { + "parameters": parameters, + "comparison": "Stim Lists vs. Non-stim Lists", + "stim": (n_correct_stim_list_recalls, + n_stim_list_words), + "non-stim": (n_correct_nonstim_list_recalls, n_nonstim_list_words), + "delta_recall": 100 * ((n_correct_stim_list_recalls/n_stim_list_words) - + (n_correct_nonstim_list_recalls/n_nonstim_list_words)) / (n_correct_recalls / n_items), + "t-stat": tstat_list, + "p-value": pval_list} # stim items vs. non-stim low biomarker items n_correct_stim_item_recalls = group[group.is_stim_item == True].recalled.sum( @@ -1071,13 +1081,15 @@ def recall_test_results(summaries, experiment): [n_correct_stim_item_recalls, n_correct_nonstim_item_recalls], [n_stim_items, n_nonstim_items]) - results.append({ + single_target_results['item'] = { "parameters": parameters, "comparison": "Stim Items vs. Low Biomarker Non-stim Items", "stim": (n_correct_stim_item_recalls, n_stim_items), "non-stim": (n_correct_nonstim_item_recalls, n_nonstim_items), + "delta_recall": 100 * ((n_correct_stim_item_recalls/n_stim_items) - + (n_correct_nonstim_item_recalls/n_nonstim_items)) / (n_correct_recalls / n_items), "t-stat": tstat_list, - "p-value": pval_list}) + "p-value": pval_list} # post stim items vs. non-stim low biomarker items n_correct_post_stim_item_recalls = group[group.is_post_stim_item == True].recalled.sum( @@ -1089,14 +1101,17 @@ def recall_test_results(summaries, experiment): [n_correct_post_stim_item_recalls, n_correct_nonstim_item_recalls], [n_post_stim_items, n_nonstim_items]) - results.append({ + single_target_results['post_stim_item'] = { "parameters": parameters, "comparison": "Post-stim Items vs. Low Biomarker Non-stim Items", "stim": (n_correct_post_stim_item_recalls, n_post_stim_items), "non-stim": (n_correct_nonstim_item_recalls, n_nonstim_items), + "delta_recall": 100 * ((n_correct_post_stim_item_recalls/n_post_stim_items) - + (n_correct_nonstim_item_recalls/n_nonstim_items)) / (n_correct_recalls / n_items), "t-stat": tstat_list, - "p-value": pval_list}) - + "p-value": pval_list + } + results.append(single_target_results) return results @staticmethod diff --git a/ramutils/reports/templates/clinical_base.html b/ramutils/reports/templates/clinical_base.html new file mode 100644 index 000000000..efd3bbbad --- /dev/null +++ b/ramutils/reports/templates/clinical_base.html @@ -0,0 +1,33 @@ +{% set sections = [] %} + + + + + {% block title %}{% endblock %} + + + {% for stylesheet in css.values() %} + + {% endfor %} + + {% for script in js.values() %} + + {% endfor %} + + +
+
+

Stimulation Report

+

Courtesy of Nia Theraputics: {{ datetime.now().strftime('%Y-%m-%d %H:%M') }}

+
+
+ {% block report %}{% endblock %} +
+
+ + + + + + + \ No newline at end of file diff --git a/ramutils/reports/templates/clinical_behavioral.html b/ramutils/reports/templates/clinical_behavioral.html new file mode 100644 index 000000000..48b823715 --- /dev/null +++ b/ramutils/reports/templates/clinical_behavioral.html @@ -0,0 +1,30 @@ +
+
+
Serial Position Curve
+
+
+
+
+
+ 'stim' and 'non-stim' refer to stim lists and non-stim lists. 'First recall' indicates the plot is based only on + the serial position of the first recalled word. 'Overall' means that the recall order is not taken into account. +
+ +
+
+
+
+
+
Stimulation and Serial Position
+
+
+
+
+
+
+
\ No newline at end of file diff --git a/ramutils/reports/templates/clinical_behavioral_modulation.html b/ramutils/reports/templates/clinical_behavioral_modulation.html new file mode 100644 index 000000000..dff7f62d9 --- /dev/null +++ b/ramutils/reports/templates/clinical_behavioral_modulation.html @@ -0,0 +1,23 @@ +
+ {% for target in recall_tests %} +
+
{{ target["target"] }}
+
+
    +
  • + List + {{ "{:.1f}".format(target["list"]["delta_recall"]) }}% +
  • +
  • + Item + {{ "{:.1f}".format(target["item"]["delta_recall"]) }}% +
  • +
  • + Post-Stim Item + {{ "{:.1f}".format(target["post_stim_item"]["delta_recall"]) }}% +
  • +
+
+
+ {% endfor %} +
\ No newline at end of file diff --git a/ramutils/reports/templates/clinical_classifier_performance.html b/ramutils/reports/templates/clinical_classifier_performance.html new file mode 100644 index 000000000..d03d33cd9 --- /dev/null +++ b/ramutils/reports/templates/clinical_classifier_performance.html @@ -0,0 +1,43 @@ +
+
Classifier Performance
+
+
+
+
+
+ (Left) ROC curve.(Right) Subject + recall performance represented as percentage deviation from the (subject) + mean, separated by tercile of the classifier encoding efficiency estimate + for each encoded word. +
+
+
+ +
+ +
diff --git a/ramutils/reports/templates/clinical_neuropsych.html b/ramutils/reports/templates/clinical_neuropsych.html new file mode 100644 index 000000000..e49356249 --- /dev/null +++ b/ramutils/reports/templates/clinical_neuropsych.html @@ -0,0 +1,5 @@ +
+
Neuropsych
+
+
+
\ No newline at end of file diff --git a/ramutils/reports/templates/clinical_patient_summary.html b/ramutils/reports/templates/clinical_patient_summary.html new file mode 100644 index 000000000..ff9abe528 --- /dev/null +++ b/ramutils/reports/templates/clinical_patient_summary.html @@ -0,0 +1,59 @@ +
+
+
Patient Summary
+
+
    +
  • + Patient + Patient ID: 0 +
  • +
  • + Date + {{ date.strftime('%Y-%m-%d') }} +
  • +
  • + Type + Target Selection +
  • +
+
+
+
+
Targets
+
+
    +
  • + LA10-LA11 + Left Middle Temporal +
  • +
  • + LF4-LF5 + Left Superior Frontal +
  • +
  • + RFI4-RFI5 + Right Superior Frontal +
  • +
  • + RMI1-RMI2 + Right Caudal Middle Frontal +
  • +
+
+
+
+
Classification
+
+
    +
  • + Performance + Excellent +
  • +
  • + Estimated Memory Improvement + 26% +
  • +
+
+
+
diff --git a/ramutils/reports/templates/clinical_stim_behavior.html b/ramutils/reports/templates/clinical_stim_behavior.html new file mode 100644 index 000000000..2411f207a --- /dev/null +++ b/ramutils/reports/templates/clinical_stim_behavior.html @@ -0,0 +1,25 @@ +
+
+
Stimulation and Recall
+
+
+
+
+ Blue circles represent the number of recalled items from non-stim lists. + Orange circles represent the number of recalled items from stim lists. +
+
+
+
+
+ + \ No newline at end of file diff --git a/ramutils/reports/templates/clinical_stim_report.html b/ramutils/reports/templates/clinical_stim_report.html new file mode 100644 index 000000000..a660ab54d --- /dev/null +++ b/ramutils/reports/templates/clinical_stim_report.html @@ -0,0 +1,17 @@ +{% extends 'clinical_base.html' %} +{% block title %} + Stimulation Report for John Doe +{% endblock %} + +{% block report %} +{% include 'clinical_patient_summary.html' %} +{% include 'clinical_targets.html' %} + +{% if multistim %} + {% include 'clinical_behavioral_modulation.html' %} +{% endif %} + +{% include 'clinical_behavioral.html' %} +{% include 'clinical_classifier_performance.html' %} +{% include 'clinical_stim_behavior.html' %} +{% endblock %} \ No newline at end of file diff --git a/ramutils/reports/templates/clinical_target_selection.html b/ramutils/reports/templates/clinical_target_selection.html new file mode 100644 index 000000000..e84054086 --- /dev/null +++ b/ramutils/reports/templates/clinical_target_selection.html @@ -0,0 +1,11 @@ +{% extends 'clinical_base.html' %} +{% block title %} + Clinical Report: Target Selection +{% endblock %} + +{% block report %} +{% include 'clinical_patient_summary.html' %} +{% include 'clinical_target_selection_table.html' %} + +{% include 'clinical_classifier_performance.html' %} +{% endblock %} \ No newline at end of file diff --git a/ramutils/reports/templates/clinical_target_selection_table.html b/ramutils/reports/templates/clinical_target_selection_table.html new file mode 100644 index 000000000..5e8a4d8d4 --- /dev/null +++ b/ramutils/reports/templates/clinical_target_selection_table.html @@ -0,0 +1,88 @@ +
+ {% for stim_param in stim_params %} +
+
Target Selection
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TargetLocationTMISMEDelta RecallDelta ClassifierRank
LA10-LA11Left Middle Temporal2.874.4527%.061
LF4-LF5Let Superior Frontal2.643.1230%.082
RFI4-RFI5Right Superior Frontal3.012.7512%0.043
RMI1-RMI2Right Caudal Middle Frontal1.751.043%0.014
+
+
+ {% endfor %} +
diff --git a/ramutils/reports/templates/clinical_targets.html b/ramutils/reports/templates/clinical_targets.html new file mode 100644 index 000000000..1eb560e62 --- /dev/null +++ b/ramutils/reports/templates/clinical_targets.html @@ -0,0 +1,61 @@ +
+ {% for stim_param in stim_params %} +
+
{{ "-".join([stim_param['stimAnodeTag'], stim_param['stimCathodeTag']]) }}
+ +
+
    +
  • + Location + {{ stim_param['location'] }} +
  • + Theta Modulation Index + ??? +
  • + Location + {{ stim_param['location'] }} +
  • +
  • + Amplitude [mA] + {{ stim_param['amplitude'].split(",")[0] }} +
  • +
  • + Duration [ms] + {{ stim_param['stim_duration'].split(",")[0] }} +
  • +
  • + Pulse Frequency [Hz] + {{ stim_param['pulse_freq'].split(",")[0] }} +
  • +
+
+
+ {% endfor %} +
diff --git a/ramutils/tasks/reports.py b/ramutils/tasks/reports.py index 387dc468b..f582752be 100644 --- a/ramutils/tasks/reports.py +++ b/ramutils/tasks/reports.py @@ -14,7 +14,8 @@ @task(cache=False) def build_static_report(subject, experiment, session_summaries, math_summaries, delta_hfa_table, classifier_summaries, dest, - hmm_results={}, save=True, aggregated_report=False): + hmm_results={}, save=True, aggregated_report=False, + clinical=False): """ Given a set of summary objects, generate a static HTML report """ # Subject IDs are at most 8 characters, so this is a quick check to see @@ -24,7 +25,8 @@ def build_static_report(subject, experiment, session_summaries, math_summaries, generator = ReportGenerator(subject, experiment, session_summaries, math_summaries, delta_hfa_table, classifier_summaries, - dest=dest, hmm_results=hmm_results) + dest=dest, hmm_results=hmm_results, + clinical=clinical) report = generator.generate() sessions = [str(summary.session_number) for summary in session_summaries]