diff --git a/.dockerignore b/.dockerignore index e416cce..ac2b830 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,8 +2,14 @@ .git test test_data -output_folder +output_folder/** +!output_folder/NiChart_sMRI_Demo1 +!output_folder/NiChart_sMRI_Demo2 **/*.nii.gz build **/build src/NiChart_Website + +# Ignore hidden files and directories +.* + diff --git a/.gitignore b/.gitignore index b8d149a..5462bd9 100644 --- a/.gitignore +++ b/.gitignore @@ -181,3 +181,4 @@ test/test_output ## make sure no data go to the repo ##*.nii.gz + diff --git a/Dockerfile b/Dockerfile index fe65994..614e553 100644 --- a/Dockerfile +++ b/Dockerfile @@ -46,16 +46,17 @@ RUN grep -v -E '^(torch)' /tmp/requirements.txt > /tmp/requirements2.txt USER root RUN apt-get update && apt-get install -y python3-tk git USER $MAMBA_USER -RUN pip install --verbose -r /tmp/requirements2.txt && pip uninstall -y torch && pip install --verbose torch==2.3.1 --index-url https://download.pytorch.org/whl/cu${CUDA_VERSION} +RUN pip install --verbose -r /tmp/requirements2.txt RUN mkdir ~/dummyinput && mkdir ~/dummyoutput RUN git clone https://github.com/CBICA/PredCRD.git && cd PredCRD && pip install -e . RUN git clone https://github.com/CBICA/DLWMLS.git && cd DLWMLS && pip install -e . && DLWMLS -i ~/dummyinput -o ~/dummyoutput +RUN pip uninstall -y torch && pip install --verbose torch==2.3.1 --index-url https://download.pytorch.org/whl/cu${CUDA_VERSION} ## Cache DLMUSE and DLICV models with an empty job so no download is needed later RUN DLMUSE -i ~/dummyinput -o ~/dummyoutput && DLICV -i ~/dummyinput -o ~/dummyoutput USER root COPY . /app/ -RUN useradd -s /bin/bash streamlit && mkdir /app/output_folder && \ - chmod a+w /app/output_folder && chmod a-rw / && chmod a-w /app && touch /app/src/viewer/pipeline.log && \ +RUN useradd -s /bin/bash streamlit && \ + chmod -R a+rw /app/output_folder && chmod a-rw / && chmod a-w /app && touch /app/src/viewer/pipeline.log && \ chmod a+rw /app/src/viewer/pipeline.log USER streamlit WORKDIR /app/src/viewer/ diff --git a/cloud-config.json b/cloud-config.json new file mode 100644 index 0000000..342aeb0 --- /dev/null +++ b/cloud-config.json @@ -0,0 +1,5 @@ +{ + "lambda_urls": { + "update_stats": "https://gpzw2o0kxd.execute-api.us-east-1.amazonaws.com/default/cbica-nichart-updatestats" + } +} diff --git a/docs2/api.rst b/docs2/api.rst new file mode 100644 index 0000000..fd4b5f9 --- /dev/null +++ b/docs2/api.rst @@ -0,0 +1,22 @@ +Developers - API +=========================== + +Submodules +---------- + df_muse = pd.DataFrame( + columns=['MRID', '702', '701', '600', '601', '...'], + data=[ + ['Subj1', '...', '...', '...', '...', '...'], + ['Subj2', '...', '...', '...', '...', '...'], + ['Subj3', '...', '...', '...', '...', '...'], + ['...', '...', '...', '...', '...', '...'] + ] + ) + st.markdown( + """ + ### DLMUSE File: + The DLMUSE CSV file contains volumes of ROIs (Regions of Interest) segmented by the DLMUSE algorithm. This file is generated as output when DLMUSE is applied to a set of images. + """ + ) + st.write('Example MUSE data file:') + st.dataframe(df_muse) diff --git a/src/viewer/pages/home.py b/src/viewer/pages/home.py index 4e18091..4c7bcd4 100644 --- a/src/viewer/pages/home.py +++ b/src/viewer/pages/home.py @@ -3,7 +3,7 @@ import utils.utils_session as utilss import utils.utils_st as utilst from streamlit_extras.stylable_container import stylable_container -import webbrowser +from streamlitextras.webutils import stxs_javascript # Page config should be called for each page utilss.config_page() @@ -94,14 +94,14 @@ def set_pipeline() -> None: """ ) -#st.markdown(""" -# -#""", unsafe_allow_html=True) +# st.markdown(""" +# +# """, unsafe_allow_html=True) with st.container(border=True): @@ -111,35 +111,43 @@ def set_pipeline() -> None: ) with stylable_container( - key="my_button_container", + key="my_button_container", css_styles=""" button { background-color: #FF7944; color: white; border-radius: 20px; } - """ + """, ): if st.button( - '📝 NiChart User Experience', + "📝 NiChart User Experience", ): - webbrowser.open_new_tab('https://forms.office.com/r/mM1kx1XsgS') + # This code only works locally, not on a container or server. + # webbrowser.open_new_tab('https://forms.office.com/r/mM1kx1XsgS') + stxs_javascript( + """window.open('https://forms.office.com/r/mM1kx1XsgS', '_blank').focus()""" + ) if st.button( - '📝 Shaping the Future of NiChart', + "📝 Shaping the Future of NiChart", ): - webbrowser.open_new_tab('https://forms.office.com/r/acwgn2WCc4') - - ### Bg color on link_button was not supported in styllable container - #st.link_button( - #'📝 NiChart User Experience', - #'https://forms.office.com/r/mM1kx1XsgS', - #) - - #st.link_button( - #'📝 Shaping the Future of NiChart', - #'https://forms.office.com/r/acwgn2WCc4', - #) + # This code only works locally, not on a container or server. + # webbrowser.open_new_tab('https://forms.office.com/r/acwgn2WCc4') + stxs_javascript( + """window.open('https://forms.office.com/r/acwgn2WCc4', '_blank').focus()""" + ) + + # Bg color on link_button was not supported in styllable container + # st.link_button( + # '📝 NiChart User Experience', + # 'https://forms.office.com/r/mM1kx1XsgS', + # ) + + # st.link_button( + # '📝 Shaping the Future of NiChart', + # 'https://forms.office.com/r/acwgn2WCc4', + # ) # FIXME: For DEBUG utilst.add_debug_panel() diff --git a/src/viewer/pages/pipeline_dlwmls.py b/src/viewer/pages/pipeline_dlwmls.py index b039f2e..f8b6d90 100644 --- a/src/viewer/pages/pipeline_dlwmls.py +++ b/src/viewer/pages/pipeline_dlwmls.py @@ -1,7 +1,8 @@ +import os + import streamlit as st import utils.utils_menu as utilmenu import utils.utils_session as utilss -import os # Page config should be called for each page utilss.config_page() diff --git a/src/viewer/pages/plot_sMRI_vars_study.py b/src/viewer/pages/plot_sMRI_vars_study.py index a5c35eb..878beca 100644 --- a/src/viewer/pages/plot_sMRI_vars_study.py +++ b/src/viewer/pages/plot_sMRI_vars_study.py @@ -5,9 +5,9 @@ import pandas as pd import streamlit as st import utils.utils_dataframe as utildf +import utils.utils_io as utilio import utils.utils_menu as utilmenu import utils.utils_nifti as utilni -import utils.utils_io as utilio import utils.utils_plot as utilpl import utils.utils_rois as utilroi import utils.utils_session as utilss @@ -35,14 +35,14 @@ ) # Update status of checkboxes -if '_check_view_wdir' in st.session_state: - st.session_state.checkbox['view_wdir'] = st.session_state._check_view_wdir -if '_check_view_in' in st.session_state: - st.session_state.checkbox['view_in'] = st.session_state._check_view_in -if '_check_view_select' in st.session_state: - st.session_state.checkbox['view_select'] = st.session_state._check_view_select -if '_check_view_plot' in st.session_state: - st.session_state.checkbox['view_plot'] = st.session_state._check_view_plot +if "_check_view_wdir" in st.session_state: + st.session_state.checkbox["view_wdir"] = st.session_state._check_view_wdir +if "_check_view_in" in st.session_state: + st.session_state.checkbox["view_in"] = st.session_state._check_view_in +if "_check_view_select" in st.session_state: + st.session_state.checkbox["view_select"] = st.session_state._check_view_select +if "_check_view_plot" in st.session_state: + st.session_state.checkbox["view_plot"] = st.session_state._check_view_plot def panel_wdir() -> None: @@ -50,10 +50,10 @@ def panel_wdir() -> None: Panel for selecting the working dir """ icon = st.session_state.icon_thumb[st.session_state.flags["dir_out"]] - show_panel_wdir = st.checkbox( + st.checkbox( f":material/folder_shared: Working Directory {icon}", - key='_check_view_wdir', - value=st.session_state.checkbox['view_wdir'] + key="_check_view_wdir", + value=st.session_state.checkbox["view_wdir"], ) if not st.session_state._check_view_wdir: return @@ -62,21 +62,21 @@ def panel_wdir() -> None: utilst.util_panel_workingdir(st.session_state.app_type) if os.path.exists(st.session_state.paths["dset"]): - list_subdir = utilio.get_subfolders( - st.session_state.paths["dset"] - ) + list_subdir = utilio.get_subfolders(st.session_state.paths["dset"]) st.success( f"Working directory is set to: {st.session_state.paths['dset']}", icon=":material/thumb_up:", ) if len(list_subdir) > 0: st.info( - 'Working directory already includes the following folders: ' + ', '.join(list_subdir) + "Working directory already includes the following folders: " + + ", ".join(list_subdir) ) st.session_state.flags["dir_out"] = True utilst.util_workingdir_get_help() + def panel_incsv() -> None: """ Panel for selecting the input csv @@ -85,23 +85,20 @@ def panel_incsv() -> None: msg = st.session_state.app_config[st.session_state.app_type]["msg_infile"] icon = st.session_state.icon_thumb[st.session_state.flags["csv_plot"]] - show_panel_incsv = st.checkbox( + st.checkbox( f":material/upload: {msg} Data {icon}", disabled=not st.session_state.flags["dir_out"], - key='_check_view_in', - value=st.session_state.checkbox['view_in'] + key="_check_view_in", + value=st.session_state.checkbox["view_in"], ) if not st.session_state._check_view_in: return # Read data if working dir changed if st.session_state.plot_var["df_data"].shape[0] == 0: - df_tmp = utildf.read_dataframe( - st.session_state.paths["csv_plot"] - ) + df_tmp = utildf.read_dataframe(st.session_state.paths["csv_plot"]) st.session_state.plot_var["df_data"] = utildf.rename_rois( - df_tmp, - st.session_state.rois["roi_dict"] + df_tmp, st.session_state.rois["roi_dict"] ) utilss.reset_plots() st.session_state.is_updated["csv_plot"] = False @@ -132,23 +129,20 @@ def panel_incsv() -> None: # Read input csv if st.session_state.is_updated["csv_plot"]: - df_tmp = utildf.read_dataframe( - st.session_state.paths["csv_plot"] - ) + df_tmp = utildf.read_dataframe(st.session_state.paths["csv_plot"]) st.session_state.plot_var["df_data"] = utildf.rename_rois( - df_tmp, - st.session_state.rois["roi_dict"] + df_tmp, st.session_state.rois["roi_dict"] ) utilss.reset_plots() st.session_state.is_updated["csv_plot"] = False # Show input data if os.path.exists(st.session_state.paths["csv_plot"]): - with st.expander('Show input data', expanded=False): + with st.expander("Show input data", expanded=False): st.dataframe(st.session_state.plot_var["df_data"]) - s_title="Input Data" - s_text=""" + s_title = "Input Data" + s_text = """ - Choose a CSV file. Primarily designed for DLMUSE and ML score data, but also supports other files with numeric values. """ utilst.util_get_help(s_title, s_text) @@ -212,8 +206,8 @@ def panel_rename() -> None: st.session_state.plot_var["df_data"] = df st.success("Variables are renamed!") - s_title="Rename Data Columns" - s_text=""" + s_title = "Rename Data Columns" + s_text = """ - If your data includes numeric columns """ utilst.util_get_help(s_title, s_text) @@ -223,11 +217,11 @@ def panel_select() -> None: """ Panel for selecting variables """ - show_panel_select = st.checkbox( + st.checkbox( ":material/playlist_add: Select Variables (optional)", disabled=not st.session_state.flags["csv_plot"], - key='_check_view_select', - value=st.session_state.checkbox['view_select'] + key="_check_view_select", + value=st.session_state.checkbox["view_select"], ) if not st.session_state._check_view_select: return @@ -236,8 +230,10 @@ def panel_select() -> None: df = st.session_state.plot_var["df_data"] - if 'MRID' not in df.columns: - st.warning('The data file does not contain the required "MRID" column. The operation cannot proceed.') + if "MRID" not in df.columns: + st.warning( + 'The data file does not contain the required "MRID" column. The operation cannot proceed.' + ) return with open(st.session_state.dict_categories, "r") as f: @@ -291,22 +287,21 @@ def panel_select() -> None: ] + st.session_state.plot_sel_vars sel_vars = st.session_state.plot_sel_vars st.success(f"Selected variables: {sel_vars}") - + # Add centile vars vars_cent = [] for tmp_var in sel_vars: - c_var=tmp_var + '_centiles' + c_var = tmp_var + "_centiles" if c_var in df.columns and c_var not in sel_vars: vars_cent.append(c_var) sel_vars_wcent = sel_vars + vars_cent - + df = df[sel_vars_wcent] st.session_state.plot_var["df_data"] = df - with st.expander('Show selected data', expanded=False): + with st.expander("Show selected data", expanded=False): st.dataframe(st.session_state.plot_var["df_data"]) - col1, col2 = st.columns([0.5, 0.1]) with col2: if st.button("Revert to initial data", use_container_width=True): @@ -318,8 +313,8 @@ def panel_select() -> None: st.session_state.plot_sel_vars = [] st.rerun() - s_title="Variable Selection" - s_text=""" + s_title = "Variable Selection" + s_text = """ - This step allows you to optionally select a subset of variables for analysis. - Variables are grouped into categories. - Select a category. The selection box displays variables from that category that are present in your data. An empty selection box signifies no overlap between the selected category and your dataset. @@ -328,6 +323,7 @@ def panel_select() -> None: """ utilst.util_get_help(s_title, s_text) + def panel_filter() -> None: """ Panel filter @@ -357,8 +353,8 @@ def show_img() -> None: if st.session_state.sel_roi_img == "": st.warning("Please select an ROI!") return - - ## Insert duplicate suffix fix + + # Insert duplicate suffix fix sel_mrid = st.session_state.sel_mrid if sel_mrid is not None: st.session_state.paths["sel_img"] = os.path.join( @@ -366,9 +362,10 @@ def show_img() -> None: st.session_state.paths["T1"], re.sub(r"_T1$", "", sel_mrid) + st.session_state.suff_t1img, ) - + st.session_state.paths["sel_seg"] = os.path.join( - st.session_state.paths["dlmuse"], re.sub(r"_T1$", "", sel_mrid) + st.session_state.suff_seg + st.session_state.paths["dlmuse"], + re.sub(r"_T1$", "", sel_mrid) + st.session_state.suff_seg, ) if not os.path.exists(st.session_state.paths["sel_img"]): @@ -416,7 +413,11 @@ def show_img() -> None: ) else: utilst.show_img3D( - img_masked, ind_view, mask_bounds[ind_view, :], tmp_orient, size_auto + img_masked, + ind_view, + mask_bounds[ind_view, :], + tmp_orient, + size_auto, ) @@ -486,11 +487,11 @@ def panel_plot() -> None: """ # Panel for displaying plots - show_panel_plots = st.checkbox( + st.checkbox( ":material/bid_landscape: Plot Data", disabled=not st.session_state.flags["csv_plot"], - key='_check_view_plot', - value=st.session_state.checkbox['view_plot'] + key="_check_view_plot", + value=st.session_state.checkbox["view_plot"], ) if not st.session_state._check_view_plot: return @@ -502,7 +503,7 @@ def panel_plot() -> None: # ) df = st.session_state.plot_var["df_data"] if df.shape[0] == 0: - st.warning('Dataframe has 0 rows!') + st.warning("Dataframe has 0 rows!") return # Add sidebar parameters diff --git a/src/viewer/pages/prep_sMRI_dicomtonifti.py b/src/viewer/pages/prep_sMRI_dicomtonifti.py index d516a00..0263623 100644 --- a/src/viewer/pages/prep_sMRI_dicomtonifti.py +++ b/src/viewer/pages/prep_sMRI_dicomtonifti.py @@ -2,6 +2,7 @@ from typing import Any import streamlit as st +import utils.utils_cloud as utilcloud import utils.utils_dicom as utildcm import utils.utils_io as utilio import utils.utils_menu as utilmenu @@ -20,33 +21,36 @@ st.write("# Dicom to Nifti Conversion") # Update status of checkboxes -if '_check_dicoms_wdir' in st.session_state: - st.session_state.checkbox['dicoms_wdir'] = st.session_state._check_dicoms_wdir -if '_check_dicoms_in' in st.session_state: - st.session_state.checkbox['dicoms_in'] = st.session_state._check_dicoms_in -if '_check_dicoms_series' in st.session_state: - st.session_state.checkbox['dicoms_series'] = st.session_state._check_dicoms_series -if '_check_dicoms_run' in st.session_state: - st.session_state.checkbox['dicoms_run'] = st.session_state._check_dicoms_run -if '_check_dicoms_view' in st.session_state: - st.session_state.checkbox['dicoms_view'] = st.session_state._check_dicoms_view -if '_check_dicoms_download' in st.session_state: - st.session_state.checkbox['dicoms_download'] = st.session_state._check_dicoms_download +if "_check_dicoms_wdir" in st.session_state: + st.session_state.checkbox["dicoms_wdir"] = st.session_state._check_dicoms_wdir +if "_check_dicoms_in" in st.session_state: + st.session_state.checkbox["dicoms_in"] = st.session_state._check_dicoms_in +if "_check_dicoms_series" in st.session_state: + st.session_state.checkbox["dicoms_series"] = st.session_state._check_dicoms_series +if "_check_dicoms_run" in st.session_state: + st.session_state.checkbox["dicoms_run"] = st.session_state._check_dicoms_run +if "_check_dicoms_view" in st.session_state: + st.session_state.checkbox["dicoms_view"] = st.session_state._check_dicoms_view +if "_check_dicoms_download" in st.session_state: + st.session_state.checkbox["dicoms_download"] = ( + st.session_state._check_dicoms_download + ) def progress(p: int, i: int, decoded: Any) -> None: with result_holder.container(): st.progress(p, f"Progress: Token position={i}") + def panel_wdir() -> None: """ Panel for selecting the working dir """ icon = st.session_state.icon_thumb[st.session_state.flags["dir_out"]] st.checkbox( - f":material/folder_shared: Working Directory {icon}", - key='_check_dicoms_wdir', - value=st.session_state.checkbox['dicoms_wdir'] + f":material/folder_shared: Working Directory {icon}", + key="_check_dicoms_wdir", + value=st.session_state.checkbox["dicoms_wdir"], ) if not st.session_state._check_dicoms_wdir: return @@ -54,21 +58,21 @@ def panel_wdir() -> None: with st.container(border=True): utilst.util_panel_workingdir(st.session_state.app_type) if os.path.exists(st.session_state.paths["dset"]): - list_subdir = utilio.get_subfolders( - st.session_state.paths["dset"] - ) + list_subdir = utilio.get_subfolders(st.session_state.paths["dset"]) st.success( f"Working directory is set to: {st.session_state.paths['dset']}", icon=":material/thumb_up:", ) if len(list_subdir) > 0: st.info( - 'Working directory already includes the following folders: ' + ', '.join(list_subdir) + "Working directory already includes the following folders: " + + ", ".join(list_subdir) ) st.session_state.flags["dir_out"] = True utilst.util_workingdir_get_help() + def panel_indicoms() -> None: """ Panel for selecting input dicoms @@ -78,8 +82,8 @@ def panel_indicoms() -> None: st.checkbox( f":material/upload: {msg} Dicoms {icon}", disabled=not st.session_state.flags["dir_out"], - key='_check_dicoms_in', - value=st.session_state.checkbox['dicoms_in'] + key="_check_dicoms_in", + value=st.session_state.checkbox["dicoms_in"], ) if not st.session_state._check_dicoms_in: return @@ -119,8 +123,8 @@ def panel_indicoms() -> None: icon=":material/thumb_up:", ) - s_title="DICOM Data" - s_text=""" + s_title = "DICOM Data" + s_text = """ - Upload or select the input DICOM folder containing all DICOM files. Nested folders are supported. - On the desktop app, a symbolic link named **"Dicoms"** will be created in the **working directory**, pointing to your input DICOM folder. @@ -131,6 +135,7 @@ def panel_indicoms() -> None: """ utilst.util_get_help(s_title, s_text) + def panel_detect() -> None: """ Panel for detecting dicom series @@ -139,8 +144,8 @@ def panel_detect() -> None: st.checkbox( f":material/manage_search: Detect Dicom Series {icon}", disabled=not st.session_state.flags["dir_dicom"], - key='_check_dicoms_series', - value=st.session_state.checkbox['dicoms_series'] + key="_check_dicoms_series", + value=st.session_state.checkbox["dicoms_series"], ) if not st.session_state._check_dicoms_series: return @@ -170,11 +175,11 @@ def panel_detect() -> None: icon=":material/thumb_up:", ) - with st.expander('Show dicom metadata', expanded=False): + with st.expander("Show dicom metadata", expanded=False): st.dataframe(st.session_state.df_dicoms) - s_title="DICOM Series" - s_text=""" + s_title = "DICOM Series" + s_text = """ - The system verifies all files within the DICOM folder. - Valid DICOM files are processed to extract the DICOM header information, which is used to identify and group images into their respective series - The DICOM field **"SeriesDesc"** is used to identify series @@ -190,8 +195,8 @@ def panel_extract() -> None: st.checkbox( f":material/auto_awesome_motion: Extract Scans {icon}", disabled=not st.session_state.flags["dicom_series"], - key='_check_dicoms_run', - value=st.session_state.checkbox['dicoms_run'] + key="_check_dicoms_run", + value=st.session_state.checkbox["dicoms_run"], ) if not st.session_state._check_dicoms_run: return @@ -235,19 +240,26 @@ def panel_extract() -> None: f"_{st.session_state.sel_mod}.nii.gz", ) except: - st.warning(':material/thumb_down: Nifti conversion failed!') + st.warning(":material/thumb_down: Nifti conversion failed!") num_nifti = utilio.get_file_count( st.session_state.paths[st.session_state.sel_mod], ".nii.gz" ) if num_nifti == 0: - st.warning(':material/thumb_down: The extraction process did not produce any Nifti images!') + st.warning( + ":material/thumb_down: The extraction process did not produce any Nifti images!" + ) + else: + if st.session_state.has_cloud_session: + utilcloud.update_stats_db( + st.session_state.cloud_user_id, "NIFTIfromDICOM", num_nifti + ) df_files = utilio.get_file_names( st.session_state.paths[st.session_state.sel_mod], ".nii.gz" ) - num_nifti=df_files.shape[0] - + num_nifti = df_files.shape[0] + if num_nifti > 0: st.session_state.flags["dir_nifti"] = True st.session_state.flags[st.session_state.sel_mod] = True @@ -256,17 +268,18 @@ def panel_extract() -> None: icon=":material/thumb_up:", ) - with st.expander('View NIFTI image list'): + with st.expander("View NIFTI image list"): st.dataframe(df_files) - s_title="Nifti Conversion" - s_text=""" + s_title = "Nifti Conversion" + s_text = """ - The user specifies the desired modality and selects the associated series. - Selected series are converted into Nifti image format. - - Nifti images are renamed with the following format: **{PatientID}\_{StudyDate}\_{modality}.nii.gz** + - Nifti images are renamed with the following format: **{PatientID}_{StudyDate}_{modality}.nii.gz** """ utilst.util_get_help(s_title, s_text) + def panel_view() -> None: """ Panel for viewing extracted nifti images @@ -274,8 +287,8 @@ def panel_view() -> None: st.checkbox( ":material/visibility: View Scans", disabled=not st.session_state.flags["dir_nifti"], - key='_check_dicoms_view', - value=st.session_state.checkbox['dicoms_view'] + key="_check_dicoms_view", + value=st.session_state.checkbox["dicoms_view"], ) if not st.session_state._check_dicoms_view: return @@ -335,7 +348,7 @@ def panel_view() -> None: with st.spinner("Wait for it..."): - try: + try: # Prepare final 3d matrix to display img = utilni.prep_image(st.session_state.paths["sel_img"]) @@ -353,10 +366,17 @@ def panel_view() -> None: ind_view = utilni.img_views.index(tmp_orient) size_auto = True utilst.show_img3D( - img, ind_view, img_bounds[ind_view, :], tmp_orient, size_auto + img, + ind_view, + img_bounds[ind_view, :], + tmp_orient, + size_auto, ) except: - st.warning(':material/thumb_down: Image parsing failed. Please confirm that the image file represents a 3D volume using an external tool.') + st.warning( + ":material/thumb_down: Image parsing failed. Please confirm that the image file represents a 3D volume using an external tool." + ) + def panel_download() -> None: """ @@ -365,8 +385,8 @@ def panel_download() -> None: st.checkbox( ":material/download: Download Scans", disabled=not st.session_state.flags["dir_nifti"], - key='_check_dicoms_download', - value=st.session_state.checkbox['dicoms_download'] + key="_check_dicoms_download", + value=st.session_state.checkbox["dicoms_download"], ) if not st.session_state._check_dicoms_download: return @@ -405,7 +425,7 @@ def panel_download() -> None: disabled=False, ) else: - st.warning(':material/thumb_down: No images found for download!') + st.warning(":material/thumb_down: No images found for download!") st.markdown( diff --git a/src/viewer/pages/process_sMRI_DLMUSE.py b/src/viewer/pages/process_sMRI_DLMUSE.py index 036f1ea..9c32f7c 100644 --- a/src/viewer/pages/process_sMRI_DLMUSE.py +++ b/src/viewer/pages/process_sMRI_DLMUSE.py @@ -1,9 +1,9 @@ import os -import re import NiChart_DLMUSE as ncd import pandas as pd import streamlit as st +import utils.utils_cloud as utilcloud import utils.utils_io as utilio import utils.utils_menu as utilmenu import utils.utils_nifti as utilni @@ -27,16 +27,18 @@ ) # Update status of checkboxes -if '_check_dlmuse_wdir' in st.session_state: - st.session_state.checkbox['dlmuse_wdir'] = st.session_state._check_dlmuse_wdir -if '_check_dlmuse_in' in st.session_state: - st.session_state.checkbox['dlmuse_in'] = st.session_state._check_dlmuse_in -if '_check_dlmuse_run' in st.session_state: - st.session_state.checkbox['dlmuse_run'] = st.session_state._check_dlmuse_run -if '_check_dlmuse_view' in st.session_state: - st.session_state.checkbox['dlmuse_view'] = st.session_state._check_dlmuse_view -if '_check_dlmuse_download' in st.session_state: - st.session_state.checkbox['dlmuse_download'] = st.session_state._check_dlmuse_download +if "_check_dlmuse_wdir" in st.session_state: + st.session_state.checkbox["dlmuse_wdir"] = st.session_state._check_dlmuse_wdir +if "_check_dlmuse_in" in st.session_state: + st.session_state.checkbox["dlmuse_in"] = st.session_state._check_dlmuse_in +if "_check_dlmuse_run" in st.session_state: + st.session_state.checkbox["dlmuse_run"] = st.session_state._check_dlmuse_run +if "_check_dlmuse_view" in st.session_state: + st.session_state.checkbox["dlmuse_view"] = st.session_state._check_dlmuse_view +if "_check_dlmuse_download" in st.session_state: + st.session_state.checkbox["dlmuse_download"] = ( + st.session_state._check_dlmuse_download + ) def panel_wdir() -> None: @@ -46,8 +48,8 @@ def panel_wdir() -> None: icon = st.session_state.icon_thumb[st.session_state.flags["dir_out"]] st.checkbox( f":material/folder_shared: Working Directory {icon}", - key='_check_dlmuse_wdir', - value=st.session_state.checkbox['dlmuse_wdir'] + key="_check_dlmuse_wdir", + value=st.session_state.checkbox["dlmuse_wdir"], ) if not st.session_state._check_dlmuse_wdir: return @@ -56,21 +58,21 @@ def panel_wdir() -> None: utilst.util_panel_workingdir(st.session_state.app_type) if os.path.exists(st.session_state.paths["dset"]): - list_subdir = utilio.get_subfolders( - st.session_state.paths["dset"] - ) + list_subdir = utilio.get_subfolders(st.session_state.paths["dset"]) st.success( f"Working directory is set to: {st.session_state.paths['dset']}", icon=":material/thumb_up:", ) if len(list_subdir) > 0: st.info( - 'Working directory already includes the following folders: ' + ', '.join(list_subdir) + "Working directory already includes the following folders: " + + ", ".join(list_subdir) ) st.session_state.flags["dir_out"] = True utilst.util_workingdir_get_help() + def panel_int1() -> None: """ Panel for uploading input t1 images @@ -82,8 +84,8 @@ def panel_int1() -> None: st.checkbox( f":material/upload: {msg} T1 Images {icon}", disabled=not st.session_state.flags["dir_out"], - key='_check_dlmuse_in', - value=st.session_state.checkbox['dlmuse_in'] + key="_check_dlmuse_in", + value=st.session_state.checkbox["dlmuse_in"], ) if not st.session_state._check_dlmuse_in: return @@ -122,8 +124,8 @@ def panel_int1() -> None: icon=":material/thumb_up:", ) - s_title="Input T1 Scans" - s_text=""" + s_title = "Input T1 Scans" + s_text = """ - Upload or select input T1 scans. DLMUSE can be directly applied to raw T1 scans. Nested folders are not supported. - The result file with segmented ROI volumes includes an **"MRID"** column that uniquely identifies each scan. **MRID** is extracted from image file names by removing the common suffix to all images. Using consistent input image names is **strongly recommended** @@ -145,15 +147,15 @@ def panel_dlmuse() -> None: st.checkbox( f":material/new_label: Run DLMUSE {icon}", disabled=not st.session_state.flags["dir_t1"], - key='_check_dlmuse_run', - value=st.session_state.checkbox['dlmuse_run'] + key="_check_dlmuse_run", + value=st.session_state.checkbox["dlmuse_run"], ) if not st.session_state._check_dlmuse_run: return with st.container(border=True): # Device type - if st.session_state.app_type != 'cloud': + if st.session_state.app_type != "cloud": helpmsg = "Choose 'cuda' if your computer has an NVIDIA GPU, 'mps' if you have an Apple M-series chip, and 'cpu' if you have a standard CPU." device = utilst.user_input_select( "Device", @@ -164,7 +166,7 @@ def panel_dlmuse() -> None: False, ) else: - device = 'cuda' + device = "cuda" # Button to run DLMUSE btn_seg = st.button("Run DLMUSE", disabled=False) @@ -174,23 +176,32 @@ def panel_dlmuse() -> None: with st.spinner("Wait for it..."): fcount = utilio.get_file_count(st.session_state.paths["T1"]) + if st.session_state.has_cloud_session: + utilcloud.update_stats_db( + st.session_state.cloud_user_id, "DLMUSE", fcount + ) + progress_bar = stqdm(total=9, desc="Current step", position=0) progress_bar.set_description("Starting...") - - ncd.run_pipeline(st.session_state.paths['T1'], st.session_state.paths['dlmuse'], - device, dlmuse_extra_args='-nps 1 -npp 1', dlicv_extra_args='-nps 1 -npp 1', progress_bar=progress_bar) - - #dlmuse_cmd = f"NiChart_DLMUSE -i {st.session_state.paths['T1']} -o {st.session_state.paths['dlmuse']} -d {device} --cores 1" - #st.info(f"Running: {dlmuse_cmd}", icon=":material/manufacturing:") + + ncd.run_pipeline( + st.session_state.paths["T1"], + st.session_state.paths["dlmuse"], + device, + dlmuse_extra_args="-nps 1 -npp 1", + dlicv_extra_args="-nps 1 -npp 1", + progress_bar=progress_bar, + ) + + # dlmuse_cmd = f"NiChart_DLMUSE -i {st.session_state.paths['T1']} -o {st.session_state.paths['dlmuse']} -d {device} --cores 1" + # st.info(f"Running: {dlmuse_cmd}", icon=":material/manufacturing:") # FIXME : bypass dlmuse run - #print(f"About to run: {dlmuse_cmd}") - #os.system(dlmuse_cmd) + # print(f"About to run: {dlmuse_cmd}") + # os.system(dlmuse_cmd) out_csv = f"{st.session_state.paths['dlmuse']}/DLMUSE_Volumes.csv" - num_dlmuse = utilio.get_file_count( - st.session_state.paths["dlmuse"], ".nii.gz" - ) + num_dlmuse = utilio.get_file_count(st.session_state.paths["dlmuse"], ".nii.gz") if os.path.exists(out_csv): st.session_state.paths["csv_dlmuse"] = out_csv st.session_state.flags["csv_dlmuse"] = True @@ -199,12 +210,12 @@ def panel_dlmuse() -> None: icon=":material/thumb_up:", ) - with st.expander('View DLMUSE volumes'): - df_dlmuse=pd.read_csv(st.session_state.paths["csv_dlmuse"]) + with st.expander("View DLMUSE volumes"): + df_dlmuse = pd.read_csv(st.session_state.paths["csv_dlmuse"]) st.dataframe(df_dlmuse) - s_title="DLMUSE Segmentation" - s_text=""" + s_title = "DLMUSE Segmentation" + s_text = """ - Raw T1 images are segmented into anatomical regions of interest (ROIs) using DLMUSE. - The output folder (**"DLMUSE"**) will contain the segmentation mask for each scan, and a single CSV file with volumes of all ROIs. The result file will include single ROIs (segmented regions) and composite ROIs (obtained by merging single ROIs within a tree structure). """ @@ -218,8 +229,8 @@ def panel_view() -> None: st.checkbox( ":material/new_label: View Scans", disabled=not st.session_state.flags["csv_dlmuse"], - key='_check_dlmuse_view', - value=st.session_state.checkbox['dlmuse_view'] + key="_check_dlmuse_view", + value=st.session_state.checkbox["dlmuse_view"], ) if not st.session_state._check_dlmuse_view: return @@ -283,27 +294,19 @@ def panel_view() -> None: return st.session_state.paths["sel_img"] = utilio.get_image_path( - st.session_state.paths["T1"], - sel_mrid, - ["nii.gz", ".nii"] + st.session_state.paths["T1"], sel_mrid, ["nii.gz", ".nii"] ) st.session_state.paths["sel_seg"] = utilio.get_image_path( - st.session_state.paths["dlmuse"], - sel_mrid, - ["nii.gz", ".nii"] + st.session_state.paths["dlmuse"], sel_mrid, ["nii.gz", ".nii"] ) if not os.path.exists(st.session_state.paths["sel_img"]): - st.warning( - 'Could not locate underlay image!' - ) + st.warning("Could not locate underlay image!") return if not os.path.exists(st.session_state.paths["sel_seg"]): - st.warning( - 'Could not locate overlay image!' - ) + st.warning("Could not locate overlay image!") return with st.spinner("Wait for it..."): @@ -330,7 +333,11 @@ def panel_view() -> None: size_auto = True if is_show_overlay is False: utilst.show_img3D( - img, ind_view, mask_bounds[ind_view, :], tmp_orient, size_auto + img, + ind_view, + mask_bounds[ind_view, :], + tmp_orient, + size_auto, ) else: utilst.show_img3D( @@ -338,7 +345,7 @@ def panel_view() -> None: ind_view, mask_bounds[ind_view, :], tmp_orient, - size_auto + size_auto, ) @@ -349,8 +356,8 @@ def panel_download() -> None: st.checkbox( ":material/new_label: Download Scans", disabled=not st.session_state.flags["csv_dlmuse"], - key='_check_dlmuse_download', - value=st.session_state.checkbox['dlmuse_download'] + key="_check_dlmuse_download", + value=st.session_state.checkbox["dlmuse_download"], ) if not st.session_state._check_dlmuse_download: return diff --git a/src/viewer/pages/process_sMRI_DLWMLS.py b/src/viewer/pages/process_sMRI_DLWMLS.py index 10fadff..16c8a40 100644 --- a/src/viewer/pages/process_sMRI_DLWMLS.py +++ b/src/viewer/pages/process_sMRI_DLWMLS.py @@ -2,6 +2,7 @@ import pandas as pd import streamlit as st +import utils.utils_cloud as utilcloud import utils.utils_io as utilio import utils.utils_menu as utilmenu import utils.utils_nifti as utilni @@ -24,16 +25,18 @@ ) # Update status of checkboxes -if '_check_dlwmls_wdir' in st.session_state: - st.session_state.checkbox['dlwmls_wdir'] = st.session_state._check_dlwmls_wdir -if '_check_dlwmls_in' in st.session_state: - st.session_state.checkbox['dlwmls_in'] = st.session_state._check_dlwmls_in -if '_check_dlwmls_run' in st.session_state: - st.session_state.checkbox['dlwmls_run'] = st.session_state._check_dlwmls_run -if '_check_dlwmls_view' in st.session_state: - st.session_state.checkbox['dlwmls_view'] = st.session_state._check_dlwmls_view -if '_check_dlwmls_download' in st.session_state: - st.session_state.checkbox['dlwmls_download'] = st.session_state._check_dlwmls_download +if "_check_dlwmls_wdir" in st.session_state: + st.session_state.checkbox["dlwmls_wdir"] = st.session_state._check_dlwmls_wdir +if "_check_dlwmls_in" in st.session_state: + st.session_state.checkbox["dlwmls_in"] = st.session_state._check_dlwmls_in +if "_check_dlwmls_run" in st.session_state: + st.session_state.checkbox["dlwmls_run"] = st.session_state._check_dlwmls_run +if "_check_dlwmls_view" in st.session_state: + st.session_state.checkbox["dlwmls_view"] = st.session_state._check_dlwmls_view +if "_check_dlwmls_download" in st.session_state: + st.session_state.checkbox["dlwmls_download"] = ( + st.session_state._check_dlwmls_download + ) def panel_wdir() -> None: @@ -43,8 +46,8 @@ def panel_wdir() -> None: icon = st.session_state.icon_thumb[st.session_state.flags["dir_out"]] st.checkbox( f":material/folder_shared: Working Directory {icon}", - key='_check_dlwmls_wdir', - value=st.session_state.checkbox['dlwmls_wdir'] + key="_check_dlwmls_wdir", + value=st.session_state.checkbox["dlwmls_wdir"], ) if not st.session_state._check_dlwmls_wdir: return @@ -53,21 +56,21 @@ def panel_wdir() -> None: utilst.util_panel_workingdir(st.session_state.app_type) if os.path.exists(st.session_state.paths["dset"]): - list_subdir = utilio.get_subfolders( - st.session_state.paths["dset"] - ) + list_subdir = utilio.get_subfolders(st.session_state.paths["dset"]) st.success( f"Working directory is set to: {st.session_state.paths['dset']}", icon=":material/thumb_up:", ) if len(list_subdir) > 0: st.info( - 'Working directory already includes the following folders: ' + ', '.join(list_subdir) + "Working directory already includes the following folders: " + + ", ".join(list_subdir) ) st.session_state.flags["dir_out"] = True utilst.util_workingdir_get_help() + def panel_infl() -> None: """ Panel for uploading input t1 images @@ -79,8 +82,8 @@ def panel_infl() -> None: st.checkbox( f":material/upload: {msg} FL Images {icon}", disabled=not st.session_state.flags["dir_out"], - key='_check_dlwmls_in', - value=st.session_state.checkbox['dlwmls_in'] + key="_check_dlwmls_in", + value=st.session_state.checkbox["dlwmls_in"], ) if not st.session_state._check_dlwmls_in: return @@ -119,8 +122,8 @@ def panel_infl() -> None: icon=":material/thumb_up:", ) - s_title="Input FL Scans" - s_text=""" + s_title = "Input FL Scans" + s_text = """ - Upload or select input FL scans. DLWMLS can be directly applied to raw FL scans. Nested folders are not supported. - The result file with total segmented WMLS volume includes an **"MRID"** column that uniquely identifies each scan. **MRID** is extracted from image file names by removing the common suffix to all images. Using consistent input image names is **strongly recommended** @@ -133,16 +136,17 @@ def panel_infl() -> None: """ utilst.util_get_help(s_title, s_text) + def panel_dlwmls() -> None: """ Panel for running DLWMLS """ icon = st.session_state.icon_thumb[st.session_state.flags["csv_dlwmls"]] - show_panel_dlwmls = st.checkbox( + st.checkbox( f":material/new_label: Run DLWMLS {icon}", disabled=not st.session_state.flags["dir_fl"], - key='_check_dlwmls_run', - value=st.session_state.checkbox['dlwmls_run'] + key="_check_dlwmls_run", + value=st.session_state.checkbox["dlwmls_run"], ) if not st.session_state._check_dlwmls_run: return @@ -150,12 +154,17 @@ def panel_dlwmls() -> None: with st.container(border=True): # Device type helpmsg = "Choose 'cuda' if your computer has an NVIDIA GPU, 'mps' if you have an Apple M-series chip, and 'cpu' if you have a standard CPU." - if st.session_state.app_type != 'cloud': + if st.session_state.app_type != "cloud": device = utilst.user_input_select( - "Device", "key_select_device", ["cuda", "cpu", "mps"], None, helpmsg, False + "Device", + "key_select_device", + ["cuda", "cpu", "mps"], + None, + helpmsg, + False, ) else: - device = 'cuda' + device = "cuda" # Button to run DLWMLS btn_seg = st.button("Run DLWMLS", disabled=False) @@ -164,6 +173,12 @@ def panel_dlwmls() -> None: os.makedirs(st.session_state.paths["dlwmls"]) with st.spinner("Wait for it..."): + fcount = utilio.get_file_count(st.session_state.paths["FL"]) + if st.session_state.has_cloud_session: + utilcloud.update_stats_db( + st.session_state.cloud_user_id, "DLWMLS", fcount + ) + dlwmls_cmd = f"DLWMLS -i {st.session_state.paths['FL']} -o {st.session_state.paths['dlwmls']} -d {device}" st.info(f"Running: {dlwmls_cmd}", icon=":material/manufacturing:") @@ -183,9 +198,7 @@ def panel_dlwmls() -> None: print("all done") out_csv = f"{st.session_state.paths['dlwmls']}/DLWMLS_Volumes.csv" - num_dlwmls = utilio.get_file_count( - st.session_state.paths["dlwmls"], ".nii.gz" - ) + num_dlwmls = utilio.get_file_count(st.session_state.paths["dlwmls"], ".nii.gz") if os.path.exists(out_csv): st.session_state.paths["csv_dlwmls"] = out_csv st.session_state.flags["csv_dlwmls"] = True @@ -194,26 +207,27 @@ def panel_dlwmls() -> None: icon=":material/thumb_up:", ) - with st.expander('View WM lesion volumes'): - df_dlwmls=pd.read_csv(st.session_state.paths["csv_dlwmls"]) + with st.expander("View WM lesion volumes"): + df_dlwmls = pd.read_csv(st.session_state.paths["csv_dlwmls"]) st.dataframe(df_dlwmls) - s_title="WM Lesion Segmentation" - s_text=""" + s_title = "WM Lesion Segmentation" + s_text = """ - WM lesions are segmented on raw FL images using DLWMLS. - The output folder (**"DLWMLS"**) will contain the segmentation mask for each scan, and a single CSV file with WML volumes. """ utilst.util_get_help(s_title, s_text) + def panel_view() -> None: """ Panel for viewing images """ - show_panel_view = st.checkbox( + st.checkbox( ":material/new_label: View Scans", disabled=not st.session_state.flags["csv_dlwmls"], - key='_check_dlwmls_view', - value=st.session_state.checkbox['dlwmls_view'] + key="_check_dlwmls_view", + value=st.session_state.checkbox["dlwmls_view"], ) if not st.session_state._check_dlwmls_view: return @@ -287,11 +301,19 @@ def panel_view() -> None: size_auto = True if is_show_overlay is False: utilst.show_img3D( - img, ind_view, mask_bounds[ind_view, :], tmp_orient, size_auto + img, + ind_view, + mask_bounds[ind_view, :], + tmp_orient, + size_auto, ) else: utilst.show_img3D( - img_masked, ind_view, mask_bounds[ind_view, :], tmp_orient, size_auto + img_masked, + ind_view, + mask_bounds[ind_view, :], + tmp_orient, + size_auto, ) @@ -300,31 +322,31 @@ def panel_download() -> None: Panel for downloading results """ if st.session_state.app_type == "cloud": - show_panel_view = st.checkbox( + st.checkbox( ":material/new_label: Download Scans", disabled=not st.session_state.flags["csv_dlwmls"], - key='_check_dlwmls_download', - value=st.session_state.checkbox['dlwmls_download'] - ) + key="_check_dlwmls_download", + value=st.session_state.checkbox["dlwmls_download"], + ) if not st.session_state._check_dlwmls_download: return - with st.container(border=True): - - # Zip results and download - out_zip = bytes() - if not False: - if not os.path.exists(st.session_state.paths["download"]): - os.makedirs(st.session_state.paths["download"]) - f_tmp = os.path.join(st.session_state.paths["download"], "DLWMLS") - out_zip = utilio.zip_folder(st.session_state.paths["dlwmls"], f_tmp) - - st.download_button( - "Download DLWMLS results", - out_zip, - file_name=f"{st.session_state.dset}_DLWMLS.zip", - disabled=False, - ) + with st.container(border=True): + + # Zip results and download + out_zip = bytes() + if not False: + if not os.path.exists(st.session_state.paths["download"]): + os.makedirs(st.session_state.paths["download"]) + f_tmp = os.path.join(st.session_state.paths["download"], "DLWMLS") + out_zip = utilio.zip_folder(st.session_state.paths["dlwmls"], f_tmp) + + st.download_button( + "Download DLWMLS results", + out_zip, + file_name=f"{st.session_state.dset}_DLWMLS.zip", + disabled=False, + ) st.divider() diff --git a/src/viewer/pages/tutorial_dlmuse.py b/src/viewer/pages/tutorial_dlmuse.py index e3c3146..7397922 100644 --- a/src/viewer/pages/tutorial_dlmuse.py +++ b/src/viewer/pages/tutorial_dlmuse.py @@ -9,36 +9,38 @@ utilmenu.menu() + @st.dialog("Video tutorial") # type:ignore def show_video(f_video): - video_file = open(f_video, 'rb') + video_file = open(f_video, "rb") video_bytes = video_file.read() st.video(video_bytes) + st.markdown( """ ### How to use this pipeline ❓ - **Follow the Steps:** Proceed through the pipeline steps on the left. - **Data Flow:** The output of each step automatically becomes the input for the next. - - **Flexibility:** You can skip steps if you already have the required data for subsequent steps. + - **Flexibility:** You can skip steps if you already have the required data for subsequent steps. """ ) st.markdown( """ - ### Required user data 🧠 + ### Required user data 🧠 - **MRI Scans:** One or more T1-weighted MRI scan(s) as input, provided in either DICOM or NIfti format. - **Demographics:** A CSV file containing essential demographic information. - + This file must include at least: - MRID: Unique identifier for the subject (scan timepoint) - Age: Age of the subject. - Sex: Sex of the subject (M/F). - - Optionally, include: + + Optionally, include: - DX: Diagnosis (e.g., Alzheimer's Disease (AD), Control (CN)). - SITE: The site where the MRI scans were acquired (used to define scanner batches for harmonization) - + - **DLMUSE ROI Volumes (Optional):** If you have already segmented your MRI data using the DLMUSE method, you can provide the resulting ROI (Region of Interest) volumes in CSV format. This can speed up the process. """ ) @@ -56,13 +58,14 @@ def show_video(f_video): """ **Video tutorial 1:** - DLMUSE segmentation on a small dataset provided as raw dicom files - - Data: /test_data/dicoms.zip + - Data: /test_data/dicoms.zip """ ) -if st.button(':material/play_circle: DICOMs to Biomarkers'): - f_video=os.path.join(st.session_state.paths['root'], 'resources', 'videos', 'nichart_vtutorial_smri_1.webm') - show_video(f_video) +if st.button(":material/play_circle: DICOMs to Biomarkers"): + url_video="https://youtu.be/RrnibCrNiHA" + st.video(url_video) + st.markdown( """ @@ -72,15 +75,14 @@ def show_video(f_video): """ ) -if st.button(':material/play_circle: ROIs to Biomarkers'): - f_video=os.path.join(st.session_state.paths['root'], 'resources', 'videos', 'nichart_vtutorial_smri_2.webm') - show_video(f_video) +if st.button(":material/play_circle: ROIs to Biomarkers"): + url_video="https://youtu.be/aw5AK0UQtgo" + st.video(url_video) st.markdown( """ - Before applying the pipeline to your own data, we recommend replicating the provided examples. - Example datasets are provided in the folder "test_data". If you are using the desktop version, test_data folder is already in the default search path for file/folder selection. - If you are using the cloud version, you can download the test_data from "https://github.com/CBICA/NiChart_Project/test_data". - """ + """ ) - diff --git a/src/viewer/pages/workflow_sMRI_MLScores.py b/src/viewer/pages/workflow_sMRI_MLScores.py index df70593..cef22ae 100644 --- a/src/viewer/pages/workflow_sMRI_MLScores.py +++ b/src/viewer/pages/workflow_sMRI_MLScores.py @@ -3,6 +3,7 @@ import pandas as pd import streamlit as st +import utils.utils_cloud as utilcloud import utils.utils_io as utilio import utils.utils_menu as utilmenu import utils.utils_session as utilss @@ -28,26 +29,27 @@ ) # Update status of checkboxes -if '_check_ml_wdir' in st.session_state: - st.session_state.checkbox['ml_wdir'] = st.session_state._check_ml_wdir -if '_check_ml_inrois' in st.session_state: - st.session_state.checkbox['ml_inrois'] = st.session_state._check_ml_inrois -if '_check_ml_indemog' in st.session_state: - st.session_state.checkbox['ml_indemog'] = st.session_state._check_ml_indemog -if '_check_ml_run' in st.session_state: - st.session_state.checkbox['ml_run'] = st.session_state._check_ml_run -if '_check_ml_download' in st.session_state: - st.session_state.checkbox['ml_download'] = st.session_state._check_ml_download +if "_check_ml_wdir" in st.session_state: + st.session_state.checkbox["ml_wdir"] = st.session_state._check_ml_wdir +if "_check_ml_inrois" in st.session_state: + st.session_state.checkbox["ml_inrois"] = st.session_state._check_ml_inrois +if "_check_ml_indemog" in st.session_state: + st.session_state.checkbox["ml_indemog"] = st.session_state._check_ml_indemog +if "_check_ml_run" in st.session_state: + st.session_state.checkbox["ml_run"] = st.session_state._check_ml_run +if "_check_ml_download" in st.session_state: + st.session_state.checkbox["ml_download"] = st.session_state._check_ml_download + def panel_wdir() -> None: """ Panel for selecting the working dir """ icon = st.session_state.icon_thumb[st.session_state.flags["dir_out"]] - show_panel_wdir = st.checkbox( + st.checkbox( f":material/folder_shared: Working Directory {icon}", - key='_check_ml_wdir', - value=st.session_state.checkbox['ml_wdir'] + key="_check_ml_wdir", + value=st.session_state.checkbox["ml_wdir"], ) if not st.session_state._check_ml_wdir: return @@ -56,21 +58,21 @@ def panel_wdir() -> None: utilst.util_panel_workingdir(st.session_state.app_type) if os.path.exists(st.session_state.paths["dset"]): - list_subdir = utilio.get_subfolders( - st.session_state.paths["dset"] - ) + list_subdir = utilio.get_subfolders(st.session_state.paths["dset"]) st.success( f"Working directory is set to: {st.session_state.paths['dset']}", icon=":material/thumb_up:", ) if len(list_subdir) > 0: st.info( - 'Working directory already includes the following folders: ' + ', '.join(list_subdir) + "Working directory already includes the following folders: " + + ", ".join(list_subdir) ) st.session_state.flags["dir_out"] = True utilst.util_workingdir_get_help() + def panel_inrois() -> None: """ Panel for uploading input rois @@ -80,8 +82,8 @@ def panel_inrois() -> None: st.checkbox( f":material/upload: {msg} ROIs {icon}", disabled=not st.session_state.flags["dir_out"], - key='_check_ml_inrois', - value=st.session_state.checkbox['ml_inrois'] + key="_check_ml_inrois", + value=st.session_state.checkbox["ml_inrois"], ) if not st.session_state._check_ml_inrois: return @@ -105,25 +107,25 @@ def panel_inrois() -> None: ) if os.path.exists(st.session_state.paths["csv_dlmuse"]): - p_dlmuse = st.session_state.paths["csv_dlmuse"] + p_dlmuse = st.session_state.paths["csv_dlmuse"] st.session_state.flags["csv_dlmuse"] = True st.success(f"Data is ready ({p_dlmuse})", icon=":material/thumb_up:") df_rois = pd.read_csv(st.session_state.paths["csv_dlmuse"]) - with st.expander('Show ROIs', expanded=False): + with st.expander("Show ROIs", expanded=False): st.dataframe(df_rois) - + # Check the input data @st.dialog("Input data requirements") # type:ignore def help_inrois_data(): df_muse = pd.DataFrame( - columns=['MRID', '702', '701', '600', '601', '...'], + columns=["MRID", "702", "701", "600", "601", "..."], data=[ - ['Subj1', '...', '...', '...', '...', '...'], - ['Subj2', '...', '...', '...', '...', '...'], - ['Subj3', '...', '...', '...', '...', '...'], - ['...', '...', '...', '...', '...', '...'] - ] + ["Subj1", "...", "...", "...", "...", "..."], + ["Subj2", "...", "...", "...", "...", "..."], + ["Subj3", "...", "...", "...", "...", "..."], + ["...", "...", "...", "...", "...", "..."], + ], ) st.markdown( """ @@ -131,13 +133,16 @@ def help_inrois_data(): The DLMUSE CSV file contains volumes of ROIs (Regions of Interest) segmented by the DLMUSE algorithm. This file is generated as output when DLMUSE is applied to a set of images. """ ) - st.write('Example MUSE data file:') + st.write("Example MUSE data file:") st.dataframe(df_muse) col1, col2 = st.columns([0.5, 0.1]) with col2: - if st.button('Get help 🤔', key='key_btn_help_mlinrois', use_container_width=True): - help_inrois_data() + if st.button( + "Get help 🤔", key="key_btn_help_mlinrois", use_container_width=True + ): + help_inrois_data() + def panel_indemog() -> None: """ @@ -145,35 +150,33 @@ def panel_indemog() -> None: """ msg = st.session_state.app_config[st.session_state.app_type]["msg_infile"] icon = st.session_state.icon_thumb[st.session_state.flags["csv_demog"]] - st.checkbox( f":material/upload: {msg} Demographics {icon}", disabled=not st.session_state.flags["csv_dlmuse"], - key='_check_ml_indemog', - value=st.session_state.checkbox['ml_indemog'] + key="_check_ml_indemog", + value=st.session_state.checkbox["ml_indemog"], ) if not st.session_state._check_ml_indemog: return with st.container(border=True): - flag_manual = st.checkbox( - 'Enter data manually', - False - ) + flag_manual = st.checkbox("Enter data manually", False) if flag_manual: - st.info('Please enter values for your sample') + st.info("Please enter values for your sample") df_rois = pd.read_csv(st.session_state.paths["csv_dlmuse"]) - df_tmp = pd.DataFrame({'MRID': df_rois['MRID'], 'Age': None, 'Sex': None}) + df_tmp = pd.DataFrame({"MRID": df_rois["MRID"], "Age": None, "Sex": None}) df_user = st.data_editor(df_tmp) - if st.button('Save data'): - if not os.path.exists(os.path.dirname(st.session_state.paths["csv_demog"])): + if st.button("Save data"): + if not os.path.exists( + os.path.dirname(st.session_state.paths["csv_demog"]) + ): os.makedirs(os.path.dirname(st.session_state.paths["csv_demog"])) df_user.to_csv(st.session_state.paths["csv_demog"], index=False) st.success(f"Data saved to {st.session_state.paths['csv_demog']}") - + else: if st.session_state.app_type == "cloud": utilst.util_upload_file( @@ -191,19 +194,19 @@ def panel_indemog() -> None: st.session_state.paths["csv_demog"], st.session_state.paths["file_search_dir"], ) - + if os.path.exists(st.session_state.paths["csv_demog"]): p_demog = st.session_state.paths["csv_demog"] - st.session_state.flags["csv_demog"] = True + st.session_state.flags["csv_demog"] = True st.success(f"Data is ready ({p_demog})", icon=":material/thumb_up:") df_demog = pd.read_csv(st.session_state.paths["csv_demog"]) - with st.expander('Show demographics data', expanded=False): + with st.expander("Show demographics data", expanded=False): st.dataframe(df_demog) # Check the input data - if os.path.exists(st.session_state.paths["csv_demog"]): - if st.button('Verify input data'): + if os.path.exists(st.session_state.paths["csv_demog"]): + if st.button("Verify input data"): [f_check, m_check] = w_mlscores.check_input( st.session_state.paths["csv_dlmuse"], st.session_state.paths["csv_demog"], @@ -221,13 +224,13 @@ def panel_indemog() -> None: @st.dialog("Input data requirements") # type:ignore def help_indemog_data(): df_demog = pd.DataFrame( - columns=['MRID', 'Age', 'Sex'], + columns=["MRID", "Age", "Sex"], data=[ - ['Subj1', '57', 'F'], - ['Subj2', '65', 'M'], - ['Subj3', '44', 'F'], - ['...', '...', '...'] - ] + ["Subj1", "57", "F"], + ["Subj2", "65", "M"], + ["Subj3", "44", "F"], + ["...", "...", "..."], + ], ) st.markdown( """ @@ -240,12 +243,14 @@ def help_indemog_data(): - **Matching MRIDs:** Ensure the MRID values in this file match the corresponding MRIDs in the DLMUSE file for merging the data files. """ ) - st.write('Example demographic data file:') + st.write("Example demographic data file:") st.dataframe(df_demog) col1, col2 = st.columns([0.5, 0.1]) with col2: - if st.button('Get help 🤔', key='key_btn_help_mlindemog', use_container_width=True): + if st.button( + "Get help 🤔", key="key_btn_help_mlindemog", use_container_width=True + ): help_indemog_data() @@ -257,8 +262,8 @@ def panel_run() -> None: st.checkbox( f":material/new_label: Run MLScores {icon}", disabled=not st.session_state.flags["csv_mlscores"], - key='_check_ml_run', - value=st.session_state.checkbox['ml_run'] + key="_check_ml_run", + value=st.session_state.checkbox["ml_run"], ) if not st.session_state._check_ml_run: return @@ -278,12 +283,19 @@ def panel_run() -> None: flag_harmonize = False st.warning("Sample size is small. The data will not be harmonized!") - if 'SITE' not in df_tmp.columns: - st.warning("SITE column missing, assuming all samples are from the same site!") - df_tmp['SITE'] = 'SITE1' + if "SITE" not in df_tmp.columns: + st.warning( + "SITE column missing, assuming all samples are from the same site!" + ) + df_tmp["SITE"] = "SITE1" with st.spinner("Wait for it..."): st.info("Running: mlscores_workflow ", icon=":material/manufacturing:") + fcount = df_tmp.shape[0] + if st.session_state.has_cloud_session: + utilcloud.update_stats_db( + st.session_state.cloud_user_id, "MLScores", fcount + ) try: if flag_harmonize: @@ -303,7 +315,7 @@ def panel_run() -> None: st.session_state.paths["mlscores"], ) except: - st.warning(':material/thumb_up: ML scores calculation failed!') + st.warning(":material/thumb_up: ML scores calculation failed!") # Check out file if os.path.exists(st.session_state.paths["csv_mlscores"]): @@ -323,12 +335,12 @@ def panel_run() -> None: p_plot = st.session_state.paths["csv_plot"] print(f"Data copied to {p_plot}") - with st.expander('View output data with ROIs and ML scores'): - df_ml=pd.read_csv(st.session_state.paths["csv_mlscores"]) + with st.expander("View output data with ROIs and ML scores"): + df_ml = pd.read_csv(st.session_state.paths["csv_mlscores"]) st.dataframe(df_ml) - s_title="ML Biomarkers" - s_text=""" + s_title = "ML Biomarkers" + s_text = """ - DLMUSE ROI volumes are harmonized to reference data. - SPARE scores are calculated using harmonized ROI values and pre-trained models - SurrealGAN scores are calculated using harmonized ROI values and pre-trained models @@ -336,6 +348,7 @@ def panel_run() -> None: """ utilst.util_get_help(s_title, s_text) + def panel_download() -> None: """ Panel for downloading results @@ -343,8 +356,8 @@ def panel_download() -> None: st.checkbox( ":material/new_label: Download Results", disabled=not st.session_state.flags["csv_mlscores"], - key='_check_ml_download', - value=st.session_state.checkbox['ml_download'] + key="_check_ml_download", + value=st.session_state.checkbox["ml_download"], ) if not st.session_state._check_ml_download: return @@ -363,6 +376,7 @@ def panel_download() -> None: disabled=False, ) + st.divider() panel_wdir() panel_inrois() diff --git a/src/viewer/utils/utils_cloud.py b/src/viewer/utils/utils_cloud.py new file mode 100644 index 0000000..3d2b8c9 --- /dev/null +++ b/src/viewer/utils/utils_cloud.py @@ -0,0 +1,47 @@ +import json +import os +from typing import Any + +import requests + + +def load_cloud_config(config_file: str) -> Any: + """ + Load Lambda URLs from a local JSON file. + """ + try: + with open(config_file, "r") as f: + config = json.load(f) + return config + except FileNotFoundError: + raise FileNotFoundError(f"Configuration file '{config_file}' not found.") + except json.JSONDecodeError: + raise ValueError(f"Error parsing the configuration file '{config_file}'.") + + +def update_stats_db(user_id: str, job_type: str, count: int) -> None: + current_dir = os.path.dirname(os.path.abspath(__file__)) + + # Traverse up to the top-level directory (assuming 'config.json' is in the repo root) + repo_root = os.path.abspath(os.path.join(current_dir, "..", "..", "..")) + print(f"repo root: {repo_root}") + + # Construct the full path to the config file + config_file = os.path.join(repo_root, "cloud-config.json") + cloud_config = load_cloud_config(config_file) + + lambda_urls = cloud_config.get("lambda_urls", None) + if lambda_urls is None: + print("Lambda URL not provided in cloud config!") + + update_stats_url = lambda_urls["update_stats"] + + payload = {"user_id": user_id, "job_type": job_type, "count": count} + headers = {"Content-Type": "application/json"} + + try: + response = requests.post(update_stats_url, json=payload, headers=headers) + # response.raise_for_status() + print("Success:", response.json()) + except requests.exceptions.RequestException as e: + print("Error:", e) diff --git a/src/viewer/utils/utils_dataframe.py b/src/viewer/utils/utils_dataframe.py index 3fe8732..0819b35 100644 --- a/src/viewer/utils/utils_dataframe.py +++ b/src/viewer/utils/utils_dataframe.py @@ -16,18 +16,19 @@ def read_dataframe(fname: str) -> pd.DataFrame: try: df = pd.read_csv(fname) # FIXME: this will be resolved in a more systematic way - df = df.rename(columns={'DLICV':'ICV', 'DLICV_centiles':'ICV_centiles'}) + df = df.rename(columns={"DLICV": "ICV", "DLICV_centiles": "ICV_centiles"}) except: df = pd.DataFrame() return df + def rename_rois(df: pd.DataFrame, roi_dict: dict) -> pd.DataFrame: df_out = df.rename(columns=roi_dict) - print(f'AAAA {df_out.shape}') - duplicate_columns = df_out.columns.duplicated()==False - print(f'bbbAAAA {df_out.columns.duplicated().sum()}') + print(f"AAAA {df_out.shape}") + duplicate_columns = df_out.columns.duplicated() is False + print(f"bbbAAAA {df_out.columns.duplicated().sum()}") df_out = df_out.loc[:, duplicate_columns] return df_out diff --git a/src/viewer/utils/utils_io.py b/src/viewer/utils/utils_io.py index e061ade..a208523 100644 --- a/src/viewer/utils/utils_io.py +++ b/src/viewer/utils/utils_io.py @@ -4,6 +4,7 @@ import zipfile from tkinter import filedialog from typing import Any, BinaryIO, List, Optional + import pandas as pd # https://stackoverflow.com/questions/64719918/how-to-write-streamlit-uploadedfile-to-temporary-in_dir-with-original-filenam @@ -119,6 +120,7 @@ def get_file_count(folder_path: str, file_suff: str = "") -> int: count += 1 return count + def get_subfolders(path: str) -> list: subdirs = [] for item in os.listdir(path): @@ -127,6 +129,7 @@ def get_subfolders(path: str) -> list: subdirs.append(item) return subdirs + def get_file_names(folder_path: str, file_suff: str = "") -> pd.DataFrame: f_names = [] if os.path.exists(folder_path): @@ -138,9 +141,10 @@ def get_file_names(folder_path: str, file_suff: str = "") -> pd.DataFrame: for file in files: if file.endswith(file_suff): f_names.append([file]) - df_out = pd.DataFrame(columns=['FileName'], data=f_names) + df_out = pd.DataFrame(columns=["FileName"], data=f_names) return df_out + def get_file_list(folder_path: str, file_suff: str = "") -> List: list_nifti: List[str] = [] if not os.path.exists(folder_path): @@ -150,6 +154,7 @@ def get_file_list(folder_path: str, file_suff: str = "") -> List: list_nifti.append(f) return list_nifti + def get_image_path(folder_path: str, file_pref: str, file_suff_list: list) -> str: if not os.path.exists(folder_path): return "" @@ -157,3 +162,4 @@ def get_image_path(folder_path: str, file_pref: str, file_suff_list: list) -> st for f in os.listdir(folder_path): if f.startswith(file_pref) and f.endswith(tmp_suff): return os.path.join(folder_path, f) + return "" diff --git a/src/viewer/utils/utils_menu.py b/src/viewer/utils/utils_menu.py index 176a3c6..f1bcc34 100644 --- a/src/viewer/utils/utils_menu.py +++ b/src/viewer/utils/utils_menu.py @@ -2,22 +2,25 @@ import streamlit as st from streamlitextras.webutils import stxs_javascript -from typing import NoReturn -def redirect(url: str) -> NoReturn: + +def redirect(url: str) -> None: stxs_javascript(f"window.location.replace('{url}');") + def menu() -> Any: - ## Force redirect to the home page if anything is not properly instantiated. - if 'instantiated' not in st.session_state: - print("Redirected to home page as a required instantiation variable was missing.") - st.switch_page('pages/home.py') + # Force redirect to the home page if anything is not properly instantiated. + if "instantiated" not in st.session_state: + print( + "Redirected to home page as a required instantiation variable was missing." + ) + st.switch_page("pages/home.py") if st.session_state.has_cloud_session: - email = st.session_state.cloud_session_token['email'] + email = st.session_state.cloud_session_token["email"] logout_url = "https://cbica-nichart.auth.us-east-1.amazoncognito.com/logout?client_id=4shr6mm2h0p0i4o9uleqpu33fj&response_type=code&scope=email+openid+phone&redirect_uri=https://cbica-nichart-alb-272274500.us-east-1.elb.amazonaws.com/oauth2/idpresponse" st.sidebar.info(f"Logged in as {email}.") st.sidebar.info(f"User ID: {st.session_state.cloud_user_id}") - ## TODO: Make this button also delete user data automatically + # TODO: Make this button also delete user data automatically st.sidebar.button("Log out", on_click=redirect, args=(logout_url,)) if st.session_state.pipeline == "Home": st.sidebar.page_link("pages/home.py", label="Home") diff --git a/src/viewer/utils/utils_plot.py b/src/viewer/utils/utils_plot.py index c8b0fd8..5d562f6 100644 --- a/src/viewer/utils/utils_plot.py +++ b/src/viewer/utils/utils_plot.py @@ -6,36 +6,39 @@ import streamlit as st import utils.utils_trace as utiltr -def add_items_to_list(my_list, items_to_add): - """Adds multiple items to a list, avoiding duplicates. - - Args: - my_list: The list to add items to. - items_to_add: A list of items to add. - - Returns: - The modified list. - """ - for item in items_to_add: - if item not in my_list: - my_list.append(item) - return my_list - -def remove_items_from_list(my_list, items_to_remove): - """Removes multiple items from a list. - - Args: - my_list: The list to remove items from. - items_to_remove: A list of items to remove. - - Returns: - The modified list. - """ - out_list=[] - for item in my_list: - if item not in items_to_remove: - out_list.append(item) - return out_list + +def add_items_to_list(my_list: list, items_to_add: list) -> list: + """Adds multiple items to a list, avoiding duplicates. + + Args: + my_list: The list to add items to. + items_to_add: A list of items to add. + + Returns: + The modified list. + """ + for item in items_to_add: + if item not in my_list: + my_list.append(item) + return my_list + + +def remove_items_from_list(my_list: list, items_to_remove: list) -> list: + """Removes multiple items from a list. + + Args: + my_list: The list to remove items from. + items_to_remove: A list of items to remove. + + Returns: + The modified list. + """ + out_list = [] + for item in my_list: + if item not in items_to_remove: + out_list.append(item) + return out_list + def add_plot() -> None: """ @@ -63,7 +66,8 @@ def add_plot() -> None: st.session_state.plot_var["centtype"], ] st.session_state.plot_index += 1 - + + # Remove a plot def remove_plot(plot_id: str) -> None: """ @@ -83,179 +87,185 @@ def get_index_in_list(in_list: list, in_item: str) -> Optional[int]: else: return in_list.index(in_item) + def set_x_bounds( - df: pd.DataFrame, - df_plots: pd.DataFrame, - plot_id: str, - xvar: str -) -> list: + df: pd.DataFrame, df_plots: pd.DataFrame, plot_id: str, xvar: str +) -> None: # Set x and y min/max if not set - # Values include some margin added for viewing purposes - xmin=df[xvar].min() - xmax=df[xvar].max() - dx=xmax-xmin - if dx==0: # Margin defined based on the value if delta is 0 - xmin = xmin - xmin/8 - xmax = xmax + xmax/8 - else: # Margin defined based on the delta otherwise - xmin = xmin - dx/5 - xmax = xmax + dx/5 - - df_plots.loc[plot_id, "xmax"]=xmax - df_plots.loc[plot_id, "xmin"]=xmin - + # Values include some margin added for viewing purposes + xmin = df[xvar].min() + xmax = df[xvar].max() + dx = xmax - xmin + if dx == 0: # Margin defined based on the value if delta is 0 + xmin = xmin - xmin / 8 + xmax = xmax + xmax / 8 + else: # Margin defined based on the delta otherwise + xmin = xmin - dx / 5 + xmax = xmax + dx / 5 + + df_plots.loc[plot_id, "xmax"] = xmax + df_plots.loc[plot_id, "xmin"] = xmin + + def set_y_bounds( - df: pd.DataFrame, - df_plots: pd.DataFrame, - plot_id: str, - yvar: str -) -> list: + df: pd.DataFrame, df_plots: pd.DataFrame, plot_id: str, yvar: str +) -> None: # Set x and y min/max if not set - # Values include some margin added for viewing purposes - ymin=df[yvar].min() - ymax=df[yvar].max() - dy=ymax-ymin - if dy==0: # Margin defined based on the value if delta is 0 - ymin = ymin - ymin/8 - ymax = ymax + ymax/8 - else: # Margin defined based on the delta otherwise - ymin = ymin - dy/5 - ymax = ymax + dy/5 - - df_plots.loc[plot_id, "ymax"]=ymax - df_plots.loc[plot_id, "ymin"]=ymin + # Values include some margin added for viewing purposes + ymin = df[yvar].min() + ymax = df[yvar].max() + dy = ymax - ymin + if dy == 0: # Margin defined based on the value if delta is 0 + ymin = ymin - ymin / 8 + ymax = ymax + ymax / 8 + else: # Margin defined based on the delta otherwise + ymin = ymin - dy / 5 + ymax = ymax + dy / 5 + + df_plots.loc[plot_id, "ymax"] = ymax + df_plots.loc[plot_id, "ymin"] = ymin + def add_plot_tabs( df: pd.DataFrame, df_plots: pd.DataFrame, plot_id: str ) -> pd.DataFrame: # Set xy bounds initially to make plots consistent - if df_plots.loc[plot_id, "xmin"]==-1: + if df_plots.loc[plot_id, "xmin"] == -1: set_x_bounds(df, df_plots, plot_id, df_plots.loc[plot_id, "xvar"]) - if df_plots.loc[plot_id, "ymin"]==-1: + if df_plots.loc[plot_id, "ymin"] == -1: set_y_bounds(df, df_plots, plot_id, df_plots.loc[plot_id, "yvar"]) - + ptabs = st.tabs(["Settings", "Layers", ":material/x:"]) - + # Tab 1: Plot settings with ptabs[0]: # Get df columns - list_cols = df.columns[df.columns.str.contains('_centiles')==False].to_list() + list_cols = df.columns[df.columns.str.contains("_centiles") is False].to_list() list_cols_ext = [""] + list_cols list_trends = st.session_state.plot_const["trend_types"] - # Set x var - def on_xvar_change(): - key=f"plot_xvar_{plot_id}" - sel_val=st.session_state[key] - df_plots.loc[plot_id, "xvar"]=sel_val - + def on_xvar_change() -> None: + key = f"plot_xvar_{plot_id}" + sel_val = st.session_state[key] + df_plots.loc[plot_id, "xvar"] = sel_val + # Update x bounds set_x_bounds(df, df_plots, plot_id, df_plots.loc[plot_id, "xvar"]) # Reset centiles if x is not Age - if sel_val != 'Age': - df_plots.loc[plot_id, "centtype"]='' + if sel_val != "Age": + df_plots.loc[plot_id, "centtype"] = "" df_plots.at[plot_id, "traces"] = remove_items_from_list( df_plots.loc[plot_id, "traces"], - st.session_state.plot_const['centile_trace_types'] + st.session_state.plot_const["centile_trace_types"], ) - xind = get_index_in_list(list_cols, df_plots.loc[plot_id, "xvar"]) + xind = get_index_in_list(list_cols, df_plots.loc[plot_id, "xvar"]) st.selectbox( "X Var", list_cols, key=f"plot_xvar_{plot_id}", index=xind, - on_change=on_xvar_change + on_change=on_xvar_change, ) - + if df_plots.loc[plot_id, "plot_type"] == "Scatter Plot": # Set y var - def on_yvar_change(): - key=f"plot_yvar_{plot_id}" - sel_val=st.session_state[key] - df_plots.loc[plot_id, "yvar"]=sel_val + def on_yvar_change() -> None: + key = f"plot_yvar_{plot_id}" + sel_val = st.session_state[key] + df_plots.loc[plot_id, "yvar"] = sel_val # Check if centile value exists if df_plots.loc[plot_id, "plot_cent_normalized"]: - y_new=df_plots.loc[plot_id, "yvar"] + '_centiles' + y_new = df_plots.loc[plot_id, "yvar"] + "_centiles" if y_new not in df.columns: df_plots.loc[plot_id, "plot_cent_normalized"] = False # Update y bounds if df_plots.loc[plot_id, "plot_cent_normalized"]: - set_y_bounds(df, df_plots, plot_id, df_plots.loc[plot_id, "yvar"]+'_centiles') + set_y_bounds( + df, + df_plots, + plot_id, + df_plots.loc[plot_id, "yvar"] + "_centiles", + ) else: set_y_bounds(df, df_plots, plot_id, df_plots.loc[plot_id, "yvar"]) - + yind = get_index_in_list(list_cols, df_plots.loc[plot_id, "yvar"]) st.selectbox( "Y Var", list_cols, key=f"plot_yvar_{plot_id}", index=yind, - on_change=on_yvar_change + on_change=on_yvar_change, ) # Set hvar - def on_hvar_change(): - key=f"plot_hvar_{plot_id}" - sel_val=st.session_state[key] - df_plots.loc[plot_id, "hvar"]=sel_val - df_plots.at[plot_id, "hvals"]=[] - + def on_hvar_change() -> None: + key = f"plot_hvar_{plot_id}" + sel_val = st.session_state[key] + df_plots.loc[plot_id, "hvar"] = sel_val + df_plots.at[plot_id, "hvals"] = [] + hind = get_index_in_list(list_cols_ext, df_plots.loc[plot_id, "hvar"]) st.selectbox( "Group by", list_cols_ext, key=f"plot_hvar_{plot_id}", index=hind, - on_change=on_hvar_change + on_change=on_hvar_change, ) if "ICV" in list_cols: # Set icv corr flag - def on_check_icvcorr_change(): - key=f"key_check_icv_{plot_id}" - sel_val=st.session_state[key] - df_plots.loc[plot_id, "corr_icv"]=sel_val - + def on_check_icvcorr_change() -> None: + key = f"key_check_icv_{plot_id}" + sel_val = st.session_state[key] + df_plots.loc[plot_id, "corr_icv"] = sel_val + st.checkbox( "Correct ICV", value=df_plots.loc[plot_id, "corr_icv"], help="Correct regional volumes using the intra-cranial volume to account for differences in head size", key=f"key_check_icv_{plot_id}", - on_change=on_check_icvcorr_change + on_change=on_check_icvcorr_change, ) if df_plots.loc[plot_id, "plot_type"] == "Scatter Plot": # Set view cent norm data flag - def on_check_centnorm_change(): - key=f"key_check_centiles_{plot_id}" - sel_val=st.session_state[key] - df_plots.loc[plot_id, "plot_cent_normalized"]=sel_val + def on_check_centnorm_change() -> None: + key = f"key_check_centiles_{plot_id}" + sel_val = st.session_state[key] + df_plots.loc[plot_id, "plot_cent_normalized"] = sel_val if sel_val: - set_y_bounds(df, df_plots, plot_id, df_plots.loc[plot_id, "yvar"] + '_centiles') + set_y_bounds( + df, + df_plots, + plot_id, + df_plots.loc[plot_id, "yvar"] + "_centiles", + ) else: set_y_bounds(df, df_plots, plot_id, df_plots.loc[plot_id, "yvar"]) - y_new=df_plots.loc[plot_id, "yvar"] + '_centiles' + y_new = df_plots.loc[plot_id, "yvar"] + "_centiles" if y_new in df.columns: st.checkbox( "Plot ROIs normalized by centiles", value=df_plots.loc[plot_id, "plot_cent_normalized"], help="Show ROI values normalized by reference centiles", key=f"key_check_centiles_{plot_id}", - on_change=on_check_centnorm_change + on_change=on_check_centnorm_change, ) # Select trend - def on_trend_sel_change(): - key=f"trend_type_{plot_id}" - sel_val=st.session_state[key] - df_plots.loc[plot_id, "trend"]=sel_val + def on_trend_sel_change() -> None: + key = f"trend_type_{plot_id}" + sel_val = st.session_state[key] + df_plots.loc[plot_id, "trend"] = sel_val if sel_val == "Linear": df_plots.at[plot_id, "traces"] = add_items_to_list( df_plots.loc[plot_id, "traces"], ["lin_fit"] @@ -264,23 +274,24 @@ def on_trend_sel_change(): df_plots.at[plot_id, "traces"] = remove_items_from_list( df_plots.loc[plot_id, "traces"], ["lin_fit", "conf_95%"] ) - - tind = get_index_in_list(list_trends, df_plots.loc[plot_id, "trend"]) + + tind = get_index_in_list(list_trends, df_plots.loc[plot_id, "trend"]) st.selectbox( "Trend Line", list_trends, key=f"trend_type_{plot_id}", index=tind, - on_change=on_trend_sel_change + on_change=on_trend_sel_change, ) - # Select lowess smoothness - if df_plots.loc[plot_id, "trend"]=="Smooth LOWESS Curve": - def on_sel_lowessval_change(): - key=f"lowess_sm_{plot_id}" - sel_val=st.session_state[key] - df_plots.loc[plot_id, "lowess_s"]=sel_val - + # Select lowess smoothness + if df_plots.loc[plot_id, "trend"] == "Smooth LOWESS Curve": + + def on_sel_lowessval_change() -> None: + key = f"lowess_sm_{plot_id}" + sel_val = st.session_state[key] + df_plots.loc[plot_id, "lowess_s"] = sel_val + st.slider( "Smoothness", min_value=0.4, @@ -288,7 +299,7 @@ def on_sel_lowessval_change(): value=0.7, step=0.1, key=f"lowess_sm_{plot_id}", - on_change=on_sel_lowessval_change + on_change=on_sel_lowessval_change, ) if df_plots.loc[plot_id, "plot_type"] == "Distribution Plot": @@ -298,93 +309,100 @@ def on_sel_lowessval_change(): with ptabs[1]: if df_plots.loc[plot_id, "plot_type"] == "Scatter Plot": - - if df_plots.loc[plot_id, "xvar"] != 'Age': - st.warning('To view data centiles please select the "Age" as the x axis variable!') + + if df_plots.loc[plot_id, "xvar"] != "Age": + st.warning( + 'To view data centiles please select the "Age" as the x axis variable!' + ) else: # Select centile type - def on_centile_sel_change(): + def on_centile_sel_change() -> None: # Set centile selection to plot var - key=f"cent_type_{plot_id}" - sel_val=st.session_state[key] - df_plots.loc[plot_id, "centtype"]=sel_val + key = f"cent_type_{plot_id}" + sel_val = st.session_state[key] + df_plots.loc[plot_id, "centtype"] = sel_val if sel_val == "": df_plots.at[plot_id, "traces"] = remove_items_from_list( df_plots.loc[plot_id, "traces"], - st.session_state.plot_const['centile_trace_types'] + st.session_state.plot_const["centile_trace_types"], ) else: df_plots.at[plot_id, "traces"] = add_items_to_list( df_plots.loc[plot_id, "traces"], - st.session_state.plot_const['centile_trace_types'] + st.session_state.plot_const["centile_trace_types"], ) centtype = df_plots.loc[plot_id, "centtype"] centind = st.session_state.plot_const["centile_types"].index(centtype) - sel_options = st.selectbox( + st.selectbox( "Centile Type", st.session_state.plot_const["centile_types"], key=f"cent_type_{plot_id}", index=centind, - on_change=on_centile_sel_change + on_change=on_centile_sel_change, ) # Select group (hue) values - hvar=df_plots.loc[plot_id, "hvar"] + hvar = df_plots.loc[plot_id, "hvar"] if hvar != "": - def on_hvals_change(): - key=f"key_select_hvals_{plot_id}" - sel_val=st.session_state[key] - df_plots.at[plot_id, "hvals"]=sel_val - #if sel_val != '': - + + def on_hvals_change() -> None: + key = f"key_select_hvals_{plot_id}" + sel_val = st.session_state[key] + df_plots.at[plot_id, "hvals"] = sel_val + # if sel_val != '': + vals_hue = sorted(df[hvar].unique()) df_plots.at[plot_id, "hvals"] = st.multiselect( "Select groups", vals_hue, vals_hue, key=f"key_select_hvals_{plot_id}", - on_change=on_hvals_change + on_change=on_hvals_change, ) if df_plots.loc[plot_id, "plot_type"] == "Scatter Plot": - ## Update current list of traces - #df_plots.at[plot_id, "traces"] = [x for x in df_plots.loc[plot_id, "traces"] if x in list_traces] + # Update current list of traces + # df_plots.at[plot_id, "traces"] = [x for x in df_plots.loc[plot_id, "traces"] if x in list_traces] # Read user selection for traces - def on_scatter_trace_sel_change(): - key=f"key_sel_traces_scatter_{plot_id}" - sel_trace=st.session_state[key] - df_plots.at[plot_id, "traces"]=sel_trace + def on_scatter_trace_sel_change() -> None: + key = f"key_sel_traces_scatter_{plot_id}" + sel_trace = st.session_state[key] + df_plots.at[plot_id, "traces"] = sel_trace - list_traces = ['data'] + list_traces = ["data"] if df_plots.loc[plot_id, "trend"] == "Linear": - list_traces = list_traces + st.session_state.plot_const["linfit_trace_types"] + list_traces = ( + list_traces + st.session_state.plot_const["linfit_trace_types"] + ) if df_plots.loc[plot_id, "centtype"] != "": - list_traces = list_traces + st.session_state.plot_const["centile_trace_types"] + list_traces = ( + list_traces + st.session_state.plot_const["centile_trace_types"] + ) - sel_trace = st.multiselect( + st.multiselect( "Select traces", list_traces, df_plots.loc[plot_id, "traces"], key=f"key_sel_traces_scatter_{plot_id}", - on_change=on_scatter_trace_sel_change + on_change=on_scatter_trace_sel_change, ) if df_plots.loc[plot_id, "plot_type"] == "Distribution Plot": # Read user selection for traces - def on_dist_trace_sel_change(): - key=f"key_sel_trace_dist_{plot_id}" - sel_trace=st.session_state[key] - df_plots.at[plot_id, "traces"]=sel_trace + def on_dist_trace_sel_change() -> None: + key = f"key_sel_trace_dist_{plot_id}" + sel_trace = st.session_state[key] + df_plots.at[plot_id, "traces"] = sel_trace df_plots.at[plot_id, "traces"] = st.multiselect( "Select traces", st.session_state.plot_const["distplot_trace_types"], df_plots.loc[plot_id, "traces"], key=f"key_sel_trace_dist_{plot_id}", - on_change=on_dist_trace_sel_change + on_change=on_dist_trace_sel_change, ) # Tab 3: Reset parameters and/or delete plot @@ -413,8 +431,8 @@ def callback_plot_clicked() -> None: # Detect MRID from the click info and save to session_state hind = get_index_in_list(df.columns.tolist(), curr_plot["hvar"]) - - sel_info = st.session_state[f'bubble_chart_{plot_id}'] + + sel_info = st.session_state[f"bubble_chart_{plot_id}"] if len(sel_info["selection"]["points"]) > 0: sind = sel_info["selection"]["point_indices"][0] if hind is None: @@ -430,7 +448,7 @@ def callback_plot_clicked() -> None: st.session_state.sel_roi_img = sel_roi st.session_state.paths["sel_img"] = "" st.session_state.paths["sel_seg"] = "" - #st.rerun() + # st.rerun() # Main container for the plot with st.container(border=True): @@ -468,8 +486,10 @@ def callback_plot_clicked() -> None: if curr_plot["plot_cent_normalized"]: yvar = f'{curr_plot["yvar"]}_centiles' if yvar not in df.columns: - st.warning(f'Centile values not available for variable: {curr_plot["yvar"]}') - yvar=curr_plot["yvar"] + st.warning( + f'Centile values not available for variable: {curr_plot["yvar"]}' + ) + yvar = curr_plot["yvar"] # Add axis labels fig.update_layout( @@ -543,9 +563,9 @@ def callback_plot_clicked() -> None: yvar, curr_plot["ymin"], curr_plot["ymax"], - curr_plot['traces'], + curr_plot["traces"], st.session_state.plot_var["hide_legend"], - fig + fig, ) # Highlight selected data point @@ -566,12 +586,13 @@ def callback_plot_clicked() -> None: # Catch clicks on plot # - on_select: when clicked it will rerun and return the info - sel_info = st.plotly_chart( + st.plotly_chart( fig, key=f"bubble_chart_{plot_id}", on_select=callback_plot_clicked ) return fig + def display_dist_plot( df: pd.DataFrame, plot_id: str, show_settings: bool, sel_mrid: str ) -> Any: diff --git a/src/viewer/utils/utils_session.py b/src/viewer/utils/utils_session.py index 1eb1c1d..e968dd6 100644 --- a/src/viewer/utils/utils_session.py +++ b/src/viewer/utils/utils_session.py @@ -1,14 +1,16 @@ import os +import shutil +from typing import Any +import jwt import pandas as pd import plotly.express as px import streamlit as st -import utils.utils_rois as utilroi import utils.utils_io as utilio +import utils.utils_rois as utilroi from PIL import Image -from streamlit.web.server.websocket_headers import _get_websocket_headers -import jwt +# from streamlit.web.server.websocket_headers import _get_websocket_headers def config_page() -> None: @@ -25,17 +27,21 @@ def config_page() -> None: }, ) -## Function to parse AWS login (if available) -def process_session_token(): - #headers = _get_websocket_headers() + +# Function to parse AWS login (if available) +def process_session_token() -> Any: + # headers = _get_websocket_headers() headers = st.context.headers if not headers or "X-Amzn-Oidc-Data" not in headers: return {} return jwt.decode( - headers["X-Amzn-Oidc-Data"], algorithms=["ES256"], options={"verify_signature": False} + headers["X-Amzn-Oidc-Data"], + algorithms=["ES256"], + options={"verify_signature": False}, ) - -def process_session_user_id(): + + +def process_session_user_id() -> Any: headers = st.context.headers if not headers or "X-Amzn-Oidc-Identity" not in headers: return "NO_USER_FOUND" @@ -59,9 +65,9 @@ def init_session_state() -> None: "cloud": {"msg_infile": "Upload"}, "desktop": {"msg_infile": "Select"}, } - + # Store user session info for later retrieval - if st.session_state.app_type == 'cloud': + if st.session_state.app_type == "cloud": st.session_state.cloud_session_token = process_session_token() if st.session_state.cloud_session_token: st.session_state.has_cloud_session = True @@ -70,8 +76,7 @@ def init_session_state() -> None: st.session_state.has_cloud_session = False else: st.session_state.has_cloud_session = False - - + ################################### ################################### @@ -111,6 +116,11 @@ def init_session_state() -> None: "dlmuse_run": False, "dlmuse_view": False, "dlmuse_download": False, + "dlwmls_wdir": False, + "dlwmls_in": False, + "dlwmls_run": False, + "dlwmls_view": False, + "dlwmls_download": False, "ml_wdir": False, "ml_inrois": False, "ml_indemog": False, @@ -119,7 +129,7 @@ def init_session_state() -> None: "view_wdir": False, "view_in": False, "view_select": False, - "view_plot": False + "view_plot": False, } # Flags for various i/o @@ -208,11 +218,34 @@ def init_session_state() -> None: st.session_state.paths["dir_out"] = os.path.join( st.session_state.paths["root"], "output_folder" ) - - + if not os.path.exists(st.session_state.paths["dir_out"]): os.makedirs(st.session_state.paths["dir_out"]) + # Copy demo folders into user folders as needed + if st.session_state.has_cloud_session: + # Copy demo dirs to user folder (TODO: make this less hardcoded) + demo_dir_paths = [ + os.path.join( + st.session_state.paths["root"], + "output_folder", + "NiChart_sMRI_Demo1", + ), + os.path.join( + st.session_state.paths["root"], + "output_folder", + "NiChart_sMRI_Demo2", + ), + ] + for demo in demo_dir_paths: + demo_name = os.path.basename(demo) + destination_path = os.path.join( + st.session_state.paths["dir_out"], demo_name + ) + if os.path.exists(destination_path): + shutil.rmtree(destination_path) + shutil.copytree(demo, destination_path, dirs_exist_ok=True) + ############ # FIXME : set init folder to test folder outside repo st.session_state.paths["init"] = os.path.join( @@ -281,7 +314,11 @@ def init_session_state() -> None: "centile_types": ["", "CN", "CN_Males", "CN_Females", "CN_ICV_Corrected"], "linfit_trace_types": ["lin_fit", "conf_95%"], "centile_trace_types": [ - "centile_5", "centile_25", "centile_50", "centile_75", "centile_95" + "centile_5", + "centile_25", + "centile_50", + "centile_75", + "centile_95", ], "distplot_trace_types": ["histogram", "density", "rug"], "min_per_row": 1, @@ -453,7 +490,6 @@ def update_default_paths() -> None: st.session_state.plot_var["df_data"] = pd.DataFrame() - def reset_flags() -> None: """ Resets flags if the working dir changed @@ -461,7 +497,7 @@ def reset_flags() -> None: for tmp_key in st.session_state.flags.keys(): st.session_state.flags[tmp_key] = False st.session_state.flags["dset"] = True - + # Check dicom folder fcount = utilio.get_file_count(st.session_state.paths["dicom"]) if fcount > 0: @@ -473,25 +509,25 @@ def reset_plots() -> None: Reset plot variables when data file changes """ st.session_state.plots = pd.DataFrame(columns=st.session_state.plots.columns) - st.session_state.checkbox['view_select']=False - st.session_state.checkbox['view_plot']=False + st.session_state.checkbox["view_select"] = False + st.session_state.checkbox["view_plot"] = False st.session_state.plot_sel_vars = [] - st.session_state.plot_var['hide_settings'] = False - st.session_state.plot_var['hide_legend'] = False - st.session_state.plot_var['show_img'] = False - st.session_state.plot_var['plot_type'] = False - st.session_state.plot_var['xvar'] = '' - st.session_state.plot_var['xmin'] = -1.0 - st.session_state.plot_var['xmax'] = -1.0 - st.session_state.plot_var['yvar'] = '' - st.session_state.plot_var['ymin'] = -1.0 - st.session_state.plot_var['ymax'] = -1.0 - st.session_state.plot_var['hvar'] = '' - st.session_state.plot_var['hvals'] = [] - st.session_state.plot_var['corr_icv'] = False - st.session_state.plot_var['plot_cent_normalized'] = False - st.session_state.plot_var['trend'] = 'Linear' - st.session_state.plot_var['traces'] = ["data", "lin_fit"] - st.session_state.plot_var['lowess_s'] = 0.5 - st.session_state.plot_var['centtype'] = '' - st.session_state.plot_var['h_coeff'] = 1.0 + st.session_state.plot_var["hide_settings"] = False + st.session_state.plot_var["hide_legend"] = False + st.session_state.plot_var["show_img"] = False + st.session_state.plot_var["plot_type"] = False + st.session_state.plot_var["xvar"] = "" + st.session_state.plot_var["xmin"] = -1.0 + st.session_state.plot_var["xmax"] = -1.0 + st.session_state.plot_var["yvar"] = "" + st.session_state.plot_var["ymin"] = -1.0 + st.session_state.plot_var["ymax"] = -1.0 + st.session_state.plot_var["hvar"] = "" + st.session_state.plot_var["hvals"] = [] + st.session_state.plot_var["corr_icv"] = False + st.session_state.plot_var["plot_cent_normalized"] = False + st.session_state.plot_var["trend"] = "Linear" + st.session_state.plot_var["traces"] = ["data", "lin_fit"] + st.session_state.plot_var["lowess_s"] = 0.5 + st.session_state.plot_var["centtype"] = "" + st.session_state.plot_var["h_coeff"] = 1.0 diff --git a/src/viewer/utils/utils_st.py b/src/viewer/utils/utils_st.py index b7c0d94..d52a679 100644 --- a/src/viewer/utils/utils_st.py +++ b/src/viewer/utils/utils_st.py @@ -1,11 +1,11 @@ import os +import shutil from typing import Any import numpy as np import streamlit as st import utils.utils_io as utilio import utils.utils_session as utilses -import shutil # from wfork_streamlit_profiler import Profiler # import pyinstrument @@ -16,10 +16,7 @@ def user_input_textfield( - label: str, - init_val: str, - help_msg: str, - flag_disabled: bool + label: str, init_val: str, help_msg: str, flag_disabled: bool ) -> Any: """ Single text field to read a text input from the user @@ -31,6 +28,7 @@ def user_input_textfield( ) return out_text + def user_input_select( label: Any, key: Any, @@ -54,6 +52,7 @@ def user_input_select( ) return out_sel + def user_input_multiselect( label: str, key: Any, @@ -68,15 +67,11 @@ def user_input_multiselect( tmpcol = st.columns((COL_LEFT, COL_RIGHT_EMPTY)) with tmpcol[0]: out_sel = st.multiselect( - label, - options, - init_val, - key=key, - help=help_msg, - disabled=flag_disabled + label, options, init_val, key=key, help=help_msg, disabled=flag_disabled ) return out_sel + def user_input_filename( label_btn: Any, key_st: Any, @@ -93,11 +88,7 @@ def user_input_filename( # Button to select file with tmpcol[1]: - if st.button( - label_btn, - key=f"key_btn_{key_st}", - use_container_width=True - ): + if st.button(label_btn, key=f"key_btn_{key_st}", use_container_width=True): tmp_sel = utilio.browse_file(search_dir) if tmp_sel is not None and os.path.exists(tmp_sel): out_path = os.path.abspath(tmp_sel) @@ -112,9 +103,10 @@ def user_input_filename( ) if os.path.exists(tmp_sel): out_path = tmp_sel - + return out_path + def user_input_foldername( label_btn: Any, key_st: Any, @@ -131,11 +123,7 @@ def user_input_foldername( # Button to select folder with tmpcol[1]: - if st.button( - label_btn, - key=f"btn_{key_st}", - use_container_width=True - ): + if st.button(label_btn, key=f"btn_{key_st}", use_container_width=True): tmp_sel = utilio.browse_folder(search_dir) if tmp_sel is not None and os.path.exists(tmp_sel): out_path = os.path.abspath(tmp_sel) @@ -143,10 +131,7 @@ def user_input_foldername( # Text field to select folder with tmpcol[0]: tmp_sel = st.text_input( - label_txt, - key=f"txt2_{key_st}", - value=out_path, - help=help_msg + label_txt, key=f"txt2_{key_st}", value=out_path, help=help_msg ) if os.path.exists(tmp_sel): out_path = os.path.abspath(tmp_sel) @@ -195,24 +180,29 @@ def show_img3D( else: st.image(img[:, :, slice_index], width=w_img) -def util_get_help(s_title, s_text) -> None: + +def util_get_help(s_title: str, s_text: str) -> None: @st.dialog(s_title) # type:ignore def help_working_dir(): st.markdown(s_text) + col1, col2 = st.columns([0.5, 0.1]) with col2: - if st.button('Get help 🤔', key='key_btn_help_' + s_title, use_container_width=True): + if st.button( + "Get help 🤔", key="key_btn_help_" + s_title, use_container_width=True + ): help_working_dir() + def util_workingdir_get_help() -> None: @st.dialog("Working Directory") # type:ignore def help_working_dir(): st.markdown( """ - A NiChart pipeline executes a series of steps, with input/output files organized in a predefined folder structure. - + - Results for an **experiment** (a new analysis on a new dataset) are kept in a dedicated **working directory**. - + - Set an **"output path"** (desktop app only) and an **"experiment name"** to define the **working directory** for your analysis. You only need to set the working directory once. - The **experiment name** can be any identifier that describes your analysis or data; it does not need to match the input study or data folder name. @@ -223,13 +213,16 @@ def help_working_dir(): st.warning( """ On the cloud app, uploaded data and results of experiments are deleted in regular intervals! - + Accordingly, the data for a previous experiment may not be available. """ ) + col1, col2 = st.columns([0.5, 0.1]) with col2: - if st.button('Get help 🤔', key='key_btn_help_working_dir', use_container_width=True): + if st.button( + "Get help 🤔", key="key_btn_help_working_dir", use_container_width=True + ): help_working_dir() @@ -259,33 +252,19 @@ def util_panel_workingdir(app_type: str) -> None: st.write("Experiment name") tmp_cols = st.columns(2) with tmp_cols[0]: - helpmsg = ( - "Will set the working directory to an existing experiment, with all the data previously uploaded or generated." - ) - list_exp = [''] + utilio.get_subfolders( - st.session_state.paths["dir_out"] - ) + helpmsg = "Will set the working directory to an existing experiment, with all the data previously uploaded or generated." + list_exp = [""] + utilio.get_subfolders(st.session_state.paths["dir_out"]) sel_ind = list_exp.index(st.session_state.dset) - sel_tmp = st.selectbox( - 'Select existing', - list_exp, - sel_ind, - help=helpmsg - ) - if sel_tmp is not None and sel_tmp != '': + sel_tmp = st.selectbox("Select existing", list_exp, sel_ind, help=helpmsg) + if sel_tmp is not None and sel_tmp != "": st.session_state.dset = sel_tmp with tmp_cols[1]: - helpmsg = ( - "Will create a dedicated working directory for a new experiment. All input and output data associated with the analysis will be stored in the new working directory." - ) + helpmsg = "Will create a dedicated working directory for a new experiment. All input and output data associated with the analysis will be stored in the new working directory." st.session_state.dset = st.text_input( - "Create new", - st.session_state.dset, - help=helpmsg + "Create new", st.session_state.dset, help=helpmsg ) - # Create results folder if st.session_state.dset != "" and st.session_state.paths["dir_out"] != "": st.session_state.paths["dset"] = os.path.join( @@ -302,6 +281,7 @@ def util_panel_workingdir(app_type: str) -> None: utilses.update_default_paths() utilses.reset_flags() + def copy_uploaded_to_dir() -> None: # Copies files to local storage if len(st.session_state["uploaded_input"]) > 0: @@ -311,10 +291,7 @@ def copy_uploaded_to_dir() -> None: def util_upload_folder( - dir_out: str, - title_txt: str, - flag_disabled: bool, - help_txt: str + dir_out: str, title_txt: str, flag_disabled: bool, help_txt: str ) -> None: """ Upload user data to target folder @@ -369,6 +346,7 @@ def util_upload_file( utilio.copy_uploaded_file(in_file, out_file) return True + def util_select_folder( key_selector: str, title_txt: str, @@ -404,11 +382,11 @@ def util_select_folder( os.unlink(dir_out) else: shutil.rmtree(dir_out) - + # Create parent dir of output dir if not os.path.exists(os.path.dirname(dir_out)): os.makedirs(os.path.dirname(dir_out)) - + # Link user input dicoms if not os.path.exists(dir_out): os.symlink(sel_dir, dir_out) @@ -461,13 +439,22 @@ def add_debug_panel() -> None: if not st.session_state.forced_cloud: st.sidebar.divider() st.sidebar.write("*** Debugging Flags ***") - is_cloud_mode = (st.session_state.app_type == "cloud") + is_cloud_mode = st.session_state.app_type == "cloud" if st.sidebar.checkbox("Switch to cloud?", value=is_cloud_mode): st.session_state.app_type = "cloud" else: st.session_state.app_type = "desktop" - list_vars = ["", "All", "plots", "plot_var", "rois", "paths", "flags", "checkbox"] + list_vars = [ + "", + "All", + "plots", + "plot_var", + "rois", + "paths", + "flags", + "checkbox", + ] # list_vars = st.session_state.keys() sel_var = st.sidebar.selectbox("View session state vars", list_vars, index=0) if sel_var != "": diff --git a/src/viewer/utils/utils_stats.py b/src/viewer/utils/utils_stats.py index 647860b..81d09c4 100644 --- a/src/viewer/utils/utils_stats.py +++ b/src/viewer/utils/utils_stats.py @@ -97,7 +97,7 @@ def linreg_model(df: pd.DataFrame, xvar: str, yvar: str, hvar: str) -> Any: # return dict_out -#@st.cache_data() # type:ignore +# @st.cache_data() # type:ignore def lowess_model( df: pd.DataFrame, xvar: str, yvar: str, hvar: str, lowess_s: float ) -> Any: diff --git a/src/viewer/utils/utils_trace.py b/src/viewer/utils/utils_trace.py index 38aecd3..8847d85 100644 --- a/src/viewer/utils/utils_trace.py +++ b/src/viewer/utils/utils_trace.py @@ -20,7 +20,7 @@ def dist_plot( ) -> Any: # Set colormap colors = st.session_state.plot_colors["data"] - + # Add a tmp column if group var is not set dft = df.copy() if hvar == "": @@ -36,9 +36,7 @@ def dist_plot( bin_sizes = [] colors_sel = [] for hname in hvals: - col_ind = vals_hue_all.index( - hname - ) # Select index of colour for the category + col_ind = vals_hue_all.index(hname) # Select index of colour for the category dfh = dft[dft[hvar] == hname] x_tmp = dfh[xvar] x_range = x_tmp.max() - x_tmp.min() @@ -242,6 +240,7 @@ def lowess_trace( fig.update_layout(xaxis_range=[xmin, xmax]) fig.update_layout(yaxis_range=[ymin, ymax]) + def dot_trace( df: pd.DataFrame, sel_mrid: str, xvar: str, yvar: str, hide_legend: bool, fig: Any ) -> Any: @@ -269,7 +268,7 @@ def percentile_trace( ymax: float, traces: list, hide_legend: bool, - fig: Any + fig: Any, ) -> Any: # Set colormap @@ -299,11 +298,7 @@ def percentile_trace( return fig -def dots_trace( - df: pd.DataFrame, - xvar: str, - yvar: str -) -> Any: +def dots_trace(df: pd.DataFrame, xvar: str, yvar: str) -> Any: trace = go.Scatter( x=df[xvar], y=df[yvar],