Custom evaluators

Custom evaluators allow you to alter cost, restriction, and descriptor attribute values for network elements during a solve operation using a Python script. They can query for an element’s base value and update it 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 an object identifier read from an external file.
  • Dynamically scale cost of streets based on the time of day.

To implement custom evaluators, create a Python class that inherits from the arcpy.nax.AttributeEvaluator class, and associate 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 solve operations, as well as solve operations 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, several objects can be used to gather information about the network and the elements, such as Attribute, Edge, Junction, and others.

Custom evaluator class

To create a custom evaluator, define a class that inherits from arcpy.nax.AttributeEvaluator class and implement at least one of the element value methods (for example, edgeValue or edgeValueAtTime). You can also use a custom evaluator to 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).

You an use the attach method implemented in a custom evaluator 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 operation, before any elements are evaluated. Any internal state (for example, cached values or nonnetwork 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 solve operations, and methods without that suffix are invoked when time is not being used.

In general, during a solve operation 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 operation. 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 doubles 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 doubles 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 is invoked during a solve operation: 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 in 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 in a script that performs a solve operation.

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, and 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 with 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, you can provide a list of specific network source names 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 will be invoked whenever a solve operation is performed using that network dataset. This is referred to as persisted since the reference is part of the network dataset. 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 AllSource 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 AllSource Python environment's site-packages folder so that the network dataset can find it.

Learn more about Python environments

Note:

If the customization script needs to use third-party Python packages that are not included with the default ArcGIS AllSource Python environment, it is recommended that you clone the default Python environment before installing additional packages. Follow the workflow in the Package Manager topic to create a clone of the default ArcGIS AllSource Python environment, add the packages, and activate the environment. The custom evaluator Python file must be stored in the site-packages directory of the active Python environment, in this case, a clone of the default ArcGIS AllSource Python environment.

This also applies to ArcGIS Server. If you need to deploy persisted customization to the ArcGIS Server site and need to use additional third-party packages that are not included with the ArcGIS Server default Python environment, follow the steps in the Deploy custom Python packages for ArcGIS Server topic to clone the default Python environment and add packages, then activate the cloned environment. When you deploy persisted customization to ArcGIS Server, the customization must be copied to the site-packages directory of the active Python environment on ArcGIS Server.

Use a persisted custom evaluator when you need to invoke a custom evaluator while performing a solve operation outside of a Python script, such as in ArcGIS AllSource 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 (in the site-packages directory), the file name the class is in, and the class name.

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 AllSource 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, you can provide a list of specific network source names 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. The following is an 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 AllSource Python environment's site packages folder. If no attribute, source names, package, or class is found, the custom evaluator will not be used. The solve will complete with a warning message stating that there was an issue using the custom evaluator.

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

Object life cycle

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. The lifetime can vary depending on the framework being used (such as ArcGIS AllSource, ArcGIS Server, or Python).

Since a specific instance of a custom evaluator object can be used over multiple solves operations, 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, reset the variable used to track this value in the refresh method.

In the context of ArcGIS Server, each server object component (SOC) process (at startup time and at recycle time) will construct a new network dataset object, and create a new instance of the custom evaluator object. This instance of the custom evaluator object will be used throughout the life cycle of the SOC process; only the refresh and Value methods will run at each request.

Limitations

The following limitations apply to custom evaluators:

  • Custom evaluators are only available in ArcGIS AllSource 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.

Create and use a temporary custom evaluator

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

  1. Solve a route analysis
  2. Define a custom evaluator class
  3. Associate a custom evaluator with the network dataset

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

The code samples below 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 print the travel time for the route.

Note:

The path to the geodatabase in the code below must be updated to reflect where the data resides 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. 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. Perform this workflow component 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]}")

Create and use a persisted custom evaluator

The following sections describe how to create and use a persisted custom evaluator. This workflow creates a custom evaluator class in the active Python environment, updates the network dataset to use the custom evaluator, and tests it by solving a route analysis. The workflow components are as follows:

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

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

Clone the default Python environment (optional)

