Skip to content

Commit

Permalink
address review comments by JeeH-K
Browse files Browse the repository at this point in the history
  • Loading branch information
axelwalter committed Feb 14, 2024
1 parent 3643a7f commit 7c939da
Show file tree
Hide file tree
Showing 8 changed files with 54 additions and 176 deletions.
154 changes: 8 additions & 146 deletions pages/6_📖_TOPP-Workflow_Docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from src.workflow.Files import Files
from src.workflow.CommandExecutor import CommandExecutor
from src.common import page_setup
from inspect import getsource

page_setup()

Expand Down Expand Up @@ -70,76 +71,7 @@

with st.expander("**Complete example for custom Workflow class**", expanded=False):
st.code(
"""
import streamlit as st
from .workflow.WorkflowManager import WorkflowManager
from .workflow.Files import Files
class TOPPWorkflow(WorkflowManager):
# Setup pages for upload, parameter settings and define workflow steps.
# For layout use any streamlit components such as tabs (as shown in example), columns, or even expanders.
def __init__(self):
# Initialize the parent class with the workflow name.
super().__init__("TOPP Workflow")
def upload(self):
t = st.tabs(["MS data", "Example with fallback data"])
with t[0]:
# Use the upload method from StreamlitUI to handle mzML file uploads.
self.ui.upload(key="mzML-files", name="MS data", file_type="mzML")
with t[1]:
# Example with fallback data (not used in workflow)
self.ui.upload(key="image", file_type="png", fallback="assets/OpenMS.png")
def input(self) -> None:
# Allow users to select mzML files for the analysis.
self.ui.select_input_file("mzML-files", multiple=True)
# Create tabs for different analysis steps.
t = st.tabs(
["**Feature Detection**", "**Adduct Detection**", "**SIRIUS Export**"]
)
with t[0]:
self.ui.input_TOPP("FeatureFinderMetabo")
with t[1]:
self.ui.input("run-adduct-detection", True, "Adduct Detection")
self.ui.input_TOPP("MetaboliteAdductDecharger")
with t[2]:
self.ui.input_TOPP("SiriusExport")
def workflow(self) -> None:
# Wrap mzML files into a Files object for processing.
in_mzML = Files(self.params["mzML-files"], "mzML")
self.logger.log(f"Number of input mzML files: {len(in_mzML)}")
# Prepare output files for feature detection.
out_ffm = Files(in_mzML, "featureXML", "feature-detection")
# Run FeatureFinderMetabo tool with input and output files.
self.executor.run_topp(
"FeatureFinderMetabo", {"in": in_mzML, "out": out_ffm}, False
)
# Check if adduct detection should be run.
if self.params["run-adduct-detection"]:
# Run MetaboliteAdductDecharger for adduct detection.
self.executor.run_topp(
"MetaboliteAdductDecharger", {"in": out_ffm, "out_fm": out_ffm}, False
)
# Combine input files for SiriusExport.
in_mzML.combine()
out_ffm.combine()
# Prepare output files for SiriusExport.
out_se = Files(["sirius-export.ms"], "ms", "sirius-export")
# Run SiriusExport tool with the combined files.
self.executor.run_topp(
"SiriusExport",
{"in": in_mzML, "in_featureinfo": out_ffm, "out": out_se},
False,
)
"""
getsource(Workflow)
)


