From 19182a673542f67c44639e5e038cd1df74a06c1d Mon Sep 17 00:00:00 2001 From: Jochen Sieg Date: Thu, 16 May 2024 16:13:55 +0200 Subject: [PATCH 1/6] mol2rdkit_phys_chem: make failure on any error optional - Add a parameter fails_on_any_error to MolToRDKitPhysChem to get partial results when the computation of a single physchem descriptor fails. --- molpipeline/mol2any/mol2rdkit_phys_chem.py | 17 +++- .../test_mol2any/test_mol2rdkit_phys_chem.py | 83 +++++++++++++++++-- 2 files changed, 89 insertions(+), 11 deletions(-) diff --git a/molpipeline/mol2any/mol2rdkit_phys_chem.py b/molpipeline/mol2any/mol2rdkit_phys_chem.py index 9b9d7255..aa158a99 100644 --- a/molpipeline/mol2any/mol2rdkit_phys_chem.py +++ b/molpipeline/mol2any/mol2rdkit_phys_chem.py @@ -43,6 +43,7 @@ class MolToRDKitPhysChem(MolToDescriptorPipelineElement): def __init__( self, descriptor_list: Optional[list[str]] = None, + fails_on_any_error: bool = True, standardizer: Optional[AnyTransformer] = StandardScaler(), name: str = "Mol2RDKitPhysChem", n_jobs: int = 1, @@ -54,6 +55,9 @@ def __init__( ---------- descriptor_list: Optional[list[str]], optional (default=None) List of descriptor names to calculate. If None, DEFAULT_DESCRIPTORS are used. + fails_on_any_error: bool, optional (default = True) + If True, the pipeline element will fail if any descriptor calculation fails. + If False, the pipeline element will return a vector with NaN values for failed calculations. standardizer: Optional[AnyTransformer], optional (default=StandardScaler()) Standardizer to use. name: str, optional (default="Mol2RDKitPhysChem") @@ -64,6 +68,7 @@ def __init__( UUID of the PipelineElement. If None, a new UUID is generated. """ self._descriptor_list = descriptor_list or DEFAULT_DESCRIPTORS + self._fails_on_any_error = fails_on_any_error super().__init__( standardizer=standardizer, name=name, @@ -111,7 +116,7 @@ def pretransform_single( vec = np.array( [RDKIT_DESCRIPTOR_DICT[name](value) for name in self._descriptor_list] ) - if np.any(np.isnan(vec)): + if self._fails_on_any_error and np.any(np.isnan(vec)): return InvalidInstance(self.uuid, "NaN in descriptor vector", self.name) return vec @@ -131,8 +136,10 @@ def get_params(self, deep: bool = True) -> dict[str, Any]: parent_dict = dict(super().get_params(deep=deep)) if deep: parent_dict["descriptor_list"] = copy.deepcopy(self._descriptor_list) + parent_dict["fails_on_any_error"] = copy.deepcopy(self._fails_on_any_error) else: parent_dict["descriptor_list"] = self._descriptor_list + parent_dict["fails_on_any_error"] = self._fails_on_any_error return parent_dict def set_params(self, **parameters: dict[str, Any]) -> Self: @@ -149,8 +156,10 @@ def set_params(self, **parameters: dict[str, Any]) -> Self: Self """ parameters_shallow_copy = dict(parameters) - descriptor_list = parameters_shallow_copy.pop("descriptor_list", None) - if descriptor_list is not None: - self._descriptor_list = descriptor_list # type: ignore + params_list = ["descriptor_list", "fails_on_any_error"] + for param_name in params_list: + if param_name in parameters: + setattr(self, f"_{param_name}", parameters[param_name]) + parameters_shallow_copy.pop(param_name) super().set_params(**parameters_shallow_copy) return self diff --git a/tests/test_elements/test_mol2any/test_mol2rdkit_phys_chem.py b/tests/test_elements/test_mol2any/test_mol2rdkit_phys_chem.py index 2b9c7c95..741e52d0 100644 --- a/tests/test_elements/test_mol2any/test_mol2rdkit_phys_chem.py +++ b/tests/test_elements/test_mol2any/test_mol2rdkit_phys_chem.py @@ -7,7 +7,7 @@ import pandas as pd from sklearn.preprocessing import StandardScaler -from molpipeline import Pipeline +from molpipeline import ErrorFilter, FilterReinserter, Pipeline from molpipeline.any2mol import SmilesToMol from molpipeline.mol2any import MolToRDKitPhysChem from molpipeline.mol2any.mol2rdkit_phys_chem import DEFAULT_DESCRIPTORS @@ -262,9 +262,7 @@ def test_descriptor_calculation(self) -> None: property_vector = expected_df[descriptor_names].values output = pipeline.fit_transform(smiles) - difference = output - property_vector - - self.assertTrue(difference.max() < 0.0001) # add assertion here + self.assertTrue(np.allclose(output, property_vector)) # add assertion here def test_descriptor_normalization(self) -> None: """Test if the normalization of RDKitPhysChem Descriptors works as expected. @@ -297,10 +295,81 @@ def test_descriptor_normalization(self) -> None: output = pipeline.fit_transform(smiles) non_zero_descriptors = output[:, (np.abs(output).sum(axis=0) != 0)] self.assertTrue( - non_zero_descriptors.mean(axis=0).max() < 0.0000001 + np.allclose(non_zero_descriptors.mean(axis=0), 0.0) ) # add assertion here - self.assertTrue(non_zero_descriptors.std(axis=0).max() < 1.0000001) - self.assertTrue(non_zero_descriptors.std(axis=0).min() > 0.9999999) + self.assertTrue(np.allclose(non_zero_descriptors.std(axis=0), 1.0)) + + def test_optional_nan_value_handling(self) -> None: + """Test the handling of partly failed descriptor calculations.""" + + ok_smiles_list = [ + "CC", + "C(C)CCO", + ] + bad_smiles_list = [ + "F[P-](F)(F)(F)(F)F.CCCC[N+]1=CC=CC=C1C", + ] + + # test with fails_on_any_error=True + property_element = MolToRDKitPhysChem( + standardizer=None, fails_on_any_error=True + ) + + error_filter = ErrorFilter.from_element_list([property_element]) + error_replacer = FilterReinserter.from_error_filter( + error_filter, fill_value=np.nan + ) + + # note that we need the error filter and replacer here. Otherwise, the pipeline would fail on any error + # irrespective of the fails_on_any_error parameter + pipeline = Pipeline( + [ + ("smi2mol", SmilesToMol()), + ("property_element", property_element), + ("error_filter", error_filter), + ("error_replacer", error_replacer), + ] + ) + + output = pipeline.fit_transform(bad_smiles_list + ok_smiles_list) + self.assertEqual(len(output), len(bad_smiles_list + ok_smiles_list)) + # check expect-to-fail rows are ALL nan values + self.assertTrue( + np.equal(np.isnan(output).all(axis=1), [True, False, False]).all() + ) + # check expected-not-to-fail rows contain zero nan values + self.assertTrue( + np.equal(np.isnan(output).any(axis=1), [True, False, False]).all() + ) + + # test with fails_on_any_error=False + property_element2 = MolToRDKitPhysChem( + standardizer=None, fails_on_any_error=False + ) + + error_filter2 = ErrorFilter.from_element_list([property_element2]) + filter_reinserter = FilterReinserter.from_error_filter( + error_filter2, fill_value=np.nan + ) + pipeline2 = Pipeline( + [ + ("smi2mol", SmilesToMol()), + ("property_element", property_element2), + ("error_filter", error_filter2), + ("error_replacer", filter_reinserter), + ] + ) + + output2 = pipeline2.fit_transform(bad_smiles_list + ok_smiles_list) + self.assertEqual(len(output2), len(bad_smiles_list + ok_smiles_list)) + # check expect-to-fail rows are ALL nan values + self.assertTrue( + np.equal(np.isnan(output2).all(axis=1), [False, False, False]).all() + ) + # check expected-not-to-fail rows contain zero nan values + self.assertTrue( + np.equal(np.isnan(output2).any(axis=1), [True, False, False]).all() + ) if __name__ == "__main__": From 998bdb824c8ba7f7b338f83372ecc988a5a580c6 Mon Sep 17 00:00:00 2001 From: Jochen Sieg Date: Thu, 16 May 2024 17:30:38 +0200 Subject: [PATCH 2/6] mol2rdkit_phys_chem: catch and log exceptions - Exceptions are now catched for individual descriptors. - Exceptions can optionally be logged. - Validity of user-defined descriptor names is checked in the constructor. --- molpipeline/mol2any/mol2rdkit_phys_chem.py | 44 +++++++++++++------ .../test_mol2any/test_mol2rdkit_phys_chem.py | 36 +++++++++++++-- 2 files changed, 63 insertions(+), 17 deletions(-) diff --git a/molpipeline/mol2any/mol2rdkit_phys_chem.py b/molpipeline/mol2any/mol2rdkit_phys_chem.py index aa158a99..cb20fd43 100644 --- a/molpipeline/mol2any/mol2rdkit_phys_chem.py +++ b/molpipeline/mol2any/mol2rdkit_phys_chem.py @@ -15,6 +15,7 @@ import numpy as np import numpy.typing as npt +from loguru import logger from rdkit import Chem from rdkit.Chem import Descriptors from sklearn.preprocessing import StandardScaler @@ -43,8 +44,9 @@ class MolToRDKitPhysChem(MolToDescriptorPipelineElement): def __init__( self, descriptor_list: Optional[list[str]] = None, - fails_on_any_error: bool = True, + return_with_errors: bool = False, standardizer: Optional[AnyTransformer] = StandardScaler(), + log_exceptions: bool = True, name: str = "Mol2RDKitPhysChem", n_jobs: int = 1, uuid: Optional[str] = None, @@ -55,11 +57,13 @@ def __init__( ---------- descriptor_list: Optional[list[str]], optional (default=None) List of descriptor names to calculate. If None, DEFAULT_DESCRIPTORS are used. - fails_on_any_error: bool, optional (default = True) - If True, the pipeline element will fail if any descriptor calculation fails. - If False, the pipeline element will return a vector with NaN values for failed calculations. + return_with_errors: bool, optional (default = False) + If False, the pipeline element will return an InvalidInstance if any error occurs during calculations. + If True, the pipeline element will return a vector with NaN values for failed descriptor calculations. standardizer: Optional[AnyTransformer], optional (default=StandardScaler()) Standardizer to use. + log_exceptions: bool, optional (default=True) + If True, traceback of exceptions occurring during descriptor calculation are logged. name: str, optional (default="Mol2RDKitPhysChem") Name of the PipelineElement. n_jobs: int, optional (default=1) @@ -67,8 +71,15 @@ def __init__( uuid: Optional[str], optional (default=None) UUID of the PipelineElement. If None, a new UUID is generated. """ - self._descriptor_list = descriptor_list or DEFAULT_DESCRIPTORS - self._fails_on_any_error = fails_on_any_error + if descriptor_list is not None: + for name in descriptor_list: + if name not in RDKIT_DESCRIPTOR_DICT: + raise ValueError(f"Unknown descriptor function with name: {name}") + self._descriptor_list = descriptor_list + else: + self._descriptor_list = DEFAULT_DESCRIPTORS + self._return_with_errors = return_with_errors + self._log_exceptions = log_exceptions super().__init__( standardizer=standardizer, name=name, @@ -113,10 +124,15 @@ def pretransform_single( Optional[npt.NDArray[np.float_]] Descriptor vector for given molecule. None if calculation failed. """ - vec = np.array( - [RDKIT_DESCRIPTOR_DICT[name](value) for name in self._descriptor_list] - ) - if self._fails_on_any_error and np.any(np.isnan(vec)): + vec = np.full((len(self._descriptor_list),), np.nan) + for i, name in enumerate(self._descriptor_list): + descriptor_func = RDKIT_DESCRIPTOR_DICT[name] + try: + vec[i] = descriptor_func(value) + except Exception as e: + if self._log_exceptions: + logger.exception(f"Failed calculating descriptor: {name}") + if not self._return_with_errors and np.any(np.isnan(vec)): return InvalidInstance(self.uuid, "NaN in descriptor vector", self.name) return vec @@ -136,10 +152,12 @@ def get_params(self, deep: bool = True) -> dict[str, Any]: parent_dict = dict(super().get_params(deep=deep)) if deep: parent_dict["descriptor_list"] = copy.deepcopy(self._descriptor_list) - parent_dict["fails_on_any_error"] = copy.deepcopy(self._fails_on_any_error) + parent_dict["fails_on_any_error"] = copy.deepcopy(self._return_with_errors) + parent_dict["log_exceptions"] = copy.deepcopy(self._log_exceptions) else: parent_dict["descriptor_list"] = self._descriptor_list - parent_dict["fails_on_any_error"] = self._fails_on_any_error + parent_dict["fails_on_any_error"] = self._return_with_errors + parent_dict["log_exceptions"] = self._log_exceptions return parent_dict def set_params(self, **parameters: dict[str, Any]) -> Self: @@ -156,7 +174,7 @@ def set_params(self, **parameters: dict[str, Any]) -> Self: Self """ parameters_shallow_copy = dict(parameters) - params_list = ["descriptor_list", "fails_on_any_error"] + params_list = ["descriptor_list", "fails_on_any_error", "log_exceptions"] for param_name in params_list: if param_name in parameters: setattr(self, f"_{param_name}", parameters[param_name]) diff --git a/tests/test_elements/test_mol2any/test_mol2rdkit_phys_chem.py b/tests/test_elements/test_mol2any/test_mol2rdkit_phys_chem.py index 741e52d0..f1579b39 100644 --- a/tests/test_elements/test_mol2any/test_mol2rdkit_phys_chem.py +++ b/tests/test_elements/test_mol2any/test_mol2rdkit_phys_chem.py @@ -310,9 +310,9 @@ def test_optional_nan_value_handling(self) -> None: "F[P-](F)(F)(F)(F)F.CCCC[N+]1=CC=CC=C1C", ] - # test with fails_on_any_error=True + # test with return_with_errors=False property_element = MolToRDKitPhysChem( - standardizer=None, fails_on_any_error=True + standardizer=None, return_with_errors=False ) error_filter = ErrorFilter.from_element_list([property_element]) @@ -342,9 +342,9 @@ def test_optional_nan_value_handling(self) -> None: np.equal(np.isnan(output).any(axis=1), [True, False, False]).all() ) - # test with fails_on_any_error=False + # test with return_with_errors=True property_element2 = MolToRDKitPhysChem( - standardizer=None, fails_on_any_error=False + standardizer=None, return_with_errors=True ) error_filter2 = ErrorFilter.from_element_list([property_element2]) @@ -371,6 +371,34 @@ def test_optional_nan_value_handling(self) -> None: np.equal(np.isnan(output2).any(axis=1), [True, False, False]).all() ) + def test_unknown_descriptor_name(self) -> None: + """Test the handling of unknown descriptor names.""" + + self.assertRaises( + ValueError, + MolToRDKitPhysChem, + **{"descriptor_list": ["__NotADescriptor11Name4374737834hggghgddd"]}, + ) + + def test_exception_handling(self) -> None: + """Test exception handling during descriptor calculation.""" + + pipeline = Pipeline( + [ + ("smi2mol", SmilesToMol()), + ( + "property_element", + MolToRDKitPhysChem( + standardizer=None, return_with_errors=True, log_exceptions=False + ), + ), + ] + ) + + # Without exception handling [HH] would raise a division-by-zero exception because it has 0 heavy atoms + output = pipeline.fit_transform(["[HH]"]) + self.assertTrue(output.shape == (1, len(DEFAULT_DESCRIPTORS))) + if __name__ == "__main__": unittest.main() From 37cb2bdd61e0e07f32852bf1929ea02a45e847ab Mon Sep 17 00:00:00 2001 From: Jochen Sieg Date: Thu, 16 May 2024 17:44:14 +0200 Subject: [PATCH 3/6] mol2rdkit_phys_chem: better naming and linting suggestions - Rename local variable to not shadow another. - Suppress pylint warning about to broad exceptions. --- molpipeline/mol2any/mol2rdkit_phys_chem.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/molpipeline/mol2any/mol2rdkit_phys_chem.py b/molpipeline/mol2any/mol2rdkit_phys_chem.py index cb20fd43..4f2840e8 100644 --- a/molpipeline/mol2any/mol2rdkit_phys_chem.py +++ b/molpipeline/mol2any/mol2rdkit_phys_chem.py @@ -72,9 +72,11 @@ def __init__( UUID of the PipelineElement. If None, a new UUID is generated. """ if descriptor_list is not None: - for name in descriptor_list: - if name not in RDKIT_DESCRIPTOR_DICT: - raise ValueError(f"Unknown descriptor function with name: {name}") + for descriptor_name in descriptor_list: + if descriptor_name not in RDKIT_DESCRIPTOR_DICT: + raise ValueError( + f"Unknown descriptor function with name: {descriptor_name}" + ) self._descriptor_list = descriptor_list else: self._descriptor_list = DEFAULT_DESCRIPTORS @@ -129,7 +131,7 @@ def pretransform_single( descriptor_func = RDKIT_DESCRIPTOR_DICT[name] try: vec[i] = descriptor_func(value) - except Exception as e: + except Exception: # pylint: disable=broad-except if self._log_exceptions: logger.exception(f"Failed calculating descriptor: {name}") if not self._return_with_errors and np.any(np.isnan(vec)): From 0749536ae990a1cfe465fc44375b887cfac4f68c Mon Sep 17 00:00:00 2001 From: Jochen Sieg Date: Thu, 16 May 2024 17:52:07 +0200 Subject: [PATCH 4/6] mol2rdkit_phys_chem: remove usage of old param name --- molpipeline/mol2any/mol2rdkit_phys_chem.py | 6 +++--- .../test_elements/test_mol2any/test_mol2rdkit_phys_chem.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/molpipeline/mol2any/mol2rdkit_phys_chem.py b/molpipeline/mol2any/mol2rdkit_phys_chem.py index 4f2840e8..cad79703 100644 --- a/molpipeline/mol2any/mol2rdkit_phys_chem.py +++ b/molpipeline/mol2any/mol2rdkit_phys_chem.py @@ -154,11 +154,11 @@ def get_params(self, deep: bool = True) -> dict[str, Any]: parent_dict = dict(super().get_params(deep=deep)) if deep: parent_dict["descriptor_list"] = copy.deepcopy(self._descriptor_list) - parent_dict["fails_on_any_error"] = copy.deepcopy(self._return_with_errors) + parent_dict["return_with_errors"] = copy.deepcopy(self._return_with_errors) parent_dict["log_exceptions"] = copy.deepcopy(self._log_exceptions) else: parent_dict["descriptor_list"] = self._descriptor_list - parent_dict["fails_on_any_error"] = self._return_with_errors + parent_dict["return_with_errors"] = self._return_with_errors parent_dict["log_exceptions"] = self._log_exceptions return parent_dict @@ -176,7 +176,7 @@ def set_params(self, **parameters: dict[str, Any]) -> Self: Self """ parameters_shallow_copy = dict(parameters) - params_list = ["descriptor_list", "fails_on_any_error", "log_exceptions"] + params_list = ["descriptor_list", "return_with_errors", "log_exceptions"] for param_name in params_list: if param_name in parameters: setattr(self, f"_{param_name}", parameters[param_name]) diff --git a/tests/test_elements/test_mol2any/test_mol2rdkit_phys_chem.py b/tests/test_elements/test_mol2any/test_mol2rdkit_phys_chem.py index f1579b39..ab04e013 100644 --- a/tests/test_elements/test_mol2any/test_mol2rdkit_phys_chem.py +++ b/tests/test_elements/test_mol2any/test_mol2rdkit_phys_chem.py @@ -321,7 +321,7 @@ def test_optional_nan_value_handling(self) -> None: ) # note that we need the error filter and replacer here. Otherwise, the pipeline would fail on any error - # irrespective of the fails_on_any_error parameter + # irrespective of the return_with_errors parameter pipeline = Pipeline( [ ("smi2mol", SmilesToMol()), From 64061a2e707717f8a50d7198b7f1d39a397e8bd3 Mon Sep 17 00:00:00 2001 From: Jochen Sieg Date: Fri, 17 May 2024 16:27:28 +0200 Subject: [PATCH 5/6] mol2rdkit_phys_chem: Add setter for descriptor list - Add setter for descriptor list to validate user provided descriptor names at a central position in the code. --- molpipeline/mol2any/mol2rdkit_phys_chem.py | 24 ++++++++++++------- .../test_mol2any/test_mol2rdkit_phys_chem.py | 2 +- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/molpipeline/mol2any/mol2rdkit_phys_chem.py b/molpipeline/mol2any/mol2rdkit_phys_chem.py index cad79703..0ca9a753 100644 --- a/molpipeline/mol2any/mol2rdkit_phys_chem.py +++ b/molpipeline/mol2any/mol2rdkit_phys_chem.py @@ -71,15 +71,7 @@ def __init__( uuid: Optional[str], optional (default=None) UUID of the PipelineElement. If None, a new UUID is generated. """ - if descriptor_list is not None: - for descriptor_name in descriptor_list: - if descriptor_name not in RDKIT_DESCRIPTOR_DICT: - raise ValueError( - f"Unknown descriptor function with name: {descriptor_name}" - ) - self._descriptor_list = descriptor_list - else: - self._descriptor_list = DEFAULT_DESCRIPTORS + self.descriptor_list = descriptor_list self._return_with_errors = return_with_errors self._log_exceptions = log_exceptions super().__init__( @@ -111,6 +103,20 @@ def descriptor_list(self) -> list[str]: """ return self._descriptor_list[:] + @descriptor_list.setter + def descriptor_list(self, descriptor_list: list[str] | None) -> None: + if descriptor_list is None or descriptor_list is DEFAULT_DESCRIPTORS: + # if None or DEFAULT_DESCRIPTORS are used, set the default descriptors + self._descriptor_list = DEFAULT_DESCRIPTORS + else: + # check all user defined descriptors are valid + for descriptor_name in descriptor_list: + if descriptor_name not in RDKIT_DESCRIPTOR_DICT: + raise ValueError( + f"Unknown descriptor function with name: {descriptor_name}" + ) + self._descriptor_list = descriptor_list + def pretransform_single( self, value: RDKitMol ) -> Union[npt.NDArray[np.float_], InvalidInstance]: diff --git a/tests/test_elements/test_mol2any/test_mol2rdkit_phys_chem.py b/tests/test_elements/test_mol2any/test_mol2rdkit_phys_chem.py index ab04e013..8f9e2090 100644 --- a/tests/test_elements/test_mol2any/test_mol2rdkit_phys_chem.py +++ b/tests/test_elements/test_mol2any/test_mol2rdkit_phys_chem.py @@ -377,7 +377,7 @@ def test_unknown_descriptor_name(self) -> None: self.assertRaises( ValueError, MolToRDKitPhysChem, - **{"descriptor_list": ["__NotADescriptor11Name4374737834hggghgddd"]}, + **{"descriptor_list": ["__NotADescriptor11Name:)"]}, ) def test_exception_handling(self) -> None: From 0cf3896d9989925b22076dcb8cf12a6aef4b4a44 Mon Sep 17 00:00:00 2001 From: Jochen Sieg Date: Fri, 17 May 2024 16:48:48 +0200 Subject: [PATCH 6/6] mol2rdkit_phys_chem: descriptor_list ignore None type in setter - Ignore None type in setter for descriptor_list to enable optional input validation and an optional None. - See https://github.com/python/mypy/issues/3004 as reference. --- molpipeline/mol2any/mol2rdkit_phys_chem.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/molpipeline/mol2any/mol2rdkit_phys_chem.py b/molpipeline/mol2any/mol2rdkit_phys_chem.py index 0ca9a753..2682fdfa 100644 --- a/molpipeline/mol2any/mol2rdkit_phys_chem.py +++ b/molpipeline/mol2any/mol2rdkit_phys_chem.py @@ -71,7 +71,7 @@ def __init__( uuid: Optional[str], optional (default=None) UUID of the PipelineElement. If None, a new UUID is generated. """ - self.descriptor_list = descriptor_list + self.descriptor_list = descriptor_list # type: ignore self._return_with_errors = return_with_errors self._log_exceptions = log_exceptions super().__init__( @@ -105,6 +105,18 @@ def descriptor_list(self) -> list[str]: @descriptor_list.setter def descriptor_list(self, descriptor_list: list[str] | None) -> None: + """Set the descriptor list. + + Parameters + ---------- + descriptor_list: list[str] | None + List of descriptor names to calculate. If None, DEFAULT_DESCRIPTORS are used. + + Raises + ------ + ValueError + If an unknown descriptor name is used. + """ if descriptor_list is None or descriptor_list is DEFAULT_DESCRIPTORS: # if None or DEFAULT_DESCRIPTORS are used, set the default descriptors self._descriptor_list = DEFAULT_DESCRIPTORS