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 Target: 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:

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:

# 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 Target: 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.

match_managers.py
 1from tom_targets.base_models import TargetMatchManager
 2
 3
 4class CustomTargetMatchManager(TargetMatchManager):
 5    """
 6    Custom Match Manager for extending the built in TargetMatchManager.
 7    """
 8
 9    def match_name(self, name):
10        """
11        Returns a queryset exactly matching name that is received
12        :param name: The string against which target names will be matched.
13        :return: queryset containing matching Target(s).
14        """
15        queryset = self.match_exact_name(name)
16        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.

match_managers.py
 1from tom_targets.base_models import TargetMatchManager
 2
 3
 4class CustomTargetMatchManager(TargetMatchManager):
 5    """
 6    Custom Match Manager for extending the built in TargetMatchManager.
 7    """
 8
 9    def match_target(self, target, *args, **kwargs):
10        """
11        Returns a queryset containing any targets that are both a fuzzy match and within 2 arcsec of
12        the target that is received
13        :param target: The target object to be checked.
14        :return: queryset containing matching Target(s).
15        """
16        queryset = super().match_target(target, *args, **kwargs)
17        radius = 2  # Arcseconds
18        cone_search_queryset = self.match_cone_search(target.ra, target.dec, radius)
19        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:

1from django.core.exceptions import ValidationError
2from tom_targets.models import Target
3
4target = Target(name='My Target', ra=10.68458, dec=41.26906)
5try:
6    target.validate_unique()  # or `target.full_clean()`
7    target.save()
8except ValidationError as e:
9    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’.

match_managers.py
 1from tom_targets.base_models import TargetMatchManager
 2
 3
 4class CustomTargetMatchManager(TargetMatchManager):
 5    """
 6    Custom Match Manager for extending the built in TargetMatchManager.
 7    """
 8
 9    def simplify_name(self, name):
10        """
11        Create a custom simplified name to be used for comparison in ``match_fuzzy_name``.
12        """
13        simple_name = super().simplify_name(name)  # Use the default simplification
14        if simple_name.startswith('at'):
15            simple_name = simple_name.replace('at', 'sn', 1)
16        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.