Custom evaluators

Custom evaluators allow cost, restriction, and descriptor attribute values for network elements to be altered during a solve using a Python script. They can query for an element’s base value and update as needed or set a different value based on other inputs. This allows you to set attribute values at solve time without updating the underlying network dataset or the source features.

Learn more about network attributes and evaluators

A custom evaluator can be used to do the following:

  • Scale the costs of streets based on querying a table in another database
  • Restrict street features based on OID’s read from an external file
  • Dynamically scale cost of streets based on the time of day

Custom evaluators are implemented by creating a Python class that inherits from the arcpy.nax.AttributeEvaluator and associating the Python class with a particular network dataset. They can be associated with cost, restriction, or descriptor attributes. They can update edges, junctions, or turns for time-enabled solves as well as solves that are not time enabled. Depending on the changes made, the updates may alter the costs and paths found during analysis.

An individual network attribute should have only a single custom evaluator associated with it, but any number of network attributes can have an associated custom evaluator. When implementing a custom evaluator, there are several objects that can be used to gather information about the network and the elements, such as Attribute, Edge, Junction, and others.

Custom evaluator class

A custom evaluator is created by defining a class that inherits from arcpy.nax.AttributeEvaluator and implementing at least one of the element value methods (for example, edgeValue or edgeValueAtTime). A custom evaluator can also implement the __init__, attach, and refresh methods.

Custom evaluator methods

The subsections below describe the methods that can be used to set the attribute values at solve time.

Initializer

You can implement the initializer __init__ method. If implemented, the base class's initializer method must be explicitly called before adding additional initialization logic. If not implemented, the base class's initializer method is automatically invoked.

The base class initializer will use the passed-in attribute name and source names to set the self.attributeName and self.sourceNames properties on the object.

class CustomEvaluatorExample(arcpy.nax.AttributeEvaluator):
    """Example custom evaluator."""

    def __init__(self, attributeName, sourceNames=None):
        """Example initializer."""
        super().__init__(attributeName, sourceNames)
        # Do additional custom initialization

Attach

When a custom evaluator is associated with a network dataset, an internal attach method is invoked. The internal code queries the network dataset attributes for the specified attribute name. If the attribute name is found, the self.attribute property is set to the index value of the attribute; otherwise, the custom evaluator is not associated with the network dataset and no other class methods are invoked (including any user-implemented attach method).

The attach method implemented in a custom evaluator can be used to inspect and validate the network dataset to ensure that it complies with the requirements of the custom evaluator code, such as checking whether other attributes exist. If the network dataset is valid, the attach method returns True; otherwise, it returns False. When False is returned, the custom evaluator will not be associated with the network dataset and no other methods will be invoked.

Implementing the attach method in a custom evaluator is optional.

Note:

A network dataset can be opened multiple times depending on the number of threads the application is using to access a network dataset.

Refresh

A custom evaluator's refresh method is invoked at the beginning of each solve, before any elements are evaluated. Any internal state (for example, cached values or non-network dataset validation) that varies per solve can be set in this method. The self.networkQuery property is available in this method. This property is set internally after the attach method completes successfully.

Implementing the refresh method in a custom evaluator is optional.

Value methods

The following value methods can be implemented: edgeValue, edgeValueAtTime, junctionValue, junctionValueAtTime, turnValue, and turnValueAtTime. Methods prefixed with edge affect the attribute values for edge sources, methods prefixed with junction affect the attribute values for junction sources, and methods prefixed with turn affect the attribute values for turn sources. Methods with the AtTime suffix are invoked during time-enabled solves, and methods without that suffix are invoked when time is not being used.

In general, during a solve when an attribute is evaluated and there is an attached custom evaluator, it will override the primary evaluator. The value returned from the related element method (for example, edgeValue) will be used by the solver engine in the remainder of the analysis. Using these value methods, you can implement the custom logic to set the final value for an element’s attribute.

The ValueAtTime methods provide a datetime parameter, which is the date and time the element will be encountered along a potential route. These methods are called for each element, potentially multiple times, during a solve. Any code in these methods should be performant.

