Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions docs/example_multiple_instances.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
"""
Example workflow demonstrating the use of the same TOPP tool multiple times
with different configurations using tool_instance_name parameter.

This example shows how to use IDFilter twice in a DDA-TMT workflow:
1. First with strict filtering (PSM FDR 0.01)
2. Then with lenient filtering (PSM FDR 0.05)
"""

import streamlit as st
from src.workflow.WorkflowManager import WorkflowManager
from pathlib import Path


class MultipleToolInstancesExample(WorkflowManager):
"""Example workflow showing how to use the same tool multiple times."""

def __init__(self) -> None:
super().__init__("Multiple Tool Instances Example", st.session_state["workspace"])

def upload(self) -> None:
"""Upload idXML files for filtering."""
self.ui.upload_widget(
key="idXML-files",
name="Identification files",
file_types="idXML",
)

@st.fragment
def configure(self) -> None:
"""Configure parameters for two IDFilter instances."""
# Select input files
self.ui.select_input_file("idXML-files", multiple=True)

# Create tabs for two different filtering stages
t = st.tabs(["**Strict Filtering**", "**Lenient Filtering**"])

with t[0]:
st.info("First filtering stage with strict FDR threshold")
# First instance of IDFilter with custom defaults for strict filtering
self.ui.input_TOPP(
"IDFilter",
tool_instance_name="IDFilter-strict",
custom_defaults={"score:pep": 0.01}, # Strict FDR threshold
)

with t[1]:
st.info("Second filtering stage with lenient FDR threshold")
# Second instance of IDFilter with custom defaults for lenient filtering
self.ui.input_TOPP(
"IDFilter",
tool_instance_name="IDFilter-lenient",
custom_defaults={"score:pep": 0.05}, # Lenient FDR threshold
)

def execution(self) -> None:
"""Execute workflow with two different IDFilter instances."""
# Check if files are selected
if not self.params["idXML-files"]:
self.logger.log("ERROR: No idXML files selected.")
return

# Get input files
in_files = self.file_manager.get_files(self.params["idXML-files"])
self.logger.log(f"Processing {len(in_files)} identification files")

# First filtering stage: Strict filtering
self.logger.log("Running strict filtering (FDR 0.01)...")
out_strict = self.file_manager.get_files(
in_files, "idXML", "strict-filtering"
)
self.executor.run_topp(
"IDFilter",
input_output={"in": in_files, "out": out_strict},
tool_instance_name="IDFilter-strict" # Use strict instance parameters
)

# Second filtering stage: Lenient filtering
self.logger.log("Running lenient filtering (FDR 0.05)...")
out_lenient = self.file_manager.get_files(
in_files, "idXML", "lenient-filtering"
)
self.executor.run_topp(
"IDFilter",
input_output={"in": in_files, "out": out_lenient},
tool_instance_name="IDFilter-lenient" # Use lenient instance parameters
)

self.logger.log("Filtering completed!")

@st.fragment
def results(self) -> None:
"""Display results."""
strict_dir = Path(self.workflow_dir, "results", "strict-filtering")
lenient_dir = Path(self.workflow_dir, "results", "lenient-filtering")

if strict_dir.exists() and lenient_dir.exists():
st.success("Both filtering stages completed successfully!")

col1, col2 = st.columns(2)
with col1:
st.subheader("Strict Filtering Results")
strict_files = list(strict_dir.glob("*.idXML"))
st.info(f"Files created: {len(strict_files)}")
for f in strict_files:
st.write(f"- {f.name}")

with col2:
st.subheader("Lenient Filtering Results")
lenient_files = list(lenient_dir.glob("*.idXML"))
st.info(f"Files created: {len(lenient_files)}")
for f in lenient_files:
st.write(f"- {f.name}")
else:
st.warning("No results yet. Please run the workflow first.")
26 changes: 26 additions & 0 deletions docs/toppframework.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,32 @@ def content():

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.

**Using the same TOPP tool multiple times:**

When using the same TOPP tool multiple times in a workflow with different parameter configurations, provide a unique **tool_instance_name** to distinguish between the instances:

```python
# First instance of IDFilter with strict filtering
self.ui.input_TOPP("IDFilter", tool_instance_name="IDFilter-strict")

# Second instance of IDFilter with lenient filtering
self.ui.input_TOPP("IDFilter", tool_instance_name="IDFilter-lenient")
```

When executing the tool, pass the same **tool_instance_name** to `self.executor.run_topp()`:

