Open Assessor

Illustration on a dark street grid: a code window with curly braces feeds a box with a gear, which links lit buildings on a lime route, while dark buildings beside it stay on dashed paths

This guide describes the Open Assessor of JOpt.TourOptimizer, the mechanism for custom business rules: you implement a restriction class, attach it to the optimization scheme and run the optimizer, without a change to the solver. It is written for Java developers whose rules have no built-in feature in JOpt. You need a Java optimization with nodes and resources first; see Basic elements.

Table of contents

Before you start

What it does

JOpt.TourOptimizer provides the Open Assessor as its primary extensibility mechanism. It injects custom cost contributions, custom violations and custom scoring logic into the optimization process. The rule takes part in every solution evaluation.

In the examples, each rule is a Java class that extends AbstractCustomNodeLevelRestriction or AbstractCustomRouteLevelRestriction and overrides one method, invokeRestriction(...).

The restriction receives the current route and node context, applies the business logic and returns cost adjustments and violation details. The optimizer treats these custom contributions exactly like its built-in cost components.

Two levels: node and route

The Open Assessor works on two levels:

  • Node-level restrictions evaluate each visited node in its route context. They have access to the node, the previous element (sequence context), the planned arrival time, the chosen opening-hours window and the route's cost controller. This is the level for rules about single tasks: deadlines, priority-based placement, sequence preferences and per-node compliance checks.
  • Route-level restrictions evaluate the route as a whole. They have access to the complete list of assigned nodes, the total distance, the total working time and the route's cost controller. This is the level for rules about the whole route: a maximum number of a task type, territory consistency, minimum or maximum stop counts, and compliance policies that span the full shift.

Both levels can be used at the same time. One optimization can have several node-level and several route-level restrictions active.

When to use it

Every dispatch operation has rules that are unique to the business, for example "High-priority jobs must be visited early", "A route entering territory X must end in territory X" or "No more than two hazardous-goods stops per route". These rules are too specific for a generic optimizer to include as built-in features. To our best knowledge, ready-made routing services with a fixed model, such as VROOM or Timefold Field Service Routing, accept only their built-in constraints and weights. To our best knowledge, solver libraries take custom rules as code: Timefold Solver as constraints on a domain model you build yourself, Google OR-Tools as callbacks, and jsprit as constraint classes beside its built-in capacity and time-window constraints. In JOpt, a rule is a Java class that runs beside the built-in constraints of the routing model.

Node-level rules can be, for example:

  • "High-priority nodes must be visited close to opening start"
  • "Nodes of type X must not be visited after 11:00 AM"
  • "Penalize sequences where node A immediately follows node B"
  • "Apply exponential late-penalty beyond a custom deadline"

Route-level rules can be, for example:

  • "A route must not contain more than 2 long-haul legs"
  • "If a route enters territory X, it must end in territory X"
  • "A route must visit at least one replenishment point if load exceeds threshold"
  • "Penalize routes with too many direction changes"
  • "Prefer routes that maintain technician-customer pairing"

Use the Open Assessor for preferences, acceptance-driven shaping and domain-specific scoring. A rule that must always hold belongs to a hard constraint (see Limits).

How to use it

Step 1: Implement the restriction

Write a Java class that extends the base class of its level:

  • AbstractCustomNodeLevelRestriction for node-level rules;
  • AbstractCustomRouteLevelRestriction for route-level rules.

The class overrides the method invokeRestriction(...), which receives the current context and returns cost adjustments and optional violation details.

Step 2: Attach the restriction in an optimization scheme

The restriction is attached to the optimization through a custom optimization scheme. In the scheme's postCreate() method, create the restriction and call attachCustomNodeLevelRestriction(...) or attachCustomRouteLevelRestriction(...):

public class MyCustomScheme extends DefaultOptimizationScheme {

    public MyCustomScheme(IOptimization optimization) {
        super(optimization);
    }

    @Override
    public void postCreate() {
        ICustomNodeLevelRestriction myRule =
            new MyBusinessRule(this.getOptimization().getPropertyProvider());
        this.attachCustomNodeLevelRestriction(myRule);
    }
}

Step 3: Set the scheme on the optimization

Set the custom scheme on the optimization instance before the run:

opti.setOptimizationScheme(new MyCustomScheme(opti));

No change to the solver and no recompilation of the engine are needed. The restriction is evaluated on every solution candidate and contributes to the cost function next to all built-in components.

Node-level restrictions

A node-level restriction is invoked for every visited node during the solution evaluation. The method signature provides the context:

public IEntityRestrictionResult invokeRestriction(
    IEntity en,
    ILogicEntityRoute route,
    INode node,
    IOptimizationElement lastGeoLocationElement,
    IOptimizationElement prevElement,
    EvaluatedNodeDataHolder holder,
    IEntityCostAssessor ca,
    boolean resultRequested)

The inputs for your business logic:

