Pillar nodes (CapturedNodes)

Illustration on a dark street grid: a lime route through several pins passes a glowing pillar with a clock and fixed time markers above it, while dashed lines show other connections

This feature guide covers pillar nodes, also called CapturedNodes: nodes whose time windows JOpt.TourOptimizer treats as hard constraints, so that their SLA is either matched or the node is removed, but never violated. It is for developers who model contractual appointments, compliance deadlines or other time windows that must not be violated. It assumes that you know nodes and opening hours from Basic elements.

Table of contents

What it does

In JOpt.TourOptimizer, the SLA of a node is an OpeningHours with a particular time window. Optimizers usually position nodes by cost and sometimes violate an SLA, depending on the cost-benefit ratio. That makes it hard to tell an optimizer that being 10 minutes late at one node is fine, but at another node it is not acceptable.

To our best knowledge, most optimizers address this by assigning extremely high cost penalties. In JOpt, a hard constraint is a property of the architecture, not of the cost: the optimizer is designed so that hard constraints cannot be violated, and the search space is structurally cut to exclude invalid solutions.

A CapturedNode (also called a pillar or PillarNode) is the primary mechanism for this. Its opening hours cannot be violated under any circumstances. The other nodes of the schedule flow around the pillar: they shift, reorder or move to other resources to keep the pillar's time window intact.

Why not penalties

Penalty-based "hard" constraints are pseudo constraints: expensive soft constraints that pretend to be hard. As the complexity of a problem grows, three things go wrong in practice:

  1. The optimizer wastes its budget. The search spends most of its capacity on inflated penalty terms instead of on the actual schedule. Soft constraints such as travel time, overtime and fairness are starved.
  2. Pseudo-hard constraints fight each other. With dozens of penalty-based "hard" constraints in the same cost function, the optimizer trades violations between them. A solution that violates one SLA to save another is "optimal" by the numbers but unacceptable for the business.
  3. There is no guarantee. A sufficiently complex problem finds ways to violate even very high penalties. A cost function cannot reliably distinguish "preferred" from "legally required".

This limitation was not theoretical. It was reported by customers running large-scale field-service operations with thousands of daily dispatches and contractual SLAs. As their problem complexity grew, penalty-based solvers produced increasingly problematic results. The motivation for Pillar Nodes came directly from these real-world scenarios.

How pillars work

A pillar acts as a fixed anchor in the route timeline. Its opening hours are absolute feasibility boundaries:

  • The visit must be placed within the time window.
  • The pillar can move within its window, or be fully constrained to a specific slot.
  • In all cases, the window boundaries are inviolable.
  • Arriving outside the Pillar's time window is structurally impossible. The optimizer will not generate such a solution.

This follows from JOpt's attempt-based architecture: every permutation of nodes and resources leads to a valid solution, because the optimizer cuts the invalid solutions out of the solution space. In return, every solution it finds is valid. It is not necessarily converged or at optimal cost, but it contains no hard constraint violation.

Because hard constraints are a property of the data and not a parameter of the cost function, they never compete with soft constraints. Soft constraints are optimized freely within the feasible space that the hard constraints define. The two layers are separate.

Both nodes and CapturedNodes can also be defined as events without a geographical location. This is useful for online meetings, phone calls and other tasks without a location that still require strict time-window compliance.

Conflict resolution

When a pillar conflicts with other nodes, the optimizer follows a strict hierarchy:

  1. Reorder or shift normal nodes so that the pillar SLA is met. Normal nodes are moved earlier or later in the timeline.
  2. Accept soft violations on normal nodes. If a normal node must be late to protect the pillar, the cost function handles that trade-off.
  3. Move work to other resources. If the current resource cannot serve both the pillar and a conflicting node, the optimizer assigns the normal node to another resource.
  4. Cancel the pillar rather than violate it. In extreme cases where no feasible schedule exists, the pillar is removed from the plan. A cancelled contract is preferred over a violated one, as damage control by design.

The following cases show how the optimizer resolves each type of conflict. In the figures, John is a resource with working hours, nodes are light blue (opening hours) with their task duration in dark blue, and CapturedNodes are orange.

Case 1: No conflict

In the usual case, matching the opening hours to the working hours of a resource is unproblematic. Visiting CapturedNodes and nodes in the same route causes no conflict, and the CapturedNode behaves like a normal node. John can visit both nodes within their opening hours and finish the tasks, and fitting in the CapturedNode causes no violation.

Timeline of John from 8 am to 4 pm with two nodes and a CapturedNode, each visit inside its time window

Figure: CapturedNode case 1, no conflict

Case 2: Conflict solved by moving a node

In some cases, visiting a node before a CapturedNode would cause a late violation at the CapturedNode. The optimizer recognizes the potential late violation and solves it by repositioning the node.

Before and after: on the left, John visits a node first and arrives late at the CapturedNode; on the right, the node comes after the CapturedNode and both visits are inside their time windows

Figure: CapturedNode case 2, solved by moving a node (left: before, right: after)

Case 3: Conflict solved via late violation at the node

Sometimes a node and a CapturedNode conflict so that only one of them can be visited without a late violation. By definition, late violations at CapturedNodes are not allowed. The optimizer therefore accepts a late violation at the normal node to protect the CapturedNode.

Before and after: on the left, visiting the node first makes John late at the CapturedNode; on the right, John visits the CapturedNode first inside its time window and reaches the node after its time window

Figure: CapturedNode case 3, late violation accepted at the normal node

Case 4: Conflict solved by utilizing an additional resource

In bigger problems, more resources are usually available. Often the best way to solve a conflict is to move a conflicting node to another resource. Which node stays in John's route depends on factors such as driving times, required skills or the optional mandatory resource.