```python
# Run first instance
self.executor.run_topp("IDFilter",
input_output={"in": in_files_1, "out": out_files_1},
tool_instance_name="IDFilter-strict")

# Run second instance with different parameters
self.executor.run_topp("IDFilter",
input_output={"in": in_files_2, "out": out_files_2},
tool_instance_name="IDFilter-lenient")
```

**3. Choose `self.ui.input_python` to automatically generate complete input sections for a custom Python tool:**

Takes the obligatory **script_file** argument. The default location for the Python script files is in `src/python-tools` (in this case the `.py` file extension is optional in the **script_file** argument), however, any other path can be specified as well. Parameters need to be specified in the Python script in the **DEFAULTS** variable with the mandatory **key** and **value** parameters.
Expand Down
12 changes: 9 additions & 3 deletions src/workflow/CommandExecutor.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ def read_stderr():
stdout_thread.join()
stderr_thread.join()

def run_topp(self, tool: str, input_output: dict, custom_params: dict = {}) -> None:
def run_topp(self, tool: str, input_output: dict, custom_params: dict = {}, tool_instance_name: str = None) -> None:
"""
Constructs and executes commands for the specified tool OpenMS TOPP tool based on the given
input and output configurations. Ensures that all input/output file lists
Expand All @@ -175,6 +175,7 @@ def run_topp(self, tool: str, input_output: dict, custom_params: dict = {}) -> N
tool (str): The executable name or path of the tool.
input_output (dict): A dictionary specifying the input/output parameter names (as key) and their corresponding file paths (as value).
custom_params (dict): A dictionary of custom parameters to pass to the tool.
tool_instance_name (str, optional): Unique identifier for this tool instance. Use when the same tool is configured multiple times with different parameters.

Raises:
ValueError: If the lengths of input/output file lists are inconsistent,
Expand All @@ -196,6 +197,8 @@ def run_topp(self, tool: str, input_output: dict, custom_params: dict = {}) -> N

# Load parameters for non-defaults
params = self.parameter_manager.get_parameters_from_json()
# Use tool_instance_name if provided, otherwise use tool name
param_key = tool_instance_name if tool_instance_name else tool
# Construct commands for each process
for i in range(n_processes):
command = [tool]
Expand All @@ -215,8 +218,11 @@ def run_topp(self, tool: str, input_output: dict, custom_params: dict = {}) -> N
else:
command += [value[i]]
# Add non-default TOPP tool parameters
if tool in params.keys():
for k, v in params[tool].items():
if param_key in params.keys():
for k, v in params[param_key].items():
# Skip metadata keys (starting with underscore)
if k.startswith("_"):
continue
command += [f"-{k}"]
if isinstance(v, str) and "\n" in v:
command += v.split("\n")
Expand Down
99 changes: 80 additions & 19 deletions src/workflow/ParameterManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ def save_parameters(self) -> None:
# Advanced parameters are only in session state if the view is active
json_params = self.get_parameters_from_json() | json_params

# get a list of TOPP tools which are in session state
current_topp_tools = list(
# get a list of TOPP tool instances (or tools) which are in session state
current_topp_instances = list(
set(
[
k.replace(self.topp_param_prefix, "").split(":1:")[0]
Expand All @@ -52,31 +52,86 @@ def save_parameters(self) -> None:
]
)
)
# for each TOPP tool, open the ini file
for tool in current_topp_tools:
if tool not in json_params:
json_params[tool] = {}
# for each TOPP tool instance, open the ini file
for instance in current_topp_instances:
if instance not in json_params:
json_params[instance] = {}
# Extract actual tool name from instance name (instance might be the tool name itself)
# We need to check which ini file exists to determine the actual tool name
tool_name = self._get_tool_name_from_instance(instance)
if not tool_name:
continue
# load the param object
param = poms.Param()
poms.ParamXMLFile().load(str(Path(self.ini_dir, f"{tool}.ini")), param)
# get all session state param keys and values for this tool
poms.ParamXMLFile().load(str(Path(self.ini_dir, f"{tool_name}.ini")), param)
# get all session state param keys and values for this tool instance
for key, value in st.session_state.items():
if key.startswith(f"{self.topp_param_prefix}{tool}:1:"):
# get ini_key
ini_key = key.replace(self.topp_param_prefix, "").encode()
if key.startswith(f"{self.topp_param_prefix}{instance}:1:"):
# get ini_key by replacing instance with actual tool name
ini_key_str = key.replace(self.topp_param_prefix, "").replace(f"{instance}:1:", f"{tool_name}:1:")
ini_key = ini_key_str.encode()
# get ini (default) value by ini_key
ini_value = param.getValue(ini_key)
# check if value is different from default
if (
(ini_value != value)
or (key.split(":1:")[1] in json_params[tool])
or (key.split(":1:")[1] in json_params[instance])
):
# store non-default value
json_params[tool][key.split(":1:")[1]] = value
json_params[instance][key.split(":1:")[1]] = value
# Save to json file
with open(self.params_file, "w", encoding="utf-8") as f:
json.dump(json_params, f, indent=4)

def _get_tool_name_from_instance(self, instance: str) -> str:
"""
Get the actual TOPP tool name from an instance identifier.
If the instance name corresponds to an existing ini file, it's the tool name itself.
Otherwise, extract from stored metadata in params.