You must implement at least one of these value methods.

Examples

The examples below show the basic implementation of a custom evaluator class.

Example 1: The following code is a custom evaluator class that would double the cost of the specified attribute on all the evaluated edges for a time-neutral solve without implementing any of the optional methods:

import arcpy

class EdgeCustomizer(arcpy.nax.AttributeEvaluator):
    """Defines a custom evaluator that multiplies the edge cost by 2."""

    def edgeValue(self, edge: arcpy.nax.Edge):
        """Multiplies the edge cost by 2."""
        base_value = self.networkQuery.attributeValue(edge, self.attribute)
        return base_value * 2

Example 2: The following code is a custom evaluator class that would double the cost of the specified attribute on all the evaluated edges for both a time-neutral solve and a time-enabled solve with minimal implementation of the optional methods:

import datetime
from typing import Union, Optional, List
import arcpy

class EdgeCustomizer(arcpy.nax.AttributeEvaluator):
    """Defines a custom evaluator that multiplies the edge cost by 2."""

    def __init__(self, attributeName: str, sourceNames: Optional[List] = None):
        """Example initializer."""
        super().__init__(attributeName, sourceNames)
        # Do additional custom initialization

    def attach(self, network_query: arcpy.nax.NetworkQuery) -> bool:
        """Connect to and validate the network dataset."""
        # Do additional validation checks before returning Boolean
        return True

    def refresh(self) -> None:
        """Reset internal state before solve."""
        # Reset internal state in this method as needed
        pass

    def edgeValue(self, edge: arcpy.nax.Edge):
        """Multiplies the edge cost by 2."""
        base_value = self.networkQuery.attributeValue(edge, self.attribute)
        return base_value * 2

    def edgeValueAtTime(
            self, edge: arcpy.nax.Edge,
            time: datetime.datetime, time_usage: arcpy.nax.NetworkTimeUsage
    ) -> Union[int, float, bool]:
        """Multiplies the edge cost by 2 when the solve uses a time of day."""
        base_value_at_time = self.networkQuery.attributeValue(
            edge, self.attribute, time_usage, time)
        return base_value_at_time * 2

Associate a custom evaluator with a network dataset

There are two ways to deploy a custom evaluator so that it is associated with a network dataset and the customization logic gets invoked during a solve. These two methods are referred to as temporary and persisted.

Tip:

Create the custom evaluator class and test it as a temporary custom evaluator. Then run the script in debug mode in an editor such as Visual Studio Code. Once it is working as expected, and if required, you can make it a persisted custom evaluator. Then validate the persisted custom evaluator to ensure that everything is working correctly.

Temporary custom evaluator

Temporary custom evaluators are only associated with a network dataset object created within a script; they are not saved permanently to the network dataset. Temporary custom evaluators are configured using the customEvaluators property on a network dataset object within a script that performs a solve.

A temporary custom evaluator can be used for applications in which the custom evaluator must be invoked from a Python script. They can also be useful for development and debugging persisted custom evaluators.

Configure a temporary custom evaluator

To set up a temporary custom evaluator, create a custom evaluator object, then use the customEvaluators property on the network dataset object to associate the custom evaluator object with it.

The example below shows how to instantiate a custom evaluator object that customizes the TravelTime network attribute and associate it to the network dataset object. To invoke the custom evaluator at solve time, instantiate a route solver object using the network dataset object.

# Instantiate a custom evaluator object that will customize the
# TravelTime cost attribute
travel_time_customizer = EdgeCustomizer("TravelTime")

# Create a network dataset object
network_dataset = arcpy.nax.NetworkDataset(
    r"C:\Data\Tutorial\SanFrancisco.gdb\Transportation\Streets_ND")

# Attach the custom evaluator object to the network dataset
network_dataset.customEvaluators = [travel_time_customizer]

# Instantiate a route analysis
route = arcpy.nax.Route(network_dataset)
Note:

When instantiating a custom evaluator, optionally, a list of specific network source names can be provided that specify the sources the custom evaluator will apply to. If no list is provided, the custom evaluator will apply to all sources. For more information see the AttributeEvaluator documentation.