Expand All @@ -161,18 +93,7 @@ def workflow(self) -> None:
""")

st.code(
"""
# Overwrite the upload method in your workflow class.
class YourWorkflow(WorkflowManager):
def upload(self) -> None:
t = st.tabs(["MS data", "Example with fallback data"])
with t[0]:
# Use the upload method from StreamlitUI to handle mzML file uploads.
self.ui.upload(key="mzML-files", name="MS data", file_type="mzML")
with t[1]:
# Example with fallback data (not used in workflow).
self.ui.upload(key="image", file_type="png", fallback="assets/OpenMS.png")
"""
getsource(Workflow.upload)
)
st.info("💡 Use the same **key** for parameter widgets, to select which of the uploaded files to use for analysis.")

Expand All @@ -187,15 +108,15 @@ def upload(self) -> None:
Generating parameter input widgets is done with the `self.ui.input` method for any parameter and the `self.ui.input_TOPP` method for TOPP tools.
**1. Choose `self.ui.input` for any paramter not-related to a TOPP tool or `self.ui.select_input_file` for any input file:**
**1. Choose `self.ui.input_widget` for any paramter not-related to a TOPP tool or `self.ui.select_input_file` for any input file:**
It takes the obligatory **key** parameter. The key is used to access the parameter value in the workflow parameters dictionary `self.params`. Default values do not need to be specified in a separate file. Instead they are determined from the widgets default value automatically. Widget types can be specified or automatically determined from **default** and **options** parameters. It's suggested to add a **help** text and other parameters for numerical input.
Make sure to match the **key** of the upload widget when calling `self.ui.input_TOPP`.
**2. Choose `self.ui.input_TOPP` to automatically generate complete input sections for a TOPP tool:**
It takes the obligatory **topp_tool_name** parameter and generates input widgets for each parameter present in the **ini** file (automatically created) except for input and output file parameters. For all input file parameters a widget needs to be created with `self.ui.input` with an appropriate **key**. For TOPP tool parameters only non-default values are stored.
It takes the obligatory **topp_tool_name** parameter and generates input widgets for each parameter present in the **ini** file (automatically created) except for input and output file parameters. For all input file parameters a widget needs to be created with `self.ui.select_input_file` with an appropriate **key**. For TOPP tool parameters only non-default values are stored.
**3. Choose `self.ui.input_python` to automatically generate complete input sections for a custom Python tool:**
Expand All @@ -221,36 +142,12 @@ def upload(self) -> None:
""")

st.code(
"""
def parameter(self) -> None:
# Allow users to select mzML files for the analysis.
self.ui.select_input_file("mzML-files", multiple=True)
# Create tabs for different analysis steps.
t = st.tabs(
["**Feature Detection**", "**Adduct Detection**", "**SIRIUS Export**", "**Python Custom Tool**"]
)
with t[0]:
# Parameters for FeatureFinderMetabo TOPP tool.
self.ui.input_TOPP("FeatureFinderMetabo")
with t[1]:
# A single checkbox widget for workflow logic.
self.ui.input("run-adduct-detection", False, "Adduct Detection")
# Paramters for MetaboliteAdductDecharger TOPP tool.
self.ui.input_TOPP("MetaboliteAdductDecharger")
with t[2]:
# Paramters for SiriusExport TOPP tool
self.ui.input_TOPP("SiriusExport")
with t[3]:
# Generate input widgets for a custom Python tool, located at src/python-tools.
# Parameters are specified within the file in the DEFAULTS dictionary.
self.ui.input_python("example")
"""
getsource(Workflow.configure)
)
st.info("💡 Access parameter widget values by their **key** in the `self.params` object, e.g. `self.params['mzML-files']` will give all selected mzML files.")

with st.expander("**Code documentation**", expanded=True):
st.help(StreamlitUI.input)
st.help(StreamlitUI.input_widget)
st.help(StreamlitUI.select_input_file)
st.help(StreamlitUI.input_TOPP)
st.help(StreamlitUI.input_python)
Expand Down Expand Up @@ -351,42 +248,7 @@ def parameter(self) -> None:
st.markdown("**Example for a complete workflow section:**")

st.code(
"""
def execution(self) -> None:
# Wrap mzML files into a Files object for processing.
in_mzML = Files(self.params["mzML-files"], "mzML")
# Log any messages.
self.logger.log(f"Number of input mzML files: {len(in_mzML)}")
self.logger.log(in_mzML)
# Prepare output files for feature detection.
out_ffm = Files(in_mzML, "featureXML", "feature-detection")
self.logger.log(in_mzML)
# Run FeatureFinderMetabo tool with input and output files.
self.executor.run_topp(
"FeatureFinderMetabo", input_output={"in": in_mzML, "out": out_ffm}
)
# Check if adduct detection should be run.
if self.params["run-adduct-detection"]:
# Run MetaboliteAdductDecharger for adduct detection, with disabled logs.
# Without a new Files object for output, the input files will be overwritten in this case.
self.executor.run_topp(
"MetaboliteAdductDecharger", {"in": out_ffm, "out_fm": out_ffm}, write_log=False
)
# Example for a custom Python tool, which is located in src/python-tools.
self.executor.run_python("example", {"in": in_mzML})
# Prepare output file for SiriusExport.
out_se = Files(["sirius-export.ms"], "ms", "sirius-export")
# Run SiriusExport tool with the collected files.
self.executor.run_topp("SiriusExport", {"in": in_mzML.collect(), "in_featureinfo": out_ffm.collect(), "out": out_se})
"""
getsource(Workflow.execution)
)

