diff --git a/CHANGELOG.md b/CHANGELOG.md index e13d170..651e8e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,16 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm N/A -## [0.2.1] 27-05-2024 +## [0.3.0] 03-06-2024 + +### Changed + +- [Breaking change] Parameter settings for `additional_modules/feature_filtering` were changed from a list of ranges to a dictionary with explicit values. +- Reworked score assignment for qualitative phenotype data: phenotype-associated features now always receive a score of 1.0, and non-associated ones a score of 0.0. +- For all modules with runtime restriction, the 'maximum_runtime' parameter was set to a default of '0' (unlimited runtime). Therefore, runtime restriction must now be specified explicitly. +- Added a 'module_passed' parameter to all modules. This allows a more accurate description via the SummaryWriter (e.g. module was activated but timed out, and lack of e.g. annotation is due to premature ending and not because there were no hits). + +## [0.2.2] 27-05-2024 ### Changed @@ -30,7 +39,7 @@ N/A - Replaced global logger with logger specific for `main_cli()`. `main()` now needs an argument `logger` - Reworked output file naming: all output files now start with `out.fermo.` and a suffix specifying their type - Removed output directory selection: the output directory is now always `results` located in the directory in which the peaktable resides. -- Features now always have default result values (before, some Features could have an empty dictionary) +- Features now always have default result values (previously, some Features could have an empty dictionary) - MS2Query assignment now uses temporary directories for data reading/writing ### Fixed @@ -45,4 +54,4 @@ N/A ## [0.1.0] 19-05-2024 -First public release of `fermo_core` \ No newline at end of file +- First public release of `fermo_core` \ No newline at end of file diff --git a/example_data/case_study_parameters.json b/example_data/case_study_parameters.json index fdf76fd..f0c72c6 100644 --- a/example_data/case_study_parameters.json +++ b/example_data/case_study_parameters.json @@ -66,14 +66,10 @@ "additional_modules": { "feature_filtering": { "activate_module": true, - "filter_rel_int_range": [ - 0.1, - 1.0 - ], - "filter_rel_area_range": [ - 0.1, - 1.0 - ] + "filter_rel_int_range_min": 0.1, + "filter_rel_int_range_max": 1.0, + "filter_rel_area_range_min": 0.1, + "filter_rel_area_range_max": 1.0 }, "blank_assignment": { "activate_module": true, @@ -148,4 +144,4 @@ } } } -} \ No newline at end of file +} diff --git a/fermo_core/config/schema.json b/fermo_core/config/schema.json index 39d4f5f..d9bd355 100644 --- a/fermo_core/config/schema.json +++ b/fermo_core/config/schema.json @@ -351,27 +351,29 @@ "title": "Specifies whether this module should be run.", "type": "boolean" }, - "filter_rel_int_range": { - "title": "(Optional): restrict data analysis to molecular features that have a relative intensity (intensity relative to the highest feature intensity per sample) that is inside the specified range. If a feature is detected in multiple samples, at least one occurrence must be inside the specified range. Else, the feature is not considered.", - "type": "array", - "minItems": 2, - "maxItems": 2, - "items": { - "type": "number", - "minimum": 0.0, - "maximum": 1.0 - } + "filter_rel_int_range_min": { + "title": "(Optional): restrict data analysis to molecular features that have a relative intensity (intensity relative to the highest feature intensity per sample) that greater or equal to the specified value. If a feature is detected in multiple samples, at least one occurrence must be inside the specified range. Else, the feature is not considered.", + "type": "number", + "minimum": 0.0, + "maximum": 1.0 }, - "filter_rel_area_range": { - "title": "(Optional): restrict data analysis to molecular features that have a relative area (area relative to the highest feature area per sample that is inside the specified range. If a feature is detected in multiple samples, at least one occurrence must be inside the specified range. Else, the feature is not considered.", - "type": "array", - "minItems": 2, - "maxItems": 2, - "items": { - "type": "number", - "minimum": 0.0, - "maximum": 1.0 - } + "filter_rel_int_range_max": { + "title": "(Optional): restrict data analysis to molecular features that have a relative intensity (intensity relative to the highest feature intensity per sample) that less or equal to the specified value. If a feature is detected in multiple samples, at least one occurrence must be inside the specified range. Else, the feature is not considered.", + "type": "number", + "minimum": 0.0, + "maximum": 1.0 + }, + "filter_rel_area_range_min": { + "title": "(Optional): restrict data analysis to molecular features that have a relative area (area relative to the highest feature area per sample) that greater or equal to the specified value. If a feature is detected in multiple samples, at least one occurrence must be inside the specified range. Else, the feature is not considered.", + "type": "number", + "minimum": 0.0, + "maximum": 1.0 + }, + "filter_rel_area_range_max": { + "title": "(Optional): restrict data analysis to molecular features that have a relative area (area relative to the highest feature area per sample) that less or equal to the specified value. If a feature is detected in multiple samples, at least one occurrence must be inside the specified range. Else, the feature is not considered.", + "type": "number", + "minimum": 0.0, + "maximum": 1.0 } } }, @@ -518,7 +520,7 @@ ] }, "p_val_cutoff": { - "title": "(Optional): Minimum Bonferroni-corrected p-value to consider. A value of zero disables the cutoff filtering (automatically applies to both score and p-value).", + "title": "(Optional): Maximum Bonferroni-corrected p-value to consider. A value of zero disables the cutoff filtering (automatically applies to both score and p-value).", "type": "number", "minimum": 0.0, "maximum": 1.0 @@ -566,7 +568,7 @@ ] }, "p_val_cutoff": { - "title": "(Optional): Minimum Bonferroni-corrected p-value to consider. A value of zero disables the cutoff filtering (automatically applies to both score and p-value).", + "title": "(Optional): Maximum Bonferroni-corrected p-value to consider. A value of zero disables the cutoff filtering (automatically applies to both score and p-value).", "type": "number", "minimum": 0.0, "maximum": 1.0 diff --git a/fermo_core/data_analysis/annotation_manager/class_annotation_manager.py b/fermo_core/data_analysis/annotation_manager/class_annotation_manager.py index 2765df1..e9c61ac 100644 --- a/fermo_core/data_analysis/annotation_manager/class_annotation_manager.py +++ b/fermo_core/data_analysis/annotation_manager/class_annotation_manager.py @@ -68,13 +68,15 @@ class AnnotationManager(BaseModel): features: Repository samples: Repository - def return_attrs(self: Self) -> tuple[Stats, Repository, Repository]: + def return_attrs( + self: Self, + ) -> tuple[Stats, Repository, Repository, ParameterManager]: """Returns modified attributes from AnnotationManager to the calling function Returns: Tuple containing Stats, Feature Repository and Sample Repository objects. """ - return self.stats, self.features, self.samples + return self.stats, self.features, self.samples, self.params def run_analysis(self: Self): """Organizes calling of data analysis steps.""" @@ -179,6 +181,7 @@ def run_user_lib_mod_cosine_matching(self: Self): mod_cosine_annotator.calculate_scores_mod_cosine() mod_cosine_annotator.extract_userlib_scores() self.features = mod_cosine_annotator.return_features() + self.params.SpectralLibMatchingCosineParameters.module_passed = True except Exception as e: logger.error(str(e)) logger.error( @@ -216,6 +219,7 @@ def run_user_lib_ms2deepscore_matching(self: Self): ms2deepscore_annotator.calculate_scores_ms2deepscore() ms2deepscore_annotator.extract_userlib_scores() self.features = ms2deepscore_annotator.return_features() + self.params.SpectralLibMatchingDeepscoreParameters.module_passed = True except Exception as e: logger.error(str(e)) logger.error( @@ -242,6 +246,7 @@ def run_feature_adduct_annotation(self: Self): ) adduct_annotator.run_analysis() self.features = adduct_annotator.return_features() + self.params.AdductAnnotationParameters.module_passed = True except Exception as e: logger.error(str(e)) logger.error( @@ -264,6 +269,7 @@ def run_neutral_loss_annotation(self: Self): ) neutralloss_annotator.run_analysis() self.features = neutralloss_annotator.return_features() + self.params.NeutralLossParameters.module_passed = True except Exception as e: logger.error(str(e)) logger.error( @@ -287,6 +293,7 @@ def run_fragment_annotation(self: Self): ) fragment_annotator.run_analysis() self.features = fragment_annotator.return_features() + self.params.FragmentAnnParameters.module_passed = True except Exception as e: logger.error(str(e)) logger.error( @@ -323,6 +330,7 @@ def run_ms2query_results_assignment(self: Self): self.params.MS2QueryResultsParameters.filepath, ) self.features = ms2query_annotator.return_features() + except Exception as e: logger.error(str(e)) logger.error( @@ -331,8 +339,7 @@ def run_ms2query_results_assignment(self: Self): return logger.info( - "'AnnotationManager': completed annotation from existing MS2Query " - "results" + "'AnnotationManager': completed annotation from existing MS2Query results." ) def run_ms2query_annotation(self: Self): @@ -359,6 +366,7 @@ def run_ms2query_annotation(self: Self): ) ms2query_annotator.run_ms2query() self.features = ms2query_annotator.return_features() + self.params.Ms2QueryAnnotationParameters.module_passed = True except Exception as e: logger.error(str(e)) logger.error( @@ -401,7 +409,6 @@ def run_as_kcb_cosine_annotation(self: Self): ) mibig_bgcs = {key for key, value in kcb_results.items()} spec_library = UtilityMethodManager().create_mibig_spec_lib(mibig_bgcs) - kcb_annotator = ModCosAnnotator( features=self.features, active_features=self.stats.active_features, @@ -417,6 +424,7 @@ def run_as_kcb_cosine_annotation(self: Self): kcb_annotator.calculate_scores_mod_cosine() kcb_annotator.extract_mibig_scores(kcb_results) self.features = kcb_annotator.return_features() + self.params.AsKcbCosineMatchingParams.module_passed = True except Exception as e: logger.error(str(e)) logger.error( @@ -478,6 +486,7 @@ def run_as_kcb_deepscore_annotation(self: Self): kcb_annotator.calculate_scores_ms2deepscore() kcb_annotator.extract_mibig_scores(kcb_results) self.features = kcb_annotator.return_features() + self.params.AsKcbDeepscoreMatchingParams.module_passed = True except Exception as e: logger.error(str(e)) logger.error( diff --git a/fermo_core/data_analysis/class_analysis_manager.py b/fermo_core/data_analysis/class_analysis_manager.py index bc522a9..401470b 100644 --- a/fermo_core/data_analysis/class_analysis_manager.py +++ b/fermo_core/data_analysis/class_analysis_manager.py @@ -67,13 +67,15 @@ class AnalysisManager(BaseModel): features: Repository samples: Repository - def return_attributes(self: Self) -> tuple[Stats, Repository, Repository]: + def return_attributes( + self: Self, + ) -> tuple[Stats, Repository, Repository, ParameterManager]: """Returns modified attributes to the calling function Returns: Tuple containing Stats, Feature Repository and Sample Repository objects. """ - return self.stats, self.features, self.samples + return self.stats, self.features, self.samples, self.params def analyze(self: Self): """Organizes calling of data analysis steps.""" @@ -108,6 +110,7 @@ def run_feature_filter(self: Self): ) feature_filter.filter() self.stats, self.features, self.samples = feature_filter.return_values() + self.params.FeatureFilteringParameters.module_passed = True except Exception as e: logger.warning(str(e)) return @@ -138,6 +141,7 @@ def run_blank_assignment(self: Self): ) blank_assigner.run_analysis() self.stats, self.features = blank_assigner.return_attrs() + self.params.BlankAssignmentParameters.module_passed = True except Exception as e: logger.warning(str(e)) return @@ -181,6 +185,7 @@ def run_group_factor_assignment(self: Self): ) group_fact_ass.run_analysis() self.features = group_fact_ass.return_features() + self.params.GroupFactAssignmentParameters.module_passed = True except Exception as e: logger.warning(str(e)) return @@ -211,7 +216,7 @@ def run_phenotype_manager(self: Self): samples=self.samples, ) phenotype_manager.run_analysis() - self.stats, self.features = phenotype_manager.return_attrs() + self.stats, self.features, self.params = phenotype_manager.return_attrs() except Exception as e: logger.warning(str(e)) return @@ -241,11 +246,9 @@ def run_sim_networks_manager(self: Self): samples=self.samples, ) sim_networks_manager.run_analysis() - ( - self.stats, - self.features, - self.samples, - ) = sim_networks_manager.return_attrs() + (self.stats, self.features, self.samples, self.params) = ( + sim_networks_manager.return_attrs() + ) except Exception as e: logger.warning(str(e)) return @@ -261,7 +264,9 @@ def run_annotation_manager(self: Self): samples=self.samples, ) annotation_manager.run_analysis() - self.stats, self.features, self.samples = annotation_manager.return_attrs() + self.stats, self.features, self.samples, self.params = ( + annotation_manager.return_attrs() + ) except Exception as e: logger.warning(str(e)) return diff --git a/fermo_core/data_analysis/feature_filter/class_feature_filter.py b/fermo_core/data_analysis/feature_filter/class_feature_filter.py index f29abde..13b48ed 100644 --- a/fermo_core/data_analysis/feature_filter/class_feature_filter.py +++ b/fermo_core/data_analysis/feature_filter/class_feature_filter.py @@ -1,6 +1,6 @@ """Class to manage methods to filter features for various parameters. -Copyright (c) 2022-2023 Mitja Maximilian Zdouc, PhD +Copyright (c) 2022 to present Mitja Maximilian Zdouc, PhD Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -64,19 +64,12 @@ def filter(self: Self): logger.info("'FeatureFilter': started filtering of molecular features.") modules = ( - ( - self.params.FeatureFilteringParameters.filter_rel_int_range, - self.filter_rel_int_range, - ), - ( - self.params.FeatureFilteringParameters.filter_rel_area_range, - self.filter_rel_area_range, - ), + self.filter_rel_int_range, + self.filter_rel_area_range, ) for module in modules: - if module[0] is not None: - module[1]() + module() self.remove_filtered_features() @@ -104,11 +97,22 @@ def filter_rel_int_range(self: Self): """Retain features inside relative intensity range in at least one sample.""" logger.info("'FeatureFilter': started filtering for relative intensity.") + if ( + self.params.FeatureFilteringParameters.filter_rel_int_range_min == 0.0 + and self.params.FeatureFilteringParameters.filter_rel_int_range_max == 1.0 + ): + logger.info( + "'FeatureFilter': Relative intensity range set to a 'min' of '0.0' and " + "a 'max' of '1.0'. No filtering performed - SKIP" + ) + return + inactive = self.filter_features_for_range( - self.stats, - self.samples, - self.params.FeatureFilteringParameters.filter_rel_int_range, - "rel_intensity", + r_list=[ + self.params.FeatureFilteringParameters.filter_rel_int_range_min, + self.params.FeatureFilteringParameters.filter_rel_int_range_max, + ], + param="rel_intensity", ) self.stats.inactive_features.update(inactive) @@ -123,11 +127,22 @@ def filter_rel_area_range(self: Self): """Retain features inside relative area range in at least one sample.""" logger.info("'FeatureFilter': started filtering for relative area.") + if ( + self.params.FeatureFilteringParameters.filter_rel_area_range_min == 0.0 + and self.params.FeatureFilteringParameters.filter_rel_area_range_max == 1.0 + ): + logger.info( + "'FeatureFilter': Relative area range set to a 'min' of '0.0' and a " + "'max' of '1.0'. No filtering performed - SKIP" + ) + return + inactive = self.filter_features_for_range( - self.stats, - self.samples, - self.params.FeatureFilteringParameters.filter_rel_area_range, - "rel_area", + r_list=[ + self.params.FeatureFilteringParameters.filter_rel_area_range_min, + self.params.FeatureFilteringParameters.filter_rel_area_range_max, + ], + param="rel_area", ) self.stats.inactive_features.update(inactive) @@ -138,15 +153,10 @@ def filter_rel_area_range(self: Self): logger.info("'FeatureFilter': completed filtering for relative area.") - @staticmethod - def filter_features_for_range( - stats: Stats, samples: Repository, r_list: list, param: str - ) -> set: + def filter_features_for_range(self: Self, r_list: list, param: str) -> set: """Determine features with values outside of given range Arguments: - stats: a Stats object - samples: a repository object r_list: a list of two floats indicating the range param: the parameter to check against range @@ -157,8 +167,8 @@ def filter_features_for_range( inside_range = set() outside_range = set() - for sample_id in stats.samples: - sample = samples.get(sample_id) + for sample_id in self.stats.samples: + sample = self.samples.get(sample_id) for feature_id in sample.feature_ids: feature = sample.features.get(feature_id) if r_list[0] <= getattr(feature, param) <= r_list[1]: diff --git a/fermo_core/data_analysis/phenotype_manager/class_phen_qual_assigner.py b/fermo_core/data_analysis/phenotype_manager/class_phen_qual_assigner.py index 85d03fe..9e9ae64 100644 --- a/fermo_core/data_analysis/phenotype_manager/class_phen_qual_assigner.py +++ b/fermo_core/data_analysis/phenotype_manager/class_phen_qual_assigner.py @@ -103,7 +103,7 @@ def collect_sets(self: Self): feature = self.add_annotation_attribute(feature=feature) feature.Annotations.phenotypes.append( Phenotype( - score=0, format="qualitative", descr="only in positive samples" + score=1.0, format="qualitative", descr="only in positive samples" ) ) self.features.modify(f_id, feature) @@ -168,7 +168,11 @@ def bin_intersection(self: Self): if factor >= self.params.PhenoQualAssgnParams.factor: feature = self.add_annotation_attribute(feature=feature) feature.Annotations.phenotypes.append( - Phenotype(score=factor, format="qualitative") + Phenotype( + score=1.0, + format="qualitative", + descr=f"Fold-difference: '{round(factor, 2)}'", + ) ) self.stats.phenotypes[0].f_ids_positive.add(f_id) case "mean": @@ -176,7 +180,11 @@ def bin_intersection(self: Self): if factor >= self.params.PhenoQualAssgnParams.factor: feature = self.add_annotation_attribute(feature=feature) feature.Annotations.phenotypes.append( - Phenotype(score=factor, format="qualitative") + Phenotype( + score=1.0, + format="qualitative", + descr=f"Fold-difference: '{round(factor, 2)}'", + ) ) self.stats.phenotypes[0].f_ids_positive.add(f_id) case "median": @@ -184,7 +192,11 @@ def bin_intersection(self: Self): if factor >= self.params.PhenoQualAssgnParams.factor: feature = self.add_annotation_attribute(feature=feature) feature.Annotations.phenotypes.append( - Phenotype(score=factor, format="qualitative") + Phenotype( + score=1.0, + format="qualitative", + descr=f"Fold-difference: '{round(factor, 2)}'", + ) ) self.stats.phenotypes[0].f_ids_positive.add(f_id) case _: diff --git a/fermo_core/data_analysis/phenotype_manager/class_phenotype_manager.py b/fermo_core/data_analysis/phenotype_manager/class_phenotype_manager.py index 2d3e1e0..3f557de 100644 --- a/fermo_core/data_analysis/phenotype_manager/class_phenotype_manager.py +++ b/fermo_core/data_analysis/phenotype_manager/class_phenotype_manager.py @@ -57,13 +57,13 @@ class PhenotypeManager(BaseModel): features: Repository samples: Repository - def return_attrs(self: Self) -> tuple[Stats, Repository]: + def return_attrs(self: Self) -> tuple[Stats, Repository, ParameterManager]: """Returns modified attributes from PhenotypeManager to the calling function Returns: Tuple containing Stats, Feature Repository objects. """ - return self.stats, self.features + return self.stats, self.features, self.params def run_analysis(self: Self): """Organizes calling of phenotype annotation steps.""" @@ -103,6 +103,7 @@ def run_assigner_qualitative(self: Self): ) qual_assigner.run_analysis() self.stats, self.features = qual_assigner.return_values() + self.params.PhenoQualAssgnParams.module_passed = True except Exception as e: logger.error(str(e)) logger.error( @@ -138,6 +139,7 @@ def run_assigner_quant_percentage(self: Self): ) quant_p_assigner.run_analysis() self.stats, self.features = quant_p_assigner.return_values() + self.params.PhenoQuantPercentAssgnParams.module_passed = True except Exception as e: logger.error(str(e)) logger.error( @@ -174,6 +176,7 @@ def run_assigner_quant_concentration(self: Self): ) quant_conc_assigner.run_analysis() self.stats, self.features = quant_conc_assigner.return_values() + self.params.PhenoQuantConcAssgnParams.module_passed = True except Exception as e: logger.error(str(e)) logger.error( diff --git a/fermo_core/data_analysis/sim_networks_manager/class_sim_networks_manager.py b/fermo_core/data_analysis/sim_networks_manager/class_sim_networks_manager.py index 41c6d58..cb3336e 100644 --- a/fermo_core/data_analysis/sim_networks_manager/class_sim_networks_manager.py +++ b/fermo_core/data_analysis/sim_networks_manager/class_sim_networks_manager.py @@ -121,13 +121,15 @@ def log_timeout_ms2deepscore(max_time: str): f"filter out low-intensity/area peaks with 'feature_filtering' - SKIP" ) - def return_attrs(self: Self) -> tuple[Stats, Repository, Repository]: + def return_attrs( + self: Self, + ) -> tuple[Stats, Repository, Repository, ParameterManager]: """Returns modified attributes from SimNetworksManager to the calling function Returns: Tuple containing Stats, Feature Repository and Sample Repository objects. """ - return self.stats, self.features, self.samples + return self.stats, self.features, self.samples, self.params def run_analysis(self: Self): """Organizes calling of data analysis steps.""" @@ -186,7 +188,7 @@ def run_modified_cosine_alg(self: Self): self.store_network_data( "modified_cosine", network_data, tuple(filtered_features.get("included")) ) - + self.params.SpecSimNetworkCosineParameters.module_passed = True logger.info("'SimNetworksManager/ModCosineNetworker': completed calculation") def run_ms2deepscore_alg(self: Self): @@ -241,7 +243,7 @@ def run_ms2deepscore_alg(self: Self): self.store_network_data( "ms2deepscore", network_data, tuple(filtered_features.get("included")) ) - + self.params.SpecSimNetworkDeepscoreParameters.module_passed = True logger.info("'SimNetworksManager/Ms2deepscoreNetworker': completed calculation") def filter_input_spectra( diff --git a/fermo_core/input_output/additional_module_parameter_managers.py b/fermo_core/input_output/additional_module_parameter_managers.py index 337712f..39aa5ac 100644 --- a/fermo_core/input_output/additional_module_parameter_managers.py +++ b/fermo_core/input_output/additional_module_parameter_managers.py @@ -1,6 +1,6 @@ """Organizes classes that hold and validate parameters for additional modules. -Copyright (c) 2022-2023 Mitja Maximilian Zdouc, PhD +Copyright (c) 2022 to present Mitja Maximilian Zdouc, PhD Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -36,53 +36,40 @@ class FeatureFilteringParameters(BaseModel): Attributes: activate_module: bool to indicate if module should be executed. - filter_rel_int_range: A range of relative peak intensity to retain features. - filter_rel_area_range: A range of relative area to retain features. - - Raise: - pydantic.ValidationError: Pydantic validation failed during instantiation. - ValueError: raised by 'validate_range_zero_one', malformed range. + filter_rel_int_range_min: min value to filter feature for rel int + filter_rel_int_range_max: max value to filter feature for rel int + filter_rel_area_range_min: min value to filter feature for rel area + filter_rel_area_range_max: max value to filter feature for rel area + module_passed: indicates that the module ran without errors """ activate_module: bool = False - filter_rel_int_range: Optional[list[float]] = None - filter_rel_area_range: Optional[list[float]] = None + filter_rel_int_range_min: float = 0.0 + filter_rel_int_range_max: float = 1.0 + filter_rel_area_range_min: float = 0.0 + filter_rel_area_range_max: float = 1.0 + module_passed: bool = False @model_validator(mode="after") def validate_attrs(self): - if self.filter_rel_int_range is not None: - self.filter_rel_int_range = self.order_and_validate_range( - self.filter_rel_int_range - ) - - if self.filter_rel_area_range is not None: - self.filter_rel_area_range = self.order_and_validate_range( - self.filter_rel_area_range - ) - + ValidationManager.validate_range_zero_one( + [self.filter_rel_int_range_min, self.filter_rel_int_range_max] + ) + ValidationManager.validate_range_zero_one( + [self.filter_rel_area_range_min, self.filter_rel_area_range_max] + ) return self - @staticmethod - def order_and_validate_range(r_list: list[float]) -> list[float]: - """Order the list and validate format. - - Attributes: - r_list: a range of two floats - - Returns: - The modified list of a range of two floats - """ - r_list = [min(r_list), max(r_list)] - ValidationManager.validate_range_zero_one(r_list) - return r_list - def to_json(self: Self) -> dict: """Convert attributes to json-compatible ones.""" if self.activate_module: return { "activate_module": self.activate_module, - "filter_rel_int_range": self.filter_rel_int_range, - "filter_rel_area_range": self.filter_rel_area_range, + "filter_rel_int_range_min": self.filter_rel_int_range_min, + "filter_rel_int_range_max": self.filter_rel_int_range_max, + "filter_rel_area_range_min": self.filter_rel_area_range_min, + "filter_rel_area_range_max": self.filter_rel_area_range_max, + "module_passed": self.module_passed, } else: return {"activate_module": self.activate_module} @@ -96,15 +83,14 @@ class BlankAssignmentParameters(BaseModel): factor: An integer fold-change to differentiate blank features. algorithm: the algorithm to summarize values of different samples. value: the type of value to use for determination - - Raise: - pydantic.ValidationError: Pydantic validation failed during instantiation. + module_passed: indicates that the module ran without errors """ activate_module: bool = False factor: PositiveInt = 10 algorithm: str = "mean" value: str = "area" + module_passed: bool = False @model_validator(mode="after") def validate_strs(self): @@ -129,6 +115,7 @@ def to_json(self: Self) -> dict: "factor": int(self.factor), "algorithm": str(self.algorithm), "value": str(self.value), + "module_passed": self.module_passed, } else: return {"activate_module": self.activate_module} @@ -141,11 +128,13 @@ class GroupFactAssignmentParameters(BaseModel): activate_module: bool to indicate if module should be executed. algorithm: the algorithm to summarize values of different samples. value: the type of value to use for comparison + module_passed: indicates that the module ran without errors """ activate_module: bool = False algorithm: str = "mean" value: str = "area" + module_passed: bool = False @model_validator(mode="after") def validate_strs(self): @@ -169,6 +158,7 @@ def to_json(self: Self) -> dict: "activate_module": self.activate_module, "algorithm": str(self.algorithm), "value": str(self.value), + "module_passed": self.module_passed, } else: return {"activate_module": self.activate_module} @@ -182,12 +172,14 @@ class PhenoQualAssgnParams(BaseModel): factor: An integer fold-change to differentiate phenotype-assoc. features. algorithm: the algorithm to summarize values of active vs inactive samples. value: the type of value to use for determination + module_passed: indicates that the module ran without errors """ activate_module: bool = False factor: PositiveInt = 10 algorithm: str = "minmax" value: str = "area" + module_passed: bool = False @model_validator(mode="after") def validate_strs(self): @@ -212,6 +204,7 @@ def to_json(self: Self) -> dict: "factor": int(self.factor), "algorithm": str(self.algorithm), "value": str(self.value), + "module_passed": self.module_passed, } else: return {"activate_module": self.activate_module} @@ -227,6 +220,7 @@ class PhenoQuantPercentAssgnParams(BaseModel): algorithm: the statistical algorithm to calculate correlation p_val_cutoff: minimum Bonferroni-corrected p-value to consider in assignment coeff_cutoff: minimum correlation coefficient cutoff to consider in assignment + module_passed: indicates that the module ran without errors """ activate_module: bool = False @@ -235,6 +229,7 @@ class PhenoQuantPercentAssgnParams(BaseModel): algorithm: str = "pearson" p_val_cutoff: float = 0.05 coeff_cutoff: float = 0.7 + module_passed: bool = False @model_validator(mode="after") def validate_strs(self): @@ -290,6 +285,7 @@ def to_json(self: Self) -> dict: "algorithm": self.algorithm, "p_val_cutoff": self.p_val_cutoff, "coeff_cutoff": self.coeff_cutoff, + "module_passed": self.module_passed, } else: return {"activate_module": self.activate_module} @@ -305,6 +301,7 @@ class PhenoQuantConcAssgnParams(BaseModel): algorithm: the statistical algorithm to calculate correlation p_val_cutoff: minimum Bonferroni-corrected p-value to consider in assignment coeff_cutoff: minimum correlation coefficient cutoff to consider in assignment + module_passed: indicates that the module ran without errors """ activate_module: bool = False @@ -313,6 +310,7 @@ class PhenoQuantConcAssgnParams(BaseModel): algorithm: str = "pearson" p_val_cutoff: float = 0.05 coeff_cutoff: float = 0.7 + module_passed: bool = False @model_validator(mode="after") def validate_strs(self): @@ -368,6 +366,7 @@ def to_json(self: Self) -> dict: "algorithm": self.algorithm, "p_val_cutoff": self.p_val_cutoff, "coeff_cutoff": self.coeff_cutoff, + "module_passed": self.module_passed, } else: return {"activate_module": self.activate_module} @@ -384,10 +383,8 @@ class SpectralLibMatchingCosineParameters(BaseModel): min_nr_matched_peaks: peak cutoff to consider a match of two MS/MS spectra score_cutoff: score cutoff to consider a match of two MS/MS spectra max_precursor_mass_diff: maximum precursor mass difference - maximum_runtime: maximum runtime in seconds - - Raise: - pydantic.ValidationError: Pydantic validation failed during instantiation. + maximum_runtime: maximum runtime in seconds ('0' indicates unlimited runtime) + module_passed: indicates that the module ran without errors """ activate_module: bool = False @@ -395,7 +392,8 @@ class SpectralLibMatchingCosineParameters(BaseModel): min_nr_matched_peaks: PositiveInt = 5 score_cutoff: PositiveFloat = 0.7 max_precursor_mass_diff: PositiveInt = 600 - maximum_runtime: int = 600 + maximum_runtime: int = 0 + module_passed: bool = False def to_json(self: Self) -> dict: """Convert attributes to json-compatible ones.""" @@ -407,6 +405,7 @@ def to_json(self: Self) -> dict: "score_cutoff": float(self.score_cutoff), "max_precursor_mass_diff": int(self.max_precursor_mass_diff), "maximum_runtime": int(self.maximum_runtime), + "module_passed": self.module_passed, } else: return {"activate_module": self.activate_module} @@ -421,16 +420,15 @@ class SpectralLibMatchingDeepscoreParameters(BaseModel): activate_module: bool to indicate if module should be executed. score_cutoff: score cutoff to consider a match of two MS/MS spectra. max_precursor_mass_diff: max allowed precursor mz difference to accept a match - maximum_runtime: maximum runtime in seconds - - Raise: - pydantic.ValidationError: Pydantic validation failed during instantiation. + maximum_runtime: maximum runtime in seconds ('0' indicates unlimited runtime) + module_passed: indicates that the module ran without errors """ activate_module: bool = False score_cutoff: PositiveFloat = 0.8 max_precursor_mass_diff: PositiveInt = 600 - maximum_runtime: int = 600 + maximum_runtime: int = 0 + module_passed: bool = False def to_json(self: Self) -> dict: """Convert attributes to json-compatible ones.""" @@ -440,6 +438,7 @@ def to_json(self: Self) -> dict: "score_cutoff": float(self.score_cutoff), "max_precursor_mass_diff": int(self.max_precursor_mass_diff), "maximum_runtime": int(self.maximum_runtime), + "module_passed": self.module_passed, } else: return {"activate_module": self.activate_module} @@ -451,15 +450,14 @@ class Ms2QueryAnnotationParameters(BaseModel): Attributes: activate_module: bool to indicate if module should be executed. score_cutoff: only matches with a score higher or equal to are retained - maximum_runtime: maximum runtime in seconds - - Raise: - pydantic.ValidationError: Pydantic validation failed during instantiation. + maximum_runtime: maximum runtime in seconds ('0' indicates unlimited runtime) + module_passed: indicates that the module ran without errors """ activate_module: bool = False score_cutoff: PositiveFloat = 0.7 - maximum_runtime: int = 600 + maximum_runtime: int = 0 + module_passed: bool = False def to_json(self: Self) -> dict: """Convert attributes to json-compatible ones.""" @@ -468,6 +466,7 @@ def to_json(self: Self) -> dict: "activate_module": self.activate_module, "score_cutoff": self.score_cutoff, "maximum_runtime": self.maximum_runtime, + "module_passed": self.module_passed, } else: return {"activate_module": self.activate_module} @@ -487,10 +486,8 @@ class AsKcbCosineMatchingParams(BaseModel): min_nr_matched_peaks: peak cutoff to consider a match of two MS/MS spectra score_cutoff: score cutoff to consider a match of two MS/MS spectra max_precursor_mass_diff: maximum precursor mass difference - maximum_runtime: maximum runtime in seconds - - Raise: - pydantic.ValidationError: Pydantic validation failed during instantiation. + maximum_runtime: maximum runtime in seconds ('0' indicates unlimited runtime) + module_passed: indicates that the module ran without errors """ activate_module: bool = False @@ -498,7 +495,8 @@ class AsKcbCosineMatchingParams(BaseModel): min_nr_matched_peaks: PositiveInt = 5 score_cutoff: PositiveFloat = 0.5 max_precursor_mass_diff: PositiveInt = 600 - maximum_runtime: int = 600 + maximum_runtime: int = 0 + module_passed: bool = False def to_json(self: Self) -> dict: """Convert attributes to json-compatible ones.""" @@ -510,6 +508,7 @@ def to_json(self: Self) -> dict: "score_cutoff": float(self.score_cutoff), "max_precursor_mass_diff": int(self.max_precursor_mass_diff), "maximum_runtime": int(self.maximum_runtime), + "module_passed": self.module_passed, } else: return {"activate_module": self.activate_module} @@ -527,7 +526,8 @@ class AsKcbDeepscoreMatchingParams(BaseModel): activate_module: bool to indicate if module should be executed. score_cutoff: score cutoff to consider a match of two MS/MS spectra. max_precursor_mass_diff: max allowed precursor mz difference to accept a match - maximum_runtime: maximum runtime in seconds + maximum_runtime: maximum runtime in seconds ('0' indicates unlimited runtime) + module_passed: indicates that the module ran without errors Raise: pydantic.ValidationError: Pydantic validation failed during instantiation. @@ -536,7 +536,8 @@ class AsKcbDeepscoreMatchingParams(BaseModel): activate_module: bool = False score_cutoff: PositiveFloat = 0.7 max_precursor_mass_diff: PositiveInt = 600 - maximum_runtime: int = 600 + maximum_runtime: int = 0 + module_passed: bool = False def to_json(self: Self) -> dict: """Convert attributes to json-compatible ones.""" @@ -546,6 +547,7 @@ def to_json(self: Self) -> dict: "score_cutoff": float(self.score_cutoff), "max_precursor_mass_diff": int(self.max_precursor_mass_diff), "maximum_runtime": int(self.maximum_runtime), + "module_passed": self.module_passed, } else: return {"activate_module": self.activate_module} diff --git a/fermo_core/input_output/class_summary_writer.py b/fermo_core/input_output/class_summary_writer.py index a462a22..2659879 100644 --- a/fermo_core/input_output/class_summary_writer.py +++ b/fermo_core/input_output/class_summary_writer.py @@ -118,38 +118,61 @@ def summarize_asresultsparameters(self: Self): ) def summarize_featurefilteringparameters(self: Self): - if self.params.FeatureFilteringParameters.activate_module: - if self.params.FeatureFilteringParameters.filter_rel_int_range is not None: - self.summary.append( - f"Molecular features were filtered and only retained if they were " - f"inside the relative intensity(height) range of " - f"'{self.params.FeatureFilteringParameters.filter_rel_int_range[0]}" - f"-" - f"{self.params.FeatureFilteringParameters.filter_rel_int_range[1]}'" - f" in at least one sample (relative to the feature with the " - f"highest intensity(height) in the sample)." - ) - if self.params.FeatureFilteringParameters.filter_rel_area_range is not None: - self.summary.append( - f"Molecular features were filtered and only retained if they were " - f"inside the relative area range of " - f"'{self.params.FeatureFilteringParameters.filter_rel_area_range[0]}" - f"-" - f"{self.params.FeatureFilteringParameters.filter_rel_area_range[1]}'" - f" in at least one sample (relative to the feature with the " - f"highest area in the sample)." - ) + if ( + self.params.FeatureFilteringParameters.activate_module + and self.params.FeatureFilteringParameters.module_passed + ): + self.summary.append( + f"Molecular features were filtered and only retained if they were " + f"inside the relative intensity(height) range of " + f"'{self.params.FeatureFilteringParameters.filter_rel_int_range_min}" + f"-" + f"{self.params.FeatureFilteringParameters.filter_rel_int_range_max}'" + f" in at least one sample (relative to the feature with the " + f"highest intensity(height) in the sample)." + ) + self.summary.append( + f"Molecular features were filtered and only retained if they were " + f"inside the relative area range of " + f"'{self.params.FeatureFilteringParameters.filter_rel_area_range_min}" + f"-" + f"{self.params.FeatureFilteringParameters.filter_rel_area_range_max}'" + f" in at least one sample (relative to the feature with the " + f"highest area in the sample)." + ) + elif ( + self.params.FeatureFilteringParameters.activate_module + and not self.params.FeatureFilteringParameters.module_passed + ): + self.summary.append( + f"During filtering of molecular features, an error occurred, and the " + f"module terminated prematurely. For more information, see the logs." + ) def summarize_adductannotationparameters(self: Self): - if self.params.AdductAnnotationParameters.activate_module: + if ( + self.params.AdductAnnotationParameters.activate_module + and self.params.AdductAnnotationParameters.module_passed + ): self.summary.append( f"For each sample, overlapping molecular features were annotated for " f"ion adducts using a cutoff mass deviation value of " f"'{self.params.AdductAnnotationParameters.mass_dev_ppm}' ppm." ) + elif ( + self.params.AdductAnnotationParameters.activate_module + and not self.params.AdductAnnotationParameters.module_passed + ): + self.summary.append( + f"During adduct annotation, an error occurred, and the " + f"module terminated prematurely. For more information, see the logs." + ) def summarize_neutrallossparameters(self: Self): - if self.params.NeutralLossParameters.activate_module: + if ( + self.params.NeutralLossParameters.activate_module + and self.params.NeutralLossParameters.module_passed + ): self.summary.append( f"For each molecular feature, neutral losses were calculated between " f"the precursor m/z and each MS/MS fragment peak m/z and matched " @@ -157,17 +180,39 @@ def summarize_neutrallossparameters(self: Self): f"library of annotated neutral losses, using a cutoff mass deviation " f"value of '{self.params.NeutralLossParameters.mass_dev_ppm}' ppm." ) + elif ( + self.params.NeutralLossParameters.activate_module + and not self.params.NeutralLossParameters.module_passed + ): + self.summary.append( + f"During neutral loss annotation, an error occurred, and the " + f"module terminated prematurely. For more information, see the logs." + ) def summarize_fragmentannparameters(self: Self): - if self.params.FragmentAnnParameters.activate_module: + if ( + self.params.FragmentAnnParameters.activate_module + and self.params.FragmentAnnParameters.module_passed + ): self.summary.append( f"For each molecular feature, MS/MS fragments were matched against a " f"library of annotated MS/MS fragments, using a cutoff mass deviation " f"value of '{self.params.FragmentAnnParameters.mass_dev_ppm}' ppm." ) + elif ( + self.params.FragmentAnnParameters.activate_module + and not self.params.FragmentAnnParameters.module_passed + ): + self.summary.append( + f"During fragment annotation, an error occurred, and the " + f"module terminated prematurely. For more information, see the logs." + ) def summarize_specsimnetworkcosineparameters(self: Self): - if self.params.SpecSimNetworkCosineParameters.activate_module: + if ( + self.params.SpecSimNetworkCosineParameters.activate_module + and self.params.SpecSimNetworkCosineParameters.module_passed + ): self.summary.append( f"MS/MS spectra of all molecular features with more than '" f"{self.params.SpecSimNetworkCosineParameters.msms_min_frag_nr}' " @@ -184,9 +229,21 @@ def summarize_specsimnetworkcosineparameters(self: Self): f"{self.params.SpecSimNetworkCosineParameters.max_nr_links}' highest " f"scoring edges remained." ) + elif ( + self.params.SpecSimNetworkCosineParameters.activate_module + and not self.params.SpecSimNetworkCosineParameters.module_passed + ): + self.summary.append( + f"During spectral similarity networking calculation using the " + f"modified cosine algorithm, an error occurred, and the " + f"module terminated prematurely. For more information, see the logs." + ) def summarize_specsimnetworkdeepscoreparameters(self: Self): - if self.params.SpecSimNetworkDeepscoreParameters.activate_module: + if ( + self.params.SpecSimNetworkDeepscoreParameters.activate_module + and self.params.SpecSimNetworkDeepscoreParameters.module_passed + ): self.summary.append( f"MS/MS spectra of all molecular features with more than '" f"{self.params.SpecSimNetworkDeepscoreParameters.msms_min_frag_nr}' " @@ -201,9 +258,21 @@ def summarize_specsimnetworkdeepscoreparameters(self: Self): f"{self.params.SpecSimNetworkDeepscoreParameters.max_nr_links}' highest" f" scoring edges remained." ) + elif ( + self.params.SpecSimNetworkDeepscoreParameters.activate_module + and not self.params.SpecSimNetworkDeepscoreParameters.module_passed + ): + self.summary.append( + f"During spectral similarity networking calculation using the " + f"MS2DeepScore algorithm, an error occurred, and the " + f"module terminated prematurely. For more information, see the logs." + ) def summarize_blankassignmentparameters(self: Self): - if self.params.BlankAssignmentParameters.activate_module: + if ( + self.params.BlankAssignmentParameters.activate_module + and self.params.BlankAssignmentParameters.module_passed + ): self.summary.append( f"Molecular features only detected in sample-blanks were considered " f"blank-associated, as were features that had a quotient of less than " @@ -212,9 +281,20 @@ def summarize_blankassignmentparameters(self: Self): f"'{self.params.BlankAssignmentParameters.value}' between samples and " f"sample blanks was compared." ) + elif ( + self.params.BlankAssignmentParameters.activate_module + and not self.params.BlankAssignmentParameters.module_passed + ): + self.summary.append( + f"During blank assignment, an error occurred, and the " + f"module terminated prematurely. For more information, see the logs." + ) def summarize_groupfactassignmentparameters(self: Self): - if self.params.GroupFactAssignmentParameters.activate_module: + if ( + self.params.GroupFactAssignmentParameters.activate_module + and self.params.GroupFactAssignmentParameters.module_passed + ): self.summary.append( f"Samples were grouped according to the provided group metadata " f"information. For each molecular feature observed in more than one " @@ -223,9 +303,20 @@ def summarize_groupfactassignmentparameters(self: Self): f"'{self.params.GroupFactAssignmentParameters.value}' " f"of groups was calculated pairwise." ) + elif ( + self.params.GroupFactAssignmentParameters.activate_module + and not self.params.GroupFactAssignmentParameters.module_passed + ): + self.summary.append( + f"During group metadata assignment, an error occurred, and the " + f"module terminated prematurely. For more information, see the logs." + ) def summarize_phenoqualassgnparams(self: Self): - if self.params.PhenoQualAssgnParams.activate_module: + if ( + self.params.PhenoQualAssgnParams.activate_module + and self.params.PhenoQualAssgnParams.module_passed + ): self.summary.append( f"Molecular feature only detected in phenotype-associated samples " f"were considered phenotype-associated, as were feature that had a " @@ -236,9 +327,20 @@ def summarize_phenoqualassgnparams(self: Self): f"phenotype-associated and not phenotype-associated " f"samples was compared." ) + elif ( + self.params.PhenoQualAssgnParams.activate_module + and not self.params.PhenoQualAssgnParams.module_passed + ): + self.summary.append( + f"During assignment of phenotype data, an error occurred, and the " + f"module terminated prematurely. For more information, see the logs." + ) def summarize_phenoquantpercentassgnparams(self: Self): - if self.params.PhenoQuantPercentAssgnParams.activate_module: + if ( + self.params.PhenoQuantPercentAssgnParams.activate_module + and self.params.PhenoQuantPercentAssgnParams.module_passed + ): self.summary.append( f"For each molecular feature detected in more than two " f"phenotype-associated samples, the " @@ -253,9 +355,20 @@ def summarize_phenoquantpercentassgnparams(self: Self): f"'{self.params.PhenoQuantPercentAssgnParams.p_val_cutoff}'" f"." ) + elif ( + self.params.PhenoQuantPercentAssgnParams.activate_module + and not self.params.PhenoQuantPercentAssgnParams.module_passed + ): + self.summary.append( + f"During assignment of phenotype data, an error occurred, and the " + f"module terminated prematurely. For more information, see the logs." + ) def summarize_phenoquantconcassgnparams(self: Self): - if self.params.PhenoQuantConcAssgnParams.activate_module: + if ( + self.params.PhenoQuantConcAssgnParams.activate_module + and self.params.PhenoQuantConcAssgnParams.module_passed + ): self.summary.append( f"For each molecular feature detected in more than two " f"phenotype-associated samples, the " @@ -270,9 +383,20 @@ def summarize_phenoquantconcassgnparams(self: Self): f"'{self.params.PhenoQuantConcAssgnParams.p_val_cutoff}'" f"." ) + elif ( + self.params.PhenoQuantConcAssgnParams.activate_module + and not self.params.PhenoQuantConcAssgnParams.module_passed + ): + self.summary.append( + f"During assignment of phenotype data, an error occurred, and the " + f"module terminated prematurely. For more information, see the logs." + ) def summarize_spectrallibmatchingcosineparameters(self: Self): - if self.params.SpectralLibMatchingCosineParameters.activate_module: + if ( + self.params.SpectralLibMatchingCosineParameters.activate_module + and self.params.SpectralLibMatchingCosineParameters.module_passed + ): self.summary.append( f"The MS/MS spectrum of each molecular feature was matched pairwise " f"against the user-provided spectral library using the 'modified " @@ -287,9 +411,21 @@ def summarize_spectrallibmatchingcosineparameters(self: Self): f"'{self.params.SpectralLibMatchingCosineParameters.max_precursor_mass_diff}" f"' was not exceeded." ) + elif ( + self.params.SpectralLibMatchingCosineParameters.activate_module + and not self.params.SpectralLibMatchingCosineParameters.module_passed + ): + self.summary.append( + f"During spectral library matching using the modified cosine " + f"algorithm, an error occurred, and the " + f"module terminated prematurely. For more information, see the logs." + ) def summarize_spectrallibmatchingdeepscoreparameters(self: Self): - if self.params.SpectralLibMatchingDeepscoreParameters.activate_module: + if ( + self.params.SpectralLibMatchingDeepscoreParameters.activate_module + and self.params.SpectralLibMatchingDeepscoreParameters.module_passed + ): self.summary.append( f"The MS/MS spectrum of each molecular feature was matched pairwise " f"against the user-provided spectral library using the 'MS2DeepScore" @@ -300,11 +436,21 @@ def summarize_spectrallibmatchingdeepscoreparameters(self: Self): f"'{self.params.SpectralLibMatchingDeepscoreParameters.max_precursor_mass_diff}" f"' was not exceeded." ) + elif ( + self.params.SpectralLibMatchingDeepscoreParameters.activate_module + and not self.params.SpectralLibMatchingDeepscoreParameters.module_passed + ): + self.summary.append( + f"During spectral library matching using the MS2DeepScore " + f"algorithm, an error occurred, and the " + f"module terminated prematurely. For more information, see the logs." + ) def summarize_ms2queryannotationparameters(self: Self): if ( self.params.MS2QueryResultsParameters is None and self.params.Ms2QueryAnnotationParameters.activate_module + and self.params.Ms2QueryAnnotationParameters.module_passed ): self.summary.append( f"For each molecular feature, annotation using the algorithm " @@ -312,9 +458,21 @@ def summarize_ms2queryannotationparameters(self: Self): f"exceeded the cutoff score of " f"'{self.params.Ms2QueryAnnotationParameters.score_cutoff}'." ) + elif ( + self.params.MS2QueryResultsParameters is None + and self.params.Ms2QueryAnnotationParameters.activate_module + and not self.params.Ms2QueryAnnotationParameters.module_passed + ): + self.summary.append( + f"During MS2Query annotation, an error occurred, and the " + f"module terminated prematurely. For more information, see the logs." + ) def summarize_askcbcosinematchingparams(self: Self): - if self.params.AsKcbCosineMatchingParams.activate_module: + if ( + self.params.AsKcbCosineMatchingParams.activate_module + and self.params.AsKcbCosineMatchingParams.module_passed + ): self.summary.append( f"The MS/MS spectrum of each molecular feature was matched pairwise " f"against a targeted spectral library constructed from relevant " @@ -331,9 +489,21 @@ def summarize_askcbcosinematchingparams(self: Self): f"'{self.params.AsKcbCosineMatchingParams.max_precursor_mass_diff}" f"' was not exceeded." ) + elif ( + self.params.AsKcbCosineMatchingParams.activate_module + and not self.params.AsKcbCosineMatchingParams.module_passed + ): + self.summary.append( + f"During annotation using the antiSMASH KnownClusterBlast results, " + f"an error occurred in the modified cosine-based matching, and the " + f"module terminated prematurely. For more information, see the logs." + ) def summarize_askcbdeepscorematchingparams(self: Self): - if self.params.AsKcbDeepscoreMatchingParams.activate_module: + if ( + self.params.AsKcbDeepscoreMatchingParams.activate_module + and self.params.AsKcbDeepscoreMatchingParams.module_passed + ): self.summary.append( f"The MS/MS spectrum of each molecular feature was matched pairwise " f"against a targeted spectral library constructed from relevant " @@ -346,6 +516,15 @@ def summarize_askcbdeepscorematchingparams(self: Self): f"'{self.params.AsKcbDeepscoreMatchingParams.max_precursor_mass_diff}" f"' was not exceeded." ) + elif ( + self.params.AsKcbDeepscoreMatchingParams.activate_module + and not self.params.AsKcbDeepscoreMatchingParams.module_passed + ): + self.summary.append( + f"During annotation using the antiSMASH KnownClusterBlast results, " + f"an error occurred in the MS2DeepScore-based matching, and the " + f"module terminated prematurely. For more information, see the logs." + ) def assemble_summary(self: Self): """Call methods to assemble the summary file""" diff --git a/fermo_core/input_output/core_module_parameter_managers.py b/fermo_core/input_output/core_module_parameter_managers.py index 409c3e1..e90dfd6 100644 --- a/fermo_core/input_output/core_module_parameter_managers.py +++ b/fermo_core/input_output/core_module_parameter_managers.py @@ -34,14 +34,12 @@ class AdductAnnotationParameters(BaseModel): Attributes: activate_module: bool to indicate if module should be executed. mass_dev_ppm: The estimated maximum mass deviation in ppm. - - Raise: - ValueError: Mass deviation unreasonably high. - pydantic.ValidationError: Pydantic validation failed during instantiation. + module_passed: indicates that the module ran without errors """ activate_module: bool = True mass_dev_ppm: PositiveFloat = 10.0 + module_passed: bool = False @model_validator(mode="after") def validate_adduct_annotation_parameters(self): @@ -55,6 +53,7 @@ def to_json(self: Self) -> dict: return { "activate_module": self.activate_module, "mass_dev_ppm": float(self.mass_dev_ppm), + "module_passed": self.module_passed, } else: return {"activate_module": self.activate_module} @@ -67,15 +66,13 @@ class NeutralLossParameters(BaseModel): activate_module: bool to indicate if module should be executed. mass_dev_ppm: The estimated maximum mass deviation in ppm. nonbiological: Switch on comparison against losses in 'generic_other_pos.csv' - - Raise: - ValueError: Mass deviation unreasonably high. - pydantic.ValidationError: Pydantic validation failed during instantiation. + module_passed: indicates that the module ran without errors """ activate_module: bool = True mass_dev_ppm: PositiveFloat = 10.0 nonbiological: bool = False + module_passed: bool = False @model_validator(mode="after") def validate_adduct_annotation_parameters(self): @@ -90,6 +87,7 @@ def to_json(self: Self) -> dict: "activate_module": self.activate_module, "mass_dev_ppm": float(self.mass_dev_ppm), "nonbiological": self.nonbiological, + "module_passed": self.module_passed, } else: return {"activate_module": self.activate_module} @@ -101,14 +99,12 @@ class FragmentAnnParameters(BaseModel): Attributes: activate_module: bool to indicate if module should be executed. mass_dev_ppm: The estimated maximum mass deviation in ppm. - - Raise: - ValueError: Mass deviation unreasonably high. - pydantic.ValidationError: Pydantic validation failed during instantiation. + module_passed: indicates that the module ran without errors """ activate_module: bool = True mass_dev_ppm: PositiveFloat = 10.0 + module_passed: bool = False @model_validator(mode="after") def validate_fragment_annotation_parameters(self): @@ -122,6 +118,7 @@ def to_json(self: Self) -> dict: return { "activate_module": self.activate_module, "mass_dev_ppm": float(self.mass_dev_ppm), + "module_passed": self.module_passed, } else: return {"activate_module": self.activate_module} @@ -138,10 +135,8 @@ class SpecSimNetworkCosineParameters(BaseModel): fragment_tol: the tolerance between matched fragments, in m/z units. score_cutoff: the minimum similarity score between two spectra. max_nr_links: max nr of connections from a node. - maximum_runtime: max runtime of module, in seconds; 0 indicates no runtime limit - - Raise: - pydantic.ValidationError: Pydantic validation failed during instantiation. + maximum_runtime: maximum runtime in seconds ('0' indicates unlimited runtime) + module_passed: indicates that the module ran without errors """ activate_module: bool = True @@ -149,7 +144,8 @@ class SpecSimNetworkCosineParameters(BaseModel): fragment_tol: PositiveFloat = 0.1 score_cutoff: PositiveFloat = 0.7 max_nr_links: PositiveInt = 10 - maximum_runtime: int = 1200 + maximum_runtime: int = 0 + module_passed: bool = False def to_json(self: Self) -> dict: """Convert attributes to json-compatible ones.""" @@ -161,6 +157,7 @@ def to_json(self: Self) -> dict: "score_cutoff": float(self.score_cutoff), "max_nr_links": int(self.max_nr_links), "maximum_runtime": int(self.maximum_runtime), + "module_passed": self.module_passed, } else: return {"activate_module": self.activate_module} @@ -176,17 +173,16 @@ class SpecSimNetworkDeepscoreParameters(BaseModel): score_cutoff: the minimum similarity score between two spectra. max_nr_links: max links to a single spectra. msms_min_frag_nr: minimum number of fragments in MS2 to run it in analysis - maximum_runtime: max runtime of module, in seconds; 0 indicates no runtime limit - - Raise: - pydantic.ValidationError: Pydantic validation failed during instantiation. + maximum_runtime: maximum runtime in seconds ('0' indicates unlimited runtime) + module_passed: indicates that the module ran without errors """ activate_module: bool = True score_cutoff: PositiveFloat = 0.8 max_nr_links: PositiveInt = 10 msms_min_frag_nr: PositiveInt = 5 - maximum_runtime: int = 1200 + maximum_runtime: int = 0 + module_passed: bool = False def to_json(self: Self) -> dict: """Convert attributes to json-compatible ones.""" @@ -197,6 +193,7 @@ def to_json(self: Self) -> dict: "max_nr_links": int(self.max_nr_links), "msms_min_frag_nr": int(self.msms_min_frag_nr), "maximum_runtime": int(self.maximum_runtime), + "module_passed": self.module_passed, } else: return {"activate_module": self.activate_module} diff --git a/fermo_core/main.py b/fermo_core/main.py index 378f186..0533df2 100644 --- a/fermo_core/main.py +++ b/fermo_core/main.py @@ -59,7 +59,7 @@ def main(params: ParameterManager, starttime: datetime, logger: logging.Logger): params=params, stats=stats, features=features, samples=samples ) analysis_manager.analyze() - stats, features, samples = analysis_manager.return_attributes() + stats, features, samples, params = analysis_manager.return_attributes() export_manager = ExportManager( params=params, stats=stats, features=features, samples=samples diff --git a/pyproject.toml b/pyproject.toml index 0eb32f2..32ec92c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "fermo_core" -version = "0.2.2" +version = "0.3.0" description = "Data processing/analysis functionality of metabolomics dashboard FERMO" readme = "README.md" requires-python = ">=3.11,<3.12" @@ -192,5 +192,7 @@ ignore = [ # unnecessary-comprehension-in-call "C419", # function-uses-loop-variable - "B023" + "B023", + # unnecessary-literal-within-tuple-cal. + "C409" ] \ No newline at end of file diff --git a/tests/test_data/test.parameters.json b/tests/test_data/test.parameters.json index 9aac9e0..255b98d 100644 --- a/tests/test_data/test.parameters.json +++ b/tests/test_data/test.parameters.json @@ -66,14 +66,10 @@ "additional_modules": { "feature_filtering": { "activate_module": true, - "filter_rel_int_range": [ - 0.1, - 1.0 - ], - "filter_rel_area_range": [ - 0.1, - 1.0 - ] + "filter_rel_int_range_min": 0.1, + "filter_rel_int_range_max": 1.0, + "filter_rel_area_range_min": 0.1, + "filter_rel_area_range_max": 1.0 }, "blank_assignment": { "activate_module": true, diff --git a/tests/test_data_analysis/test_analysis_manager/test_class_analysis_manager.py b/tests/test_data_analysis/test_analysis_manager/test_class_analysis_manager.py index 9f8f1b8..c37d24e 100644 --- a/tests/test_data_analysis/test_analysis_manager/test_class_analysis_manager.py +++ b/tests/test_data_analysis/test_analysis_manager/test_class_analysis_manager.py @@ -20,7 +20,7 @@ def test_init_valid(analysis_manager_instance): def test_return_valid(analysis_manager_instance): - stats, features, samples = analysis_manager_instance.return_attributes() + stats, features, samples, params = analysis_manager_instance.return_attributes() assert stats is not None diff --git a/tests/test_data_analysis/test_feature_filter/test_feature_filter.py b/tests/test_data_analysis/test_feature_filter/test_feature_filter.py index 3ef396a..d5b8248 100644 --- a/tests/test_data_analysis/test_feature_filter/test_feature_filter.py +++ b/tests/test_data_analysis/test_feature_filter/test_feature_filter.py @@ -1,10 +1,10 @@ import pytest from fermo_core.data_analysis.feature_filter.class_feature_filter import FeatureFilter -from fermo_core.data_processing.class_stats import Stats -from fermo_core.data_processing.class_repository import Repository -from fermo_core.data_processing.builder_sample.dataclass_sample import Sample from fermo_core.data_processing.builder_feature.dataclass_feature import Feature +from fermo_core.data_processing.builder_sample.dataclass_sample import Sample +from fermo_core.data_processing.class_repository import Repository +from fermo_core.data_processing.class_stats import Stats from fermo_core.input_output.class_parameter_manager import ParameterManager @@ -66,28 +66,38 @@ def test_filter_rel_area_range_valid(feature_filter_instance): def test_filter_rel_int_range_mod_valid(feature_filter_instance): - feature_filter_instance.params.FeatureFilteringParameters.filter_rel_int_range = [ - 0.06, - 1.0, - ] + feature_filter_instance.params.FeatureFilteringParameters.filter_rel_int_range_min = ( + 0.06 + ) + feature_filter_instance.params.FeatureFilteringParameters.filter_rel_int_range_max = ( + 1.0 + ) feature_filter_instance.filter_rel_int_range() assert len(feature_filter_instance.stats.active_features) == 140 assert len(feature_filter_instance.stats.inactive_features) == 3 def test_filter_rel_area_range_mod_valid(feature_filter_instance): - feature_filter_instance.params.FeatureFilteringParameters.filter_rel_area_range = [ - 0.06, - 1.0, - ] + feature_filter_instance.params.FeatureFilteringParameters.filter_rel_area_range_min = ( + 0.06 + ) + feature_filter_instance.params.FeatureFilteringParameters.filter_rel_area_range_max = ( + 1.0 + ) feature_filter_instance.filter_rel_area_range() assert len(feature_filter_instance.stats.active_features) == 104 assert len(feature_filter_instance.stats.inactive_features) == 39 def test_filter_features_for_range_valid(dummy_data): - filtered = FeatureFilter.filter_features_for_range( - dummy_data["stats"], dummy_data["samples"], [0.0, 0.5], "rel_intensity" + feature_filter = FeatureFilter( + params=ParameterManager(), + stats=dummy_data["stats"], + features=dummy_data["features"], + samples=dummy_data["samples"], + ) + filtered = feature_filter.filter_features_for_range( + r_list=[0.0, 0.5], param="rel_intensity" ) assert filtered == {2} diff --git a/tests/test_data_analysis/test_sim_networks_manager/test_class_sim_networks_manager.py b/tests/test_data_analysis/test_sim_networks_manager/test_class_sim_networks_manager.py index d10a9a7..d785e5a 100644 --- a/tests/test_data_analysis/test_sim_networks_manager/test_class_sim_networks_manager.py +++ b/tests/test_data_analysis/test_sim_networks_manager/test_class_sim_networks_manager.py @@ -1,15 +1,14 @@ import pytest -from fermo_core.data_analysis.sim_networks_manager.class_sim_networks_manager import ( - SimNetworksManager, -) - from fermo_core.data_analysis.sim_networks_manager.class_mod_cosine_networker import ( ModCosineNetworker, ) from fermo_core.data_analysis.sim_networks_manager.class_ms2deepscore_networker import ( Ms2deepscoreNetworker, ) +from fermo_core.data_analysis.sim_networks_manager.class_sim_networks_manager import ( + SimNetworksManager, +) @pytest.fixture @@ -39,7 +38,7 @@ def test_log_filtered_feature_nr_fragments_valid(sim_networks_manager_instance): def test_return_valid(sim_networks_manager_instance): - stats, features, samples = sim_networks_manager_instance.return_attrs() + stats, features, samples, params = sim_networks_manager_instance.return_attrs() assert stats is not None diff --git a/tests/test_data_processing/test_parser/test_general_parser/case_study_parameters.json b/tests/test_data_processing/test_parser/test_general_parser/case_study_parameters.json index 76a275a..818d1ac 100644 --- a/tests/test_data_processing/test_parser/test_general_parser/case_study_parameters.json +++ b/tests/test_data_processing/test_parser/test_general_parser/case_study_parameters.json @@ -65,14 +65,10 @@ "additional_modules": { "feature_filtering": { "activate_module": true, - "filter_rel_int_range": [ - 0.1, - 1.0 - ], - "filter_rel_area_range": [ - 0.1, - 1.0 - ] + "filter_rel_int_range_min": 0.1, + "filter_rel_int_range_max": 1.0, + "filter_rel_area_range_min": 0.1, + "filter_rel_area_range_max": 1.0 }, "blank_assignment": { "activate_module": true, diff --git a/tests/test_input_output/test_additional_module_parameter_managers/test_feature_filtering_parameters.py b/tests/test_input_output/test_additional_module_parameter_managers/test_feature_filtering_parameters.py index e390ebf..d9cb757 100644 --- a/tests/test_input_output/test_additional_module_parameter_managers/test_feature_filtering_parameters.py +++ b/tests/test_input_output/test_additional_module_parameter_managers/test_feature_filtering_parameters.py @@ -8,8 +8,10 @@ def test_init_feature_filtering_parameters_valid(): json_dict = { "activate_module": True, - "filter_rel_int_range": [0.0, 1.0], - "filter_rel_area_range": [0.0, 1.0], + "filter_rel_int_range_min": 0.1, + "filter_rel_int_range_max": 1.0, + "filter_rel_area_range_min": 0.1, + "filter_rel_area_range_max": 1.0, } assert isinstance( FeatureFilteringParameters(**json_dict), FeatureFilteringParameters @@ -21,35 +23,16 @@ def test_init_feature_filtering_parameters_fail(): FeatureFilteringParameters(None) -def test_rel_int_range_scrambled_valid(): - json_dict = { - "filter_rel_int_range": [1.0, 0.0], - } - assert isinstance( - FeatureFilteringParameters(**json_dict), FeatureFilteringParameters - ) - - -def test_rel_area_range_scrambled_valid(): - json_dict = { - "filter_rel_area_range": [1.0, 0.0], - } - assert isinstance( - FeatureFilteringParameters(**json_dict), FeatureFilteringParameters - ) - - def test_rel_int_range_invalid(): with pytest.raises(ValueError): json_dict = { - "filter_rel_int_range": [0.0], + "filter_rel_int_range_min": 1.0, + "filter_rel_int_range_max": 0.1, } FeatureFilteringParameters(**json_dict) def test_rel_area_range_invalid(): with pytest.raises(ValueError): - json_dict = { - "filter_rel_area_range": [0.0], - } + json_dict = {"filter_rel_area_range_min": 1.0, "filter_rel_area_range_max": 0.1} FeatureFilteringParameters(**json_dict) diff --git a/tests/test_input_output/test_additional_module_parameter_managers/test_pheno_qual_assgn_params.py b/tests/test_input_output/test_additional_module_parameter_managers/test_pheno_qual_assgn_params.py index 75b04fa..41f7c76 100644 --- a/tests/test_input_output/test_additional_module_parameter_managers/test_pheno_qual_assgn_params.py +++ b/tests/test_input_output/test_additional_module_parameter_managers/test_pheno_qual_assgn_params.py @@ -11,6 +11,7 @@ def test_json_export_valid(): "factor": 10, "algorithm": "minmax", "value": "area", + "module_passed": True, } obj = PhenoQualAssgnParams(**dict_in) dict_out = obj.to_json() diff --git a/tests/test_input_output/test_additional_module_parameter_managers/test_pheno_quant_conc_assgn_params.py b/tests/test_input_output/test_additional_module_parameter_managers/test_pheno_quant_conc_assgn_params.py index 3013a0d..c74176e 100644 --- a/tests/test_input_output/test_additional_module_parameter_managers/test_pheno_quant_conc_assgn_params.py +++ b/tests/test_input_output/test_additional_module_parameter_managers/test_pheno_quant_conc_assgn_params.py @@ -13,6 +13,7 @@ def test_json_export_valid(): "algorithm": "pearson", "p_val_cutoff": 0.05, "coeff_cutoff": 0.7, + "module_passed": True, } obj = PhenoQuantConcAssgnParams(**dict_in) dict_out = obj.to_json() diff --git a/tests/test_input_output/test_additional_module_parameter_managers/test_pheno_quant_percent_assgn_params.py b/tests/test_input_output/test_additional_module_parameter_managers/test_pheno_quant_percent_assgn_params.py index 84c3113..9a7ea27 100644 --- a/tests/test_input_output/test_additional_module_parameter_managers/test_pheno_quant_percent_assgn_params.py +++ b/tests/test_input_output/test_additional_module_parameter_managers/test_pheno_quant_percent_assgn_params.py @@ -13,6 +13,7 @@ def test_json_export_valid(): "algorithm": "pearson", "p_val_cutoff": 0.05, "coeff_cutoff": 0.7, + "module_passed": True, } obj = PhenoQuantPercentAssgnParams(**dict_in) dict_out = obj.to_json() diff --git a/tests/test_input_output/test_parameter_manager/test_class_parameter_manager.py b/tests/test_input_output/test_parameter_manager/test_class_parameter_manager.py index 2e290bb..0b11cf4 100644 --- a/tests/test_input_output/test_parameter_manager/test_class_parameter_manager.py +++ b/tests/test_input_output/test_parameter_manager/test_class_parameter_manager.py @@ -269,8 +269,10 @@ def test_assign_feature_filtering_valid(): params.assign_feature_filtering( { "activate_module": True, - "filter_rel_int_range": [0.0, 1.0], - "filter_rel_area_range": [0.0, 1.0], + "filter_rel_int_range_min": 0.0, + "filter_rel_int_range_max": 1.0, + "filter_rel_area_range_min": 0.0, + "filter_rel_area_range_max": 1.0, } ) assert isinstance(params.FeatureFilteringParameters, FeatureFilteringParameters) @@ -279,7 +281,10 @@ def test_assign_feature_filtering_valid(): def test_assign_feature_filtering_invalid(): params = ParameterManager() params.assign_feature_filtering({"asdfg": "asdfg"}) - assert params.FeatureFilteringParameters.filter_rel_int_range is None + assert params.FeatureFilteringParameters.filter_rel_area_range_min == 0.0 + assert params.FeatureFilteringParameters.filter_rel_area_range_max == 1.0 + assert params.FeatureFilteringParameters.filter_rel_int_range_min == 0.0 + assert params.FeatureFilteringParameters.filter_rel_int_range_max == 1.0 def test_assign_blank_assignment_valid():