Persisted custom evaluators

Persisted custom evaluators store a reference to a custom evaluator class as part of the network dataset schema, which is stored in the geodatabase. These custom evaluators get invoked whenever a solve is done using that network dataset. This is referred to as persisted since the reference is part of the network dataset itself. They are configured using the updateNetworkDatasetSchema on a network dataset object.

When a network with a persisted custom evaluator is opened, the attach method is invoked and the custom evaluator is loaded and cached. This cache is retained for the lifetime of the application. This means that any changes made to a persisted custom evaluator class will not be read until the application that opened the network dataset is closed and restarted. This applies to both ArcGIS Pro and ArcGIS Server.

It is important to note that for persisted custom evaluators, the network dataset’s schema only contains a reference to a custom evaluator; it does not contain the class code. This reference allows the network dataset, when it is accessed, to implicitly find and load its referenced custom evaluators. A file containing the class code must reside in the active ArcGIS Pro Python environment's site-packages folder so that the network dataset can find it.

Learn more about Python environments

Note:
It is recommended that you clone the default ArcGIS Pro Python environment before making any modifications. When adding a custom evaluators Python file to the site packages directory, it is recommended that you first clone the environment and activate the cloned environment.

A persisted custom evaluator would be used when you need to invoke a custom evaluator while doing a solve outside of a Python script, such as in ArcGIS Pro or ArcGIS Server.

If a network analyst layer using a network dataset that has a persisted custom evaluator is published as a service, the custom evaluator package must be manually copied to the ArcGIS Server Python environment's site packages directory. Also, when a custom evaluator is used on a service, any external resource it uses (such as files) should be accessible by the ArcGIS Server user, as it is dependent on how the server is configured.

Configure a persisted custom evaluator

Use the updateNetworkDatasetSchema method on a network dataset object to permanently update the network dataset schema, passing in a dictionary that defines the network attribute for which the custom evaluator should be invoked and the path to the custom evaluator class. The path uses the dot notation to define the folder name (within the site-packages directory), the file name the class is in, and the class name itself.

The example below shows how to update a network dataset with a persisted custom evaluator class. The class in this example is called EdgeCustomizer, and its code is in a Python module called customization.py in a folder called na_customizers, which is in the site-packages folder of an ArcGIS Pro active Python environment.

import arcpy

# Create a network dataset object
network_dataset = arcpy.nax.NetworkDataset(
    r"C:\Data\Tutorial\SanFrancisco_Persisted.gdb\Transportation\Streets_ND")

# Create a dictionary referencing the custom evaluators to apply to the
# TravelTime attribute
my_custom_evaluators = {
    "TravelTime": {"class": "na_customizers.customization.EdgeCustomizer"}
}

# Update the network dataset to use the custom evaluator
network_dataset.updateNetworkDatasetSchema(custom_evaluators=my_custom_evaluators)
Note:

When updating the schema with a reference to a custom evaluator, optionally, a list of specific network source names can be provided that specify the sources the custom evaluator will apply to. If no list is provided, the custom evaluator will apply to all sources. To set this, include a sourceNames key set to a list of sources names. For example:

{
    "TravelTime": {
        "class": "na_customizers.customization.EdgeCustomizer",
        "sourceNames": ["Streets"]
    }
}

When this network dataset and the designated attribute are used in a network analysis, the network dataset will invoke the custom evaluator at solve time. The network will validate the existence of the specified attribute and sources and find and load the specified package and class from the active ArcGIS Pro Python environment's site packages folder. If no attribute, source names, package, or class are found, the custom evaluator will not be used. The solve will complete with a warning message reporting that there was an issue using the custom evaluator.

When a network dataset has persisted custom evaluators, they will be listed in the General > Summary section of the network dataset properties dialog box. They will also be displayed with the referenced network attribute when viewing the relevant tab, for example, Travel Attributes > Cost. If there was an issue loading the class, warning messages appear.

Object lifecycle

When a network dataset is initially constructed, and it has a persisted custom evaluator, it instantiates a custom evaluator object that is referenced throughout its lifetime, where the lifetime can vary depending on the framework being used (such as, ArcGIS Pro, ArcGIS Server, or Python).