ParameterWhat it provides
routeThe current route (access to cost controller, all elements)
nodeThe node being evaluated
prevElementThe previous element in the sequence
holderEvaluated timing data: planned arrival time, chosen opening-hours window
caThe entity cost assessor for cost adjustment scaling
resultRequestedWhether the caller needs a structured violation report
Two nodes, A open from 8 am to 5 pm and B open from 9 am to 5 pm, each a one-hour drive from the start and 20 minutes apart; on the left the route visits A first, on the right B first

Figure 1: By default, both orders have the same cost. A node-level rule can prefer the left order, in which A, open since 8 am, is visited first.

Route-level restrictions

A route-level restriction is invoked once per route during the solution evaluation:

public IEntityRestrictionResult invokeRestriction(
    Optional<ILogicEntityRoute> previousRoute,
    ILogicEntityRoute route,
    IEntityCostAssessor costAssessor,
    boolean resultRequested)

The route object provides access to the full list of optimizable elements, the total cost, the total distance, the working-time consumption and the cost and violation controller.

Two routes, one with five nodes and one with three nodes, each starting and ending at its start and end point

Figure 2: Two routes with an odd number of nodes. A route-level rule can prefer such routes.

How cost and violations are injected

Node-level and route-level restrictions inject their results through the route's RouteCostAndViolationController. A typical restriction that detects a violation makes three updates, as the route-level example does; the node-level examples add their cost with setCostLate instead of setCostInjectedRestriction:

// 1. Increment the constraint violation counter
controller.setNumConstraintViolations(
    controller.getNumConstraintViolations() + 1);

// 2. Track the injected restriction cost separately
controller.setCostInjectedRestriction(
    controller.getCostInjectedRestriction() + penaltyCost);

// 3. Add the cost to the total route cost
controller.addCost(penaltyCost);

The three updates ensure that:

  • the violation shows in reporting and in comparison tools;
  • the injected cost is tracked apart from the built-in cost components, for diagnostics;
  • the cost counts toward the total optimization objective and steers the solver toward solutions that follow the rule.

Structured violation reporting

When resultRequested is true, the restriction should return an IEntityRestrictionResult with a structured violation entry:

IViolation violation = MY_VIOLATION_TYPE.copy();
violation.setValue("Node arrived 15 minutes after custom deadline", "15");
myRestrictionResult.addViolation(violation);

setValue takes two strings; the examples pass a text with the value and the value itself.

Besides debugging, structured violations can be shown in planning UIs, included in acceptance reports, logged for audits and used in comparison tooling ("why is solution A better than B?").

Example restrictions

The examples repository holds restrictions for different rule patterns:

  • Deadline rule, "M-nodes must not be late": nodes whose IDs start with "M" must be served before a custom deadline (11:00 AM). The restriction computes the time deviation and applies an exponential cost multiplier, so that a later arrival costs disproportionately more. This models service levels where a small delay is tolerable and a large one is not.
  • Priority-based early placement: high-importance nodes should be visited close to the start of their opening window. The restriction reads the node importance, computes the gap between the planned arrival and the opening start, and assigns a penalty scaled by the importance. The schedule then does the important tasks first.
  • Importance order: within a route, nodes should appear in descending order of importance. If a node of lower importance comes before a node of higher importance, the restriction assigns a penalty proportional to the importance difference. This helps when many nodes share similar locations and the order would otherwise be arbitrary.
  • Route level, odd number of elements: a route must have an odd number of elements. The rule is artificial and shows the route-level pattern: count the elements, check a condition, inject cost if it is violated and return a structured violation. The same pattern applies to any rule on the scope of a route.

Production practice

  • Keep the evaluation fast and deterministic. Restrictions are evaluated often during the optimization run. Avoid network calls, heavy IO and non-deterministic logic inside invokeRestriction(...). For expensive computations, precompute features and cache derived values by node or route ID.
  • Make costs comparable and stable. Custom costs interact with the built-in cost components, so keep penalty magnitudes reasonable. Use cost adjusters for consistent scaling. For deadline rules, prefer an explicit non-linear (exponential) scaling to arbitrary large constants.
  • Return useful violations. When resultRequested is true, return clear violation type identifiers, values with context (minutes late, threshold, route ID) and messages a person can read.
  • Wire restrictions through optimization schemes. Attach them in the scheme's postCreate() method. This keeps the configuration in one place, applies it the same way every time and makes the setup reproducible across runs and environments.
  • Start with one rule, check it, then add more. Each restriction adds evaluation work and interacts with the cost function. Implement one rule, verify that it produces the expected behavior, then add the next.

Limits

Open Assessor restrictions inject penalty costs and increment violation counters. They steer softly: the optimizer prefers to satisfy the rule, but it may violate it if other objectives dominate.

If a rule must always be satisfied:

  • model it as a native hard constraint where the feature exists, for example pillar nodes for time-window SLAs or ZoneCodes for territories;
  • use architectural constructs that prevent infeasible configurations from being generated.

Hard constraints are fulfilled by the architecture of the optimizer. Costs optimize within the feasible space, and the Open Assessor shapes the solution within that space according to business preferences.

Authors

DNA Evolutions GmbH


Added: 2026-03-29 (JOpt core 7.5.3-j17) | Last updated: 2026-10-23