Before and after: on the left, John is late at the CapturedNode; on the right, two solutions in which a second resource, Lily, takes either the CapturedNode or the node, and both visits are on time

Figure: CapturedNode case 4, solved by assigning a node to another resource

Case 5: Conflict solved by removing a CapturedNode

If John is the only resource available for two CapturedNodes that conflict with each other, the only possible solution is to remove one of the CapturedNodes. This shows the extreme case: the SLAs of CapturedNodes cannot be violated under any circumstances. If no other solution is viable, a CapturedNode contract is cancelled rather than violated.

Before and after: on the left, John cannot visit two conflicting CapturedNodes on time; on the right, one CapturedNode is removed and the other is visited inside its time window

Figure: CapturedNode case 5, contract cancelled rather than violated

CapturedNodes keep their SLAs; only an exclusive conflict removes a CapturedNode (case 5).

The special features page describes the same cases.

When to use it

Use pillars when violations are unacceptable

Use pillars for truly non-negotiable requirements:

  • contractual appointment windows with penalties for late arrival;
  • compliance deadlines imposed by regulation;
  • dangerous-goods pickups that require certified personnel within specific time slots;
  • named-person commitments, where the wrong technician showing up is as bad as showing up late.

Do not use pillars for preferences

If a time window is preferred but not mandatory, model it as a normal node and handle it through soft constraint penalties, objective weighting or business rules. Do not use pillars for, say, a hairdressing appointment where a 10-minute delay is tolerable.

Keep the two apart:

  • architecture for obligations;
  • cost function for preferences and trade-offs.

The cost function then carries no pseudo-hard constraints that consume the optimizer's budget.

How to use it

Pillar node classes

The pillar concept appears as dedicated node classes that implement IPillarNode:

ClassDescription
PillarTimeWindowGeoNodeGeo-located pillar (latitude and longitude plus strict opening hours)
PillarEventNodePillar without a location (event style, strict opening hours)

Mandatory resource

A pillar can optionally be tied to a mandatory resource:

pillar.attachResource(resource);

When attached, the node is not only time-critical but also bound to one specific resource. This is useful for:

  • skills or certifications (for example dangerous-goods handling);
  • legal requirements;
  • named-person commitments (for example "this customer expects technician John").

JOpt supports hard constraints beyond pillar time windows, including mandatory resource assignment and skill or type matching. Every hard constraint in JOpt can also be configured to behave as a soft constraint if desired, giving full control over the boundary between obligation and preference.

The code in the next three sections is adapted from the example PillarExample.java, which contains three pillar variants. The dates and times are sample data.

Geo pillar with mandatory resource

A plumbing job in Cologne with a strict 50-minute time window, assigned to John:

IOpeningHours ohPlumbing =
    new OpeningHours(
        ZonedDateTime.of(2019, JUNE.getValue(), 5, 10, 0, 0, 0,
            ZoneId.of("Europe/Berlin")),
        ZonedDateTime.of(2019, JUNE.getValue(), 5, 10, 50, 0, 0,
            ZoneId.of("Europe/Berlin")));

IPillarNode colognePlumbing =
    new PillarTimeWindowGeoNode(
        "Cologne Plumbing", 50.9333, 6.95, ohPlumbing);

// John must handle this job
colognePlumbing.attachResource(johnRes);

opti.addElement(colognePlumbing);

The optimizer guarantees that John visits this node between 10:00 and 10:50. If the visit duration equals the opening hours, it is not necessary to provide the duration separately.

Event pillar with mandatory resource

An important phone call for Jack between 10:30 and 12:00 that lasts 30 minutes. It needs no location:

IOpeningHours ohCall =
    new OpeningHours(
        ZonedDateTime.of(2019, JUNE.getValue(), 6, 10, 30, 0, 0,
            ZoneId.of("Europe/Berlin")),
        ZonedDateTime.of(2019, JUNE.getValue(), 6, 12, 0, 0, 0,
            ZoneId.of("Europe/Berlin")));

Duration visitDuration = Duration.ofMinutes(30);

IPillarNode callJack =
    new PillarEventNode("Important call", ohCall, visitDuration);

// Jack must take this call
callJack.attachResource(jackRes);

opti.addElement(callJack);

The call can be placed anywhere within the 10:30 to 12:00 window, and Jack is guaranteed to be available for 30 minutes within that range.

Geo pillar without mandatory resource

A maintenance job in Stuttgart between 13:30 and 16:00 that lasts 90 minutes. Either resource can take it:

IOpeningHours ohMaintenance =
    new OpeningHours(
        ZonedDateTime.of(2019, JUNE.getValue(), 4, 13, 30, 0, 0,
            ZoneId.of("Europe/Berlin")),
        ZonedDateTime.of(2019, JUNE.getValue(), 4, 16, 0, 0, 0,
            ZoneId.of("Europe/Berlin")));

Duration visitDuration = Duration.ofMinutes(90);

IPillarNode maintenanceJob =
    new PillarTimeWindowGeoNode(
        "Maintenance job", 48.7667, 9.18333,
        ohMaintenance, visitDuration);

// No resource attached: optimizer selects the best fit
opti.addElement(maintenanceJob);

The time window is still treated as a hard constraint. The optimizer selects whichever resource can serve this node without violating the SLA.

Limits

A CapturedNode limits the available optimization space, because the optimizer cannot freely rearrange it. This is by design. Overusing pillars (marking every node as a pillar) therefore shrinks the solution space unnecessarily and reduces the optimization quality. Reserve pillars for the constraints that truly require them.

In an exclusive conflict, the optimizer removes a pillar from the plan rather than violate it; see Case 5.

Authors

DNA Evolutions GmbH


Added: 2026-03-28 (JOpt core 7.5.3-j17) | Last updated: 2026-10-19