Since a specific instance of a custom evaluator object could be used over multiple solves, it is important to manage the state of this object, in particular resetting variables in the refresh method as needed. For example, if the custom evaluator needs to record the edge count per solve, the variable used to track this value should be reset in the refresh method.

In the context of ArcGIS Server, each SOC process (at startup time and at recycle time) will construct a new network dataset object, and hence will also create a new instance of a custom evaluator object. This instance of a custom evaluator object will be used throughout the lifecycle of the SOC process, only the refresh method and the Value methods will run per request.

Limitations

The following limitations apply to custom evaluators:

  • Custom evaluators are only available in ArcGIS Pro and ArcGIS Server.
  • Custom evaluators can only be invoked on a file or enterprise geodatabase.
  • Descriptor attributes are only invoked when referenced by a cost or restriction attribute.
  • For performance reasons, custom evaluators do not support using keywords for arguments.

Quick start guide to create and use a temporary custom evaluator

The following sections serve as a quick guide about how to create and use a temporary custom evaluator. Each code sample illustrates a specific component of the full workflow. The components are as follows:

  1. Solve a route analysis
  2. Define a custom evaluator class
  3. Create an instance of the class and associate with the network dataset that is used for the route solve

The final code sample shows how to put all the components together.

The code samples are created using the network analyst tutorial that is available for download from the data download page.

Solve a route analysis

The code sample below illustrates a workflow to solve a route analysis using the arcpy.nax solver object and retrieve the travel time for the route.

Note:

The path to the gdb in the code below must be updated to reflect where the data is on your system.

import arcpy

# Create a network dataset object
network_dataset = arcpy.nax.NetworkDataset(
    r"C:\Data\Tutorial\SanFrancisco.gdb\Transportation\Streets_ND")

# Instantiate a route analysis
route = arcpy.nax.Route(network_dataset)

# Insert stops for the route
with route.insertCursor(
    arcpy.nax.RouteInputDataType.Stops,
    ["NAME", "SHAPE@XY"]
) as cursor:
    cursor.insertRow(["Stop1", (-122.501, 37.757)])
    cursor.insertRow(["Stop2", (-122.445, 37.767)])

# Solve the route
result = route.solve()

# Print the total travel time for the route
for row in result.searchCursor(
    arcpy.nax.RouteOutputDataType.Routes,
    ["Total_Minutes"]
):
    print(f"Solved Total_Minutes: {row[0]}")

Define a custom evaluator class

The code sample below illustrates a custom evaluator class definition. The custom evaluator in this example multiplies the original travel time cost by a factor of 2. The travel time of the route solved using this custom evaluator should be twice the travel time of the same route solved without the custom evaluator.

class EdgeCustomizer(arcpy.nax.AttributeEvaluator):
    """Defines a custom evaluator that multiplies the edge cost by 2."""

    def edgeValue(self, edge: arcpy.nax.Edge):
        """Multiplies the edge cost by 2."""
        base_value = self.networkQuery.attributeValue(edge, self.attribute)
        return base_value * 2

Associate a custom evaluator with a network dataset

The code sample below illustrates creating an instance of the custom evaluator class and associating it with the network dataset object for use with the TravelTime cost attribute. This should be done before the route solve is invoked (route.solve()).

# Create a custom evaluator object that will customize the
# TravelTime cost attribute
travel_time_customizer = EdgeCustomizer("TravelTime")

# Attach the custom evaluator object to the network dataset
network_dataset.customEvaluators = [travel_time_customizer]

Combine all the components

The code sample below shows how to put all the components together into a complete workflow that defines and uses a temporary custom evaluator for a route analysis workflow.

import arcpy

class EdgeCustomizer(arcpy.nax.AttributeEvaluator):
    """Defines a custom evaluator that multiplies the edge cost by 2."""

    def edgeValue(self, edge: arcpy.nax.Edge):
        """Multiplies the edge cost by 2."""
        base_value = self.networkQuery.attributeValue(edge, self.attribute)
        return base_value * 2