with st.expander("**Example output (truncated) of the workflow code above**"):
Expand Down
6 changes: 3 additions & 3 deletions src/Workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
class Workflow(WorkflowManager):
# Setup pages for upload, parameter, execution and results.
# For layout use any streamlit components such as tabs (as shown in example), columns, or even expanders.
def __init__(self):
def __init__(self) -> None:
# Initialize the parent class with the workflow name.
super().__init__("TOPP Workflow")

def upload(self):
def upload(self)-> None:
t = st.tabs(["MS data", "Example with fallback data"])
with t[0]:
# Use the upload method from StreamlitUI to handle mzML file uploads.
Expand All @@ -31,7 +31,7 @@ def configure(self) -> None:
self.ui.input_TOPP("FeatureFinderMetabo")
with t[1]:
# A single checkbox widget for workflow logic.
self.ui.input("run-adduct-detection", False, "Adduct Detection")
self.ui.input_widget("run-adduct-detection", False, "Adduct Detection")
# Paramters for MetaboliteAdductDecharger TOPP tool.
self.ui.input_TOPP("MetaboliteAdductDecharger")
with t[2]:
Expand Down
1 change: 1 addition & 0 deletions src/python-tools/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__pycache__
Binary file removed src/python-tools/__pycache__/example.cpython-311.pyc
Binary file not shown.
21 changes: 9 additions & 12 deletions src/workflow/CommandExecutor.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,7 @@ def run_topp(self, tool: str, input_output: dict, write_log: bool = True) -> Non
Args:
tool (str): The executable name or path of the tool.
input_output (dict): A dictionary specifying the input and output
parameters and their corresponding files. The files
can be specified as single paths (strings) or lists
of paths for batch processing.
input_output (dict): A dictionary specifying the input/output parameter names (as key) and their corresponding file paths (as value).
write_log (bool): If True, enables logging of command execution details.
Raises:
Expand All @@ -149,20 +146,23 @@ def run_topp(self, tool: str, input_output: dict, write_log: bool = True) -> Non
commands = []

