From 30c95008b4fd35c13741856b34bbb9750dd08411 Mon Sep 17 00:00:00 2001 From: Andrew Pontzen Date: Mon, 9 Oct 2023 12:18:50 +0100 Subject: [PATCH] Provide option to explain why particular property classes get selected over others --- tangos/core/dictionary.py | 4 ++-- tangos/properties/__init__.py | 41 ++++++++++++++++++++++++--------- tangos/tools/property_writer.py | 6 ++++- 3 files changed, 37 insertions(+), 14 deletions(-) diff --git a/tangos/core/dictionary.py b/tangos/core/dictionary.py index 9c03ce74..611488f7 100644 --- a/tangos/core/dictionary.py +++ b/tangos/core/dictionary.py @@ -20,9 +20,9 @@ def __repr__(self): def __init__(self, text): self.text = text - def providing_class(self, handler): + def providing_class(self, handler, explain=False): from .. import properties - return properties.providing_class(self.text, handler) + return properties.providing_class(self.text, handler, explain) raise_exception = object() diff --git a/tangos/properties/__init__.py b/tangos/properties/__init__.py index 719f5ce4..593d954d 100644 --- a/tangos/properties/__init__.py +++ b/tangos/properties/__init__.py @@ -9,6 +9,7 @@ from tangos.util import timing_monitor from .. import input_handlers, parallel_tasks +from ..log import logger class PropertyCalculationMetaClass(type): @@ -419,7 +420,7 @@ def all_properties(with_particle_data=True): return pr @functools.lru_cache -def providing_class(property_name, handler_class=None, silent_fail=False): +def providing_class(property_name, handler_class=None, silent_fail=False, explain=False): """Return property calculator class for given property name when files will be loaded by specified handler. If handler_class is None, return "live" properties which can be calculated without particle data. @@ -449,7 +450,7 @@ class targetting say PynbodyInputHandler is available, it will be selected in pr if len(candidates)>=1: # return the property which is most specialised - _sort_by_class_hierarchy(candidates) + _sort_by_class_hierarchy(candidates, explain) return candidates[0] elif silent_fail: return None @@ -473,38 +474,56 @@ def all_providing_classes(property_name): return candidates -def _sort_by_class_hierarchy(candidates): +def _sort_by_class_hierarchy(candidates, explain=False): + def explanation(s): + if explain: + logger.info(s) def cmp(a, b): + a_name = a.__module__ + "." + a.__qualname__ + b_name = b.__module__ + "." + b.__qualname__ + if a is b: return 0 # Rule 1: prefer the most specialised handler - if issubclass(a.works_with_handler, b.works_with_handler): - return -1 - elif issubclass(b.works_with_handler, a.works_with_handler): - return 1 + if a.works_with_handler is not b.works_with_handler: + if issubclass(a.works_with_handler, b.works_with_handler): + explanation(f"{a_name} is preferred to {b_name} because handler ({a.works_with_handler}) is a subclass " + f"of handler {b.works_with_handler}") + return -1 + elif issubclass(b.works_with_handler, a.works_with_handler): + explanation(f"{b_name} is preferred to {a_name} because handler ({b.works_with_handler}) is a subclass " + f"of handler {a.works_with_handler}") + return 1 # Rule 2: prefer the most specialised class: if issubclass(a, b): + explanation(f"{a_name} is preferred to {b_name} because it is a subclass of {b_name}") return -1 elif issubclass(b, a): + explanation(f"{b_name} is preferred to {a_name} because it is a subclass of {a_name}") return 1 # Rule 3: prefer externally-provided classes over tangos-provided ones if a.__module__.startswith("tangos.") and not b.__module__.startswith("tangos."): + explanation(f"{b_name} is preferred to {a_name} because it is provided externally to tangos") return 1 elif b.__module__.startswith("tangos.") and not a.__module__.startswith("tangos."): + explanation(f"{a_name} is preferred to {b_name} because it is provided externally to tangos") return -1 # Rule 4: out of sensible ways to order, now we just go alphabetical a_name = a.__module__ + "." + a.__qualname__ b_name = b.__module__ + "." + b.__qualname__ if a_nameb_name: + explanation(f"{b_name} is preferred to {a_name} because of alphabetical ordering") return 1 + explanation(f"{a_name} and {b_name} could not be distinguished by any of the ordering rules") # very surprising to reach this - how can two different classes have the same module and name? return 0 @@ -512,22 +531,22 @@ def cmp(a, b): -def providing_classes(property_name_list, handler_class, silent_fail=False): +def providing_classes(property_name_list, handler_class, silent_fail=False, explain=False): """Return classes for given list of property names; see providing_class for details""" classes = [] for property_name in property_name_list: - cl = providing_class(property_name, handler_class, silent_fail) + cl = providing_class(property_name, handler_class, silent_fail, explain) if cl not in classes and cl is not None: classes.append(cl) return classes -def instantiate_classes(simulation, property_name_list, silent_fail=False): +def instantiate_classes(simulation, property_name_list, silent_fail=False, explain=False): """Instantiate appropriate property calculation classes for a given simulation and list of property names.""" instances = [] handler_class = type(simulation.get_output_handler()) for property_identifier in property_name_list: - instances.append(providing_class(property_identifier, handler_class, silent_fail)(simulation)) + instances.append(providing_class(property_identifier, handler_class, silent_fail, explain)(simulation)) return instances diff --git a/tangos/tools/property_writer.py b/tangos/tools/property_writer.py index 8ecb7f23..6e891a80 100644 --- a/tangos/tools/property_writer.py +++ b/tangos/tools/property_writer.py @@ -78,6 +78,8 @@ def add_parser_arguments(self, parser): help="Specify the paralellism backend (e.g. pypar, mpi4py)") parser.add_argument('--include-only', action='append', type=str, help="Specify a filter that describes which objects the calculation should be executed for. Multiple filters may be specified, in which case they must all evaluate to true for the object to be included.") + parser.add_argument('--explain-classes', action='store_true', + help="Log some explanation for why property classes are selected (when there is any ambiguity)") def _create_parser_obj(self): parser = argparse.ArgumentParser() @@ -446,7 +448,9 @@ def run_timestep_calculation(self, db_timestep): self.tracker = CalculationSuccessTracker() logger.info("Processing %r", db_timestep) - self._property_calculator_instances = properties.instantiate_classes(db_timestep.simulation, self.options.properties) + self._property_calculator_instances = properties.instantiate_classes(db_timestep.simulation, + self.options.properties, + explain=self.options.explain_classes) if self.options.with_prerequisites: self._add_prerequisites_to_calculator_instances(db_timestep)