# Create a custom evaluator object that will customize the
# TravelTime cost attribute
travel_time_customizer = EdgeCustomizer("TravelTime")

# Create a network dataset object
network_dataset = arcpy.nax.NetworkDataset(
    r"C:\Data\Tutorial\SanFrancisco.gdb\Transportation\Streets_ND")

# Attach the custom evaluator object to the network dataset
network_dataset.customEvaluators = [travel_time_customizer]

# Instantiate a route analysis
route = arcpy.nax.Route(network_dataset)

# Insert stops for the route
with route.insertCursor(
    arcpy.nax.RouteInputDataType.Stops,
    ["NAME", "SHAPE@XY"]
) as cursor:
    cursor.insertRow(["Stop1", (-122.501, 37.757)])
    cursor.insertRow(["Stop2", (-122.445, 37.767)])

# Solve the route
result = route.solve()

# Print the total travel time for the route
for row in result.searchCursor(
    arcpy.nax.RouteOutputDataType.Routes,
    ["Total_Minutes"]
):
    print(f"Solved Total_Minutes: {row[0]}")

Quick start guide to create and use a persisted custom evaluator

The following sections serve as a quick guide about how to create and use a persisted custom evaluator. Below, you will create a custom evaluator class in the active Python environment, update the network dataset to use the custom evaluator, and test it by solving a route analysis.

  1. Clone the default Python environment
  2. Define and save the custom evaluator class
  3. Update the network dataset schema
  4. Solve route

The code samples are created using the network analyst tutorial that is available for download from the data download page.

Clone the default Python environment

The custom evaluator code must be saved to the ArcGIS Pro Python environment. If you are using the default ArcGIS Python environment, it is recommended that you first clone and activate a new environment.

Learn more about cloning an environment

Define and save the custom evaluator class

The code sample below illustrates a custom evaluator class definition. The custom evaluator in this example multiplies the original travel time cost by a factor of 2. The travel time of the route solved using this custom evaluator should be twice the travel time of the same route solved without the custom evaluator.

import arcpy

class EdgeCustomizer(arcpy.nax.AttributeEvaluator):
    """Defines a custom evaluator that multiplies the edge cost by 2."""

    def edgeValue(self, edge: arcpy.nax.Edge):
        """Multiplies the edge cost by 2."""
        base_value = self.networkQuery.attributeValue(edge, self.attribute)
        return base_value * 2

Within the active Python environment, find the site-packages folder. Within that directory, create a folder called na_customizers. Save the above code defining a custom evaluator class to the na_customizers folder as cost_customization.py.

Note:

Following the example names used in the code sample is important, as you will update the network dataset schema with these values next.

Update the network dataset schema

Copy the Network Analyst\Tutorial\SanFrancisco.gdb from the tutorial data to SanFrancisco_Persisted.gdb.

Use the code below in a stand-alone script to permanently update the network dataset SanFrancisco_Persisted.gdb to reference the custom evaluator for the TravelTime cost attribute. The my_custom_evaluators dictionary references the folder name, file name, and class name of the custom evaluator defined in the above code sample.

import arcpy

# Create a network dataset object
network_dataset = arcpy.nax.NetworkDataset(
    r"C:\Data\Tutorial\SanFrancisco_Persisted.gdb\Transportation\Streets_ND")

# Create a dictionary referencing the custom evaluators to apply to the
# TravelTime attribute
my_custom_evaluators = {
    "TravelTime": {"class": "na_customizers.customization.EdgeCustomizer"}
}

# Update the network dataset to use the custom evaluator
network_dataset.updateNetworkDatasetSchema(custom_evaluators=my_custom_evaluators)

Solve route

In ArcGIS Pro, use the network dataset from the SanFrancisco_Persisted.gdb to solve a route using the Driving Time travel mode. Solve a second route, using SanFrancisco.gdb and the same stops and compare the travel time of the output routes. The travel time of the route referencing SanFrancisco_Persisted.gdb should be twice the travel time of the route referencing SanFrancisco.gdb because of the custom evaluator.

Learn how to solve a route analysis in ArcGIS Pro