> ## Documentation Index
> Fetch the complete documentation index at: https://developers.kardinal.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Capacities

> Model what a vehicle can carry: ceilings per vehicle, compartments set up before departure, pickups that wait for deliveries, tolerated overload, visit caps and last-in-first-out unloading, and how to check the load in a solution.

A capacity is an amount that a vehicle can carry in one dimension, such as weight, volume, or a number of pallets. Each stop loads or unloads an amount of each capacity. The engine keeps a running load for every capacity, along the tour. A tour is the route of one resource, from its start to its end. A resource is a vehicle with its driver. A waypoint is one line of a tour in the solution. This guide covers the `capacities` of resources and stops, and the constraints built on them. Each section states one rule, with a link to the API reference where the exact behavior is written. The last section shows how to check the loads in a solution.

These snippets use local times where they have times. The plan you put them in needs `"tz"`. See [Dates and time zones](https://developers.kardinal.ai/reference/data-model.md#dates-and-time-zones).

## Capacities of vehicles and stops

A heterogeneous fleet is a fleet whose vehicles don't carry the same amounts. You model it with the `capacities` of each resource. `capacities` is a map. You choose its keys, such as `weight`, `volume` or `nbPackages`, and the engine tracks each key on its own.

```json theme={null}
{
  "resources": [
    {
      "id": "van-1",
      "vehicleProfile": { "type": "car" },
      "capacities": { "weight": 800, "volume": 4.2, "nbPackages": 60 }
    }
  ],
  "orders": [
    {
      "id": "order-1",
      "stops": [
        {
          "type": "single",
          "id": "stop-1",
          "kind": "delivery",
          "position": { "lon": 2.3522, "lat": 48.8566 },
          "operationDuration": "PT5M",
          "capacities": { "weight": 40, "volume": 0.3, "nbPackages": 3 }
        }
      ]
    }
  ]
}
```

The rules are these:

* **A ceiling on the running load.** The `capacities` of a resource is a ceiling. The load of the vehicle can't go over it at any point of the tour. The ceiling applies to all the orders of the resource together. In the sandbox, a van with `weight: 100` and two deliveries of 60 kg served only one of them.
* **A pickup adds, a delivery deducts.** A `pickup` stop adds its `capacities` to the load. A `delivery` stop deducts them. A delivery whose order has no pickup is loaded at departure, so it counts in `filledCapacitiesAtBegin` from the start of the tour.
* **A key the vehicle doesn't declare has no limit.** If a stop carries `volume` and the resource declares only `weight`, nobody checks `volume` on that resource.
* **A stop that fits no vehicle is left out.** The engine lists it in `unaffectedStopIds`. A delivery of 120 kg with only a 100 kg van is an example. In the sandbox, the solution gave no other message.
* **Vehicles can differ.** Each resource has its own ceilings. With two vans of 100 kg and 300 kg, a delivery of 150 kg goes to the big van. In the sandbox, the engine put a delivery of 40 kg on the big van too, and left the small van unused.

`overlappingCapacitiesByStopTag` is a different field. It limits how many vehicles are at the same time at stops that share a tag. It doesn't limit a load, and the API reference calls it a soft limit.

See [`Resource.capacities`](https://developers.kardinal.ai/api-reference/plan/create-a-plan#body-resources) and [`overlappingCapacitiesByStopTag`](https://developers.kardinal.ai/api-reference/plan/create-a-plan#body-overlapping-capacities-by-stop-tag) for the rule.

## Compartments set up before departure

A multi-compartment vehicle has a space that you can arrange in several ways before it leaves the depot. For example, a reefer van can be set up all chilled, or half frozen and half chilled. Two business rules sound alike here, so ask which one is yours:

* **One setup for the whole day.** The driver chooses the setup at the depot. It can't change on the road. Use `atLeastOneConstraint`, as this section shows.
* **Never both on board at once.** The two kinds of goods can share the van over the day, as long as they are never on board together. Use `atLeastOneValidCapacity`, which the engine checks at every stop. See [Cold chain last mile](https://developers.kardinal.ai/case-studies/cold-chain-last-mile.md) for an example.

For one setup for the whole day, write each candidate setup as a `capacities` constraint with a `resourceTag`. Then combine the candidates with `atLeastOneConstraint`:

```json theme={null}
{
  "resources": [
    { "id": "van-1", "tags": ["reefer-van"], "vehicleProfile": { "type": "car" } }
  ],
  "additionalConstraints": [
    {
      "type": "atLeastOneConstraint",
      "name": "reefer-setup",
      "constraints": [
        { "type": "capacities", "capacities": { "frozenVolume": 0, "chilledVolume": 10 }, "resourceTag": "reefer-van" },
        { "type": "capacities", "capacities": { "frozenVolume": 5, "chilledVolume": 5 }, "resourceTag": "reefer-van" }
      ]
    }
  ]
}
```

The rules are these:

* **One setup holds for the whole tour.** The engine checks it against the highest load of each key over the day, even when the two peaks happen at different times. In the sandbox, a plan asked a van to deliver 4 units of frozen goods first. Then the van had to pick up 8 units of chilled goods and deliver them. The two would never be on board together, yet the engine dropped the frozen stop. Without the constraint, it planned all three stops.
* **A stop that fits no setup is dropped.** With the setups above, a van with 2 units of frozen goods and 7 of chilled goods drops the chilled stop. The engine gives no message.
* **A resource without the tag isn't bound.** The constraint ignores a van that doesn't have `reefer-van` in its `tags`. It gives no message either.
* **The solution doesn't name the setup.** You work it out from `maxFilledCapacities`: take the first setup whose ceilings are all at or above the peak of each key. A tour with a peak of 4 frozen and 5 chilled fails the first setup, because `chilledVolume` 10 is enough but `frozenVolume` 0 isn't. It fits the second setup, 5 and 5. When several setups fit, any of them works. See [A check in code](#a-check-in-code).

See [`AdditionalConstraintAtLeastOneConstraint`](https://developers.kardinal.ai/api-reference/plan/create-a-plan#body-additional-constraints) for the rule.

## Pickups after deliveries

A crew delivers bulky items and also collects old ones. The load area works last in, first out, so an old item collected early blocks the items still to be delivered. The rule is: collect only once every delivery is done, or once no more than 300 kg is left on board.

`atLeastOneValidCapacity` expresses it. It lists capacity thresholds. At every stop, at least one listed capacity must be at or under its threshold. This is not the same rule as `atLeastOneConstraint` in the previous section. The engine checks `atLeastOneValidCapacity` after each stop, so it can express an order.

To build the rule, add two counters next to `weight`:

1. Every delivery stop carries `nbDeliveries: 1`.
2. Every pickup stop carries `nbPickups: 1`.
3. The resource declares both counters, with ceilings that are higher than any tour needs, such as 30 and 10. The ceiling of `nbDeliveries` also caps the number of deliveries in one tour, so set it at or above the most deliveries a vehicle can make. The same holds for `nbPickups` and the pickups.

A delivery deducts its counter and a pickup adds it. The vehicle leaves with `nbDeliveries` equal to its number of deliveries and `nbPickups` at 0. Then the constraint lists the thresholds:

```json theme={null}
{
  "additionalConstraints": [
    {
      "type": "atLeastOneValidCapacity",
      "name": "pickups-late",
      "capacities": { "nbDeliveries": 0, "nbPickups": 0, "weight": 300 }
    }
  ]
}
```

At every stop, one of three things must hold. No delivery is left (`nbDeliveries` at or under 0). Or no pickup has happened yet (`nbPickups` at or under 0). Or the load is at or under 300 kg (`weight`). A pickup raises `nbPickups` to 1 for good. From then on, the vehicle must have finished its deliveries or stay at 300 kg or less.

In the sandbox, a plan with three deliveries of 200 kg and a pickup of 60 kg shows the rule at work. Without the rule, the engine did the pickup first, with 660 kg on board. With the rule, it moved the pickup after the last delivery. [Checking the solution](#checking-the-solution) shows this plan.

To forbid every pickup while a delivery remains, whatever the weight, drop the weight threshold: `{ "nbDeliveries": 0, "nbPickups": 0 }`.

See [`AdditionalConstraintAtLeastOneValidCapacity`](https://developers.kardinal.ai/api-reference/plan/create-a-plan#body-additional-constraints) for the rule.

## Tolerated overload

By default, a load over the ceiling makes a stop impossible to plan, so the engine drops it. Your business may prefer an occasional overload to a dropped stop. Overflow is the word for that case. To allow it, set the `capacities` ceiling of the resource at the absolute physical or legal limit. Then add a `costsByCapacity` entry that starts charging at the load you normally plan for:

```json theme={null}
{
  "id": "van-1",
  "capacities": { "weight": 1000 },
  "cost": {
    "costsByCapacity": {
      "weight": { "costFloor": 800, "costCoeff": 1.0, "overcostFloor": 900, "overcostCoeff": 50 }
    }
  }
}
```

Up to 800 kg, the load costs nothing. Between 800 and 900 kg, each extra kilogram costs 1. Past 900 kg, each extra kilogram costs 50. Past 1,000 kg, the ceiling still applies, and the engine drops the stop. In the sandbox, a delivery-only tour of 850 kg had a `tourCost` of 50. A tour of 950 kg had a `tourCost` of 2,600, which is 100 × 1 plus 50 × 50. At 1,050 kg, the engine dropped one stop.

Keep `overcostFloor` below the ceiling. A floor at or above the ceiling is never reached, so its `overcostCoeff` never applies.

The cost only counts through the `minimizeCosts` objective. See [Objectives](https://developers.kardinal.ai/concepts/objectives.md). It is computed on the total handled at the stops of the tour. See [Piecewise-linear cost functions](https://developers.kardinal.ai/guides/cost-modeling.md#piecewise-linear-cost-functions).

> [!WARNING]
>
> This pattern only measures overload on delivery-only tours. When a tour has pickups, each kilogram that is loaded and then unloaded counts twice, and the thresholds no longer follow what the vehicle carries. A van leaves with 700 kg, unloads it all, then picks up 150 kg. It never carries more than 700 kg, yet its 850 kg handled cross the floor of 800 kg.

See [`CostFloorsAndCoeffs`](https://developers.kardinal.ai/api-reference/plan/create-a-plan#body-resources) for the rule.

## Counting visits with a capacity

A capacity doesn't have to measure anything physical. Declare a synthetic capacity on the resource, then give each relevant stop a quantity of 1. The ceiling then caps the number of such visits in one tour. For example, a tour can make at most 3 visits to a restricted zone:

```json theme={null}
{
  "resources": [
    { "id": "van-1", "capacities": { "restrictedZoneVisits": 3 } }
  ],
  "orders": [
    { "id": "order-1", "stops": [ { "id": "stop-1", "capacities": { "restrictedZoneVisits": 1 } } ] }
  ]
}
```

In the sandbox, a van with a cap of 3 and four tagged stops served three of them. The test used only deliveries in one plan and only pickups in another. A tour that mixes both kinds doesn't count visits, because a delivery lowers the load and a pickup raises it. In the sandbox, a van with a cap of 3 made 5 restricted visits, 3 deliveries and 2 pickups, with no violation.

Two keys can also sit on the same stops, each with its own ceiling. For example, a van has `weight: 100` and `dangerousGoodsWeight: 30`, and two stops carry 40 kg with 20 kg of dangerous goods each. The weight fits, but the dangerous goods don't, so the engine dropped one stop.

### Soft version

A soft cap uses a cost instead of a capacity. Tag the relevant stops with `restrictedZone`. Then add a `costsByStopTag` entry that charges past a free allowance. A tour can then make a fourth restricted visit, at a price, instead of being blocked:

```json theme={null}
{
  "resources": [
    {
      "id": "van-1",
      "cost": {
        "costsByStopTag": {
          "restrictedZone": { "costFloor": 3, "costCoeff": 500 }
        }
      }
    }
  ],
  "orders": [
    { "id": "order-1", "stops": [ { "id": "stop-1", "tags": ["restrictedZone"] } ] }
  ]
}
```

The base is the number of `restrictedZone` stops in the tour. The first three cost nothing, and each one after that adds 500. Five restricted stops add 1,000 to the cost of the tour. This penalty is part of `tourCost`, so subtract it if you report that figure as money. Add the entry to the `cost` of every resource that may serve the zone. A resource without it pays nothing for restricted visits.

The cost only counts through `minimizeCosts`. With the default `objectives`, the engine weighs it after `maximizeMandatoryStops` and `minimizeDelay`. See [the default list](https://developers.kardinal.ai/concepts/objectives.md#the-default-list). So the engine accepts a fourth visit before it leaves a mandatory stop unplanned or arrives late against a `preferredTimeWindows`.

Choose the coefficient against the other costs of the fleet, because the penalty only competes with them. Suppose that sending a second van costs less than 500 in total. The engine then sends that van rather than make a fourth visit.

If the zone has a real legal limit, keep the hard cap as well. This hard cap holds only on tours of deliveries alone or of pickups alone. Set its ceiling above `costFloor`, for example a ceiling of 5 with a floor of 3. Each restricted stop then carries both the `restrictedZone` tag and the `restrictedZoneVisits` capacity.

See [`Capacities`](https://developers.kardinal.ai/api-reference/plan/create-a-plan#body-resources) and [`costsByStopTag`](https://developers.kardinal.ai/api-reference/plan/create-a-plan#body-resources) for the rule.

## Last in, first out

LIFO means last in, first out. A vehicle can only unload what it loaded last, like a car carrier loaded nose to tail on one deck. `removalStrategy: "lifo"` enforces this order. If the vehicle picks up P1, then P2, then P3, it has to unload D3, then D2, then D1.

```json theme={null}
{
  "additionalConstraints": [
    {
      "type": "removalStrategy",
      "name": "lifo-rule-1",
      "removalStrategy": "lifo",
      "capacities": ["volume", "length"],
      "resourceTags": ["car-carrier"]
    }
  ]
}
```

The rules are these:

* **It reorders the unloads.** In the sandbox, a van picked up two cars, a then b. Without the rule, it unloaded a before b. With the rule, it unloaded b before a.
* **It only covers the capacities in its list.** The `capacities` array names them. A capacity that isn't listed isn't ordered. In the sandbox, a rule that listed `volume` did nothing for orders that carried `weight`.
* **Give it `resourceTags`.** With `resourceTags`, only the resources that carry one of these tags follow the rule. Without them, the rule applies to every vehicle of the plan. A van without the tag is not bound.
* **Use it only for goods picked up during the tour.** The sandbox tests cover those. This guide makes no claim about goods loaded at the depot before departure.

Keep it for a vehicle that is really stacked or loaded in sequence. Some constraints can make the engine switch to slower algorithms, and LIFO is one of them. See [How the optimization engine works](https://developers.kardinal.ai/concepts/how-the-optimization-engine-works.md) and [Hard vs soft constraints](https://developers.kardinal.ai/concepts/hard-vs-soft-constraints.md).

See [`AdditionalConstraintRemovalStrategy`](https://developers.kardinal.ai/api-reference/plan/create-a-plan#body-additional-constraints) for the rule.

## Checking the solution

A capacity that doesn't fit is not reported as an error. The engine plans the stops that fit, and lists the others in `unaffectedStopIds`. So you check the loads yourself. These fields of the solution help:

* `unaffectedStopIds` lists the stops that the engine left out.
* `filledCapacitiesAtBegin` is the load of each tour when it leaves. It includes the deliveries that have no pickup.
* `filledCapacitiesAfterStop` is the load after one stop. It is on each stop waypoint.
* `maxFilledCapacities` is the highest load of each key over the tour.
* `resourceCapacities` repeats the ceilings of the resource.

### Example plan

This plan has one van with three deliveries of 200 kg, which open at 09:00, and a pickup of 60 kg. The pickup is close to the depot and its window is open all morning. The plan has the rule of [Pickups after deliveries](#pickups-after-deliveries). Every window is wide, so the order of the stops doesn't depend on road times.

```json theme={null}
{
  "tz": "Europe/Paris",
  "maxOptimizationDuration": "PT5S",
  "resources": [
    {
      "id": "truck-1",
      "vehicleProfile": { "type": "car" },
      "workingTimeWindow": { "begin": "2026-11-03 08:00", "end": "2026-11-03 17:00" },
      "departure": { "lon": 2.35, "lat": 48.85 },
      "arrival": { "lon": 2.35, "lat": 48.85 },
      "capacities": { "weight": 1200, "nbDeliveries": 30, "nbPickups": 10 }
    }
  ],
  "orders": [
    {
      "id": "order-1",
      "stops": [
        {
          "type": "single",
          "id": "delivery-1",
          "kind": "delivery",
          "position": { "lon": 2.377, "lat": 48.85 },
          "operationDuration": "PT10M",
          "capacities": { "weight": 200, "nbDeliveries": 1 },
          "authorizedTimeWindows": [{ "begin": "2026-11-03 09:00", "end": "2026-11-03 12:00" }]
        }
      ]
    },
    {
      "id": "order-2",
      "stops": [
        {
          "type": "single",
          "id": "delivery-2",
          "kind": "delivery",
          "position": { "lon": 2.391, "lat": 48.85 },
          "operationDuration": "PT10M",
          "capacities": { "weight": 200, "nbDeliveries": 1 },
          "authorizedTimeWindows": [{ "begin": "2026-11-03 09:00", "end": "2026-11-03 12:00" }]
        }
      ]
    },
    {
      "id": "order-3",
      "stops": [
        {
          "type": "single",
          "id": "delivery-3",
          "kind": "delivery",
          "position": { "lon": 2.405, "lat": 48.85 },
          "operationDuration": "PT10M",
          "capacities": { "weight": 200, "nbDeliveries": 1 },
          "authorizedTimeWindows": [{ "begin": "2026-11-03 09:00", "end": "2026-11-03 12:00" }]
        }
      ]
    },
    {
      "id": "order-4",
      "stops": [
        {
          "type": "single",
          "id": "pickup-1",
          "kind": "pickup",
          "position": { "lon": 2.364, "lat": 48.85 },
          "operationDuration": "PT10M",
          "capacities": { "weight": 60, "nbPickups": 1 },
          "authorizedTimeWindows": [{ "begin": "2026-11-03 08:00", "end": "2026-11-03 12:00" }]
        }
      ]
    }
  ],
  "additionalConstraints": [
    {
      "type": "atLeastOneValidCapacity",
      "name": "pickups-late",
      "capacities": { "nbDeliveries": 0, "nbPickups": 0, "weight": 300 }
    }
  ]
}
```

The same plan with the SDKs:

```python Python theme={null}
from kardinal.routing import models
from kardinal.routing.plans import Order, PlanCreate

depot = models.Position(lon=2.35, lat=48.85)
window = [models.TaggedTimeWindow(begin="2026-11-03 09:00", end="2026-11-03 12:00")]


def delivery(number, lon):
    stop = models.SingleStop(
        type="single",
        id=f"delivery-{number}",
        kind="delivery",
        position=models.Position(lon=lon, lat=48.85),
        operationDuration="PT10M",
        capacities=models.Capacities({"weight": 200, "nbDeliveries": 1}),
        authorizedTimeWindows=window,
    )
    return Order(id=f"order-{number}", stops=[stop])


pickup = models.SingleStop(
    type="single",
    id="pickup-1",
    kind="pickup",
    position=models.Position(lon=2.364, lat=48.85),
    operationDuration="PT10M",
    capacities=models.Capacities({"weight": 60, "nbPickups": 1}),
    authorizedTimeWindows=[models.TaggedTimeWindow(begin="2026-11-03 08:00", end="2026-11-03 12:00")],
)

truck = models.Resource(
    id="truck-1",
    vehicleProfile=models.VehicleProfileCar(type="car"),
    workingTimeWindow=models.TimeWindow(begin="2026-11-03 08:00", end="2026-11-03 17:00"),
    departure=depot,
    arrival=depot,
    capacities=models.Capacities({"weight": 1200, "nbDeliveries": 30, "nbPickups": 10}),
)

rule = models.AdditionalConstraintAtLeastOneValidCapacity(
    type="atLeastOneValidCapacity",
    name="pickups-late",
    capacities=models.Capacities({"nbDeliveries": 0, "nbPickups": 0, "weight": 300}),
)

plan = PlanCreate(
    tz="Europe/Paris",
    maxOptimizationDuration="PT5S",
    resources=[truck],
    orders=[delivery(1, 2.377), delivery(2, 2.391), delivery(3, 2.405), Order(id="order-4", stops=[pickup])],
    additionalConstraints=[rule],
)
```

```ts TypeScript theme={null}
import type { PlanCreateInput } from "@kardinal-ai/sdk";

const depot = { lon: 2.35, lat: 48.85 };
const deliveryWindow = [{ begin: "2026-11-03 09:00", end: "2026-11-03 12:00" }];

function delivery(number: number, lon: number) {
  return {
    id: `order-${number}`,
    priority: 0,
    stops: [
      {
        type: "single" as const,
        id: `delivery-${number}`,
        kind: "delivery" as const,
        position: { lon, lat: 48.85 },
        operationDuration: "PT10M",
        capacities: { weight: 200, nbDeliveries: 1 },
        authorizedTimeWindows: deliveryWindow,
      },
    ],
  };
}

const plan: PlanCreateInput = {
  tz: "Europe/Paris",
  maxOptimizationDuration: "PT5S",
  resources: [
    {
      id: "truck-1",
      priority: 0,
      vehicleProfile: { type: "car" },
      workingTimeWindow: { begin: "2026-11-03 08:00", end: "2026-11-03 17:00" },
      departure: depot,
      arrival: depot,
      capacities: { weight: 1200, nbDeliveries: 30, nbPickups: 10 },
    },
  ],
  orders: [
    delivery(1, 2.377),
    delivery(2, 2.391),
    delivery(3, 2.405),
    {
      id: "order-4",
      priority: 0,
      stops: [
        {
          type: "single",
          id: "pickup-1",
          kind: "pickup",
          position: { lon: 2.364, lat: 48.85 },
          operationDuration: "PT10M",
          capacities: { weight: 60, nbPickups: 1 },
          authorizedTimeWindows: [{ begin: "2026-11-03 08:00", end: "2026-11-03 12:00" }],
        },
      ],
    },
  ],
  additionalConstraints: [
    {
      type: "atLeastOneValidCapacity",
      name: "pickups-late",
      capacities: { nbDeliveries: 0, nbPickups: 0, weight: 300 },
    },
  ],
};
```

### What the solution shows

The sandbox returns this tour for the plan, with the offset of `Europe/Paris` and with seconds. The other fields are left out. The times come from the sandbox and depend on its routing. The order of the stops and the loads are the same in production.

```json theme={null}
{
  "resourceId": "truck-1",
  "isValid": true,
  "filledCapacitiesAtBegin": { "nbDeliveries": 3, "nbPickups": 0, "weight": 600 },
  "maxFilledCapacities": { "nbDeliveries": 3, "nbPickups": 1, "weight": 600 },
  "resourceCapacities": { "nbDeliveries": 30, "nbPickups": 10, "weight": 1200 },
  "wayPoints": [
    { "type": "begin", "departureTime": "2026-11-03T08:00:00+01:00" },
    {
      "type": "stop",
      "stopId": "delivery-3",
      "arrivalTime": "2026-11-03T08:04:50+01:00",
      "beginTime": "2026-11-03T09:00:00+01:00",
      "departureTime": "2026-11-03T09:10:00+01:00",
      "filledCapacitiesAfterStop": { "nbDeliveries": 2, "nbPickups": 0, "weight": 400 }
    },
    {
      "type": "stop",
      "stopId": "delivery-2",
      "arrivalTime": "2026-11-03T09:11:14+01:00",
      "beginTime": "2026-11-03T09:11:14+01:00",
      "departureTime": "2026-11-03T09:21:14+01:00",
      "filledCapacitiesAfterStop": { "nbDeliveries": 1, "nbPickups": 0, "weight": 200 }
    },
    {
      "type": "stop",
      "stopId": "delivery-1",
      "arrivalTime": "2026-11-03T09:22:28+01:00",
      "beginTime": "2026-11-03T09:22:28+01:00",
      "departureTime": "2026-11-03T09:32:28+01:00",
      "filledCapacitiesAfterStop": { "nbDeliveries": 0, "nbPickups": 0, "weight": 0 }
    },
    {
      "type": "stop",
      "stopId": "pickup-1",
      "arrivalTime": "2026-11-03T09:33:36+01:00",
      "beginTime": "2026-11-03T09:33:36+01:00",
      "departureTime": "2026-11-03T09:43:36+01:00",
      "filledCapacitiesAfterStop": { "nbDeliveries": 0, "nbPickups": 1, "weight": 60 }
    },
    { "type": "end", "arrivalTime": "2026-11-03T09:44:50+01:00" }
  ]
}
```

* The van leaves with 600 kg, which is the three deliveries. They have no pickup in their orders, so they are loaded at departure.
* The pickup comes last, after the last delivery, when the van carries 0 kg. `maxFilledCapacities` shows a peak of 600 kg.
* `unaffectedStopIds` is empty, because the engine planned all four stops.
* The same plan without the rule gives a different order. In the sandbox, the engine did the pickup first, at 08:01, and the van then carried 660 kg. Its `maxFilledCapacities` showed `weight` at 660. So the rule moved the pickup later.

### A check in code

This check reads the loads of a tour and compares them with the ceilings. It also finds the compartment setup that fits, when the plan has setups to choose from. It takes one tour as a dictionary, such as the JSON above. Call it on each entry of `solution["tours"]`.

```python theme={null}
def check_tour(tour, setups=()):
    """Return the problems found in one tour, and the compartment setup that fits."""
    problems = []
    ceilings = tour.get("resourceCapacities") or {}

    for key, ceiling in ceilings.items():
        peak = tour["maxFilledCapacities"].get(key, 0)
        if peak > ceiling:
            problems.append(f"{key}: peak {peak} is over the ceiling {ceiling}")

    for waypoint in tour["wayPoints"]:
        if waypoint["type"] != "stop":
            continue
        for key, load in waypoint["filledCapacitiesAfterStop"].items():
            if key in ceilings and load > ceilings[key]:
                problems.append(f"{waypoint['stopId']}: {key} {load} is over the ceiling {ceilings[key]}")

    # The solution doesn't name the compartment setup. Take the first one that fits the peaks.
    peaks = tour["maxFilledCapacities"]
    fitting = next((s for s in setups if all(peaks.get(k, 0) <= v for k, v in s.items())), None)
    return problems, fitting


setups = [{"frozenVolume": 0, "chilledVolume": 10}, {"frozenVolume": 5, "chilledVolume": 5}]
# A tour with a peak of 4 frozen and 5 chilled returns the second setup.
```

Then check the list of stops that the engine left out:

```python theme={null}
if solution["unaffectedStopIds"]:
    print("Left out:", solution["unaffectedStopIds"])
```

Use `setups` only for a plan that has an `atLeastOneConstraint` over `capacities`. The depot sets the van up in the setup that the check returns, before departure.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.