Args:
instance (str): The tool instance identifier (could be tool name or custom name)

Returns:
str: The actual tool name, or None if not found
"""
# Check if instance name corresponds to an existing ini file
if Path(self.ini_dir, f"{instance}.ini").exists():
return instance

# Check if we have metadata about this instance in the params
params = self.get_parameters_from_json()
if instance in params and isinstance(params[instance], dict):
# Check if there's a special metadata key for tool name
if "_tool_name" in params[instance]:
return params[instance]["_tool_name"]

# Otherwise, find which ini file was used for this instance by checking session state
for ini_file in Path(self.ini_dir).glob("*.ini"):
tool_name = ini_file.stem
# Check if any session state key matches the pattern for this instance and tool
for key in st.session_state.keys():
if key.startswith(f"{self.topp_param_prefix}{instance}:1:"):
# Try to see if this parameter exists in this tool's ini file
try:
param = poms.Param()
poms.ParamXMLFile().load(str(ini_file), param)
param_name = key.split(":1:")[1]
# Check if this parameter exists in the tool
test_key = f"{tool_name}:1:{param_name}".encode()
if test_key in param.keys():
# Store this mapping for future use
if instance in params:
params[instance]["_tool_name"] = tool_name
else:
params[instance] = {"_tool_name": tool_name}
with open(self.params_file, "w", encoding="utf-8") as f:
json.dump(params, f, indent=4)
return tool_name
except Exception:
continue
return None

def get_parameters_from_json(self) -> dict:
"""
Loads parameters from the JSON file if it exists and returns them as a dictionary.
Expand All @@ -98,18 +153,23 @@ def get_parameters_from_json(self) -> dict:
st.error("**ERROR**: Attempting to load an invalid JSON parameter file. Reset to defaults.")
return {}

def get_topp_parameters(self, tool: str) -> dict:
def get_topp_parameters(self, tool_or_instance: str) -> dict:
"""
Get all parameters for a TOPP tool, merging defaults with user values.
Get all parameters for a TOPP tool or tool instance, merging defaults with user values.

Args:
tool: Name of the TOPP tool (e.g., "CometAdapter")
tool_or_instance: Name of the TOPP tool (e.g., "CometAdapter") or tool instance name (e.g., "IDFilter-first")

Returns:
Dict with parameter names as keys (without tool prefix) and their values.
Returns empty dict if ini file doesn't exist.
"""
ini_path = Path(self.ini_dir, f"{tool}.ini")
# Determine if this is an instance name or actual tool name
tool_name = self._get_tool_name_from_instance(tool_or_instance)
if not tool_name:
return {}

ini_path = Path(self.ini_dir, f"{tool_name}.ini")
if not ini_path.exists():
return {}

Expand All @@ -118,7 +178,7 @@ def get_topp_parameters(self, tool: str) -> dict:
poms.ParamXMLFile().load(str(ini_path), param)

# Build dict from ini (extract short key names)
prefix = f"{tool}:1:"
prefix = f"{tool_name}:1:"
full_params = {}
for key in param.keys():
key_str = key.decode() if isinstance(key, bytes) else str(key)
Expand All @@ -127,7 +187,8 @@ def get_topp_parameters(self, tool: str) -> dict:
full_params[short_key] = param.getValue(key)

# Override with user-modified values from JSON
user_params = self.get_parameters_from_json().get(tool, {})
# Use tool_or_instance as key since that's what's stored in params
user_params = self.get_parameters_from_json().get(tool_or_instance, {})
full_params.update(user_params)

return full_params
Expand Down
Loading
Loading