Customizing a Target Matcher ---------------------------- The role of the ``TargetMatchManager`` is to return a queryset of targets that match a given set of parameters. By default, the TOM Toolkit includes a ``TargetMatchManager`` that contains several methods that are detailed in :doc:`Target: Models <../api/tom_targets/models>`. These functions can be modified or replaced by a user to alter the conditions under which a target is considered a match. Using the TargetMatchManager ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The ``TargetMatchManager`` is a django model manager defined as ``Target.matches``. Django model managers are described in more detail in the `Django Docs `_. You can use the ``TargetMatchManager`` to return a queryset of targets that satisfy a cone search with the following: .. code:: python from tom_targets.models import Target # Define the center of the cone search ra = 10.68458 # Degrees dec = 41.26906 # Degrees radius = 12 # Arcseconds # Get the queryset of targets that match the cone search targets = Target.matches.match_cone_search(ra, dec, radius) Extending the TargetMatchManager ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To start, find the ``MATCH_MANAGERS`` definition in your ``settings.py``: .. code:: python # Define MATCH_MANAGERS here. This is a dictionary that contains a dotted module path to the desired match manager # for a given model. # For example: # MATCH_MANAGERS = { # "Target": "custom_code.match_managers.CustomTargetMatchManager" # } MATCH_MANAGERS = {} Add the path to your custom ``TargetMatchManager`` to the "Target" key of the MATCH_MANAGERS dictionary as shown in the example. Once you have defined your custom ``TargetMatchManager`` in ``settings.py``, you can need to create the custom ``TargetMatchManager`` in your project. We recommend you do this inside your project's ``custom_code`` app, but can be placed anywhere. The ``TargetMatchManager`` can be extended to include additional methods or to override any of the default methods described in :doc:`Target: Models <../api/tom_targets/models>`. The following code provides an example of a custom ``TargetMatchManager`` that checks for exact name matches instead of the default fuzzy matches. This would change the default behavior for several parts of the TOM Toolkit that endeavor to determine if a target or alias is unique based on its name. .. code-block:: python :caption: match_managers.py :linenos: :emphasize-lines: 15 from tom_targets.base_models import TargetMatchManager class CustomTargetMatchManager(TargetMatchManager): """ Custom Match Manager for extending the built in TargetMatchManager. """ def match_name(self, name): """ Returns a queryset exactly matching name that is received :param name: The string against which target names will be matched. :return: queryset containing matching Target(s). """ queryset = self.match_exact_name(name) return queryset .. note:: The default behavior for ``match_name`` is to perform a "fuzzy match". This can be computationally expensive for large databases. If you have experienced this issue, you can override the ``match_name`` method to only return exact matches using the above example. Next we have another example of a ``TargetMatchManager`` that extends the ``match_target`` matcher to not only include name matches but also considers any target with an RA and DEC less than 2" away from the given target to be a match for the target. .. code-block:: python :caption: match_managers.py :linenos: :emphasize-lines: 17, 18 from tom_targets.base_models import TargetMatchManager class CustomTargetMatchManager(TargetMatchManager): """ Custom Match Manager for extending the built in TargetMatchManager. """ def match_target(self, target, *args, **kwargs): """ Returns a queryset containing any targets that are both a fuzzy match and within 2 arcsec of the target that is received :param target: The target object to be checked. :return: queryset containing matching Target(s). """ queryset = super().match_target(target, *args, **kwargs) radius = 2 # Arcseconds cone_search_queryset = self.match_cone_search(target.ra, target.dec, radius) return queryset | cone_search_queryset The highlighted lines could be replaced with any custom logic that you would like to use to determine if a target in the database is a match for the target that is being checked. This is extremely powerful since this code is ultimately used by ``Target.validate_unique()`` to determine if a new target can be saved to the database, and thus prevent your TOM from accidentally ingesting duplicate targets. Your ``MatchManager`` should subclass the ``base_model.TargetMatchManager`` which will contain both a ``match_target`` method and a ``match_name`` method, both of which should return a queryset. These methods can be modified or extended, as in the above example, as needed. A Note About Saving Targets: ++++++++++++++++++++++++++++ The `Target.validate_unique()` method is not called when using the `Target.save()` or `Target.objects.create()` methods to save a model. If you are creating targets in your TOM's custom code, you should call `validate_unique()` manually to ensure that the target is unique, or use the `full_clean()` method to make sure that all of the individual fields are valid as well. See the `Django Docs `__ for more information. If you do wish to use your new match manager to validate or updated targets your code should look something like this: .. code-block:: python :linenos: from django.core.exceptions import ValidationError from tom_targets.models import Target target = Target(name='My Target', ra=10.68458, dec=41.26906) try: target.validate_unique() # or `target.full_clean()` target.save() except ValidationError as e: print(f'{target.name} not saved: {e}') Customizing ``match_fuzzy_name`` ++++++++++++++++++++++++++++++++ The ``match_fuzzy_name`` method is used to query the database for targets whose names ~kind of~ match the given string. This method relies on ``simplify_name`` to create a processed version of the input string that can be compared to similarly processed names and aliases in the database. By default, ``simplify_name`` removes capitalization, spaces, dashes, underscores, and parentheses from the names, thus ``match_fuzzy_name`` will return targets whose names match the given string ignoring these characters. (i.e. "My Target" will match both "my_target" and "(mY)tAr-GeT"). If you would like to customize the behavior of ``match_fuzzy_name``, you can override the ``simplify_name`` method in your custom ``TargetMatchManager``. The following example demonstrates how to extend ``simplify_name`` to also consider two names to be a match if they start with either 'AT' or 'SN'. .. code-block:: python :caption: match_managers.py :linenos: :emphasize-lines: 14, 15 from tom_targets.base_models import TargetMatchManager class CustomTargetMatchManager(TargetMatchManager): """ Custom Match Manager for extending the built in TargetMatchManager. """ def simplify_name(self, name): """ Create a custom simplified name to be used for comparison in ``match_fuzzy_name``. """ simple_name = super().simplify_name(name) # Use the default simplification if simple_name.startswith('at'): simple_name = simple_name.replace('at', 'sn', 1) return simple_name The highlighted lines could be replaced with any custom logic that you would like to use to determine if a target in the database is a match for the name that is being checked. *NOTE* this will only actually be used by ``match_fuzzy_name``. If you are using ``match_exact_name`` these changes will not be used.