This step is optional. You only need to clone the default Python environment if the customization script needs to use third-party Python libraries that are not included in the default ArcGIS AllSource Python environment.

Learn more about cloning an environment

Define and save the custom evaluator class

The code sample below illustrates a custom evaluator class definition. 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

In the active Python environment, find the site-packages folder. In that directory, create a folder called na_customizers. Save the code above 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 code sample above.

import arcpy

# Check out ArcGIS Network Analyst extension
arcpy.CheckOutExtension("network")

# 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 the route

In ArcGIS AllSource, use the network dataset from 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 AllSource

Deploy custom evaluators to be used in routing services hosted on an ArcGIS GIS Server site

You can publish two types of routing services: standard routing services and custom routing services.

Standard routing services are map and geoprocessing services that provide out-of-the-box capabilities available with the ArcGIS Network Analyst extension. You can publish standard routing services to an ArcGIS GIS Server site using a network dataset, and you will get a set of routing service endpoints with predefined parameters and schema for the inputs. These services provide full capability and integration with Esri applications such as ArcGIS AllSource and Map Viewer.

Learn more about publishing standard routing services

Custom routing services are geoprocessing services with custom capabilities. The custom routing services allow you to perform a workflow that might involve multiple network analysis solvers or other geoprocessing tools. It also allows you to define custom parameters and input schema to fit the needs of the application.

Learn more about publishing custom routing services

Both standard routing services and custom routing services can invoke custom evaluators.

Invoke a custom evaluator with standard routing services

To invoke a custom evaluator with standard routing services, complete the following steps:

  1. Create a persisted custom evaluator and associate it with a network dataset.
  2. Test the custom evaluator in ArcGIS AllSource to ensure it is invoked when you solve a route.
  3. Copy the network dataset to the same directory on all the machines participating in the ArcGIS Server site that will be used to host routing services.
  4. Copy the customization folder and file to all the machines participating in the ArcGIS Server site. Place the folder and file in the ArcGIS Server active Python environment's site-packages folder. The default path on a server machine is: <install>\ArcGIS\Server\framework\runtime\ArcGIS\bin\Python\envs\arcgispro-py3\Lib\site-packages. When you copy from the ArcGIS AllSource Python environment's site-packages folder to ArcGIS Server, maintain the same folder structure for the customization code. For example, if the customization code is in the cost_customization.py file in the na_customizers folder, copy the na_customizers folder to the site-packages folder on ArcGIS Server.
  5. Publish the standard routing services.

When you use the routing services, the customization is applied.

Invoke a custom evaluator with custom routing services

To invoke a custom evaluator with custom routing services, complete the following steps:

  1. Author a script tool following the steps in the Publish custom routing services topic.
  2. Associate a temporary customizer with a network dataset in the script tool.
  3. Publish the service following the steps in the Publish custom routing services topic.

When you use the routing services, the customization is applied.

Code sample: Access feature class field values using a network descriptor

Custom evaluators operate on network elements (for example, edges), meaning source feature field values cannot be accessed directly. Instead, you can access these values indirectly by creating a descriptor attribute on your network that reads the source feature field values.

To use this in a custom evaluator, get the descriptor attribute index in the attach method. The code example below searches for the index of the RoadClass attribute and stores the value in an instance variable called road_class. If no attribute is found, the method returns False, which indicates the custom evaluator could not be attached.

    def attach(self, network_query: arcpy.nax.NetworkQuery) -> bool:
        self.road_class_att = network_query.attribute("RoadClass")
        if self.road_class_att is None:
           return False
        else:
           return True

Then, in the element evaluation functions, the code will query the element's descriptor attribute value and use it in the function as needed. In the example below, the RoadClass value is used to determine how to alter the base value.

    def edgeValue(self, edge: arcpy.nax.Edge) :
        base_value = self.networkQuery.attributeValue(edge, self.attribute) 
        road_class_value = self.networkQuery.attributeValue(edge, self.road_class_att)
        if road_class_value in [1,2,3]: 
            return base_value * 2
        else:
            return base_value