# Load parameters for non-defaults
params = self.parameter_manager.load_parameters()
params = self.parameter_manager.get_parameters_from_json()
# Construct commands for each process
for i in range(n_processes):
command = [tool]
# Add input/output files
for k in input_output.keys():
# add key as parameter name
command += [f"-{k}"]
# get value from input_output dictionary
value = input_output[k]
if isinstance(value, Files):
value = value.files
# when multiple input/output files exist (e.g., multiple mzMLs and featureXMLs), but only one additional input file (e.g., one input database file)
if len(value) == 1:
i = 0
# when the entry is a list of collected files to be passed as one [["sample1", "sample2"]]
if isinstance(value[i], list):
command += value[i]
# standard case, files was a list of strings, take the file name at index
else:
command += [value[i]]
# Add non-default TOPP tool parameters
Expand Down Expand Up @@ -211,10 +211,7 @@ def run_python(self, script_file: str, input_output: dict = {}, write_log: bool
script_file (str): The name or path of the Python script to be executed.
If the path is omitted, the method looks for the script in 'src/python-tools/'.
The '.py' extension is appended if not present.
input_output (dict, optional): A dictionary mapping parameter names to their
values. These parameters are passed to the script, overriding any default
values specified within the script. If a parameter value is an instance of
a Files object, its 'files' attribute is used as the parameter value. Defaults to {}.
input_output (dict, optional): A dictionary specifying the input/output parameter names (as key) and their corresponding file paths (as value). Defaults to {}.
write_log (bool, optional): If True, the execution process is logged. This
includes any output generated by the script as well as any errors. Defaults to True.
"""
Expand All @@ -236,7 +233,7 @@ def run_python(self, script_file: str, input_output: dict = {}, write_log: bool
spec.loader.exec_module(module)
defaults = {entry["key"]: entry["value"] for entry in getattr(module, "DEFAULTS", None)}
# load paramters from JSON file
params = {k: v for k, v in self.parameter_manager.load_parameters().items() if path.name in k}
params = {k: v for k, v in self.parameter_manager.get_parameters_from_json().items() if path.name in k}
# update defaults
for k, v in params.items():
defaults[k.replace(f"{path.name}:", "")] = v
Expand Down
7 changes: 2 additions & 5 deletions src/workflow/ParameterManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,7 @@ def save_parameters(self) -> None:
ini_value = param.getValue(ini_key)
# need to convert bool values to string values
if isinstance(value, bool):
if value == True:
value = "true"
elif value == False:
value = "false"
value = "true" if value else "false"
# convert strings with newlines to list
if isinstance(value, str):
if "\n" in value:
Expand All @@ -78,7 +75,7 @@ def save_parameters(self) -> None:
with open(self.params_file, "w", encoding="utf-8") as f:
json.dump(json_params, f, indent=4)

def load_parameters(self) -> None:
def get_parameters_from_json(self) -> None:
"""
Loads parameters from the JSON file if it exists and returns them as a dictionary.
If the file does not exist, it returns an empty dictionary.
Expand Down
19 changes: 10 additions & 9 deletions src/workflow/StreamlitUI.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class StreamlitUI:
def __init__(self, workflow_manager):
self.workflow_manager = workflow_manager
self.workflow_dir = workflow_manager.workflow_dir
self.params = self.workflow_manager.parameter_manager.load_parameters()
self.params = self.workflow_manager.parameter_manager.get_parameters_from_json()

def upload(
self,
Expand Down Expand Up @@ -104,6 +104,7 @@ def upload(
for i, f in enumerate(files):
my_bar.progress((i + 1) / len(files))
shutil.copy(f, Path(files_dir, f.name))
my_bar.empty()
st.success("Successfully copied files!")

if fallback:
Expand Down Expand Up @@ -172,15 +173,15 @@ def select_input_file(
self.params[key] = [f for f in self.params[key] if f in options]

widget_type = "multiselect" if multiple else "selectbox"
self.input(
self.input_widget(
key,
name=name,
widget_type=widget_type,
options=options,
display_file_path=display_file_path,
)

def input(
def input_widget(
self,
key: str,
default: Any = None,
Expand Down Expand Up @@ -328,7 +329,7 @@ def format_files(input: Any) -> List[str]:
if isinstance(value, bool):
st.checkbox(name, value=value, key=key, help=help)
elif isinstance(value, (int, float)):
self.input(
self.input_widget(
key,
value,
widget_type="number",
Expand All @@ -339,7 +340,7 @@ def format_files(input: Any) -> List[str]:
help=help,
)
elif (isinstance(value, str) or value == None) and options is not None:
self.input(
self.input_widget(
key,
value,
widget_type="selectbox",
Expand All @@ -348,7 +349,7 @@ def format_files(input: Any) -> List[str]:
help=help,
)
elif isinstance(value, list) and options is not None:
self.input(
self.input_widget(
key,
value,
widget_type="multiselect",
Expand All @@ -357,9 +358,9 @@ def format_files(input: Any) -> List[str]:
help=help,
)
elif isinstance(value, bool):
self.input(key, value, widget_type="checkbox", name=name, help=help)
self.input_widget(key, value, widget_type="checkbox", name=name, help=help)
else:
self.input(key, value, widget_type="text", name=name, help=help)
self.input_widget(key, value, widget_type="text", name=name, help=help)

else:
st.error(f"Unsupported widget type '{widget_type}'")
Expand Down Expand Up @@ -575,7 +576,7 @@ def input_python(
with cols[i]:
if isinstance(value, bool):
st.markdown("#")
self.input(
self.input_widget(
key=key,
default=value,
name=name,
Expand Down
Loading

0 comments on commit 7c939da

Please sign in to comment.