Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

make error handling automation-friendly #279

Merged
merged 7 commits into from
Jul 30, 2024
Merged
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
22 changes: 14 additions & 8 deletions alphadia/cli.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
#!python
"""CLI for alphaDIA.

Ideally the CLI module should have as little logic as possible so that the search behaves the same from the CLI or a jupyter notebook.
"""

# native imports
# alpha family imports
# third party imports
import argparse
import json
import logging
Expand All @@ -12,9 +13,9 @@

import yaml

# alphadia imports
import alphadia
from alphadia import utils
from alphadia.exceptions import CustomError
from alphadia.workflow import reporting

logger = logging.getLogger()
Expand Down Expand Up @@ -325,7 +326,7 @@ def run(*args, **kwargs):
try:
import matplotlib

# important to supress matplotlib output
# important to suppress matplotlib output
matplotlib.use("Agg")

from alphadia.planning import Plan
Expand All @@ -341,8 +342,13 @@ def run(*args, **kwargs):
plan.run()

except Exception as e:
import traceback
if isinstance(e, CustomError):
exit_code = 1
else:
import traceback

logger.info(traceback.format_exc())
exit_code = 127

logger.info(traceback.format_exc())
logger.error(e)
sys.exit(1)
sys.exit(exit_code)
6 changes: 2 additions & 4 deletions alphadia/data/bruker.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

from alphadia import utils
from alphadia.data.stats import log_stats
from alphadia.exceptions import NotDiaDataError

logger = logging.getLogger()

