Skip to content

Commit

Permalink
Provide option to explain why particular property classes get selecte…
Browse files Browse the repository at this point in the history
…d over others
  • Loading branch information
apontzen committed Oct 9, 2023
1 parent 1cecd1e commit 30c9500
Show file tree
Hide file tree
Showing 3 changed files with 37 additions and 14 deletions.
4 changes: 2 additions & 2 deletions tangos/core/dictionary.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
41 changes: 30 additions & 11 deletions tangos/properties/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from tangos.util import timing_monitor

from .. import input_handlers, parallel_tasks
from ..log import logger


class PropertyCalculationMetaClass(type):
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -473,61 +474,79 @@ 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_name<b_name:
explanation(f"{a_name} is preferred to {b_name} because of alphabetical ordering")
return -1
elif a_name>b_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

candidates.sort(key=functools.cmp_to_key(cmp))



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

Expand Down
6 changes: 5 additions & 1 deletion tangos/tools/property_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)

Expand Down

0 comments on commit 30c9500

Please sign in to comment.