Expand Down Expand Up @@ -62,10 +63,7 @@ def __init__(
try:
cycle_shape = self._cycle.shape[0]
except AttributeError as e:
logger.error(
"Could not find cycle shape. Please check if this is a valid DIA data set."
)
raise e
raise NotDiaDataError() from e
else:
if cycle_shape != 1:
msg = f"Unexpected cycle shape: {cycle_shape} (expected: 1). "
Expand Down
61 changes: 61 additions & 0 deletions alphadia/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""Module containing custom exceptions."""


class CustomError(Exception):
"""Custom alphaDIA error class."""

_error_code = None
_msg = None
_detail_msg = ""

@property
def error_code(self):
return self._error_code

@property
def msg(self):
return self._msg

@property
def detail_msg(self):
return self._detail_msg


class BusinessError(CustomError):
"""Custom error class for 'business' errors.

A 'business' error is an error that is caused by the input (data, configuration, ...) and not by a
malfunction in alphaDIA.
"""


class NoPsmFoundError(BusinessError):
"""Raise when no PSMs are found in the search results."""

_error_code = "NO_PSM_FOUND"

_msg = "No psm files accumulated, can't continue"


class NoRecalibrationTargetError(BusinessError):
"""Raise when no recalibration target is found."""

_error_code = "NO_RECALIBRATION_TARGET"

_msg = "Searched all data without finding recalibration target"

_detail_msg = """Search for raw file failed as not enough precursors were found for calibration.
This can have the following reasons:
1. The sample was empty and therefore no precursors were found.
2. The sample contains only very few precursors.
For small libraries, try to set recalibration_target to a lower value.
For large libraries, try to reduce the library size and reduce the calibration MS1 and MS2 tolerance.
3. There was a fundamental issue with search parameters."""


class NotDiaDataError(BusinessError):
"""Raise when data is not from DIA."""

_error_code = "NOT_DIA_DATA"

_msg = "Could not find cycle shape. Please check if this is a valid DIA data set."
10 changes: 2 additions & 8 deletions alphadia/outputtransform.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

from alphadia import fdr, grouping, libtransform, utils
from alphadia.consensus.utils import read_df, write_df
from alphadia.exceptions import NoPsmFoundError
from alphadia.outputaccumulator import (
AccumulationBroadcaster,
TransferLearningAccumulator,
Expand All @@ -27,12 +28,6 @@
logger = logging.getLogger()


class OutputGenerationError(Exception):
"""Raised when an error occurs during output generation"""

pass


def get_frag_df_generator(folder_list: list[str]):
"""Return a generator that yields a tuple of (raw_name, frag_df)

Expand Down Expand Up @@ -524,8 +519,7 @@ def build_precursor_table(
logger.warning(e)

if len(psm_df_list) == 0:
logger.error("No psm files accumulated, can't continue")
raise OutputGenerationError("No psm files accumulated, can't continue")
raise NoPsmFoundError()

logger.info("Building combined output")
psm_df = pd.concat(psm_df_list)
Expand Down
76 changes: 44 additions & 32 deletions alphadia/planning.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@

# alphadia imports
from alphadia import libtransform, outputtransform
from alphadia.exceptions import CustomError
from alphadia.workflow import peptidecentric, reporting
from alphadia.workflow.base import WorkflowBase
from alphadia.workflow.config import Config

logger = logging.getLogger()
Expand Down Expand Up @@ -340,46 +342,56 @@ def run(
psm_df.to_parquet(psm_location, index=False)
frag_df.to_parquet(frag_location, index=False)

workflow.reporter.log_string(f"Finished workflow for {raw_name}")
workflow.reporter.context.__exit__(None, None, None)
del workflow

except peptidecentric.CalibrationError:
# get full traceback
logger.error(
f"Search for {raw_name} failed as not enough precursors were found for calibration"
)
logger.error("This can have the following reasons:")
logger.error(
" 1. The sample was empty and therefore nor precursors were found"
)
logger.error(" 2. The sample contains only very few precursors.")
logger.error(
" For small libraries, try to set recalibration_target to a lower value"
)
logger.error(
" For large libraries, try to reduce the library size and reduce the calibration MS1 and MS2 tolerance"
)
logger.error(
" 3. There was a fundamental issue with search parameters"
)
except CustomError as e:
_log_exception_event(e, raw_name, workflow)
continue
mschwoer marked this conversation as resolved.
Show resolved Hide resolved

except Exception as e:
logger.error(
f"Search for {raw_name} failed with error {e}", exc_info=True
)
_log_exception_event(e, raw_name, workflow)
raise e

base_spec_lib = SpecLibBase()
base_spec_lib.load_hdf(
os.path.join(self.output_folder, "speclib.hdf"), load_mod_seq=True
)
finally:
workflow.reporter.log_string(f"Finished workflow for {raw_name}")
workflow.reporter.context.__exit__(None, None, None)
del workflow

output = outputtransform.SearchPlanOutput(self.config, self.output_folder)
output.build(workflow_folder_list, base_spec_lib)
try:
base_spec_lib = SpecLibBase()
base_spec_lib.load_hdf(
os.path.join(self.output_folder, "speclib.hdf"), load_mod_seq=True
)

output = outputtransform.SearchPlanOutput(self.config, self.output_folder)
output.build(workflow_folder_list, base_spec_lib)
except Exception as e:
_log_exception_event(e)
raise e

logger.progress("=================== Search Finished ===================")

def clean(self):
if not self.config["library_loading"]["save_hdf"]:
os.remove(os.path.join(self.output_folder, "speclib.hdf"))


def _log_exception_event(
e: Exception, raw_name: str | None = None, workflow: WorkflowBase | None = None
) -> None:
"""Log exception and emit event to reporter if available."""

prefix = (
"Error:" if raw_name is None else f"Search for {raw_name} failed with error:"
)

if isinstance(e, CustomError):
logger.error(f"{prefix} {e.error_code} {e.msg}")
logger.error(e.detail_msg)
else:
logger.error(f"{prefix} {e}", exc_info=True)

if workflow is not None and workflow.reporter:
workflow.reporter.log_string(
value=str(e),
verbosity="error",
)
workflow.reporter.log_event(name="exception", value=str(e), exception=e)
15 changes: 2 additions & 13 deletions alphadia/workflow/peptidecentric.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

# alphadia imports
from alphadia import fragcomp, plexscoring, utils
from alphadia.exceptions import NoRecalibrationTargetError
from alphadia.peakgroup import search
from alphadia.workflow import base, manager

Expand Down Expand Up @@ -98,12 +99,6 @@
)


class CalibrationError(Exception):
"""Raised when calibration fails"""

pass


class PeptideCentricWorkflow(base.WorkflowBase):
def __init__(
self,
Expand Down Expand Up @@ -488,13 +483,7 @@ def calibration(self):
else:
# check if last step has been reached
if current_step == len(self.batch_plan) - 1:
self.reporter.log_string(
"Searched all data without finding recalibration target",
verbosity="error",
)
raise CalibrationError(
"Searched all data without finding recalibration target"
)
raise NoRecalibrationTargetError()

self.end_of_epoch()

Expand Down
Loading
Loading