diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml
index e117784c9..af347e9f6 100644
--- a/.github/workflows/coverage.yaml
+++ b/.github/workflows/coverage.yaml
@@ -15,13 +15,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out the repo
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
- name: Install stable toolchain
- uses: actions-rs/toolchain@v1
+ uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
- override: true
- name: Generate code coverage
run: |
@@ -29,9 +28,11 @@ jobs:
cargo tarpaulin --exclude-files *_test.rs *tests* *benches* *heuristic-research* --out xml
- name: Upload to codecov.io
- uses: codecov/codecov-action@v1.0.2
+ uses: codecov/codecov-action@v5
with:
- token: ${{secrets.CODECOV_TOKEN}}
+ token: ${{ secrets.CODECOV_TOKEN }}
+ files: cobertura.xml
+ fail_ci_if_error: true
- name: Archive code coverage results
uses: actions/upload-artifact@v4
diff --git a/.github/workflows/maturin.yaml b/.github/workflows/maturin.yaml
deleted file mode 100644
index 40f30f71f..000000000
--- a/.github/workflows/maturin.yaml
+++ /dev/null
@@ -1,111 +0,0 @@
-name: Publish to PyPI
-
-on:
- workflow_dispatch:
-
-permissions:
- contents: read
-
-jobs:
- linux:
- runs-on: ubuntu-latest
- strategy:
- matrix:
- target: [x86_64, armv7]
- steps:
- - uses: actions/checkout@v3
- - uses: actions/setup-python@v4
- with:
- python-version: '3.10'
- - name: Build wheels
- uses: PyO3/maturin-action@v1
- with:
- working-directory: ./vrp-cli
- target: ${{ matrix.target }}
- args: --release --out dist -i python3.10
- sccache: 'true'
- manylinux: auto
- - name: Upload wheels
- uses: actions/upload-artifact@v4
- with:
- name: wheels
- path: ./vrp-cli/dist
-
- windows:
- runs-on: windows-latest
- strategy:
- matrix:
- target: [x64, x86]
- steps:
- - uses: actions/checkout@v3
- - uses: actions/setup-python@v4
- with:
- python-version: '3.10'
- architecture: ${{ matrix.target }}
- - name: Build wheels
- uses: PyO3/maturin-action@v1
- with:
- working-directory: ./vrp-cli
- target: ${{ matrix.target }}
- args: --release --out dist --find-interpreter
- sccache: 'true'
- - name: Upload wheels
- uses: actions/upload-artifact@v4
- with:
- name: wheels
- path: ./vrp-cli/dist
-
- macos:
- runs-on: macos-latest
- strategy:
- matrix:
- target: [x86_64, aarch64]
- steps:
- - uses: actions/checkout@v3
- - uses: actions/setup-python@v4
- with:
- python-version: '3.10'
- - name: Build wheels
- uses: PyO3/maturin-action@v1
- with:
- working-directory: ./vrp-cli
- target: ${{ matrix.target }}
- args: --release --out dist --find-interpreter
- sccache: 'true'
- - name: Upload wheels
- uses: actions/upload-artifact@v4
- with:
- name: wheels
- path: ./vrp-cli/dist
-
-# sdist:
-# runs-on: ubuntu-latest
-# steps:
-# - uses: actions/checkout@v3
-# - name: Build sdist
-# uses: PyO3/maturin-action@v1
-# with:
-# working-directory: ./vrp-cli
-# command: sdist
-# args: --out dist
-# - name: Upload sdist
-# uses: actions/upload-artifact@v3
-# with:
-# name: wheels
-# path: ./vrp-cli/dist
-
- release:
- name: Release
- runs-on: ubuntu-latest
- needs: [linux, windows, macos] #, sdist]
- steps:
- - uses: actions/download-artifact@v4
- with:
- name: wheels
- - name: Publish to PyPI
- uses: PyO3/maturin-action@v1
- env:
- MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
- with:
- command: upload
- args: --skip-existing *
diff --git a/.github/workflows/publish-policy.yaml b/.github/workflows/publish-policy.yaml
new file mode 100644
index 000000000..57c97e6c1
--- /dev/null
+++ b/.github/workflows/publish-policy.yaml
@@ -0,0 +1,58 @@
+name: Enforce release-only publishing
+
+on:
+ push:
+ paths:
+ - ".github/workflows/**"
+ pull_request:
+ paths:
+ - ".github/workflows/**"
+
+jobs:
+ enforce-release-only:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out the repo
+ uses: actions/checkout@v4
+
+ - name: Reject external publish patterns in workflows
+ shell: bash
+ run: |
+ set -euo pipefail
+
+ workflows_root=".github/workflows"
+ workflows_globs=(
+ --glob "*.yaml"
+ --glob "*.yml"
+ --glob "!publish-policy.yaml"
+ )
+
+ has_violations=0
+
+ check_pattern() {
+ local pattern="$1"
+ local reason="$2"
+
+ if rg -n --no-heading -e "$pattern" "${workflows_globs[@]}" "$workflows_root"; then
+ echo "::error::${reason}"
+ has_violations=1
+ fi
+ }
+
+ check_pattern "\\bcargo\\s+publish\\b" "Found crates.io publishing command (cargo publish)."
+ check_pattern "\\bcargo\\s+login\\b" "Found crates.io auth command (cargo login)."
+ check_pattern "\\bMATURIN_PYPI_TOKEN\\b" "Found PyPI publishing secret usage (MATURIN_PYPI_TOKEN)."
+ check_pattern "\\bPYPI_API_TOKEN\\b" "Found PyPI publishing secret usage (PYPI_API_TOKEN)."
+ check_pattern "\\bcommand:\\s*upload\\b" "Found upload command in workflow."
+ check_pattern "\\btwine\\s+upload\\b" "Found PyPI upload command (twine upload)."
+ check_pattern "PyO3/maturin-action@" "Found maturin wheel-build action."
+ check_pattern "\\bmaturin\\b" "Found maturin usage in workflow."
+ check_pattern "docker/build-push-action@" "Found Docker image push action."
+ check_pattern "\\bghcr\\.io\\b" "Found container registry target (ghcr.io)."
+ check_pattern "\\bcrates\\.io\\b" "Found crates.io reference in workflow."
+ check_pattern "\\bpypi\\.org\\b" "Found PyPI reference in workflow."
+
+ if [ "$has_violations" -ne 0 ]; then
+ echo "External publishing is disabled. Only GitHub release assets are allowed."
+ exit 1
+ fi
diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml
index a745e2f3e..9a25a9e89 100644
--- a/.github/workflows/publish.yaml
+++ b/.github/workflows/publish.yaml
@@ -1,83 +1,157 @@
-name: Publish packages
+name: Publish release assets
on:
- workflow_dispatch:
- inputs:
- release_tag:
- description: 'Release tag'
- required: true
- docker_tag:
- description: 'Docker image tag'
- required: true
+ release:
+ types: [published]
+
+permissions:
+ contents: write
+
+concurrency:
+ group: release-${{ github.event.release.tag_name }}
+ cancel-in-progress: false
jobs:
- push-to-registry:
- name: Push docker image to GCR
- runs-on: ubuntu-latest
+ build-binaries:
+ name: Build ${{ matrix.asset_target }}
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - os: ubuntu-latest
+ target: x86_64-unknown-linux-gnu
+ asset_target: linux-x86_64
+ archive_ext: tar.gz
+ - os: ubuntu-latest
+ target: aarch64-unknown-linux-gnu
+ asset_target: linux-aarch64
+ archive_ext: tar.gz
+ - os: macos-latest
+ target: x86_64-apple-darwin
+ asset_target: macos-x86_64
+ archive_ext: tar.gz
+ - os: macos-latest
+ target: aarch64-apple-darwin
+ asset_target: macos-aarch64
+ archive_ext: tar.gz
+ - os: windows-latest
+ target: x86_64-pc-windows-msvc
+ asset_target: windows-x86_64
+ archive_ext: zip
steps:
- name: Check out the repo
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
- - name: Push to GitHub Container Registry
- uses: docker/build-push-action@v1
+ - name: Install Rust toolchain
+ uses: dtolnay/rust-toolchain@stable
with:
- username: ${{ github.actor }}
- password: ${{ secrets.CR_PAT }}
- registry: ghcr.io
- repository: reinterpretcat/vrp/vrp-cli
- tags: ${{ github.event.inputs.docker_tag }}
-
- push-to-crates:
- name: Push crates to crates.io
- runs-on: ubuntu-latest
- steps:
- - name: Check out the repo
- uses: actions/checkout@v2
+ targets: ${{ matrix.target }}
+
+ - name: Install Linux ARM64 linker
+ if: matrix.target == 'aarch64-unknown-linux-gnu'
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y gcc-aarch64-linux-gnu
+
+ - name: Build vrp-cli (Linux ARM64)
+ if: matrix.target == 'aarch64-unknown-linux-gnu'
+ env:
+ CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
+ run: cargo build -p vrp-cli --release --target ${{ matrix.target }}
+
+ - name: Build vrp-cli (other targets)
+ if: matrix.target != 'aarch64-unknown-linux-gnu'
+ run: cargo build -p vrp-cli --release --target ${{ matrix.target }}
- - name: Publish all packages from workspace
+ - name: Package binary
+ shell: bash
run: |
- cargo login ${{ secrets.CRATES_IO_TOKEN }}
- cd rosomaxa && cargo publish && sleep 20
- cd ../vrp-core && cargo publish && sleep 20
- cd ../vrp-scientific && cargo publish && sleep 20
- cd ../vrp-pragmatic && cargo publish && sleep 20
- cd ../vrp-cli && cargo publish
-
- push-release:
- name: Push release on github
+ set -euo pipefail
+ tag="${{ github.event.release.tag_name }}"
+ target="${{ matrix.target }}"
+ asset_target="${{ matrix.asset_target }}"
+ out_dir="release-artifacts"
+ mkdir -p "$out_dir"
+
+ if [ "${{ runner.os }}" = "Windows" ]; then
+ bin_path="target/$target/release/vrp-cli.exe"
+ archive_path="$out_dir/vrp-cli-${tag}-${asset_target}.zip"
+ 7z a -tzip "$archive_path" "$bin_path" > /dev/null
+ else
+ bin_path="target/$target/release/vrp-cli"
+ archive_path="$out_dir/vrp-cli-${tag}-${asset_target}.tar.gz"
+ tar -czf "$archive_path" -C "$(dirname "$bin_path")" "$(basename "$bin_path")"
+ fi
+
+ - name: Upload packaged binary
+ uses: actions/upload-artifact@v4
+ with:
+ name: vrp-cli-${{ matrix.asset_target }}
+ path: release-artifacts/vrp-cli-${{ github.event.release.tag_name }}-${{ matrix.asset_target }}.${{ matrix.archive_ext }}
+ if-no-files-found: error
+
+ create-checksums:
+ name: Create checksums
runs-on: ubuntu-latest
+ needs: build-binaries
steps:
- - name: Check out the repo
- uses: actions/checkout@v2
+ - name: Download packaged binaries
+ uses: actions/download-artifact@v4
+ with:
+ pattern: vrp-cli-*
+ path: release-artifacts
+ merge-multiple: true
- - name: Build WebAssembly artefact
+ - name: Generate SHA256SUMS
run: |
- curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
- cd vrp-cli
- wasm-pack build --target web
+ set -euo pipefail
+ cd release-artifacts
+ shasum -a 256 vrp-cli-* > SHA256SUMS
- - name: Archive artifacts
- uses: montudor/action-zip@v0.1.0
+ - name: Upload checksums
+ uses: actions/upload-artifact@v4
with:
- args: zip -qq -r vrp_cli_wasm.zip vrp-cli/pkg
+ name: release-checksums
+ path: release-artifacts/SHA256SUMS
+ if-no-files-found: error
- - name: Create tag and release on github repo
- id: create_release
- uses: actions/create-release@v1
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ upload-release-assets:
+ name: Upload assets to GitHub Release
+ runs-on: ubuntu-latest
+ needs: [build-binaries, create-checksums]
+ steps:
+ - name: Download packaged binaries
+ uses: actions/download-artifact@v4
with:
- tag_name: ${{ github.event.inputs.release_tag }}
- release_name: ${{ github.event.inputs.release_tag }}
- draft: false
- prerelease: false
+ pattern: vrp-cli-*
+ path: release-artifacts
+ merge-multiple: true
- - name: Upload release asset
- uses: actions/upload-release-asset@v1
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ - name: Download checksums
+ uses: actions/download-artifact@v4
with:
- upload_url: ${{ steps.create_release.outputs.upload_url }}
- asset_path: ./vrp_cli_wasm.zip
- asset_name: vrp_cli_wasm.zip
- asset_content_type: application/zip
+ name: release-checksums
+ path: release-artifacts
+
+ - name: List files to upload
+ run: |
+ set -euo pipefail
+ ls -la release-artifacts
+
+ - name: Upload release assets
+ env:
+ GH_TOKEN: ${{ github.token }}
+ GH_REPO: ${{ github.repository }}
+ RELEASE_TAG: ${{ github.event.release.tag_name }}
+ run: |
+ set -euo pipefail
+ [ -n "$RELEASE_TAG" ] || { echo "RELEASE_TAG is empty"; exit 1; }
+
+ mapfile -t files < <(find release-artifacts -maxdepth 1 -type f \( -name 'vrp-cli-*' -o -name 'SHA256SUMS' \) | sort)
+ [ "${#files[@]}" -gt 0 ] || { echo "No release files found to upload"; exit 1; }
+
+ for file in "${files[@]}"; do
+ name="$(basename "$file")"
+ gh release upload "$RELEASE_TAG" "$file#$name" --clobber --repo "$GH_REPO"
+ done
diff --git a/README.md b/README.md
index 008fc1b37..15d737f12 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,5 @@
[](https://docs.rs/vrp-core)
-[](https://crates.io/crates/vrp-cli)

-[](https://crates.io/crates/vrp-core)
[](https://codecov.io/gh/reinterpretcat/vrp)
[](https://codescene.io/projects/46594)
[](https://deps.rs/crate/vrp-cli/1.25.0)
@@ -46,55 +44,25 @@ Additionally, you can check `vrp-core/examples` to see how to use the library an
# Installation
-You can install the latest release of the vrp solver using four different ways:
+You can install the latest release of the vrp solver using GitHub release assets:
-## Install with Python
+## Install from GitHub Release
-The functionality of `vrp-cli` is published to [pypi.org](https://pypi.org/project/vrp-cli/), so you can just install it
-using pip and use from python:
-
-```shell
-pip install vrp-cli
-python examples/python-interop/example.py # run test example
-```
-
-Alternatively, you can use [maturin](https://github.com/PyO3/maturin) tool to build solver locally. You need to enable
-`py_bindings` feature which is not enabled by default.
-
-Additionally, to jupyter notebook mentioned above, you can find extra information in [python example section](https://reinterpretcat.github.io/vrp/examples/interop/python.html)
-of the docs. The [full source code](./examples/python-interop/example.py) of python example is available in the repo which
-contains useful model wrappers with help of `pydantic` lib (reused by tutorial as well).
+Download the archive for your platform from the repository's **Releases** page and unpack it.
+Asset naming convention:
-## Install from Docker
+ vrp-cli--.tar.gz # linux/macos
+ vrp-cli--.zip # windows
-Another fast way to try vrp solver on your environment is to use `docker` image (not performance optimized):
-
-* **run public image** from `Github Container Registry`:
-
-```bash
- docker run -it -v $(pwd):/repo --name vrp-cli --rm ghcr.io/reinterpretcat/vrp/vrp-cli:1.25.0
-```
+Checksums are published as `SHA256SUMS`.
-* **build image locally** using `Dockerfile` provided:
+Example verification (macOS/Linux):
```bash
-docker build -t vrp_solver .
-docker run -it -v $(pwd):/repo --rm vrp_solver
+shasum -a 256 -c SHA256SUMS
```
-Please note that the docker image is built using `musl`, not `glibc` standard library. So there might be some performance
-implications.
-
-
-## Install from Cargo
-
-You can install vrp solver `cli` tool directly with `cargo install`:
-
- cargo install vrp-cli
-
-Ensure that your `$PATH` is properly configured to source the crates binaries, and then run solver using the `vrp-cli` command.
-
## Install from source
diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md
index 4cff1e980..bf36d6a12 100644
--- a/docs/src/SUMMARY.md
+++ b/docs/src/SUMMARY.md
@@ -41,6 +41,7 @@
* [Job priorities](examples/pragmatic/basics/job-priorities.md)
* [Multi day plan](examples/pragmatic/basics/multi-day.md)
* [Vehicle break](examples/pragmatic/basics/break.md)
+ * [Job time constraints](examples/pragmatic/basics/job-times.md)
* [Multiple trips](examples/pragmatic/basics/reload.md)
* [Recharge stations](examples/pragmatic/basics/recharge.md)
* [Relations](examples/pragmatic/basics/relations.md)
diff --git a/docs/src/concepts/pragmatic/errors/index.md b/docs/src/concepts/pragmatic/errors/index.md
index b664d8f2b..a9a7ec40c 100644
--- a/docs/src/concepts/pragmatic/errors/index.md
+++ b/docs/src/concepts/pragmatic/errors/index.md
@@ -2,54 +2,45 @@
This page lists errors produced by the solver.
-
## E0xxx Error
Errors from E0xxx range are generic.
-
### E0000
`cannot deserialize problem` is returned when problem definition cannot be deserialized from the input stream.
-
### E0001
`cannot deserialize matrix` is returned when routing matrix definition cannot be deserialized from the input stream.
-
### E0002
`cannot create transport costs` is returned when problem cannot be matched within routing matrix data passed.
There are two options to consider, when specifying routing matrix data:
-- *time dependent VRP* requires all matrices to have `profile` and `timestamp` properties to be se
-- *time agnostic VRP* requires `timestamp` property to be omitted, `profile` property either set or skipped for all matrices
-
+- _time dependent VRP_ requires all matrices to have `profile` and `timestamp` properties to be se
+- _time agnostic VRP_ requires `timestamp` property to be omitted, `profile` property either set or skipped for all matrices
### E0003
`cannot find any solution` is returned when no solution is found. In this case, please submit a bug and share original
problem and routing matrix.
-
### E0004
`cannot read config` is returned when algorithm configuration cannot be created. To fix it, make sure that config has
a valid json schema and valid parameters.
-
## E1xxx: Validation errors
Errors from E1xxx range are used by validation engine which checks logical correctness of the rich VRP definition.
-
### E11xx: Jobs
These errors are related to `plan.jobs` property definition.
-
#### E1100
`duplicated job ids` error is returned when `plan.jobs` has jobs with the same ids:
@@ -59,12 +50,12 @@ These errors are related to `plan.jobs` property definition.
"plan": {
"jobs": [
{
- "id": "job1",
+ "id": "job1"
/** omitted **/
},
{
/** Error: this id is already used by another job **/
- "id": "job1",
+ "id": "job1"
/** omitted **/
}
/** omitted **/
@@ -75,7 +66,6 @@ These errors are related to `plan.jobs` property definition.
Duplicated job ids are not allowed, so you need to remove all duplicates in order to fix the error.
-
#### E1101
`invalid job task demand` error is returned when job has invalid demand: `pickup`, `delivery`, `replacement` job types should
@@ -90,20 +80,19 @@ have demand specified on each job task, `service` type should have no demand spe
/** Error: delivery task should have demand set**/
"demand": null
}
- ],
- "services": [
- {
- /** omitted **/
- /** Error: service task should have no demand specified**/
- "demand": [1]
- }
- ]
+ ],
+ "services": [
+ {
+ /** omitted **/
+ /** Error: service task should have no demand specified**/
+ "demand": [1]
+ }
+ ]
}
```
To fix the error, make sure that each job task has proper demand.
-
#### E1102
`invalid pickup and delivery demand` error code is returned when job has both pickups and deliveries, but the sum of
@@ -114,17 +103,23 @@ pickups demand does not match to the sum of deliveries demand:
"id": "job",
"pickups": [
{
- "places": [/** omitted **/],
- "demand": [1],
+ "places": [
+ /** omitted **/
+ ],
+ "demand": [1]
},
{
- "places": [/** omitted **/],
+ "places": [
+ /** omitted **/
+ ],
"demand": [1]
}
],
"deliveries": [
{
- "places": [/** omitted **/],
+ "places": [
+ /** omitted **/
+ ],
/** Error: should be 2 as the sum of pickups is 2 **/
"demand": [1]
}
@@ -132,7 +127,6 @@ pickups demand does not match to the sum of deliveries demand:
}
```
-
#### E1103
`invalid time windows in jobs` error is returned when there is a job which has invalid time windows, e.g.:
@@ -140,39 +134,27 @@ pickups demand does not match to the sum of deliveries demand:
```json
{
/** Error: end time is one hour earlier than start time**/
- "times": [
- [
- "2020-07-04T12:00:00Z",
- "2020-07-04T11:00:00Z"
- ]
- ]
+ "times": [["2020-07-04T12:00:00Z", "2020-07-04T11:00:00Z"]]
}
```
Each time window must satisfy the following criteria:
-* array of two strings each of these specifies date in RFC3339 format. The first is considered as start,
-the second - as end
-* start date is earlier than end date
-* if multiple time windows are specified, they must not intersect, e.g.:
+- array of two strings each of these specifies date in RFC3339 format. The first is considered as start,
+ the second - as end
+- start date is earlier than end date
+- if multiple time windows are specified, they must not intersect, e.g.:
```json
{
/** Error: second time window intersects with first one: [13:00, 14:00] **/
"times": [
- [
- "2020-07-04T10:00:00Z",
- "2020-07-04T14:00:00Z"
- ],
- [
- "2020-07-04T13:00:00Z",
- "2020-07-04T17:00:00Z"
- ]
+ ["2020-07-04T10:00:00Z", "2020-07-04T14:00:00Z"],
+ ["2020-07-04T13:00:00Z", "2020-07-04T17:00:00Z"]
]
}
```
-
#### E1104
`reserved job id is used` error is returned when there is a job which has reserved job id:
@@ -187,7 +169,6 @@ the second - as end
To avoid confusion, the following ids are reserved: `departure`, `arrival`, `break`, and `reload`. These
ids are not allowed to be used within `job.id` property.
-
#### E1105
`empty job` error is returned when there is a job which has no or empty job tasks:
@@ -203,7 +184,6 @@ ids are not allowed to be used within `job.id` property.
To fix the error, remove job from the plan or add at least one job task to it.
-
#### E1106
`job has negative duration` error is returned when there is a job place with negative duration:
@@ -213,12 +193,16 @@ To fix the error, remove job from the plan or add at least one job task to it.
"id": "job",
"pickups": [
{
- "places": [{
- /** Error: negative duration does not make sense **/
- "duration": -10,
- "location": {/* omitted */}
- }]
- /* omitted */
+ "places": [
+ {
+ /** Error: negative duration does not make sense **/
+ "duration": -10,
+ "location": {
+ /* omitted */
+ }
+ }
+ ]
+ /* omitted */
}
]
}
@@ -226,7 +210,6 @@ To fix the error, remove job from the plan or add at least one job task to it.
To fix the error, make sure that all durations are non negative.
-
#### E1107
`job has negative demand` error is returned when there is a job with negative demand in any of dimensions:
@@ -236,7 +219,9 @@ To fix the error, make sure that all durations are non negative.
"id": "job",
"pickups": [
{
- "places": [/* omitted */],
+ "places": [
+ /* omitted */
+ ],
/** Error: negative demand is not allowed **/
"demand": [10, -1]
}
@@ -246,30 +231,25 @@ To fix the error, make sure that all durations are non negative.
To fix the error, make sure that all demand values are non negative.
-
### E12xx: Relations
These errors are related to `plan.relations` property definition.
-
#### E1200
`relation has job id which does not present in the plan` error is returned when `plan.relations` has relations with
job ids, not present in `plan.jobs`.
-
#### E1201
`relation has vehicle id which does not present in the fleet` error is returned when `plan.relations` has relations with
vehicle ids, not present in `plan.fleet`.
-
#### E1202
`relation has empty job id list` error is returned when `plan.relations` has relations with empty `jobs` list or it has
only reserved ids such as `departure`, `arrival`, `break`, `reload`.
-
#### E1203
`strict or sequence relation has job with multiple places or time windows` error is returned when `plan.relations` has
@@ -277,7 +257,6 @@ strict or sequence relation which refers one or many jobs with multiple places a
This is currently not allowed due to matching problem.
-
#### E1204
`job is assigned to different vehicles in relations` error is returned when `plan.relations` has a job assigned to several
@@ -289,13 +268,13 @@ relations with different vehicle ids:
"relations": [
{
"vehicleId": "vehicle_1",
- "jobs": ["job1"],
+ "jobs": ["job1"]
/** omitted **/
},
{
/** Error: this job id is already assigned to another vehicle **/
"vehicleId": "vehicle_2",
- "jobs": ["job1"],
+ "jobs": ["job1"]
/** omitted **/
}
]
@@ -305,31 +284,26 @@ relations with different vehicle ids:
To fix this, remove job id from one of relations.
-
#### E1205
`relation has invalid shift index` error is returned when `plan.relations` has `shiftIndex` value and no corresponding
`shift` is present in list of shifts.
-
#### E1206
`relation has special job id which is not defined on vehicle shift` error is returned when `plan.relations` has reserved
job id and corresponding property on `fleet.vehicles.shifts` is not defined. Reserved ids are `break`, `reload` and `arrival`.
-
#### E1207
`some relations have incomplete job definitions` error is returned when `plan.relations` has relation with incomplete
job definitions: e.g. job has two pickups, but in relation its job id is specified only once. To fix the issue, either
remove job ids completely or add missing ones.
-
### E13xx: Vehicles
These errors are related to `fleet.vehicles` property definition.
-
#### E1300
`duplicated vehicle type ids` error is returned when `fleet.vehicles` has vehicle types with the same `typeId`:
@@ -339,12 +313,12 @@ These errors are related to `fleet.vehicles` property definition.
"fleet": {
"vehicles": [
{
- "typeId": "vehicle_1",
+ "typeId": "vehicle_1"
/** omitted **/
},
{
/** Error: this id is already used by another vehicle type **/
- "typeId": "vehicle_1",
+ "typeId": "vehicle_1"
/** omitted **/
}
/** omitted **/
@@ -353,7 +327,6 @@ These errors are related to `fleet.vehicles` property definition.
}
```
-
#### E1301
`duplicated vehicle ids` error is returned when `fleet.vehicles` has vehicle types with the same `vehicleIds`:
@@ -369,7 +342,7 @@ These errors are related to `fleet.vehicles` property definition.
"vehicle_1_b",
/** Error: vehicle_1_b is used second time **/
"vehicle_1_b"
- ],
+ ]
/** omitted **/
},
{
@@ -378,7 +351,7 @@ These errors are related to `fleet.vehicles` property definition.
/** Error: vehicle_1_a is used second time **/
"vehicle_1_a",
"vehicle_2_b"
- ],
+ ]
/** omitted **/
}
/** omitted **/
@@ -389,13 +362,11 @@ These errors are related to `fleet.vehicles` property definition.
Please note that vehicle id should be unique across all vehicle types.
-
#### E1302
`invalid start or end times in vehicle shift` error is returned when vehicle has start/end shift times violating one of
time windows rules defined for jobs in E1103.
-
#### E1303
`invalid break time windows in vehicle shift` error is returned when vehicle has invalid time window of a break. List of
@@ -405,29 +376,23 @@ it is specified:
```json
{
"start": {
- "time": "2019-07-04T08:00:00Z",
+ "time": "2019-07-04T08:00:00Z"
/** omitted **/
},
"end": {
- "time": "2019-07-04T15:00:00Z",
+ "time": "2019-07-04T15:00:00Z"
/** omitted **/
},
"breaks": [
{
/** Error: break is outside of vehicle shift times **/
- "times": [
- [
- "2019-07-04T17:00:00Z",
- "2019-07-04T18:00:00Z"
- ]
- ],
+ "times": [["2019-07-04T17:00:00Z", "2019-07-04T18:00:00Z"]],
"duration": 3600.0
}
]
}
```
-
#### E1304
`invalid reload time windows in vehicle shift` error is returned when vehicle has invalid time window of a reload. Reload
@@ -437,23 +402,20 @@ Additionally, reload time should be inside vehicle shift it is specified:
```json
{
"start": {
- "time": "2019-07-04T08:00:00Z",
+ "time": "2019-07-04T08:00:00Z"
/** omitted **/
},
"end": {
- "time": "2019-07-04T15:00:00Z",
+ "time": "2019-07-04T15:00:00Z"
/** omitted **/
},
"reloads": [
{
/** Error: reload is outside of vehicle shift times **/
- "times": [
- [
- "2019-07-04T17:00:00Z",
- "2019-07-04T18:00:00Z"
- ]
- ],
- "location": { /** omitted **/ },
+ "times": [["2019-07-04T17:00:00Z", "2019-07-04T18:00:00Z"]],
+ "location": {
+ /** omitted **/
+ },
"duration": 3600.0
}
]
@@ -467,9 +429,7 @@ Additionally, reload time should be inside vehicle shift it is specified:
```json
{
"typeId": "vehicle",
- "vehicleIds": [
- "vehicle_1"
- ],
+ "vehicleIds": ["vehicle_1"],
"profile": {
"matrix": "car"
},
@@ -478,35 +438,13 @@ Additionally, reload time should be inside vehicle shift it is specified:
/** Error: distance and time are zero **/
"distance": 0,
"time": 0
- },
+ }
/** omitted **/
}
```
You can fix the error by defining a small value (e.g. 0.0000001) for duration or time costs.
-#### E1307
-
-`time offset interval for break is used with departure rescheduling` is returned when time offset interval is specified for break,
-but `start.latest` is not set equal to `start.earliest` in the shift.
-
-```json
- {
- "start": {
- "earliest": "2019-07-04T09:00:00Z",
- /** Error: need to set latest to "2019-07-04T09:00:00Z" explicitely **/
- "location": { "lat": 52.5316, "lng": 13.3884 }
- },
- "breaks": [{
- /** Note: offset time is used here **/
- "time": [3600, 4000],
- "places": [{ "duration": 1800 } ]
- }]
- }
-```
-
-Alternatively, you can switch to time window definition and keep `start.latest` property as you wish.
-
#### E1308
`invalid vehicle reload resource` is returned when:
@@ -514,12 +452,10 @@ Alternatively, you can switch to time window definition and keep `start.latest`
- `fleet.resources` has vehicle reloads with the same `id`
- required vehicle reload is used with resource id, which is not specified in `fleet.resources`
-
### E15xx: Routing profiles
These errors are related to routing locations and `fleet.profiles` property definitions.
-
#### E1500
`duplicate profile names` error is returned when `fleet.profiles` has more than one profile with the same name:
@@ -541,7 +477,6 @@ These errors are related to routing locations and `fleet.profiles` property defi
To fix the issue, remove all duplicates.
-
#### E1501
`empty profile collection` error is returned when `fleet.profiles` is empty:
@@ -557,35 +492,30 @@ To fix the issue, remove all duplicates.
`mixing different location types` error is returned when problem contains locations in different formats. In order to
fix the issue, change the problem definition to use one specific location type: index reference or geocoordinate.
-
#### E1503
`location indices requires routing matrix to be specified` is returned when location indices are used, but no
routing matrix provided.
-
#### E1504
`amount of locations does not match matrix dimension` is returned when:
-* location indices are used and max index is greater than matrix size
-* amount of total locations is higher than matrix size
+- location indices are used and max index is greater than matrix size
+- amount of total locations is higher than matrix size
Check locations in problem definition and matrix size.
-
#### E1505
`unknown matrix profile name in vehicle or vicinity clustering profile` is returned when vehicle has in `fleet.vehicles.profile.matrix`
or `plan.clustering.profile` value which is not specified in `fleet.profiles` collection. To fix issue, either change
value to one specified or add a corresponding profile in profiles collection.
-
### E16xx: Objectives
These errors are related to `objectives` property definition.
-
#### E1600
`an empty objective specified` error is returned when objective property is present in the problem, but no single
@@ -599,7 +529,6 @@ objective is set, e.g.:
`objectives` property is optional, just remove it to fix the problem and use default objectives.
-
#### E1601
`duplicate objective specified` error is returned when objective of specific type specified more than once:
@@ -622,7 +551,6 @@ objective is set, e.g.:
To fix this issue, just remove one, e.g. `minimize-unassigned`.
-
#### E1602
`missing one of cost objectives` error is returned when no cost objective specified:
@@ -639,34 +567,34 @@ To fix this issue, just remove one, e.g. `minimize-unassigned`.
To solve it, specify one of the cost objectives: `minimize-cost`, `minimize-distance` or `minimize-duration`.
-
#### E1603
`redundant value objective` error is returned when objectives definition is overridden with `maximize-value`, but
there is no jobs with non-zero value specified. To fix the issue, specify at least one non-zero valued job or simply
delete 'maximize-value' objective.
-
#### E1604
`redundant tour order objective` error is returned when objectives definition is overridden with `tour-order`, but
there is no jobs with non-zero order specified. To fix the issue, specify at least one job with non-zero order or simply
delete 'tour-order' objective.
-
#### E1605
`value or order of a job should be greater than zero` error is returned when job's order or value is less than 1. To
fix the issue, make sure that value or order of all jobs are greater than zero.
-
#### E1606
`multiple cost objectives specified` error is returned when more than one cost objective is specified. To fix the issue,
keep only one cost objective in the list of objectives.
-
#### E1607
`missing value objective` error is returned when plan has jobs with value set, but user defined objective doesn't
include the `maximize-value` objective.
+
+#### E1608
+
+`missing min tour size objective` error is returned when fleet has vehicles with `min_tour_size` set in their limits,
+but user defined objective doesn't include the `minimize-tour-size-violation` objective.
diff --git a/docs/src/concepts/pragmatic/problem/jobs.md b/docs/src/concepts/pragmatic/problem/jobs.md
index 8fb7fa097..6051ff6a6 100644
--- a/docs/src/concepts/pragmatic/problem/jobs.md
+++ b/docs/src/concepts/pragmatic/problem/jobs.md
@@ -34,6 +34,9 @@ defined. Each task has the following properties:
- **order** (optional): a job task assignment order which makes preferable to serve some jobs before others in the tour.
The order property is represented as integer greater than 1, where the lower value means higher priority. By default
its value is set to maximum.
+- **dueDate** (optional): a due date for the job task, used with `minimize-overdue` objective. If the job is scheduled
+ after this date, an overdue penalty is applied.
+
## Places
diff --git a/docs/src/concepts/pragmatic/problem/objectives.md b/docs/src/concepts/pragmatic/problem/objectives.md
index 131eca035..bc31cca12 100644
--- a/docs/src/concepts/pragmatic/problem/objectives.md
+++ b/docs/src/concepts/pragmatic/problem/objectives.md
@@ -4,14 +4,12 @@ A classical objective function (or simply objective) for VRP is minimization of
require different objective function or even more than one considered simultaneously. That's why the solver has a concept
of multi objective.
-
## Understanding multi objective structure
A multi objective is defined by `objectives` property which has array of objectives and defines lexicographical ordered
objective function. Here, priority of objectives decreases from first to the last element of the array. For the same
priority (or in other words, competitive) objectives, a special `multi-objective` type can be used.
-
## Available objectives
The solver already provides multiple built-in objectives distinguished by their `type`. All these objectives can be
@@ -21,10 +19,10 @@ split into the following groups.
These objectives specify how "total" cost of job insertion is calculated:
-* `minimize-cost`: minimizes total transport cost calculated for all routes. Here, total transport cost is seen as linear
+- `minimize-cost`: minimizes total transport cost calculated for all routes. Here, total transport cost is seen as linear
combination of total time and distance
-* `minimize-distance`: minimizes total distance of all routes
-* `minimize-duration`: minimizes total duration of all routes
+- `minimize-distance`: minimizes total distance of all routes
+- `minimize-duration`: minimizes total duration of all routes
One of these objectives has to be set and only one.
@@ -32,49 +30,58 @@ One of these objectives has to be set and only one.
Besides cost objectives, there are other objectives which are targeting for some scalar characteristic of solution:
-* `minimize-unassigned`: minimizes amount of unassigned jobs. Although, solver tries to minimize amount of
-unassigned jobs all the time, it is possible that solution, discovered during refinement, has more unassigned jobs than
-previously accepted. The reason of that can be conflicting objective (e.g. minimize tours) and restrictive
-constraints such as time windows. The objective has the following optional parameter:
- * `breaks`: a multiplicative coefficient to make breaks more preferable for assignment. Default value is 1. Setting
- this parameter to a value bigger than 1 is useful when it is highly desirable to have break assigned but its
- assignment leads to more jobs unassigned.
-* `minimize-tours`: minimizes total amount of tours present in solution
-* `maximize-tours`: maximizes total amount of tours present in solution
-* `minimize-arrival-time`: prefers solutions where work is finished earlier
-* `fast-service`: prefers solutions when jobs are served early in tours. Optional parameter:
- * `tolerance`: an objective tolerance specifies how different objective values have to be to consider them different.
- Relative distance metric is used.
-* `hierarchical-areas`: an experimental objective to play with clusters of jobs. Internally uses distance minimization as
+- `minimize-unassigned`: minimizes amount of unassigned jobs. Although, solver tries to minimize amount of
+ unassigned jobs all the time, it is possible that solution, discovered during refinement, has more unassigned jobs than
+ previously accepted. The reason of that can be conflicting objective (e.g. minimize tours) and restrictive
+ constraints such as time windows. The objective has the following optional parameter: \* `breaks`: a multiplicative coefficient to make breaks more preferable for assignment. Default value is 1. Setting
+ this parameter to a value bigger than 1 is useful when it is highly desirable to have break assigned but its
+ assignment leads to more jobs unassigned.
+- `minimize-tours`: minimizes total amount of tours present in solution
+- `maximize-tours`: maximizes total amount of tours present in solution
+- `minimize-arrival-time`: prefers solutions where work is finished earlier
+- `minimize-overdue`: minimizes the total overdue days for jobs with due dates. Overdue is calculated as the
+ difference between the scheduled date (route start time) and the job's due date. Jobs scheduled before their
+ due date have zero overdue. Unassigned jobs with due dates are heavily penalized (10000) to strongly encourage
+ their assignment.
+- `minimize-vehicle-distance`: penalizes assigning jobs to vehicles that are farther away than the nearest
+ compatible vehicle. For each job, the penalty is the excess distance from the job to its assigned vehicle's
+ start location compared to the nearest compatible vehicle's start location. Compatibility is determined by
+ skills and profile matching. This objective encourages jobs to be served by the closest suitable vehicle.
+- `fast-service`: prefers solutions when jobs are served early in tours. Optional parameter:
+ - `tolerance`: an objective tolerance specifies how different objective values have to be to consider them different.
+ Relative distance metric is used.
+- `hierarchical-areas`: an experimental objective to play with clusters of jobs. Internally uses distance minimization as
a base penalty.
- * `levels` - number of hierarchy levels
+ - `levels` - number of hierarchy levels
### Job distribution objectives
These objectives provide some extra control on job assignment:
-* `maximize-value`: maximizes total value of served jobs. It has optional parameters:
- * `reductionFactor`: a factor to reduce value cost compared to max routing costs
- * `breaks`: a value penalty for skipping a break. Default value is 100.
-* `tour-order`: controls desired activity order in tours
- * `isConstrained`: violating order is not allowed, even if it leads to less assigned jobs (default is true).
-* `compact-tour`: controls how tour is shaped by limiting amount of shared jobs, assigned in different routes,
- for a given job' neighbourhood. It has the following mandatory parameters:
- * `options`: options to relax objective:
- - `jobRadius`: a radius of neighbourhood, minimum is 1
- - `threshold`: a minimum shared jobs to count
- - `distance`: a minimum relative distance between counts when comparing different solutions.
- This objective is supposed to be on the same level within cost ones.
-
+- `maximize-value`: maximizes total value of served jobs. It has optional parameters:
+ - `reductionFactor`: a factor to reduce value cost compared to max routing costs
+ - `breaks`: a value penalty for skipping a break. Default value is 100.
+- `tour-order`: controls desired activity order in tours
+ - `isConstrained`: violating order is not allowed, even if it leads to less assigned jobs (default is true).
+- `compact-tour`: controls how tour is shaped by limiting amount of shared jobs, assigned in different routes,
+ for a given job' neighbourhood. It has the following mandatory parameters:
+ - `options`: options to relax objective: - `jobRadius`: a radius of neighbourhood, minimum is 1 - `threshold`: a minimum shared jobs to count - `distance`: a minimum relative distance between counts when comparing different solutions.
+ This objective is supposed to be on the same level within cost ones.
+- `minimize-tour-size-violation`: penalizes solutions where routes have fewer activities than the `min_tour_size`
+ limit defined on vehicles. Empty routes (with zero activities) are not penalized. This objective should be used
+ when vehicles have `min_tour_size` limits defined in their `limits` property.
### Work balance objectives
There are four work balance objectives available:
-* `balance-max-load`: balances max load in tour
-* `balance-activities`: balances amount of activities performed in tour
-* `balance-distance`: balances travelled distance per tour
-* `balance-duration`: balances tour durations
+- `balance-max-load`: balances max load in tour
+- `balance-activities`: balances amount of activities performed in tour
+- `balance-distance`: balances travelled distance per tour
+- `balance-duration`: balances tour durations
+- `balance-shifts`: balances how often different vehicle shifts are used. Optional parameters:
+ - `saturation` (default `0.05`): controls how strongly small variance deviations are penalized. Lower values enforce nearly equal usage, while higher values allow more imbalance before additional costs are applied.
+ - `weight` (default `1.0`): multiplies the resulting penalty so you can emphasize or de-emphasize shift balancing relative to other objectives. This is especially important when `balance-shifts` shares a multi-objective block with cost-based objectives whose raw magnitudes are much higher.
Typically, you need to use these objective with one from the cost group combined under single `multi-objective`.
@@ -104,11 +111,10 @@ If at least one job has non-zero value associated, then the following objective
If order on job task is specified, then it is also added to the list of objectives after `minimize-tours` objective.
-
## Hints
-* pay attention to the order of objectives
-* if you're using balancing objective and getting high cost or non-realistic, but balanced routes, try to use multi-objective:
+- pay attention to the order of objectives
+- if you're using balancing objective and getting high cost or non-realistic, but balanced routes, try to use multi-objective:
```json
"objectives": [
@@ -137,15 +143,14 @@ If order on job task is specified, then it is also added to the list of objectiv
## Related errors
-* [E1600 an empty objective specified](../errors/index.md#e1600)
-* [E1601 duplicate objective specified](../errors/index.md#e1601)
-* [E1602 missing one of cost objectives](../errors/index.md#e1602)
-* [E1603 redundant value objective](../errors/index.md#e1603)
-* [E1604 redundant tour order objective](../errors/index.md#e1604)
-* [E1605 value or order of a job should be greater than zero](../errors/index.md#e1605)
-* [E1606 multiple cost objectives specified](../errors/index.md#e1606)
-* [E1607 missing value objective](../errors/index.md#e1607)
-
+- [E1600 an empty objective specified](../errors/index.md#e1600)
+- [E1601 duplicate objective specified](../errors/index.md#e1601)
+- [E1602 missing one of cost objectives](../errors/index.md#e1602)
+- [E1603 redundant value objective](../errors/index.md#e1603)
+- [E1604 redundant tour order objective](../errors/index.md#e1604)
+- [E1605 value or order of a job should be greater than zero](../errors/index.md#e1605)
+- [E1606 multiple cost objectives specified](../errors/index.md#e1606)
+- [E1607 missing value objective](../errors/index.md#e1607)
## Examples
diff --git a/docs/src/concepts/pragmatic/problem/vehicles.md b/docs/src/concepts/pragmatic/problem/vehicles.md
index ccd7ada60..40fe425c4 100644
--- a/docs/src/concepts/pragmatic/problem/vehicles.md
+++ b/docs/src/concepts/pragmatic/problem/vehicles.md
@@ -3,52 +3,73 @@
A vehicle types are defined by `fleet.vehicles` property and their schema has the following properties:
- **typeId** (required): a vehicle type id
+
```json
{{#include ../../../../../examples/data/pragmatic/simple.basic.problem.json:100}}
```
- **vehicleIds** (required): a list of concrete vehicle ids available for usage.
+
```json
{{#include ../../../../../examples/data/pragmatic/simple.basic.problem.json:101:103}}
```
- **profile** (required): a vehicle profile which is defined by two properties:
- - **matrix** (required) : a name of matrix profile
- - **scale** (optional): duration scale applied to all travelling times (default is 1.0)
+ - **matrix** (required) : a name of matrix profile
+ - **scale** (optional): duration scale applied to all travelling times (default is 1.0)
+
```json
{{#include ../../../../../examples/data/pragmatic/simple.basic.problem.json:104:106}}
```
- **costs** (required): specifies how expensive is vehicle usage. It has three properties:
-
- - **fixed**: a fixed cost per vehicle tour
- - **time**: a cost per time unit
- - **distance**: a cost per distance unit
+
+ - **fixed**: a fixed cost per vehicle tour
+ - **time**: a cost per time unit
+ - **distance**: a cost per distance unit
- **shifts** (required): specify one or more vehicle shift. See detailed description below.
- **capacity** (required): specifies vehicle capacity symmetric to job demand
+
```json
{{#include ../../../../../examples/data/pragmatic/simple.basic.problem.json:130:132}}
```
- **skills** (optional): vehicle skills needed by some jobs
+
```json
{{#include ../../../../../examples/data/pragmatic/basics/skills.basic.problem.json:131:133}}
```
- **limits** (optional): vehicle limits. There are two:
-
- - **maxDuration** (optional): max tour duration
- - **maxDistance** (optional): max tour distance
- - **tourSize** (optional): max amount of activities in the tour (without departure/arrival). Please note, that
- clustered activities are counted as one in case of vicinity clustering.
+
+ - **maxDuration** (optional): max tour duration
+ - **maxDistance** (optional): max tour distance
+ - **tourSize** (optional): max amount of activities in the tour (without departure/arrival). Please note, that
+ clustered activities are counted as one in case of vicinity clustering.
+ - **minTourSize** (optional): min amount of activities in the tour (without departure/arrival). When using this
+ limit, you must include `minimize-tour-size-violation` in your objectives to guide the solver toward valid solutions.
+ Solutions with tours having fewer activities than this limit will be rejected by the checker.
+
+- **minShifts** (optional): enforces a minimum number of shifts which each `vehicleId` of this type should serve with
+ actual jobs assigned. It is defined as object with:
+ - `value`: minimum amount of shifts required for every vehicle id of this type.
+ - `allowZeroUsage` (optional, default `false`): when `true`, a vehicle id may stay completely unused; otherwise
+ zero usage counts als Verstoß, auch wenn die Mindestanzahl global erreicht wird.
+
+```json
+"minShifts": {
+ "value": 2,
+ "allowZeroUsage": false
+}
+```
An example:
```json
{{#include ../../../../../examples/data/pragmatic/simple.basic.problem.json:99:133}}
-```
+```
## Shift
@@ -65,48 +86,71 @@ Each shift can have the following properties:
- **start** (required) specifies vehicle start place defined via location, earliest (required) and latest (optional) departure time
- **end** (optional) specifies vehicle end place defined via location, earliest (reserved) and latest (required) arrival time.
- When omitted, then vehicle ends on last job location
+ When omitted, then vehicle ends on last job location
- **breaks** (optional) a list of vehicle breaks. There are two types of breaks:
- * __required__: this break is guaranteed to be assigned at cost of flexibility. It has the following properties:
- - `time` (required): a fixed time or time offset interval when the break should happen specified by `earliest` and `latest` properties.
- The break will be assigned not earlier, and not later than the range specified.
- - `duration` (required): duration of the break
- * __optional__: although such break is not guaranteed for assignment, it has some advantages over required break:
- - arbitrary break location is supported
- - the algorithm has more flexibility for assignment
+
+ - **required**: this break is guaranteed to be assigned at cost of flexibility. It has the following properties:
+ - `time` (required): a fixed time or time offset interval when the break should happen specified by `earliest` and `latest` properties.
+ The break will be assigned not earlier, and not later than the range specified.
+ For `OffsetTime` breaks, the offset is relative to the route cost span anchor: for `depot-to-depot` and
+ `depot-to-last-job` spans, the anchor is the departure time; for `first-job-to-depot` and `first-job-to-last-job`
+ spans, the anchor is the first job's arrival time. Flexible start times are supported.
+ - `duration` (required): duration of the break
+ - **optional**: although such break is not guaranteed for assignment, it has some advantages over required break:
+ - arbitrary break location is supported
+ - the algorithm has more flexibility for assignment
It is specified by:
- - `time` (required): time window or time offset interval after which a break should happen (e.g. between 3 or 4 hours after start).
- - `places`: list of alternative places defined by `location` (optional), `duration` (required) and `tag` (optional).
- If location of a break is omitted then break is stick to location of a job served before break.
- - `policy` (optional): a break skip policy. Possible values:
- * `skip-if-no-intersection`: allows to skip break if actual tour schedule doesn't intersect with vehicle time window (default)
- * `skip-if-arrival-before-end`: allows to skip break if vehicle arrives before break's time window end.
+ - `time` (required): time window or time offset interval after which a break should happen (e.g. between 3 or 4 hours after start).
+ - `places`: list of alternative places defined by `location` (optional), `duration` (required) and `tag` (optional).
+ If location of a break is omitted then break is stick to location of a job served before break.
+ - `policy` (optional): a break skip policy. Possible values:
+ - `skip-if-no-intersection`: allows to skip break if actual tour schedule doesn't intersect with vehicle time window (default)
+ - `skip-if-arrival-before-end`: allows to skip break if vehicle arrives before break's time window end.
Please note that optional break is a soft constraint and can be unassigned in some cases due to other hard constraints, such
as time windows. You can control its unassignment weight using specific property on `minimize-unassigned` objective.
See example [here](../../../examples/pragmatic/basics/break.md)
- Additionally, offset time interval requires departure time optimization to be disabled explicitly (see [E1307](../errors/index.md#e1307)).
-
- **reloads** (optional) a list of vehicle reloads. A reload is a place where vehicle can load new deliveries and unload
- pickups. It can be used to model multi trip routes.
+ pickups. It can be used to model multi trip routes.
Each reload has optional and required fields:
- - location (required): an actual place where reload activity happens
- - duration (required): duration of reload activity
- - times (optional): reload time windows
- - tag (optional): a tag which will be propagated back within the corresponding reload activity in solution
- - resourceId (optional): a shared reload resource id. It is used to limit amount of deliveries loaded at this reload.
- See examples [here](../../../examples/pragmatic/basics/reload.md).
+ - location (required): an actual place where reload activity happens
+ - duration (required): duration of reload activity
+ - times (optional): reload time windows
+ - tag (optional): a tag which will be propagated back within the corresponding reload activity in solution
+ - resourceId (optional): a shared reload resource id. It is used to limit amount of deliveries loaded at this reload.
+ See examples [here](../../../examples/pragmatic/basics/reload.md).
- **recharges** (optional, experimental) specifies recharging stations and max distance limit before recharge should happen.
See examples [here](../../../examples/pragmatic/basics/recharge.md).
+- **jobTimes** (optional) specifies time constraints for when jobs can be served during this shift. This is useful for
+ scenarios where vehicles should only serve customers during specific time windows (e.g., business hours only).
+ It has two optional properties:
+ - **earliestFirst**: the earliest time the vehicle can arrive at its first job. If the vehicle would arrive earlier,
+ it must wait until this time before starting service. Jobs whose time windows end before this time cannot be assigned.
+ - **latestLast**: the latest time the vehicle can depart from its last job. Jobs that would require departing after
+ this time cannot be assigned.
+
+ ```json
+ "jobTimes": {
+ "earliestFirst": "2019-07-04T09:00:00Z",
+ "latestLast": "2019-07-04T17:00:00Z"
+ }
+ ```
+
+ This feature is useful for:
+ - Enforcing business hours: ensuring all customer service happens within working hours
+ - Compliance requirements: meeting regulatory constraints on service times
+ - Resource scheduling: coordinating with external systems that have time-based availability
+
+ See examples [here](../../../examples/pragmatic/basics/job-times.md).
+
## Related errors
-* [E1300 duplicated vehicle type ids](../errors/index.md#e1300)
-* [E1301 duplicated vehicle ids](../errors/index.md#e1301)
-* [E1302 invalid start or end times in vehicle shift](../errors/index.md#e1302)
-* [E1303 invalid break time windows in vehicle shift](../errors/index.md#e1303)
-* [E1304 invalid reload time windows in vehicle shift](../errors/index.md#e1304)
-* [E1306 time and duration costs are zeros](../errors/index.md#e1306)
-* [E1307 time offset interval for break is used with departure rescheduling](../errors/index.md#e1307)
-* [E1308 invalid vehicle reload resource](../errors/index.md#e1308)
\ No newline at end of file
+- [E1300 duplicated vehicle type ids](../errors/index.md#e1300)
+- [E1301 duplicated vehicle ids](../errors/index.md#e1301)
+- [E1302 invalid start or end times in vehicle shift](../errors/index.md#e1302)
+- [E1303 invalid break time windows in vehicle shift](../errors/index.md#e1303)
+- [E1304 invalid reload time windows in vehicle shift](../errors/index.md#e1304)
+- [E1306 time and duration costs are zeros](../errors/index.md#e1306)
+- [E1308 invalid vehicle reload resource](../errors/index.md#e1308)
diff --git a/docs/src/examples/pragmatic/basics/break.md b/docs/src/examples/pragmatic/basics/break.md
index 3493bab8a..cd935ed66 100644
--- a/docs/src/examples/pragmatic/basics/break.md
+++ b/docs/src/examples/pragmatic/basics/break.md
@@ -26,7 +26,7 @@ This example demonstrates how to use optional vehicle break with time window and
-## Required break (experimental)
+## Required break
This example demonstrates how to use required vehicle break which has to be scheduled at specific time during travel
between two stops.
@@ -49,5 +49,7 @@ between two stops.
-Please note, that departure rescheduling is disabled by setting `shift.start.earliest` equal to `shift.start.latest`.
-At the moment, this is a hard requirement when such break type is used.
\ No newline at end of file
+When using `OffsetTime` breaks, the offset is relative to the route cost span anchor: for `depot-to-depot` and
+`depot-to-last-job` spans, the anchor is the departure time; for `first-job-to-depot` and `first-job-to-last-job`
+spans, the anchor is the first job's arrival time. Flexible start times (where `shift.start.earliest` differs from
+`shift.start.latest`) are supported.
\ No newline at end of file
diff --git a/docs/src/examples/pragmatic/basics/job-times.md b/docs/src/examples/pragmatic/basics/job-times.md
new file mode 100644
index 000000000..4ffd354dc
--- /dev/null
+++ b/docs/src/examples/pragmatic/basics/job-times.md
@@ -0,0 +1,73 @@
+# Job time constraints
+
+This example demonstrates how to use the `jobTimes` property to restrict when jobs can be served during a vehicle shift.
+
+## Overview
+
+The `jobTimes` property on a vehicle shift allows you to specify:
+
+- **earliestFirst**: The earliest time the vehicle can arrive at its first job
+- **latestLast**: The latest time the vehicle can depart from its last job
+
+This is useful for scenarios such as:
+- Enforcing business hours (e.g., deliveries only between 10:00 and 16:00)
+- Regulatory compliance (e.g., no service before or after certain times)
+- Coordinating with external systems that have time-based availability
+
+## Example
+
+In this example, we have:
+- A vehicle shift starting at 08:00 with `jobTimes` set to `earliestFirst: 10:00` and `latestLast: 16:00`
+- Two jobs:
+ - `assignableJob`: Time window 10:00-16:00 (fits within job times)
+ - `unassignableJob`: Time window 08:00-09:00 (ends before `earliestFirst`)
+
+The `unassignableJob` cannot be served because its time window ends at 09:00, but the vehicle cannot start serving jobs until 10:00 (`earliestFirst`).
+
+
+ Problem
+
+```json
+{{#include ../../../../../examples/data/pragmatic/basics/job-times.basic.problem.json}}
+```
+
+
+
+
+ Solution
+
+```json
+{{#include ../../../../../examples/data/pragmatic/basics/job-times.basic.solution.json}}
+```
+
+
+
+## Key observations
+
+1. **Vehicle waits at depot**: The vehicle departs at 09:52:30 (not 08:00) to arrive at the first job exactly at 10:00 (`earliestFirst`)
+2. **Job assigned within window**: `assignableJob` is served at 10:00-10:05, which satisfies both its own time window and the `jobTimes` constraints
+3. **Job rejected**: `unassignableJob` is unassigned because its time window (08:00-09:00) ends before `earliestFirst` (10:00)
+
+## Use cases
+
+### Business hours only
+```json
+"jobTimes": {
+ "earliestFirst": "2019-07-04T09:00:00Z",
+ "latestLast": "2019-07-04T17:00:00Z"
+}
+```
+
+### Morning deliveries only
+```json
+"jobTimes": {
+ "latestLast": "2019-07-04T12:00:00Z"
+}
+```
+
+### Afternoon start
+```json
+"jobTimes": {
+ "earliestFirst": "2019-07-04T13:00:00Z"
+}
+```
diff --git a/examples/data/pragmatic/basics/job-times.basic.problem.json b/examples/data/pragmatic/basics/job-times.basic.problem.json
new file mode 100644
index 000000000..0f937267a
--- /dev/null
+++ b/examples/data/pragmatic/basics/job-times.basic.problem.json
@@ -0,0 +1,104 @@
+{
+ "plan": {
+ "jobs": [
+ {
+ "id": "assignableJob",
+ "deliveries": [
+ {
+ "places": [
+ {
+ "location": {
+ "lat": 52.52599,
+ "lng": 13.45413
+ },
+ "duration": 300.0,
+ "times": [
+ [
+ "2019-07-04T10:00:00Z",
+ "2019-07-04T16:00:00Z"
+ ]
+ ]
+ }
+ ],
+ "demand": [
+ 1
+ ]
+ }
+ ]
+ },
+ {
+ "id": "unassignableJob",
+ "deliveries": [
+ {
+ "places": [
+ {
+ "location": {
+ "lat": 52.5225,
+ "lng": 13.4095
+ },
+ "duration": 300.0,
+ "times": [
+ [
+ "2019-07-04T08:00:00Z",
+ "2019-07-04T09:00:00Z"
+ ]
+ ]
+ }
+ ],
+ "demand": [
+ 1
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "fleet": {
+ "vehicles": [
+ {
+ "typeId": "vehicle",
+ "vehicleIds": [
+ "vehicle_1"
+ ],
+ "profile": {
+ "matrix": "normal_car"
+ },
+ "costs": {
+ "fixed": 22.0,
+ "distance": 0.0002,
+ "time": 0.004806
+ },
+ "shifts": [
+ {
+ "start": {
+ "earliest": "2019-07-04T08:00:00Z",
+ "location": {
+ "lat": 52.5316,
+ "lng": 13.3884
+ }
+ },
+ "end": {
+ "latest": "2019-07-04T18:00:00Z",
+ "location": {
+ "lat": 52.5316,
+ "lng": 13.3884
+ }
+ },
+ "jobTimes": {
+ "earliestFirst": "2019-07-04T10:00:00Z",
+ "latestLast": "2019-07-04T16:00:00Z"
+ }
+ }
+ ],
+ "capacity": [
+ 10
+ ]
+ }
+ ],
+ "profiles": [
+ {
+ "name": "normal_car"
+ }
+ ]
+ }
+}
diff --git a/examples/data/pragmatic/basics/job-times.basic.solution.json b/examples/data/pragmatic/basics/job-times.basic.solution.json
new file mode 100644
index 000000000..7b26c354f
--- /dev/null
+++ b/examples/data/pragmatic/basics/job-times.basic.solution.json
@@ -0,0 +1,114 @@
+{
+ "statistic": {
+ "cost": 29.5652,
+ "distance": 8990,
+ "duration": 1200,
+ "times": {
+ "driving": 900,
+ "serving": 300,
+ "waiting": 0,
+ "break": 0,
+ "commuting": 0,
+ "parking": 0
+ }
+ },
+ "tours": [
+ {
+ "vehicleId": "vehicle_1",
+ "typeId": "vehicle",
+ "shiftIndex": 0,
+ "stops": [
+ {
+ "location": {
+ "lat": 52.5316,
+ "lng": 13.3884
+ },
+ "time": {
+ "arrival": "2019-07-04T08:00:00Z",
+ "departure": "2019-07-04T09:52:30Z"
+ },
+ "distance": 0,
+ "load": [
+ 1
+ ],
+ "activities": [
+ {
+ "jobId": "departure",
+ "type": "departure"
+ }
+ ]
+ },
+ {
+ "location": {
+ "lat": 52.52599,
+ "lng": 13.45413
+ },
+ "time": {
+ "arrival": "2019-07-04T10:00:00Z",
+ "departure": "2019-07-04T10:05:00Z"
+ },
+ "distance": 4495,
+ "load": [
+ 0
+ ],
+ "activities": [
+ {
+ "jobId": "assignableJob",
+ "type": "delivery"
+ }
+ ]
+ },
+ {
+ "location": {
+ "lat": 52.5316,
+ "lng": 13.3884
+ },
+ "time": {
+ "arrival": "2019-07-04T10:12:30Z",
+ "departure": "2019-07-04T10:12:30Z"
+ },
+ "distance": 8990,
+ "load": [
+ 0
+ ],
+ "activities": [
+ {
+ "jobId": "arrival",
+ "type": "arrival"
+ }
+ ]
+ }
+ ],
+ "statistic": {
+ "cost": 29.5652,
+ "distance": 8990,
+ "duration": 1200,
+ "times": {
+ "driving": 900,
+ "serving": 300,
+ "waiting": 0,
+ "break": 0,
+ "commuting": 0,
+ "parking": 0
+ }
+ }
+ }
+ ],
+ "unassigned": [
+ {
+ "jobId": "unassignableJob",
+ "reasons": [
+ {
+ "code": "TIME_WINDOW_CONSTRAINT",
+ "description": "cannot be visited within time window",
+ "details": [
+ {
+ "vehicleId": "vehicle_1",
+ "shiftIndex": 0
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/python-interop/config_types.py b/examples/python-interop/config_types.py
index 6716a6166..a456e45e5 100644
--- a/examples/python-interop/config_types.py
+++ b/examples/python-interop/config_types.py
@@ -19,9 +19,6 @@ class Progress:
dumpPopulation: bool
-Telemetry.__pydantic_model__.update_forward_refs()
-
-
@dataclass
class Config:
termination: Termination
@@ -47,16 +44,7 @@ class Logging:
enabled: bool
-Logging.__pydantic_model__.update_forward_refs()
-
-
@dataclass
class Environment:
logging: Logging = Logging(enabled=True)
isExperimental: Optional[bool] = None
-
-
-Config.__pydantic_model__.update_forward_refs()
-Telemetry.__pydantic_model__.update_forward_refs()
-Termination.__pydantic_model__.update_forward_refs()
-Environment.__pydantic_model__.update_forward_refs()
diff --git a/examples/python-interop/pragmatic_types.py b/examples/python-interop/pragmatic_types.py
index f838f18fe..ac4b88071 100644
--- a/examples/python-interop/pragmatic_types.py
+++ b/examples/python-interop/pragmatic_types.py
@@ -22,7 +22,7 @@ class RoutingMatrix:
class Problem:
plan: Plan
fleet: Fleet
- objectives: Optional[List[List[Objective]]] = None
+ objectives: Optional[List[Objective]] = None
@dataclass
@@ -142,25 +142,6 @@ class Objective:
class ObjectiveOptions:
threshold: float
-
-Problem.__pydantic_model__.update_forward_refs()
-
-Plan.__pydantic_model__.update_forward_refs()
-Job.__pydantic_model__.update_forward_refs()
-JobTask.__pydantic_model__.update_forward_refs()
-JobPlace.__pydantic_model__.update_forward_refs()
-
-Fleet.__pydantic_model__.update_forward_refs()
-VehicleReload.__pydantic_model__.update_forward_refs()
-VehicleType.__pydantic_model__.update_forward_refs()
-VehicleShift.__pydantic_model__.update_forward_refs()
-VehicleShiftStart.__pydantic_model__.update_forward_refs()
-VehicleShiftEnd.__pydantic_model__.update_forward_refs()
-VehicleBreak.__pydantic_model__.update_forward_refs()
-
-Objective.__pydantic_model__.update_forward_refs()
-
-
# Solution
@dataclass
@@ -223,10 +204,3 @@ class Activity:
class Time:
start: datetime
end: datetime
-
-
-Solution.__pydantic_model__.update_forward_refs()
-Statistic.__pydantic_model__.update_forward_refs()
-Tour.__pydantic_model__.update_forward_refs()
-Stop.__pydantic_model__.update_forward_refs()
-Activity.__pydantic_model__.update_forward_refs()
diff --git a/vrp-cli/src/extensions/generate/fleet.rs b/vrp-cli/src/extensions/generate/fleet.rs
index 764202f7b..d4cdc59c5 100644
--- a/vrp-cli/src/extensions/generate/fleet.rs
+++ b/vrp-cli/src/extensions/generate/fleet.rs
@@ -3,7 +3,7 @@
mod fleet_test;
use super::*;
-use vrp_pragmatic::format::problem::{Fleet, VehicleCosts, VehicleLimits, VehicleShift, VehicleType};
+use vrp_pragmatic::format::problem::{Fleet, VehicleCosts, VehicleLimits, VehicleMinShifts, VehicleShift, VehicleType};
/// Generates fleet of vehicles.
pub(crate) fn generate_fleet(problem_proto: &Problem, vehicle_types_size: usize) -> Fleet {
@@ -16,6 +16,7 @@ pub(crate) fn generate_fleet(problem_proto: &Problem, vehicle_types_size: usize)
let skills = get_vehicle_skills(problem_proto);
let limits = get_vehicle_limits(problem_proto);
let vehicles_sizes = get_vehicles_sizes(problem_proto);
+ let min_shifts = get_vehicle_min_shifts(problem_proto);
let vehicles = (1..=vehicle_types_size)
.map(|type_idx| {
@@ -33,6 +34,7 @@ pub(crate) fn generate_fleet(problem_proto: &Problem, vehicle_types_size: usize)
capacity: get_random_item(capacities.as_slice(), &rnd).expect("cannot find any capacity").clone(),
skills: get_random_item(skills.as_slice(), &rnd).expect("cannot find any skills").clone(),
limits: get_random_item(limits.as_slice(), &rnd).expect("cannot find any limits").clone(),
+ min_shifts: get_random_item(min_shifts.as_slice(), &rnd).expect("cannot find min shifts").clone(),
}
})
.collect();
@@ -70,3 +72,7 @@ fn get_vehicle_limits(problem_proto: &Problem) -> Vec> {
fn get_vehicles_sizes(problem_proto: &Problem) -> Vec {
get_from_vehicle(problem_proto, |vehicle| vehicle.vehicle_ids.len())
}
+
+fn get_vehicle_min_shifts(problem_proto: &Problem) -> Vec> {
+ get_from_vehicle(problem_proto, |vehicle| vehicle.min_shifts.clone())
+}
diff --git a/vrp-cli/src/extensions/generate/plan.rs b/vrp-cli/src/extensions/generate/plan.rs
index 1a3bbf8b9..6ecf14698 100644
--- a/vrp-cli/src/extensions/generate/plan.rs
+++ b/vrp-cli/src/extensions/generate/plan.rs
@@ -46,6 +46,7 @@ pub(crate) fn generate_plan(
get_random_item(demands.as_slice(), &rnd).cloned()
},
order: task.order,
+ due_date: task.due_date.clone(),
})
.collect::>()
})
diff --git a/vrp-cli/src/extensions/import/csv.rs b/vrp-cli/src/extensions/import/csv.rs
index ca283f9ba..854c288f0 100644
--- a/vrp-cli/src/extensions/import/csv.rs
+++ b/vrp-cli/src/extensions/import/csv.rs
@@ -74,6 +74,7 @@ mod actual {
}],
demand: if job.demand != 0 { Some(vec![job.demand.abs()]) } else { None },
order: None,
+ due_date: None,
};
let get_tasks = |jobs: &Vec<&CsvJob>, filter: Box bool>| {
@@ -114,7 +115,7 @@ mod actual {
type_id: vehicle.id.clone(),
vehicle_ids: (1..=vehicle.amount).map(|seq| format!("{}_{}", vehicle.profile, seq)).collect(),
profile: VehicleProfile { matrix: vehicle.profile, scale: None },
- costs: VehicleCosts { fixed: Some(25.), distance: 0.0002, time: 0.005 },
+ costs: VehicleCosts { fixed: Some(25.), distance: 0.0002, time: 0.005, span: None },
shifts: vec![VehicleShift {
start: ShiftStart {
earliest: vehicle.tw_start,
@@ -125,10 +126,12 @@ mod actual {
breaks: None,
reloads: None,
recharges: None,
+ job_times: None,
}],
capacity: vec![vehicle.capacity],
skills: None,
limits: None,
+ min_shifts: None,
}
})
.collect();
diff --git a/vrp-cli/tests/helpers/generate.rs b/vrp-cli/tests/helpers/generate.rs
index 304987a7b..6d8c19ebd 100644
--- a/vrp-cli/tests/helpers/generate.rs
+++ b/vrp-cli/tests/helpers/generate.rs
@@ -16,7 +16,7 @@ pub fn create_empty_job() -> Job {
}
pub fn create_empty_job_task() -> JobTask {
- JobTask { places: vec![], demand: None, order: None }
+ JobTask { places: vec![], demand: None, order: None, due_date: None }
}
pub fn create_empty_job_place() -> JobPlace {
@@ -32,7 +32,7 @@ pub fn create_test_vehicle_type() -> VehicleType {
type_id: "vehicle".to_string(),
vehicle_ids: vec!["vehicle_1".to_string()],
profile: VehicleProfile { matrix: "car".to_string(), scale: None },
- costs: VehicleCosts { fixed: None, distance: 1., time: 0. },
+ costs: VehicleCosts { fixed: None, distance: 1., time: 0., span: None },
shifts: vec![VehicleShift {
start: ShiftStart {
earliest: "2020-05-01T09:00:00.00Z".to_string(),
@@ -43,10 +43,12 @@ pub fn create_test_vehicle_type() -> VehicleType {
breaks: None,
reloads: None,
recharges: None,
+ job_times: None,
}],
capacity: vec![10],
skills: None,
limits: None,
+ min_shifts: None,
}
}
diff --git a/vrp-core/src/construction/enablers/departure_time.rs b/vrp-core/src/construction/enablers/departure_time.rs
index bd6eae81b..6edf4d789 100644
--- a/vrp-core/src/construction/enablers/departure_time.rs
+++ b/vrp-core/src/construction/enablers/departure_time.rs
@@ -4,8 +4,9 @@ mod departure_time_test;
use crate::construction::enablers::*;
use crate::construction::heuristics::RouteContext;
-use crate::models::common::Timestamp;
+use crate::models::common::{TimeSpan, Timestamp};
use crate::models::problem::{ActivityCost, TransportCost, TravelTime};
+use crate::models::solution::Route;
use rosomaxa::prelude::Float;
/// Tries to move forward route's departure time.
@@ -15,16 +16,49 @@ pub fn advance_departure_time(
transport: &dyn TransportCost,
consider_whole_tour: bool,
) {
- if let Some(new_departure_time) = try_advance_departure_time(route_ctx, transport, consider_whole_tour) {
- update_route_departure(route_ctx, activity, transport, new_departure_time);
+ let Some(upper) = try_advance_departure_time(route_ctx, transport, consider_whole_tour) else {
+ return;
+ };
+
+ let current = route_ctx.route().tour.start().unwrap().schedule.departure;
+
+ // Fast path: try the upper bound directly
+ update_route_departure(route_ctx, activity, transport, upper);
+ if is_schedule_feasible(route_ctx.route(), activity, transport) {
+ return;
+ }
+
+ // Slow path: compute critical departure points and try from highest to lowest
+ let candidates = compute_critical_departures(route_ctx.route(), current, upper);
+ for &candidate in candidates.iter().rev() {
+ if candidate <= current || candidate >= upper {
+ continue;
+ }
+ update_route_departure(route_ctx, activity, transport, candidate);
+ if is_schedule_feasible(route_ctx.route(), activity, transport) {
+ return;
+ }
}
+
+ // Fallback: restore current departure
+ update_route_departure(route_ctx, activity, transport, current);
}
/// Tries to move backward route's departure time.
pub fn recede_departure_time(route_ctx: &mut RouteContext, activity: &dyn ActivityCost, transport: &dyn TransportCost) {
- if let Some(new_departure_time) = try_recede_departure_time(route_ctx) {
- update_route_departure(route_ctx, activity, transport, new_departure_time);
+ let Some(new_departure_time) = try_recede_departure_time(route_ctx) else {
+ return;
+ };
+
+ let current = route_ctx.route().tour.start().unwrap().schedule.departure;
+
+ update_route_departure(route_ctx, activity, transport, new_departure_time);
+ if is_schedule_feasible(route_ctx.route(), activity, transport) {
+ return;
}
+
+ // Infeasible: restore current departure
+ update_route_departure(route_ctx, activity, transport, current);
}
fn try_advance_departure_time(
@@ -86,3 +120,72 @@ fn try_recede_departure_time(route_ctx: &RouteContext) -> Option {
if max_change > 0. { Some(start.schedule.departure - max_change) } else { None }
}
+
+/// Computes critical departure time candidates where feasibility transitions may occur.
+/// These are departure values where break boundaries align exactly with job time window boundaries.
+fn compute_critical_departures(route: &Route, current: Timestamp, upper: Timestamp) -> Vec {
+ const EPSILON: f64 = 1e-6;
+
+ // Collect break offset info from route activities
+ let break_offsets: Vec<(f64, f64, f64)> = route
+ .tour
+ .all_activities()
+ .filter_map(|a| {
+ let job = a.job.as_ref()?;
+ let place = job.places.get(a.place.idx)?;
+ match place.times.iter().find(|t| matches!(t, TimeSpan::Offset(_)))? {
+ TimeSpan::Offset(offset) => Some((offset.start, offset.end, place.duration)),
+ _ => None,
+ }
+ })
+ .collect();
+
+ if break_offsets.is_empty() {
+ return vec![];
+ }
+
+ // Collect job TW boundaries from activities with fixed time windows
+ let job_tw_boundaries: Vec = route
+ .tour
+ .all_activities()
+ .filter(|a| a.job.is_some())
+ .filter(|a| {
+ a.job
+ .as_ref()
+ .and_then(|j| j.places.get(a.place.idx))
+ .map(|p| p.times.iter().any(|t| matches!(t, TimeSpan::Window(_))))
+ .unwrap_or(false)
+ })
+ .flat_map(|a| [a.place.time.start, a.place.time.end])
+ .collect();
+
+ let mut candidates = Vec::new();
+ for &(offset_start, offset_end, break_dur) in &break_offsets {
+ for &tw_boundary in &job_tw_boundaries {
+ // D + offset_end + break_dur = tw_boundary
+ let d = tw_boundary - offset_end - break_dur;
+ push_candidate(&mut candidates, d, current, upper, EPSILON);
+
+ // D + offset_end = tw_boundary
+ let d = tw_boundary - offset_end;
+ push_candidate(&mut candidates, d, current, upper, EPSILON);
+
+ // D + offset_start = tw_boundary
+ let d = tw_boundary - offset_start;
+ push_candidate(&mut candidates, d, current, upper, EPSILON);
+ }
+ }
+
+ candidates.sort_by(|a, b| a.total_cmp(b));
+ candidates.dedup();
+ candidates
+}
+
+fn push_candidate(candidates: &mut Vec, d: Timestamp, current: Timestamp, upper: Timestamp, epsilon: f64) {
+ for &offset in &[-epsilon, 0., epsilon] {
+ let val = d + offset;
+ if val > current && val < upper {
+ candidates.push(val);
+ }
+ }
+}
diff --git a/vrp-core/src/construction/enablers/reserved_time.rs b/vrp-core/src/construction/enablers/reserved_time.rs
index 7e918e231..06e49e759 100644
--- a/vrp-core/src/construction/enablers/reserved_time.rs
+++ b/vrp-core/src/construction/enablers/reserved_time.rs
@@ -2,6 +2,7 @@
#[path = "../../../tests/unit/construction/enablers/reserved_time_test.rs"]
mod reserved_time_test;
+use crate::construction::enablers::get_offset_anchor;
use crate::models::common::*;
use crate::models::problem::{ActivityCost, Actor, TransportCost, TravelTime};
use crate::models::solution::{Activity, Route};
@@ -85,8 +86,6 @@ impl ActivityCost for DynamicActivityCost {
// NOTE: do not allow to start or restart work after break finished
if activity_start + extra_duration > activity.place.time.end {
- // TODO this branch is the reason why departure rescheduling is disabled.
- // theoretically, rescheduling should be aware somehow about dynamic costs
ControlFlow::Break(departure + extra_duration)
} else {
ControlFlow::Continue(departure + extra_duration)
@@ -187,6 +186,14 @@ fn reduce_waiting_by_reserved_time(_route: &mut Route, _reserved_times_fn: &Rese
// TODO: could be added if necessary, but it should be thought carefully to keep solution feasibility
}
+type SpanGroup = (Vec, Vec);
+
+/// Sorted group data: window spans and offset spans, each independently sorted and validated.
+struct PartitionedSpans {
+ window_group: Option,
+ offset_group: Option,
+}
+
/// Creates a reserved time function from reserved time index.
pub(crate) fn create_reserved_times_fn(
reserved_times_index: ReservedTimesIndex,
@@ -196,92 +203,120 @@ pub(crate) fn create_reserved_times_fn(
}
let reserved_times = reserved_times_index.into_iter().try_fold(
- HashMap::<_, (Vec<_>, Vec<_>)>::new(),
- |mut acc, (actor, mut times)| {
- // NOTE do not allow different types to simplify interval searching
- let are_same_types = times.windows(2).all(|pair| {
- if let [ReservedTimeSpan { time: a, .. }, ReservedTimeSpan { time: b, .. }] = pair {
- matches!(
- (a, b),
- (TimeSpan::Window(_), TimeSpan::Window(_)) | (TimeSpan::Offset(_), TimeSpan::Offset(_))
- )
- } else {
- false
- }
- });
+ HashMap::<_, PartitionedSpans>::new(),
+ |mut acc, (actor, times)| -> Result<_, GenericError> {
+ // Partition into window and offset groups
+ let (mut window_spans, mut offset_spans): (Vec<_>, Vec<_>) =
+ times.into_iter().partition(|span| matches!(span.time, TimeSpan::Window(_)));
- if !are_same_types {
- return Err("has reserved types of different time span types".to_string());
- }
+ let window_group = build_span_group(&mut window_spans)?;
+ let offset_group = build_span_group(&mut offset_spans)?;
- times.sort_by(|ReservedTimeSpan { time: a, .. }, ReservedTimeSpan { time: b, .. }| {
- let (a, b) = match (a, b) {
- (TimeSpan::Window(a), TimeSpan::Window(b)) => (a.start, b.start),
- (TimeSpan::Offset(a), TimeSpan::Offset(b)) => (a.start, b.start),
- _ => unreachable!(),
- };
- a.total_cmp(&b)
- });
- let has_no_intersections = times.windows(2).all(|pair| {
- if let [ReservedTimeSpan { time: a, .. }, ReservedTimeSpan { time: b, .. }] = pair {
- !a.intersects(0., &b.to_time_window(0.))
- } else {
- false
- }
- });
-
- if has_no_intersections {
- let (indices, intervals): (Vec<_>, Vec<_>) = times
- .into_iter()
- .map(|span| {
- let start = match &span.time {
- TimeSpan::Window(time) => time.end,
- TimeSpan::Offset(time) => time.end,
- };
-
- (start as u64, span)
- })
- .unzip();
- acc.insert(actor, (indices, intervals));
-
- Ok(acc)
- } else {
- Err("reserved times have intersections".to_string())
- }
+ acc.insert(actor, PartitionedSpans { window_group, offset_group });
+ Ok(acc)
},
)?;
// NOTE: this function considers only latest time from reserved time
// reserved_time.time.start is ignored and should be handled by post processing
Ok(Arc::new(move |route: &Route, time_window: &TimeWindow| {
- reserved_times.get(&route.actor).and_then(|(indices, intervals)| {
- let offset = route.tour.start().map(|a| a.schedule.departure).unwrap_or(0.);
-
- // NOTE map external absolute time window to time span's start/end
- let (interval_start, interval_end) = match intervals.first().map(|rt| &rt.time) {
- Some(TimeSpan::Offset(_)) => (time_window.start - offset, time_window.end - offset),
- Some(TimeSpan::Window(_)) => (time_window.start, time_window.end),
- _ => unreachable!(),
- };
+ reserved_times.get(&route.actor).and_then(|partitioned| {
+ let offset = get_offset_anchor(route);
+
+ // Search window group with absolute time
+ let window_result = partitioned
+ .window_group
+ .as_ref()
+ .and_then(|(indices, intervals)| search_group(indices, intervals, time_window.start, time_window.end));
+
+ // Search offset group with offset-relative time
+ let offset_result = partitioned.offset_group.as_ref().and_then(|(indices, intervals)| {
+ let rel_start = time_window.start - offset;
+ let rel_end = time_window.end - offset;
+ search_group(indices, intervals, rel_start, rel_end)
+ });
- match indices.binary_search(&(interval_start as u64)) {
- Ok(idx) => intervals.get(idx),
- Err(idx) => (idx.max(1) - 1..=idx) // NOTE left (earliest) wins
- .map(|idx| intervals.get(idx))
- .find(|reserved_time| {
- reserved_time.is_some_and(|reserved_time| {
- let (reserved_start, reserved_end) = match &reserved_time.time {
- TimeSpan::Offset(to) => (to.end, to.end + reserved_time.duration),
- TimeSpan::Window(tw) => (tw.end, tw.end + reserved_time.duration),
- };
-
- // NOTE use exclusive intersection
- interval_start < reserved_end && reserved_start < interval_end
- })
- })
- .flatten(),
+ // Pick the earliest trigger (by reserved time start)
+ match (window_result, offset_result) {
+ (Some(w), Some(o)) => {
+ let w_start = match &w.time {
+ TimeSpan::Window(tw) => tw.end,
+ _ => unreachable!(),
+ };
+ let o_start = match &o.time {
+ TimeSpan::Offset(to) => to.end + offset,
+ _ => unreachable!(),
+ };
+ if w_start <= o_start { Some(w) } else { Some(o) }
+ }
+ (Some(w), None) => Some(w),
+ (None, Some(o)) => Some(o),
+ (None, None) => None,
}
.map(|reserved_time| reserved_time.to_reserved_time_window(offset))
})
}))
}
+
+fn build_span_group(spans: &mut Vec) -> Result, GenericError> {
+ if spans.is_empty() {
+ return Ok(None);
+ }
+
+ spans.sort_by(|ReservedTimeSpan { time: a, .. }, ReservedTimeSpan { time: b, .. }| {
+ let (a, b) = match (a, b) {
+ (TimeSpan::Window(a), TimeSpan::Window(b)) => (a.start, b.start),
+ (TimeSpan::Offset(a), TimeSpan::Offset(b)) => (a.start, b.start),
+ _ => unreachable!(),
+ };
+ a.total_cmp(&b)
+ });
+
+ let has_no_intersections = spans.windows(2).all(|pair| {
+ if let [ReservedTimeSpan { time: a, .. }, ReservedTimeSpan { time: b, .. }] = pair {
+ !a.intersects(0., &b.to_time_window(0.))
+ } else {
+ false
+ }
+ });
+
+ if !has_no_intersections {
+ return Err("reserved times have intersections".into());
+ }
+
+ let (indices, intervals): (Vec<_>, Vec<_>) = spans
+ .drain(..)
+ .map(|span| {
+ let end = match &span.time {
+ TimeSpan::Window(time) => time.end,
+ TimeSpan::Offset(time) => time.end,
+ };
+ (end as u64, span)
+ })
+ .unzip();
+
+ Ok(Some((indices, intervals)))
+}
+
+fn search_group<'a>(
+ indices: &[u64],
+ intervals: &'a [ReservedTimeSpan],
+ interval_start: f64,
+ interval_end: f64,
+) -> Option<&'a ReservedTimeSpan> {
+ let has_intersection = |reserved_time: &ReservedTimeSpan| {
+ let (reserved_start, reserved_end) = match &reserved_time.time {
+ TimeSpan::Offset(to) => (to.end, to.end + reserved_time.duration),
+ TimeSpan::Window(tw) => (tw.end, tw.end + reserved_time.duration),
+ };
+ // NOTE use exclusive intersection
+ interval_start < reserved_end && reserved_start < interval_end
+ };
+
+ match indices.binary_search(&(interval_start as u64)) {
+ Ok(idx) => intervals.get(idx).filter(|reserved_time| has_intersection(reserved_time)),
+ Err(idx) => (idx.max(1) - 1..=idx)
+ .filter_map(|idx| intervals.get(idx))
+ .find(|reserved_time| has_intersection(reserved_time)),
+ }
+}
diff --git a/vrp-core/src/construction/enablers/schedule_update.rs b/vrp-core/src/construction/enablers/schedule_update.rs
index a64770795..c88590d8c 100644
--- a/vrp-core/src/construction/enablers/schedule_update.rs
+++ b/vrp-core/src/construction/enablers/schedule_update.rs
@@ -1,9 +1,15 @@
+#[cfg(test)]
+#[path = "../../../tests/unit/construction/enablers/schedule_update_test.rs"]
+mod schedule_update_test;
+
use crate::construction::heuristics::{RouteContext, RouteState};
use crate::models::OP_START_MSG;
-use crate::models::common::{Distance, Duration, Schedule, Timestamp};
-use crate::models::problem::{ActivityCost, TransportCost, TravelTime};
+use crate::models::common::{Distance, Duration, Schedule, TimeSpan, Timestamp};
+use crate::models::problem::{ActivityCost, RouteCostSpan, RouteCostSpanDimension, TransportCost, TravelTime};
+use crate::models::solution::{Activity, Route};
use rosomaxa::prelude::Float;
use rosomaxa::utils::UnwrapValue;
+use std::ops::ControlFlow;
custom_activity_state!(pub(crate) LatestArrival typeof Timestamp);
custom_activity_state!(pub(crate) WaitingTime typeof Timestamp);
@@ -13,11 +19,72 @@ custom_tour_state!(pub(crate) LimitDuration typeof Duration);
/// Updates route schedule data.
pub fn update_route_schedule(route_ctx: &mut RouteContext, activity: &dyn ActivityCost, transport: &dyn TransportCost) {
+ let cost_span = route_ctx.route().actor.vehicle.dimens.get_route_cost_span().copied().unwrap_or_default();
+ let needs_fixed_point = matches!(cost_span, RouteCostSpan::FirstJobToDepot | RouteCostSpan::FirstJobToLastJob);
+
update_schedules(route_ctx, activity, transport);
+
+ if needs_fixed_point {
+ // For FirstJobTo* spans, the offset anchor depends on first_job.arrival which is
+ // computed during update_schedules. Re-run if the anchor changed significantly.
+ const EPSILON: f64 = 1e-6;
+ const MAX_ITERATIONS: usize = 3;
+
+ for _ in 0..MAX_ITERATIONS {
+ let anchor = get_offset_anchor(route_ctx.route());
+ update_schedules(route_ctx, activity, transport);
+ let new_anchor = get_offset_anchor(route_ctx.route());
+
+ if (new_anchor - anchor).abs() <= EPSILON {
+ break;
+ }
+ }
+ }
+
update_states(route_ctx, activity, transport);
update_statistics(route_ctx, transport);
}
+/// Returns the offset anchor timestamp based on the route's `RouteCostSpan`.
+/// For `DepotToDepot`/`DepotToLastJob`, this is the start departure time.
+/// For `FirstJobToDepot`/`FirstJobToLastJob`, this is the first job's arrival time (if available).
+pub fn get_offset_anchor(route: &Route) -> Timestamp {
+ let cost_span = route.actor.vehicle.dimens.get_route_cost_span().copied().unwrap_or_default();
+ let start_departure = route.tour.start().map(|a| a.schedule.departure).unwrap_or(0.);
+
+ match cost_span {
+ RouteCostSpan::DepotToDepot | RouteCostSpan::DepotToLastJob => start_departure,
+ RouteCostSpan::FirstJobToDepot | RouteCostSpan::FirstJobToLastJob => {
+ // First job is at index 1 (after start depot)
+ route.tour.get(1).filter(|a| a.job.is_some()).map(|a| a.schedule.arrival).unwrap_or(start_departure)
+ }
+ }
+}
+
+/// Checks whether the route schedule is feasible by simulating the forward pass of `update_schedules`.
+/// Returns `true` if no activity produces a `ControlFlow::Break` during departure estimation.
+pub fn is_schedule_feasible(route: &Route, activity: &dyn ActivityCost, transport: &dyn TransportCost) -> bool {
+ let start = route.tour.start().expect(OP_START_MSG);
+ let mut loc = start.place.location;
+ let mut dep = start.schedule.departure;
+
+ for activity_idx in 1..route.tour.total() {
+ let a = route.tour.get(activity_idx).unwrap();
+ let location = a.place.location;
+ let arrival = dep + transport.duration(route, loc, location, TravelTime::Departure(dep));
+
+ match activity.estimate_departure(route, a, arrival) {
+ ControlFlow::Break(_) => return false,
+ ControlFlow::Continue(d) => {
+ loc = location;
+ dep = d;
+ }
+ }
+ }
+
+ true
+}
+
/// Updates route departure to the new one.
pub fn update_route_departure(
route_ctx: &mut RouteContext,
@@ -25,12 +92,44 @@ pub fn update_route_departure(
transport: &dyn TransportCost,
new_departure_time: Timestamp,
) {
- let start = route_ctx.route_mut().tour.get_mut(0).unwrap();
- start.schedule.departure = new_departure_time;
+ let old_anchor = get_offset_anchor(route_ctx.route());
+
+ {
+ let start = route_ctx.route_mut().tour.get_mut(0).unwrap();
+ start.schedule.departure = new_departure_time;
+ }
+
+ let new_anchor = get_offset_anchor(route_ctx.route());
+ recompute_offset_time_windows(route_ctx, old_anchor, new_anchor);
update_route_schedule(route_ctx, activity, transport);
}
+/// Recomputes activity time windows derived from offset spans after anchor shift.
+fn recompute_offset_time_windows(route_ctx: &mut RouteContext, old_anchor: Timestamp, new_anchor: Timestamp) {
+ if old_anchor == new_anchor {
+ return;
+ }
+
+ route_ctx.route_mut().tour.all_activities_mut().for_each(|activity| {
+ let Some(job) = activity.job.as_ref() else { return };
+ let place_idx = activity.place.idx;
+
+ let Some(place_def) = job.places.get(place_idx) else { return };
+
+ // Only adjust activities whose selected time window came from an offset span.
+ let Some(span) = place_def
+ .times
+ .iter()
+ .find(|span| matches!(span, TimeSpan::Offset(_)) && span.to_time_window(old_anchor) == activity.place.time)
+ else {
+ return;
+ };
+
+ activity.place.time = span.to_time_window(new_anchor);
+ });
+}
+
fn update_schedules(route_ctx: &mut RouteContext, activity: &dyn ActivityCost, transport: &dyn TransportCost) {
let init = {
let start = route_ctx.route().tour.start().unwrap();
@@ -112,15 +211,135 @@ fn update_statistics(route_ctx: &mut RouteContext, transport: &dyn TransportCost
let start = route.tour.start().unwrap();
let end = route.tour.end().unwrap();
- let total_dur = end.schedule.departure - start.schedule.departure;
+ let total_activities = route.tour.total();
- let init = (start.place.location, start.schedule.departure, Distance::default());
- let (_, _, total_dist) = route.tour.all_activities().skip(1).fold(init, |(loc, dep, total_dist), a| {
- let total_dist = total_dist + transport.distance(route, loc, a.place.location, TravelTime::Departure(dep));
+ let cost_span = route.actor.vehicle.dimens.get_route_cost_span().copied().unwrap_or_default();
- (a.place.location, a.schedule.departure, total_dist)
- });
+ let total_dur = calculate_route_duration(route, cost_span, total_activities, start, end);
+ let total_dist = calculate_route_distance(route, transport, cost_span, total_activities);
state.set_total_distance(total_dist);
state.set_total_duration(total_dur);
}
+
+/// Returns the index of the last job activity in the route.
+/// For closed tours (with end depot): last job is at total - 2
+/// For open tours (no end depot): last job is at total - 1
+fn get_last_job_idx(route: &Route, total_activities: usize) -> Option {
+ if total_activities <= 1 {
+ return None;
+ }
+
+ // Check if the last activity is an end depot (job is None) or a job activity
+ let end = route.tour.end()?;
+ let has_end_depot = end.job.is_none();
+
+ if has_end_depot {
+ // Closed tour: [start, job1, ..., jobN, end] - last job at total - 2
+ if total_activities > 2 { Some(total_activities - 2) } else { None }
+ } else {
+ // Open tour: [start, job1, ..., jobN] - last job at total - 1
+ Some(total_activities - 1)
+ }
+}
+
+/// Returns the minimum number of activities required for the route to have jobs.
+/// For closed tours: 3 (start, at least one job, end)
+/// For open tours: 2 (start, at least one job)
+fn has_jobs(route: &Route, total_activities: usize) -> bool {
+ let end = route.tour.end();
+ let has_end_depot = end.is_some_and(|e| e.job.is_none());
+
+ if has_end_depot { total_activities > 2 } else { total_activities > 1 }
+}
+
+fn calculate_route_duration(
+ route: &Route,
+ cost_span: RouteCostSpan,
+ total_activities: usize,
+ start: &Activity,
+ end: &Activity,
+) -> Duration {
+ match cost_span {
+ RouteCostSpan::DepotToDepot => {
+ // For open tours, DepotToDepot is effectively DepotToLastJob
+ end.schedule.departure - start.schedule.departure
+ }
+ RouteCostSpan::DepotToLastJob => {
+ if let Some(last_job_idx) = get_last_job_idx(route, total_activities) {
+ let last_job = route.tour.get(last_job_idx).unwrap();
+ last_job.schedule.departure - start.schedule.departure
+ } else {
+ Duration::default()
+ }
+ }
+ RouteCostSpan::FirstJobToDepot => {
+ // For open tours, there's no depot to return to, so this behaves like FirstJobToLastJob
+ if has_jobs(route, total_activities) {
+ let first_job = route.tour.get(1).unwrap();
+ end.schedule.departure - first_job.schedule.arrival
+ } else {
+ Duration::default()
+ }
+ }
+ RouteCostSpan::FirstJobToLastJob => {
+ if let Some(last_job_idx) = get_last_job_idx(route, total_activities) {
+ let first_job = route.tour.get(1).unwrap();
+ let last_job = route.tour.get(last_job_idx).unwrap();
+ last_job.schedule.departure - first_job.schedule.arrival
+ } else {
+ Duration::default()
+ }
+ }
+ }
+}
+
+fn calculate_route_distance(
+ route: &Route,
+ transport: &dyn TransportCost,
+ cost_span: RouteCostSpan,
+ total_activities: usize,
+) -> Distance {
+ let last_job_idx = get_last_job_idx(route, total_activities);
+
+ let (start_idx, end_idx) = match cost_span {
+ RouteCostSpan::DepotToDepot => (0, total_activities),
+ RouteCostSpan::DepotToLastJob => {
+ // For open tours, last job IS the last activity
+ if let Some(last_idx) = last_job_idx {
+ (0, last_idx + 1)
+ } else {
+ return Distance::default();
+ }
+ }
+ RouteCostSpan::FirstJobToDepot => {
+ // For open tours, "depot" is the last activity (which is the last job)
+ if has_jobs(route, total_activities) {
+ (1, total_activities)
+ } else {
+ return Distance::default();
+ }
+ }
+ RouteCostSpan::FirstJobToLastJob => {
+ if let Some(last_idx) = last_job_idx {
+ (1, last_idx + 1)
+ } else {
+ return Distance::default();
+ }
+ }
+ };
+
+ let start_activity = route.tour.get(start_idx).unwrap();
+ let init = (start_activity.place.location, start_activity.schedule.departure, Distance::default());
+
+ route
+ .tour
+ .all_activities()
+ .skip(start_idx + 1)
+ .take(end_idx - start_idx - 1)
+ .fold(init, |(loc, dep, total_dist), a| {
+ let dist = total_dist + transport.distance(route, loc, a.place.location, TravelTime::Departure(dep));
+ (a.place.location, a.schedule.departure, dist)
+ })
+ .2
+}
diff --git a/vrp-core/src/construction/features/fleet_usage.rs b/vrp-core/src/construction/features/fleet_usage.rs
index 86f747fd1..575e31994 100644
--- a/vrp-core/src/construction/features/fleet_usage.rs
+++ b/vrp-core/src/construction/features/fleet_usage.rs
@@ -4,6 +4,9 @@
#[path = "../../../tests/unit/construction/features/fleet_usage_test.rs"]
mod fleet_usage_test;
+use std::collections::HashMap;
+use std::sync::Arc;
+
use super::*;
/// Creates a feature to minimize used fleet size (affects amount of tours in solution).
@@ -52,6 +55,73 @@ pub fn create_minimize_arrival_time_feature(name: &str) -> GenericResult GenericResult {
+ create_balance_shifts_feature_with_penalty(name, Arc::new(|variance| variance))
+}
+
+/// Creates a balance shifts feature with a custom penalty applied to the variance value.
+pub fn create_balance_shifts_feature_with_penalty(
+ name: &str,
+ penalty_fn: Arc Float + Send + Sync>,
+) -> GenericResult {
+ let penalty_fn_cloned = penalty_fn.clone();
+
+ FeatureBuilder::default()
+ .with_name(name)
+ .with_objective(FleetUsageObjective {
+ route_estimate_fn: Box::new(|_| 0.),
+ solution_estimate_fn: Box::new(move |solution_ctx| {
+ let variance = calculate_shift_variance(solution_ctx);
+ (penalty_fn_cloned)(variance)
+ }),
+ })
+ .build()
+}
+
+fn calculate_shift_variance(solution_ctx: &SolutionContext) -> Float {
+ if solution_ctx.routes.is_empty() {
+ return 0.;
+ }
+
+ let mut vehicle_shift_counts: HashMap = HashMap::new();
+ let mut total_available_shifts: HashMap = HashMap::new();
+
+ for route_ctx in solution_ctx.routes.iter() {
+ let actor = &route_ctx.route().actor;
+ if let Some(vehicle_id) = actor.vehicle.dimens.get_vehicle_id() {
+ *vehicle_shift_counts.entry(vehicle_id.clone()).or_insert(0) += 1;
+ total_available_shifts.entry(vehicle_id.clone()).or_insert(actor.vehicle.details.len());
+ }
+ }
+
+ if vehicle_shift_counts.is_empty() {
+ return 0.;
+ }
+
+ let ratios: Vec = vehicle_shift_counts
+ .iter()
+ .map(|(vehicle_id, &used_count)| {
+ let available = *total_available_shifts.get(vehicle_id).unwrap_or(&1) as Float;
+ used_count as Float / available
+ })
+ .collect();
+
+ let mean: Float = ratios.iter().sum::() / ratios.len() as Float;
+ let variance: Float = ratios
+ .iter()
+ .map(|&ratio| {
+ let diff = ratio - mean;
+ diff * diff
+ })
+ .sum::()
+ / ratios.len() as Float;
+
+ variance
+}
+
struct FleetUsageObjective {
route_estimate_fn: Box Cost + Send + Sync>,
solution_estimate_fn: Box Cost + Send + Sync>,
diff --git a/vrp-core/src/construction/features/job_time_limits.rs b/vrp-core/src/construction/features/job_time_limits.rs
new file mode 100644
index 000000000..827f142db
--- /dev/null
+++ b/vrp-core/src/construction/features/job_time_limits.rs
@@ -0,0 +1,129 @@
+//! A feature to enforce job time constraints on shifts.
+//!
+//! This allows configuring:
+//! - `earliest_first`: The earliest time a vehicle can arrive at its first job
+//! - `latest_last`: The latest time a vehicle can depart from its last job
+
+#[cfg(test)]
+#[path = "../../../tests/unit/construction/features/job_time_limits_test.rs"]
+mod job_time_limits_test;
+
+use super::*;
+use crate::models::problem::{Job, JobTimeConstraintsDimension, TransportCost, TravelTime};
+
+/// Creates a feature that enforces job time constraints on shifts.
+/// This is a hard constraint - jobs that violate the constraints remain unassigned.
+///
+/// # Arguments
+/// * `name` - Feature name
+/// * `transport` - Transport cost provider for calculating travel times
+/// * `activity` - Activity cost provider for estimating departures
+/// * `violation_code` - Code returned when constraint is violated
+pub fn create_job_time_limits_feature(
+ name: &str,
+ transport: Arc,
+ activity: Arc,
+ violation_code: ViolationCode,
+) -> Result {
+ FeatureBuilder::default()
+ .with_name(name)
+ .with_constraint(JobTimeLimitsConstraint { transport, activity, violation_code })
+ .build()
+}
+
+struct JobTimeLimitsConstraint {
+ transport: Arc,
+ activity: Arc,
+ violation_code: ViolationCode,
+}
+
+impl JobTimeLimitsConstraint {
+ fn evaluate_activity(
+ &self,
+ route_ctx: &RouteContext,
+ activity_ctx: &ActivityContext,
+ ) -> Option {
+ let actor = route_ctx.route().actor.as_ref();
+ let constraints = actor.vehicle.dimens.get_job_time_constraints().copied()?;
+
+ // Skip if no constraints are set
+ if constraints.earliest_first.is_none() && constraints.latest_last.is_none() {
+ return None;
+ }
+
+ let route = route_ctx.route();
+ let prev = activity_ctx.prev;
+ let target = activity_ctx.target;
+
+ // Skip if target is not a job (e.g., it's a depot or break)
+ target.job.as_ref()?;
+
+ let departure = prev.schedule.departure;
+ let arr_time_at_target = departure
+ + self.transport.duration(
+ route,
+ prev.place.location,
+ target.place.location,
+ TravelTime::Departure(departure),
+ );
+
+ // Check earliest_first constraint: applies when this is the first job
+ // (prev is the start depot, which has no job)
+ if let Some(earliest_first) = constraints.earliest_first {
+ let is_first_job = prev.job.is_none() && activity_ctx.index == 0;
+ if is_first_job && arr_time_at_target < earliest_first {
+ // Vehicle would arrive before earliest allowed time
+ // Check if we can wait - job's time window must extend past earliest_first
+ if target.place.time.end < earliest_first {
+ return ConstraintViolation::skip(self.violation_code);
+ }
+ // We can wait, but we need to ensure the adjusted arrival still works
+ // The actual arrival will be max(arr_time_at_target, earliest_first)
+ // which needs to be <= target.place.time.end (already checked above)
+ }
+ }
+
+ // Check latest_last constraint: applies when this becomes the last job
+ // (next is the end depot or None for open routes)
+ if let Some(latest_last) = constraints.latest_last {
+ let is_last_job = activity_ctx.next.is_none_or(|next| next.job.is_none());
+ if is_last_job {
+ // Calculate when we would depart from this job
+ let actual_arr_time = if let Some(earliest_first) = constraints.earliest_first {
+ let is_first_job = prev.job.is_none() && activity_ctx.index == 0;
+ if is_first_job { arr_time_at_target.max(earliest_first) } else { arr_time_at_target }
+ } else {
+ arr_time_at_target
+ };
+
+ // Respect the job's time window (might need to wait)
+ let service_start = actual_arr_time.max(target.place.time.start);
+ let departure_result = self.activity.estimate_departure(route, target, service_start);
+
+ // Extract departure time from ControlFlow (use the value regardless of Continue/Break)
+ let departure_from_target = match departure_result {
+ std::ops::ControlFlow::Continue(t) | std::ops::ControlFlow::Break(t) => t,
+ };
+
+ if departure_from_target > latest_last {
+ return ConstraintViolation::skip(self.violation_code);
+ }
+ }
+ }
+
+ None
+ }
+}
+
+impl FeatureConstraint for JobTimeLimitsConstraint {
+ fn evaluate(&self, move_ctx: &MoveContext<'_>) -> Option {
+ match move_ctx {
+ MoveContext::Route { .. } => None,
+ MoveContext::Activity { route_ctx, activity_ctx, .. } => self.evaluate_activity(route_ctx, activity_ctx),
+ }
+ }
+
+ fn merge(&self, source: Job, _: Job) -> Result {
+ Ok(source)
+ }
+}
diff --git a/vrp-core/src/construction/features/minimize_overdue.rs b/vrp-core/src/construction/features/minimize_overdue.rs
new file mode 100644
index 000000000..1089d26d0
--- /dev/null
+++ b/vrp-core/src/construction/features/minimize_overdue.rs
@@ -0,0 +1,137 @@
+//! Provides a feature to minimize total overdue days for scheduled jobs.
+
+use super::*;
+
+/// Seconds per day constant for converting timestamp difference to days.
+const SECONDS_PER_DAY: Float = 86400.0;
+
+/// A function type to extract due date from a job.
+pub type JobDueDateFn = Arc Option + Send + Sync>;
+
+/// A function type to extract scheduled date from route context.
+pub type ScheduledDateFn = Arc Float + Send + Sync>;
+
+/// A function type to calculate penalty for unassigned overdue jobs.
+/// Takes the job and returns the overdue penalty in days.
+pub type UnassignedOverduePenaltyFn = Arc Float + Send + Sync>;
+
+/// Provides a way to build a feature to minimize overdue.
+pub struct MinimizeOverdueBuilder {
+ name: String,
+ job_due_date_fn: Option,
+ scheduled_date_fn: Option,
+ unassigned_penalty_fn: Option,
+}
+
+impl MinimizeOverdueBuilder {
+ /// Creates a new instance of `MinimizeOverdueBuilder`.
+ pub fn new(name: &str) -> Self {
+ Self { name: name.to_string(), job_due_date_fn: None, scheduled_date_fn: None, unassigned_penalty_fn: None }
+ }
+
+ /// Sets the function to extract due date from a job.
+ pub fn set_job_due_date_fn(mut self, func: F) -> Self
+ where
+ F: Fn(&Job) -> Option + Send + Sync + 'static,
+ {
+ self.job_due_date_fn = Some(Arc::new(func));
+ self
+ }
+
+ /// Sets the function to extract scheduled date from route context.
+ pub fn set_scheduled_date_fn(mut self, func: F) -> Self
+ where
+ F: Fn(&RouteContext) -> Float + Send + Sync + 'static,
+ {
+ self.scheduled_date_fn = Some(Arc::new(func));
+ self
+ }
+
+ /// Sets the function to calculate penalty for unassigned overdue jobs.
+ /// This function should return the overdue penalty in days for jobs that are not scheduled.
+ /// If not set, unassigned jobs will not contribute to the overdue penalty.
+ pub fn set_unassigned_penalty_fn(mut self, func: F) -> Self
+ where
+ F: Fn(&Job) -> Float + Send + Sync + 'static,
+ {
+ self.unassigned_penalty_fn = Some(Arc::new(func));
+ self
+ }
+
+ /// Builds the feature.
+ pub fn build(mut self) -> GenericResult {
+ let job_due_date_fn =
+ self.job_due_date_fn.take().ok_or_else(|| GenericError::from("job_due_date_fn must be set"))?;
+
+ let scheduled_date_fn =
+ self.scheduled_date_fn.take().ok_or_else(|| GenericError::from("scheduled_date_fn must be set"))?;
+
+ let unassigned_penalty_fn = self.unassigned_penalty_fn.take();
+
+ FeatureBuilder::default()
+ .with_name(self.name.as_str())
+ .with_objective(MinimizeOverdueObjective { job_due_date_fn, scheduled_date_fn, unassigned_penalty_fn })
+ .build()
+ }
+}
+
+struct MinimizeOverdueObjective {
+ job_due_date_fn: JobDueDateFn,
+ scheduled_date_fn: ScheduledDateFn,
+ unassigned_penalty_fn: Option,
+}
+
+impl MinimizeOverdueObjective {
+ /// Calculates overdue in days for a single job.
+ fn calculate_overdue(&self, route_ctx: &RouteContext, job: &Job) -> Float {
+ let due_date = match (self.job_due_date_fn)(job) {
+ Some(date) => date,
+ None => return 0.0, // No due date means no overdue
+ };
+
+ let scheduled_date = (self.scheduled_date_fn)(route_ctx);
+
+ // Overdue = how many days past due date the job is scheduled
+ // If scheduled before due date, overdue is 0
+ let diff_seconds = scheduled_date - due_date;
+ if diff_seconds <= 0.0 { 0.0 } else { diff_seconds / SECONDS_PER_DAY }
+ }
+}
+
+impl FeatureObjective for MinimizeOverdueObjective {
+ fn fitness(&self, solution: &InsertionContext) -> Cost {
+ // Calculate overdue for scheduled jobs
+ let scheduled_overdue: Cost = solution
+ .solution
+ .routes
+ .iter()
+ .flat_map(|route_ctx| route_ctx.route().tour.jobs().map(|job| self.calculate_overdue(route_ctx, job)))
+ .sum();
+
+ // Calculate penalty for unassigned overdue jobs
+ let unassigned_overdue: Cost = match &self.unassigned_penalty_fn {
+ Some(penalty_fn) => solution.solution.unassigned.keys().map(|job| (penalty_fn)(job)).sum(),
+ None => 0.0,
+ };
+
+ scheduled_overdue + unassigned_overdue
+ }
+
+ fn estimate(&self, move_ctx: &MoveContext<'_>) -> Cost {
+ match move_ctx {
+ MoveContext::Route { route_ctx, job, .. } => {
+ let scheduled_overdue = self.calculate_overdue(route_ctx, job);
+
+ // If we have an unassigned penalty function, assigning this job
+ // removes its unassigned penalty (negative delta)
+ let unassigned_penalty_delta = match &self.unassigned_penalty_fn {
+ Some(penalty_fn) => -(penalty_fn)(job),
+ None => 0.0,
+ };
+
+ scheduled_overdue + unassigned_penalty_delta
+ }
+ MoveContext::Activity { .. } => Cost::default(),
+ }
+ }
+}
diff --git a/vrp-core/src/construction/features/mod.rs b/vrp-core/src/construction/features/mod.rs
index f54ca429f..f7e250bfd 100644
--- a/vrp-core/src/construction/features/mod.rs
+++ b/vrp-core/src/construction/features/mod.rs
@@ -35,6 +35,12 @@ pub use self::known_edge::create_known_edge_feature;
mod locked_jobs;
pub use self::locked_jobs::*;
+mod vehicle_distance;
+pub use self::vehicle_distance::*;
+
+mod minimize_overdue;
+pub use self::minimize_overdue::*;
+
mod minimize_unassigned;
pub use self::minimize_unassigned::*;
@@ -48,7 +54,9 @@ mod reloads;
pub use self::reloads::{ReloadFeatureFactory, ReloadIntervalsTourState, SharedResource, SharedResourceId};
mod skills;
-pub use self::skills::{JobSkills, JobSkillsDimension, VehicleSkillsDimension, create_skills_feature};
+pub use self::skills::{
+ JobSkills, JobSkillsDimension, VehicleSkillsDimension, create_skills_feature, is_job_skills_compatible,
+};
mod total_value;
pub use self::total_value::*;
@@ -59,12 +67,18 @@ pub use self::tour_compactness::*;
mod tour_limits;
pub use self::tour_limits::*;
+mod job_time_limits;
+pub use self::job_time_limits::*;
+
mod tour_order;
pub use self::tour_order::*;
mod transport;
pub use self::transport::*;
+mod vehicle_shifts;
+pub use self::vehicle_shifts::*;
+
mod work_balance;
pub use self::work_balance::{
create_activity_balanced_feature, create_distance_balanced_feature, create_duration_balanced_feature,
diff --git a/vrp-core/src/construction/features/skills.rs b/vrp-core/src/construction/features/skills.rs
index 4bfcddf06..3956afdda 100644
--- a/vrp-core/src/construction/features/skills.rs
+++ b/vrp-core/src/construction/features/skills.rs
@@ -86,6 +86,13 @@ impl FeatureConstraint for SkillsConstraint {
}
}
+/// Checks whether a job's skill requirements are compatible with a vehicle's skills.
+pub fn is_job_skills_compatible(job_skills: &JobSkills, vehicle_skills: &Option<&HashSet>) -> bool {
+ check_all_of(job_skills, vehicle_skills)
+ && check_one_of(job_skills, vehicle_skills)
+ && check_none_of(job_skills, vehicle_skills)
+}
+
fn check_all_of(job_skills: &JobSkills, vehicle_skills: &Option<&HashSet>) -> bool {
match (job_skills.all_of.as_ref(), vehicle_skills) {
(Some(job_skills), Some(vehicle_skills)) => job_skills.is_subset(vehicle_skills),
diff --git a/vrp-core/src/construction/features/tour_limits.rs b/vrp-core/src/construction/features/tour_limits.rs
index 732b87e79..4c3beebda 100644
--- a/vrp-core/src/construction/features/tour_limits.rs
+++ b/vrp-core/src/construction/features/tour_limits.rs
@@ -29,6 +29,18 @@ pub fn create_activity_limit_feature(
.build()
}
+/// Creates a minimum limit for activity amount in a tour.
+/// This is a soft constraint (objective) that penalizes solutions where routes have fewer activities than the minimum.
+/// Routes with zero activities (empty routes) are allowed.
+/// The penalty helps guide the solver toward solutions that meet the minimum, while still allowing
+/// exploration of solutions that don't meet the minimum during the search.
+pub fn create_min_activity_limit_feature(
+ name: &str,
+ min_limit_fn: ActivitySizeResolver,
+) -> Result {
+ FeatureBuilder::default().with_name(name).with_objective(MinActivityLimitObjective { min_limit_fn }).build()
+}
+
/// Creates a travel limits such as distance and/or duration.
/// This is a hard constraint.
pub fn create_travel_limit_feature(
@@ -86,6 +98,40 @@ impl FeatureConstraint for ActivityLimitConstraint {
}
}
+/// Objective that penalizes routes with fewer activities than the minimum limit.
+/// This guides the solver toward valid solutions while still allowing exploration.
+struct MinActivityLimitObjective {
+ min_limit_fn: ActivitySizeResolver,
+}
+
+impl FeatureObjective for MinActivityLimitObjective {
+ fn fitness(&self, solution: &InsertionContext) -> Cost {
+ // Calculate total penalty for all routes that violate the minimum
+ solution.solution.routes.iter().fold(0., |acc, route_ctx| {
+ let activity_count = route_ctx.route().tour.job_activity_count();
+ // Only penalize non-empty routes
+ if activity_count > 0
+ && let Some(min_limit) = (self.min_limit_fn)(route_ctx.route().actor.as_ref())
+ && activity_count < min_limit
+ {
+ // Penalty proportional to how far below the minimum we are
+ let deficit = (min_limit - activity_count) as Cost;
+ return acc + deficit;
+ }
+ acc
+ })
+ }
+
+ fn estimate(&self, move_ctx: &MoveContext<'_>) -> Cost {
+ // During insertion, we can't easily estimate the impact on min activity constraint
+ // since adding jobs generally helps meet the minimum
+ match move_ctx {
+ MoveContext::Route { .. } => Cost::default(),
+ MoveContext::Activity { .. } => Cost::default(),
+ }
+ }
+}
+
struct TravelLimitConstraint {
transport: Arc,
tour_distance_limit_fn: TravelLimitFn,
diff --git a/vrp-core/src/construction/features/vehicle_distance.rs b/vrp-core/src/construction/features/vehicle_distance.rs
new file mode 100644
index 000000000..4149269ec
--- /dev/null
+++ b/vrp-core/src/construction/features/vehicle_distance.rs
@@ -0,0 +1,257 @@
+//! Provides a feature to minimize vehicle distance penalties.
+//!
+//! For each job on a route, the penalty is the excess distance from the job to its
+//! assigned vehicle's start location compared to the nearest compatible vehicle's start.
+//! penalty = max(0, dist(job, assigned_vehicle) - dist(job, nearest_compatible_vehicle))
+
+#[cfg(test)]
+#[path = "../../../tests/unit/construction/features/vehicle_distance_test.rs"]
+mod vehicle_distance_test;
+
+use super::*;
+
+custom_solution_state!(VehicleDistancePenalty typeof Cost);
+custom_tour_state!(VehicleDistanceRouteData typeof RouteVehicleDistanceData);
+
+/// A function type that checks whether a given actor is compatible with a given job.
+pub type ActorJobCompatibilityFn = Arc bool + Send + Sync>;
+
+/// Route-level cached data for vehicle distance calculations.
+#[derive(Clone, Default)]
+pub struct RouteVehicleDistanceData {
+ /// Penalty contribution from this route.
+ pub penalty: Cost,
+}
+
+/// Provides a way to build a feature to minimize vehicle distance penalties.
+pub struct VehicleDistanceFeatureBuilder {
+ name: String,
+ transport: Option>,
+ actors: Option>>,
+ compatibility_fn: Option,
+}
+
+impl VehicleDistanceFeatureBuilder {
+ /// Creates a new instance of `VehicleDistanceFeatureBuilder`.
+ pub fn new(name: &str) -> Self {
+ Self { name: name.to_string(), transport: None, actors: None, compatibility_fn: None }
+ }
+
+ /// Sets the transport cost model.
+ pub fn set_transport(mut self, transport: Arc) -> Self {
+ self.transport = Some(transport);
+ self
+ }
+
+ /// Sets the fleet actors to consider when finding the nearest compatible vehicle.
+ pub fn set_actors(mut self, actors: Vec>) -> Self {
+ self.actors = Some(actors);
+ self
+ }
+
+ /// Sets the compatibility function that checks if an actor can serve a job.
+ pub fn set_compatibility_fn(mut self, func: F) -> Self
+ where
+ F: Fn(&Job, &Actor) -> bool + Send + Sync + 'static,
+ {
+ self.compatibility_fn = Some(Arc::new(func));
+ self
+ }
+
+ /// Builds the feature.
+ pub fn build(mut self) -> GenericResult {
+ let transport = self
+ .transport
+ .take()
+ .ok_or_else(|| GenericError::from("transport must be set for vehicle_distance feature"))?;
+
+ let actors =
+ self.actors.take().ok_or_else(|| GenericError::from("actors must be set for vehicle_distance feature"))?;
+
+ let compatibility_fn = self
+ .compatibility_fn
+ .take()
+ .ok_or_else(|| GenericError::from("compatibility_fn must be set for vehicle_distance feature"))?;
+
+ let objective = VehicleDistanceObjective {
+ transport: transport.clone(),
+ actors: actors.clone(),
+ compatibility_fn: compatibility_fn.clone(),
+ };
+ let state = VehicleDistanceState { transport, actors, compatibility_fn };
+
+ FeatureBuilder::default().with_name(self.name.as_str()).with_objective(objective).with_state(state).build()
+ }
+}
+
+/// Gets the primary location of a job.
+fn get_job_location(job: &Job) -> Option {
+ match job {
+ Job::Single(single) => single.places.first().and_then(|p| p.location),
+ Job::Multi(multi) => multi.jobs.first().and_then(|s| s.places.first().and_then(|p| p.location)),
+ }
+}
+
+/// Finds the minimum distance from a job location to the start of any compatible vehicle.
+fn find_nearest_compatible_vehicle_dist(
+ job_loc: Location,
+ job: &Job,
+ actors: &[Arc],
+ compatibility_fn: &ActorJobCompatibilityFn,
+ transport: &(dyn TransportCost + Send + Sync),
+) -> Option {
+ actors
+ .iter()
+ .filter(|actor| compatibility_fn(job, actor))
+ .filter_map(|actor| actor.detail.start.as_ref().map(|s| s.location))
+ .map(|start_loc| transport.distance_approx(&actors[0].vehicle.profile, job_loc, start_loc))
+ .min_by(|a, b| a.total_cmp(b))
+}
+
+struct VehicleDistanceObjective {
+ transport: Arc,
+ actors: Vec>,
+ compatibility_fn: ActorJobCompatibilityFn,
+}
+
+impl VehicleDistanceObjective {
+ /// Computes the penalty for a single route.
+ fn compute_route_penalty(&self, route_ctx: &RouteContext) -> Cost {
+ let route = route_ctx.route();
+ let profile = &route.actor.vehicle.profile;
+
+ let assigned_start = match route.actor.detail.start.as_ref() {
+ Some(start) => start.location,
+ None => return 0.0,
+ };
+
+ let mut total_penalty = 0.0;
+
+ for activity in route.tour.all_activities() {
+ let Some(single) = activity.job.as_ref() else { continue };
+ let job_loc = activity.place.location;
+ let job = Job::Single(single.clone());
+
+ let dist_assigned = self.transport.distance_approx(profile, job_loc, assigned_start);
+
+ let dist_nearest = find_nearest_compatible_vehicle_dist(
+ job_loc,
+ &job,
+ &self.actors,
+ &self.compatibility_fn,
+ self.transport.as_ref(),
+ )
+ .unwrap_or(dist_assigned);
+
+ let penalty = (dist_assigned - dist_nearest).max(0.0);
+ total_penalty += penalty;
+ }
+
+ total_penalty
+ }
+}
+
+impl FeatureObjective for VehicleDistanceObjective {
+ fn fitness(&self, solution: &InsertionContext) -> Cost {
+ solution.solution.state.get_vehicle_distance_penalty().copied().unwrap_or_else(|| {
+ solution.solution.routes.iter().map(|route_ctx| self.compute_route_penalty(route_ctx)).sum()
+ })
+ }
+
+ fn estimate(&self, move_ctx: &MoveContext<'_>) -> Cost {
+ match move_ctx {
+ MoveContext::Route { route_ctx, job, .. } => {
+ let Some(job_loc) = get_job_location(job) else {
+ return Cost::default();
+ };
+
+ let route = route_ctx.route();
+ let profile = &route.actor.vehicle.profile;
+
+ let Some(assigned_start) = route.actor.detail.start.as_ref().map(|s| s.location) else {
+ return Cost::default();
+ };
+
+ let dist_assigned = self.transport.distance_approx(profile, job_loc, assigned_start);
+
+ let dist_nearest = find_nearest_compatible_vehicle_dist(
+ job_loc,
+ job,
+ &self.actors,
+ &self.compatibility_fn,
+ self.transport.as_ref(),
+ )
+ .unwrap_or(dist_assigned);
+
+ (dist_assigned - dist_nearest).max(0.0)
+ }
+ MoveContext::Activity { .. } => Cost::default(),
+ }
+ }
+}
+
+struct VehicleDistanceState {
+ transport: Arc,
+ actors: Vec>,
+ compatibility_fn: ActorJobCompatibilityFn,
+}
+
+impl VehicleDistanceState {
+ /// Computes the penalty for a single route.
+ fn compute_route_penalty(&self, route_ctx: &RouteContext) -> Cost {
+ let route = route_ctx.route();
+ let profile = &route.actor.vehicle.profile;
+
+ let assigned_start = match route.actor.detail.start.as_ref() {
+ Some(start) => start.location,
+ None => return 0.0,
+ };
+
+ let mut total_penalty = 0.0;
+
+ for activity in route.tour.all_activities() {
+ let Some(single) = activity.job.as_ref() else { continue };
+ let job_loc = activity.place.location;
+ let job = Job::Single(single.clone());
+
+ let dist_assigned = self.transport.distance_approx(profile, job_loc, assigned_start);
+
+ let dist_nearest = find_nearest_compatible_vehicle_dist(
+ job_loc,
+ &job,
+ &self.actors,
+ &self.compatibility_fn,
+ self.transport.as_ref(),
+ )
+ .unwrap_or(dist_assigned);
+
+ let penalty = (dist_assigned - dist_nearest).max(0.0);
+ total_penalty += penalty;
+ }
+
+ total_penalty
+ }
+}
+
+impl FeatureState for VehicleDistanceState {
+ fn accept_insertion(&self, _: &mut SolutionContext, _: usize, _: &Job) {
+ // Route will be marked stale, recomputed in accept_solution_state
+ }
+
+ fn accept_route_state(&self, route_ctx: &mut RouteContext) {
+ let penalty = self.compute_route_penalty(route_ctx);
+ route_ctx.state_mut().set_vehicle_distance_route_data(RouteVehicleDistanceData { penalty });
+ }
+
+ fn accept_solution_state(&self, solution_ctx: &mut SolutionContext) {
+ solution_ctx.routes.iter_mut().filter(|rc| rc.is_stale()).for_each(|rc| self.accept_route_state(rc));
+
+ let total: Cost = solution_ctx
+ .routes
+ .iter()
+ .map(|rc| rc.state().get_vehicle_distance_route_data().map(|data| data.penalty).unwrap_or(0.0))
+ .sum();
+
+ solution_ctx.state.set_vehicle_distance_penalty(total);
+ }
+}
diff --git a/vrp-core/src/construction/features/vehicle_shifts.rs b/vrp-core/src/construction/features/vehicle_shifts.rs
new file mode 100644
index 000000000..ae5322093
--- /dev/null
+++ b/vrp-core/src/construction/features/vehicle_shifts.rs
@@ -0,0 +1,141 @@
+//! Provides a feature to enforce minimum shift usage per vehicle.
+
+#[cfg(test)]
+#[path = "../../../tests/unit/construction/features/vehicle_shifts_test.rs"]
+mod vehicle_shifts_test;
+
+use super::*;
+use std::collections::{HashMap, HashSet};
+
+custom_solution_state!(pub VehicleShiftSummary typeof VehicleShiftInfo);
+
+/// Provides a way to build a feature which enforces minimum shift usage per vehicle.
+pub struct MinVehicleShiftsFeatureBuilder {
+ name: String,
+ violation_code: ViolationCode,
+ requirements: Option>,
+}
+
+/// Represents minimum shift requirements per vehicle id.
+#[derive(Clone)]
+pub struct MinShiftRequirement {
+ /// Minimum number of shifts that must be used.
+ pub minimum: usize,
+ /// When true, usage of zero shifts is allowed without violating the minimum requirement.
+ pub allow_zero_usage: bool,
+}
+
+impl MinVehicleShiftsFeatureBuilder {
+ /// Creates a new builder instance.
+ pub fn new(name: &str) -> Self {
+ Self { name: name.to_string(), violation_code: ViolationCode::default(), requirements: None }
+ }
+
+ /// Sets a violation code which is used when constraint forbids an insertion.
+ pub fn with_violation_code(mut self, violation_code: ViolationCode) -> Self {
+ self.violation_code = violation_code;
+ self
+ }
+
+ /// Sets a map with required shifts per vehicle id.
+ pub fn with_requirements(mut self, requirements: HashMap) -> Self {
+ self.requirements = Some(requirements);
+ self
+ }
+
+ /// Builds a feature instance.
+ pub fn build(self) -> GenericResult {
+ let requirements = self.requirements.ok_or_else(|| "requirements map is not defined".to_string())?;
+
+ FeatureBuilder::default()
+ .with_name(self.name.as_str())
+ .with_constraint(MinVehicleShiftsConstraint { violation_code: self.violation_code })
+ .with_state(MinVehicleShiftsState { requirements })
+ .build()
+ }
+}
+
+struct MinVehicleShiftsConstraint {
+ violation_code: ViolationCode,
+}
+
+impl FeatureConstraint for MinVehicleShiftsConstraint {
+ fn evaluate(&self, move_ctx: &MoveContext<'_>) -> Option {
+ match move_ctx {
+ MoveContext::Route { solution_ctx, route_ctx, .. } => {
+ let summary = solution_ctx.state.get_vehicle_shift_summary()?;
+
+ if summary.missing_vehicle_ids.is_empty() {
+ return None;
+ }
+
+ route_ctx.route().actor.vehicle.dimens.get_vehicle_id().and_then(|vehicle_id| {
+ if summary.missing_vehicle_ids.contains(vehicle_id) {
+ None
+ } else {
+ ConstraintViolation::skip(self.violation_code)
+ }
+ })
+ }
+ MoveContext::Activity { .. } => None,
+ }
+ }
+
+ fn merge(&self, source: Job, _: Job) -> Result {
+ Ok(source)
+ }
+}
+
+struct MinVehicleShiftsState {
+ requirements: HashMap,
+}
+
+impl FeatureState for MinVehicleShiftsState {
+ fn accept_insertion(&self, solution_ctx: &mut SolutionContext, route_index: usize, _: &Job) {
+ self.accept_route_state(solution_ctx.routes.get_mut(route_index).unwrap());
+ self.accept_solution_state(solution_ctx);
+ }
+
+ fn accept_route_state(&self, _: &mut RouteContext) {}
+
+ fn accept_solution_state(&self, solution_ctx: &mut SolutionContext) {
+ let summary = build_vehicle_shift_summary(solution_ctx.routes.as_slice(), &self.requirements);
+
+ solution_ctx.state.set_vehicle_shift_summary(summary);
+ }
+}
+
+fn build_vehicle_shift_summary(
+ routes: &[RouteContext],
+ requirements: &HashMap,
+) -> VehicleShiftInfo {
+ let usage = routes.iter().fold(HashMap::new(), |mut used, route_ctx| {
+ if let Some(vehicle_id) = route_ctx.route().actor.vehicle.dimens.get_vehicle_id().cloned()
+ && requirements.contains_key(&vehicle_id)
+ && route_ctx.route().tour.has_jobs()
+ {
+ *used.entry(vehicle_id).or_insert(0) += 1;
+ }
+
+ used
+ });
+
+ let missing_vehicle_ids = requirements
+ .iter()
+ .filter_map(|(vehicle_id, requirement)| {
+ let used = usage.get(vehicle_id).copied().unwrap_or(0);
+ let below_minimum = used < requirement.minimum;
+ let zero_allowed = requirement.allow_zero_usage && used == 0;
+ if below_minimum && !zero_allowed { Some(vehicle_id.clone()) } else { None }
+ })
+ .collect();
+
+ VehicleShiftInfo { missing_vehicle_ids }
+}
+
+/// Provides aggregated vehicle shift usage information.
+#[derive(Clone, Default)]
+pub struct VehicleShiftInfo {
+ /// Vehicle ids that still require additional shifts.
+ pub missing_vehicle_ids: HashSet,
+}
diff --git a/vrp-core/src/models/problem/fleet.rs b/vrp-core/src/models/problem/fleet.rs
index 6b6c8d828..4b32b34c7 100644
--- a/vrp-core/src/models/problem/fleet.rs
+++ b/vrp-core/src/models/problem/fleet.rs
@@ -12,6 +12,34 @@ use std::sync::Arc;
custom_dimension!(pub VehicleId typeof String);
+/// Specifies which portion of a route to consider when calculating costs.
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
+pub enum RouteCostSpan {
+ /// Full round trip: depot to depot (default for backward compatibility).
+ #[default]
+ DepotToDepot,
+ /// Outbound only: depot to last job (no return leg).
+ DepotToLastJob,
+ /// Return only: first job to depot (no outbound leg).
+ FirstJobToDepot,
+ /// Jobs only: first job to last job (no depot legs).
+ FirstJobToLastJob,
+}
+
+custom_dimension!(pub RouteCostSpan typeof RouteCostSpan);
+
+/// Time constraints for jobs within a shift.
+/// Controls when the first job can start and when the last job must finish.
+#[derive(Clone, Copy, Debug, Default)]
+pub struct JobTimeConstraints {
+ /// Earliest allowed arrival time at the first job.
+ pub earliest_first: Option,
+ /// Latest allowed departure time from the last job.
+ pub latest_last: Option,
+}
+
+custom_dimension!(pub JobTimeConstraints typeof JobTimeConstraints);
+
/// Represents operating costs for driver and vehicle.
#[derive(Clone, Debug)]
pub struct Costs {
diff --git a/vrp-core/tests/unit/construction/enablers/departure_time_test.rs b/vrp-core/tests/unit/construction/enablers/departure_time_test.rs
index 88fb235b5..a8c16136d 100644
--- a/vrp-core/tests/unit/construction/enablers/departure_time_test.rs
+++ b/vrp-core/tests/unit/construction/enablers/departure_time_test.rs
@@ -2,8 +2,11 @@ use super::*;
use crate::helpers::models::problem::*;
use crate::helpers::models::solution::*;
use crate::models::common::*;
+use crate::models::problem::Place as JobPlace;
use crate::models::problem::*;
+use crate::models::solution::{Activity, Place as ActivityPlace};
use rosomaxa::prelude::Float;
+use std::sync::Arc;
parameterized_test! {can_advance_departure_time, (latest, optimize_whole_tour, tws, expected), {
let tws = tws.into_iter().map(|(start, end)| TimeWindow::new(start, end)).collect::>();
@@ -113,3 +116,43 @@ fn can_recede_departure_time_impl(
assert_eq!(departure_time, expected);
}
+
+#[test]
+fn recomputes_offset_time_windows_on_departure_shift() {
+ let offset = TimeOffset::new(10., 12.);
+ let old_departure = 0.;
+ let new_departure = 5.;
+
+ let job = {
+ let mut dimens = Dimensions::default();
+ dimens.set_job_id("break".to_string());
+
+ Arc::new(Single {
+ places: vec![JobPlace { location: Some(1), duration: 0., times: vec![TimeSpan::Offset(offset.clone())] }],
+ dimens,
+ })
+ };
+
+ let mut route_ctx = RouteContextBuilder::default()
+ .with_route(
+ RouteBuilder::default()
+ .with_vehicle(&test_fleet(), "v1")
+ .add_activity({
+ let mut activity = Activity::new_with_job(job.clone());
+ activity.place = ActivityPlace {
+ idx: 0,
+ location: job.places[0].location.unwrap(),
+ duration: job.places[0].duration,
+ time: TimeSpan::Offset(offset.clone()).to_time_window(old_departure),
+ };
+ activity
+ })
+ .build(),
+ )
+ .build();
+
+ update_route_departure(&mut route_ctx, &TestActivityCost::default(), &TestTransportCost::default(), new_departure);
+
+ let activity = route_ctx.route().tour.get(1).unwrap();
+ assert_eq!(activity.place.time, TimeSpan::Offset(offset).to_time_window(new_departure));
+}
diff --git a/vrp-core/tests/unit/construction/enablers/reserved_time_test.rs b/vrp-core/tests/unit/construction/enablers/reserved_time_test.rs
index f844293d2..daa23a3d2 100644
--- a/vrp-core/tests/unit/construction/enablers/reserved_time_test.rs
+++ b/vrp-core/tests/unit/construction/enablers/reserved_time_test.rs
@@ -37,7 +37,8 @@ parameterized_test! {can_search_for_reserved_time, (times, tests), {
can_search_for_reserved_time! {
case01: (vec![((5., 5.), 5.), ((20., 20.), 10.)],
- vec![((6., 6.), Some(0)), ((2., 6.), Some(0)), ((10., 11.), None), ((2., 4.), None),
+ vec![((6., 6.), Some(0)), ((5., 5.), None), ((2., 6.), Some(0)), ((10., 11.), None), ((2., 4.), None),
+ ((20., 20.), None),
((10., 21.), Some(1)), ((25., 27.), Some(1)), ((29., 31.), Some(1)),
((0., 3.), None), ((31., 33.), None)]),
case02: (vec![((0.,0.), 10.), ((5., 5.), 10.)], vec![]),
diff --git a/vrp-core/tests/unit/construction/enablers/schedule_update_test.rs b/vrp-core/tests/unit/construction/enablers/schedule_update_test.rs
new file mode 100644
index 000000000..8ea037ac5
--- /dev/null
+++ b/vrp-core/tests/unit/construction/enablers/schedule_update_test.rs
@@ -0,0 +1,424 @@
+use super::*;
+use crate::construction::enablers::{
+ DynamicActivityCost, DynamicTransportCost, ReservedTimeSpan, TotalDistanceTourState, TotalDurationTourState,
+};
+use crate::helpers::models::problem::*;
+use crate::helpers::models::solution::*;
+use crate::models::common::{Location, Schedule, TimeInterval, TimeSpan, TimeWindow, Timestamp};
+use crate::models::problem::{RouteCostSpan, RouteCostSpanDimension, VehicleDetail, VehiclePlace};
+use std::sync::Arc;
+
+fn create_detail(start_loc: Location, end_loc: Location) -> VehicleDetail {
+ VehicleDetail {
+ start: Some(VehiclePlace { location: start_loc, time: TimeInterval { earliest: Some(0.), latest: None } }),
+ end: Some(VehiclePlace { location: end_loc, time: TimeInterval { earliest: None, latest: Some(1000.) } }),
+ }
+}
+
+fn create_open_detail(start_loc: Location) -> VehicleDetail {
+ VehicleDetail {
+ start: Some(VehiclePlace { location: start_loc, time: TimeInterval { earliest: Some(0.), latest: None } }),
+ end: None, // Open VRP - no end depot
+ }
+}
+
+fn create_activity_with_location_and_schedule(
+ location: Location,
+ arrival: Timestamp,
+ departure: Timestamp,
+) -> Activity {
+ let mut activity = ActivityBuilder::with_location(location).build();
+ activity.schedule = Schedule::new(arrival, departure);
+ activity
+}
+
+/// Creates a route with:
+/// - Depot at location 0 (start and end)
+/// - Job 1 at location 10
+/// - Job 2 at location 30
+/// - Job 3 at location 60
+///
+/// With TestTransportCost (distance = |to - from|):
+/// - Depot(0) -> Job1(10): distance = 10
+/// - Job1(10) -> Job2(30): distance = 20
+/// - Job2(30) -> Job3(60): distance = 30
+/// - Job3(60) -> Depot(0): distance = 60
+///
+/// Total distances by span:
+/// - DepotToDepot: 10 + 20 + 30 + 60 = 120
+/// - DepotToLastJob: 10 + 20 + 30 = 60
+/// - FirstJobToDepot: 20 + 30 + 60 = 110
+/// - FirstJobToLastJob: 20 + 30 = 50
+fn create_test_route_with_cost_span(cost_span: Option) -> (RouteContext, TestTransportCost) {
+ let mut vehicle = TestVehicleBuilder::default().id("v1").details(vec![create_detail(0, 0)]).build();
+
+ if let Some(span) = cost_span {
+ vehicle.dimens.set_route_cost_span(span);
+ }
+
+ let fleet = FleetBuilder::default().add_driver(test_driver()).add_vehicle(vehicle).build();
+
+ // Build route with start at 0, jobs at 10, 30, 60, end at 0
+ // Schedules are set to reflect travel times (using location as arrival time for simplicity)
+ let route = RouteBuilder::default()
+ .with_vehicle(&fleet, "v1")
+ .with_start({
+ let mut start = ActivityBuilder::default().build();
+ start.place.location = 0;
+ start.schedule = Schedule::new(0., 0.);
+ start.job = None;
+ start
+ })
+ .with_end({
+ let mut end = ActivityBuilder::default().build();
+ end.place.location = 0;
+ end.schedule = Schedule::new(130., 130.); // arrival after traveling back from 60
+ end.job = None;
+ end
+ })
+ .add_activities(vec![
+ // Job 1 at location 10: arrive at 10 (0 + 10), depart at 10
+ create_activity_with_location_and_schedule(10, 10., 10.),
+ // Job 2 at location 30: arrive at 30 (10 + 20), depart at 30
+ create_activity_with_location_and_schedule(30, 30., 30.),
+ // Job 3 at location 60: arrive at 60 (30 + 30), depart at 60
+ create_activity_with_location_and_schedule(60, 60., 60.),
+ ])
+ .build();
+
+ let route_ctx = RouteContextBuilder::default().with_route(route).build();
+
+ (route_ctx, TestTransportCost::default())
+}
+
+#[test]
+fn can_calculate_statistics_with_depot_to_depot_span() {
+ let (mut route_ctx, transport) = create_test_route_with_cost_span(Some(RouteCostSpan::DepotToDepot));
+
+ update_statistics(&mut route_ctx, &transport);
+
+ let total_distance = route_ctx.state().get_total_distance().copied().unwrap_or(0.);
+ let total_duration = route_ctx.state().get_total_duration().copied().unwrap_or(0.);
+
+ // Distance: 0->10 + 10->30 + 30->60 + 60->0 = 10 + 20 + 30 + 60 = 120
+ assert_eq!(total_distance, 120., "DepotToDepot distance should be 120");
+ // Duration: end.departure(130) - start.departure(0) = 130
+ assert_eq!(total_duration, 130., "DepotToDepot duration should be 130");
+}
+
+#[test]
+fn can_calculate_statistics_with_depot_to_last_job_span() {
+ let (mut route_ctx, transport) = create_test_route_with_cost_span(Some(RouteCostSpan::DepotToLastJob));
+
+ update_statistics(&mut route_ctx, &transport);
+
+ let total_distance = route_ctx.state().get_total_distance().copied().unwrap_or(0.);
+ let total_duration = route_ctx.state().get_total_duration().copied().unwrap_or(0.);
+
+ // Distance: 0->10 + 10->30 + 30->60 = 10 + 20 + 30 = 60 (no return to depot)
+ assert_eq!(total_distance, 60., "DepotToLastJob distance should be 60");
+ // Duration: last_job.departure(60) - start.departure(0) = 60
+ assert_eq!(total_duration, 60., "DepotToLastJob duration should be 60");
+}
+
+#[test]
+fn can_calculate_statistics_with_first_job_to_depot_span() {
+ let (mut route_ctx, transport) = create_test_route_with_cost_span(Some(RouteCostSpan::FirstJobToDepot));
+
+ update_statistics(&mut route_ctx, &transport);
+
+ let total_distance = route_ctx.state().get_total_distance().copied().unwrap_or(0.);
+ let total_duration = route_ctx.state().get_total_duration().copied().unwrap_or(0.);
+
+ // Distance: 10->30 + 30->60 + 60->0 = 20 + 30 + 60 = 110 (no outbound from depot)
+ assert_eq!(total_distance, 110., "FirstJobToDepot distance should be 110");
+ // Duration: end.departure(130) - first_job.arrival(10) = 120
+ assert_eq!(total_duration, 120., "FirstJobToDepot duration should be 120");
+}
+
+#[test]
+fn can_calculate_statistics_with_first_job_to_last_job_span() {
+ let (mut route_ctx, transport) = create_test_route_with_cost_span(Some(RouteCostSpan::FirstJobToLastJob));
+
+ update_statistics(&mut route_ctx, &transport);
+
+ let total_distance = route_ctx.state().get_total_distance().copied().unwrap_or(0.);
+ let total_duration = route_ctx.state().get_total_duration().copied().unwrap_or(0.);
+
+ // Distance: 10->30 + 30->60 = 20 + 30 = 50 (no depot legs)
+ assert_eq!(total_distance, 50., "FirstJobToLastJob distance should be 50");
+ // Duration: last_job.departure(60) - first_job.arrival(10) = 50
+ assert_eq!(total_duration, 50., "FirstJobToLastJob duration should be 50");
+}
+
+#[test]
+fn can_calculate_statistics_with_default_span_when_not_set() {
+ // When no span is set, should default to DepotToDepot
+ let (mut route_ctx, transport) = create_test_route_with_cost_span(None);
+
+ update_statistics(&mut route_ctx, &transport);
+
+ let total_distance = route_ctx.state().get_total_distance().copied().unwrap_or(0.);
+ let total_duration = route_ctx.state().get_total_duration().copied().unwrap_or(0.);
+
+ // Should match DepotToDepot behavior
+ assert_eq!(total_distance, 120., "Default span distance should match DepotToDepot");
+ assert_eq!(total_duration, 130., "Default span duration should match DepotToDepot");
+}
+
+#[test]
+fn can_handle_single_job_route_with_all_spans() {
+ // Create a route with only one job
+ let test_cases = vec![
+ (Some(RouteCostSpan::DepotToDepot), 20., 40.), // 0->10 + 10->0 = 20, duration 40-0=40
+ (Some(RouteCostSpan::DepotToLastJob), 10., 20.), // 0->10 = 10, duration 20-0=20
+ (Some(RouteCostSpan::FirstJobToDepot), 10., 20.), // 10->0 = 10, duration 40-20=20
+ (Some(RouteCostSpan::FirstJobToLastJob), 0., 0.), // No distance between first and last (same job)
+ ];
+
+ for (span, expected_distance, expected_duration) in test_cases {
+ let mut vehicle = TestVehicleBuilder::default().id("v1").details(vec![create_detail(0, 0)]).build();
+
+ if let Some(s) = span {
+ vehicle.dimens.set_route_cost_span(s);
+ }
+
+ let fleet = FleetBuilder::default().add_driver(test_driver()).add_vehicle(vehicle).build();
+
+ let route = RouteBuilder::default()
+ .with_vehicle(&fleet, "v1")
+ .with_start({
+ let mut start = ActivityBuilder::default().build();
+ start.place.location = 0;
+ start.schedule = Schedule::new(0., 0.);
+ start.job = None;
+ start
+ })
+ .with_end({
+ let mut end = ActivityBuilder::default().build();
+ end.place.location = 0;
+ end.schedule = Schedule::new(40., 40.);
+ end.job = None;
+ end
+ })
+ .add_activities(vec![
+ // Single job at location 10: arrive at 20 (with service), depart at 20
+ create_activity_with_location_and_schedule(10, 20., 20.),
+ ])
+ .build();
+
+ let mut route_ctx = RouteContextBuilder::default().with_route(route).build();
+ let transport = TestTransportCost::default();
+
+ update_statistics(&mut route_ctx, &transport);
+
+ let total_distance = route_ctx.state().get_total_distance().copied().unwrap_or(0.);
+ let total_duration = route_ctx.state().get_total_duration().copied().unwrap_or(0.);
+
+ assert_eq!(
+ total_distance, expected_distance,
+ "Single job route with {:?} should have distance {}",
+ span, expected_distance
+ );
+ assert_eq!(
+ total_duration, expected_duration,
+ "Single job route with {:?} should have duration {}",
+ span, expected_duration
+ );
+ }
+}
+
+/// Creates an open VRP route (no end depot) with:
+/// - Depot at location 0 (start only)
+/// - Job 1 at location 10
+/// - Job 2 at location 30
+/// - Job 3 at location 60
+///
+/// With TestTransportCost (distance = |to - from|):
+/// - Depot(0) -> Job1(10): distance = 10
+/// - Job1(10) -> Job2(30): distance = 20
+/// - Job2(30) -> Job3(60): distance = 30
+///
+/// Total distance for all spans (no return to depot):
+/// - DepotToDepot: 10 + 20 + 30 = 60 (same as DepotToLastJob since no return)
+/// - DepotToLastJob: 10 + 20 + 30 = 60
+/// - FirstJobToDepot: 20 + 30 = 50 (same as FirstJobToLastJob since no return)
+/// - FirstJobToLastJob: 20 + 30 = 50
+fn create_open_vrp_route_with_cost_span(cost_span: Option) -> (RouteContext, TestTransportCost) {
+ let mut vehicle = TestVehicleBuilder::default().id("v1").details(vec![create_open_detail(0)]).build();
+
+ if let Some(span) = cost_span {
+ vehicle.dimens.set_route_cost_span(span);
+ }
+
+ let fleet = FleetBuilder::default().add_driver(test_driver()).add_vehicle(vehicle).build();
+
+ // Build route with start at 0, jobs at 10, 30, 60, NO end depot
+ let route = RouteBuilder::default()
+ .with_vehicle(&fleet, "v1")
+ .with_start({
+ let mut start = ActivityBuilder::default().build();
+ start.place.location = 0;
+ start.schedule = Schedule::new(0., 0.);
+ start.job = None;
+ start
+ })
+ // No end depot - open VRP
+ .add_activities(vec![
+ create_activity_with_location_and_schedule(10, 10., 10.),
+ create_activity_with_location_and_schedule(30, 30., 30.),
+ create_activity_with_location_and_schedule(60, 60., 60.),
+ ])
+ .build();
+
+ let route_ctx = RouteContextBuilder::default().with_route(route).build();
+
+ (route_ctx, TestTransportCost::default())
+}
+
+#[test]
+fn can_calculate_statistics_for_open_vrp_with_depot_to_depot_span() {
+ let (mut route_ctx, transport) = create_open_vrp_route_with_cost_span(Some(RouteCostSpan::DepotToDepot));
+
+ update_statistics(&mut route_ctx, &transport);
+
+ let total_distance = route_ctx.state().get_total_distance().copied().unwrap_or(0.);
+ let total_duration = route_ctx.state().get_total_duration().copied().unwrap_or(0.);
+
+ // Open VRP: no return to depot, so distance is depot to last job
+ // Distance: 0->10 + 10->30 + 30->60 = 10 + 20 + 30 = 60
+ assert_eq!(total_distance, 60., "Open VRP DepotToDepot distance should be 60");
+ // Duration: last_job.departure(60) - start.departure(0) = 60
+ assert_eq!(total_duration, 60., "Open VRP DepotToDepot duration should be 60");
+}
+
+#[test]
+fn can_calculate_statistics_for_open_vrp_with_depot_to_last_job_span() {
+ let (mut route_ctx, transport) = create_open_vrp_route_with_cost_span(Some(RouteCostSpan::DepotToLastJob));
+
+ update_statistics(&mut route_ctx, &transport);
+
+ let total_distance = route_ctx.state().get_total_distance().copied().unwrap_or(0.);
+ let total_duration = route_ctx.state().get_total_duration().copied().unwrap_or(0.);
+
+ // Distance: 0->10 + 10->30 + 30->60 = 10 + 20 + 30 = 60
+ assert_eq!(total_distance, 60., "Open VRP DepotToLastJob distance should be 60");
+ // Duration: last_job.departure(60) - start.departure(0) = 60
+ assert_eq!(total_duration, 60., "Open VRP DepotToLastJob duration should be 60");
+}
+
+#[test]
+fn can_calculate_statistics_for_open_vrp_with_first_job_to_depot_span() {
+ let (mut route_ctx, transport) = create_open_vrp_route_with_cost_span(Some(RouteCostSpan::FirstJobToDepot));
+
+ update_statistics(&mut route_ctx, &transport);
+
+ let total_distance = route_ctx.state().get_total_distance().copied().unwrap_or(0.);
+ let total_duration = route_ctx.state().get_total_duration().copied().unwrap_or(0.);
+
+ // Open VRP: no return depot, so this is first job to last job
+ // Distance: 10->30 + 30->60 = 20 + 30 = 50
+ assert_eq!(total_distance, 50., "Open VRP FirstJobToDepot distance should be 50");
+ // Duration: last_job.departure(60) - first_job.arrival(10) = 50
+ assert_eq!(total_duration, 50., "Open VRP FirstJobToDepot duration should be 50");
+}
+
+#[test]
+fn can_calculate_statistics_for_open_vrp_with_first_job_to_last_job_span() {
+ let (mut route_ctx, transport) = create_open_vrp_route_with_cost_span(Some(RouteCostSpan::FirstJobToLastJob));
+
+ update_statistics(&mut route_ctx, &transport);
+
+ let total_distance = route_ctx.state().get_total_distance().copied().unwrap_or(0.);
+ let total_duration = route_ctx.state().get_total_duration().copied().unwrap_or(0.);
+
+ // Distance: 10->30 + 30->60 = 20 + 30 = 50
+ assert_eq!(total_distance, 50., "Open VRP FirstJobToLastJob distance should be 50");
+ // Duration: last_job.departure(60) - first_job.arrival(10) = 50
+ assert_eq!(total_duration, 50., "Open VRP FirstJobToLastJob duration should be 50");
+}
+
+fn create_feasibility_detail(
+ start_loc: Location,
+ end_loc: Location,
+ time_start: Timestamp,
+ time_end: Timestamp,
+) -> VehicleDetail {
+ VehicleDetail {
+ start: Some(VehiclePlace {
+ location: start_loc,
+ time: TimeInterval { earliest: Some(time_start), latest: None },
+ }),
+ end: Some(VehiclePlace { location: end_loc, time: TimeInterval { earliest: None, latest: Some(time_end) } }),
+ }
+}
+
+fn create_feasibility_route(
+ reserved_time: ReservedTimeSpan,
+ activities: Vec<(Location, (Timestamp, Timestamp), f64)>,
+) -> (Arc, Arc, RouteContext) {
+ let detail = create_feasibility_detail(0, 0, 0., 100.);
+ let vehicle = TestVehicleBuilder::default().id("v1").details(vec![detail]).build();
+ let fleet = FleetBuilder::default().add_driver(test_driver()).add_vehicle(vehicle).build();
+ let actor = fleet.actors.first().unwrap().clone();
+
+ let reserved_times_idx =
+ vec![(actor.clone(), vec![reserved_time])].into_iter().collect::>();
+
+ let activity_cost: Arc =
+ Arc::new(DynamicActivityCost::new(reserved_times_idx.clone()).unwrap());
+ let transport: Arc =
+ Arc::new(DynamicTransportCost::new(reserved_times_idx, Arc::new(TestTransportCost::default())).unwrap());
+
+ let acts = activities.into_iter().map(|(loc, (start, end), dur)| {
+ ActivityBuilder::with_location_tw_and_duration(loc, TimeWindow::new(start, end), dur).build()
+ });
+
+ let mut route_ctx = RouteContextBuilder::default()
+ .with_route(RouteBuilder::default().with_vehicle(&fleet, "v1").add_activities(acts).build())
+ .build();
+
+ update_route_schedule(&mut route_ctx, activity_cost.as_ref(), transport.as_ref());
+
+ (activity_cost, transport, route_ctx)
+}
+
+#[test]
+fn is_schedule_feasible_returns_true_for_feasible_route_with_reserved_time() {
+ // Reserved time at t=25, duration=5. Activity at loc=10, tw=(0,100), dur=10.
+ // Activity arrives at 10, departs at 20. Reserved time at 25 doesn't cause Break.
+ let reserved_time = ReservedTimeSpan { time: TimeSpan::Window(TimeWindow::new(25., 25.)), duration: 5. };
+ let (activity_cost, transport, route_ctx) = create_feasibility_route(reserved_time, vec![(10, (0., 100.), 10.)]);
+
+ assert!(is_schedule_feasible(route_ctx.route(), activity_cost.as_ref(), transport.as_ref()));
+}
+
+#[test]
+fn is_schedule_feasible_returns_false_when_break_exceeds_activity_tw() {
+ // Reserved time at t=9, duration=12 means break runs from 9 to 21.
+ // Activity at loc=10, tw=(0,20), dur=10. With plain transport, arrival=10.
+ // estimate_departure: activity_start=10, departure=20, schedule=(10,20)
+ // reserved time intersects, extra_duration=12, 10+12=22 > 20 → Break
+ //
+ // We use DynamicActivityCost (has reserved times) but plain TestTransportCost
+ // (no reserved time in transport) so that arrival is 10, not shifted.
+ let detail = create_feasibility_detail(0, 0, 0., 100.);
+ let vehicle = TestVehicleBuilder::default().id("v1").details(vec![detail]).build();
+ let fleet = FleetBuilder::default().add_driver(test_driver()).add_vehicle(vehicle).build();
+ let actor = fleet.actors.first().unwrap().clone();
+
+ let reserved_time = ReservedTimeSpan { time: TimeSpan::Window(TimeWindow::new(9., 9.)), duration: 12. };
+ let reserved_times_idx =
+ vec![(actor.clone(), vec![reserved_time])].into_iter().collect::>();
+
+ let activity_cost: Arc =
+ Arc::new(DynamicActivityCost::new(reserved_times_idx).unwrap());
+ let transport = TestTransportCost::default();
+
+ let acts = vec![ActivityBuilder::with_location_tw_and_duration(10, TimeWindow::new(0., 20.), 10.).build()];
+ let route_ctx = RouteContextBuilder::default()
+ .with_route(RouteBuilder::default().with_vehicle(&fleet, "v1").add_activities(acts).build())
+ .build();
+
+ assert!(!is_schedule_feasible(route_ctx.route(), activity_cost.as_ref(), &transport));
+}
diff --git a/vrp-core/tests/unit/construction/features/fleet_usage_test.rs b/vrp-core/tests/unit/construction/features/fleet_usage_test.rs
index e38bf6e54..c3b8d4771 100644
--- a/vrp-core/tests/unit/construction/features/fleet_usage_test.rs
+++ b/vrp-core/tests/unit/construction/features/fleet_usage_test.rs
@@ -1,7 +1,14 @@
use super::*;
+use crate::construction::heuristics::RouteContext;
use crate::helpers::construction::heuristics::TestInsertionContextBuilder;
+use crate::helpers::models::problem::{
+ FleetBuilder, TestVehicleBuilder, test_driver, test_vehicle_detail, test_vehicle_with_id,
+};
use crate::helpers::models::solution::*;
+use crate::models::GoalContextBuilder;
+use crate::models::problem::Actor;
use std::cmp::Ordering;
+use std::sync::Arc;
fn create_test_insertion_ctx(routes: &[Float]) -> InsertionContext {
let mut insertion_ctx = TestInsertionContextBuilder::default().build();
@@ -42,3 +49,83 @@ fn can_properly_estimate_solutions_impl(left: &[Float], right: &[Float], expecte
assert_eq!(left.total_cmp(&right), expected);
}
+
+#[test]
+fn can_apply_shift_penalty_function() {
+ let mut fleet_builder = FleetBuilder::default();
+ fleet_builder.add_driver(test_driver());
+ fleet_builder.add_vehicle(test_vehicle_with_id("v1"));
+ fleet_builder.add_vehicle(test_vehicle_with_id("v2"));
+ let fleet = Arc::new(fleet_builder.build());
+
+ let build_route = |vehicle_id: &str| {
+ RouteContextBuilder::default()
+ .with_route(RouteBuilder::default().with_vehicle(fleet.as_ref(), vehicle_id).build())
+ .build()
+ };
+
+ let mut insertion_ctx = TestInsertionContextBuilder::default();
+ insertion_ctx.with_fleet(fleet.clone());
+ insertion_ctx.with_routes(vec![build_route("v1"), build_route("v1"), build_route("v2")]);
+ let insertion_ctx = insertion_ctx.build();
+
+ let penalty = Arc::new(|variance: Float| variance * 2.);
+ let feature = create_balance_shifts_feature_with_penalty("balance_shifts", penalty).unwrap();
+ let objective = feature.objective.unwrap();
+
+ let variance = super::calculate_shift_variance(&insertion_ctx.solution);
+ let fitness = objective.fitness(&insertion_ctx);
+
+ assert!((fitness - variance * 2.).abs() < 1e-9);
+}
+
+#[test]
+fn balance_shifts_objective_prefers_even_distribution() {
+ let mut vehicle_one = test_vehicle_with_id("v1");
+ vehicle_one.details = vec![test_vehicle_detail(), test_vehicle_detail()];
+
+ let mut vehicle_two = TestVehicleBuilder::default().id("v2").build();
+ vehicle_two.details =
+ vec![test_vehicle_detail(), test_vehicle_detail(), test_vehicle_detail(), test_vehicle_detail()];
+
+ let mut fleet_builder = FleetBuilder::default();
+ fleet_builder.add_driver(test_driver());
+ fleet_builder.add_vehicle(vehicle_one);
+ fleet_builder.add_vehicle(vehicle_two);
+ let fleet = Arc::new(fleet_builder.build());
+
+ let mut actors_by_vehicle: HashMap>> = HashMap::new();
+ fleet.actors.iter().cloned().for_each(|actor| {
+ let vehicle_id = actor.vehicle.dimens.get_vehicle_id().unwrap().clone();
+ actors_by_vehicle.entry(vehicle_id).or_default().push(actor);
+ });
+
+ let make_route = |actor: Arc| RouteContext::new(actor);
+
+ let balanced_routes = vec![
+ make_route(actors_by_vehicle.get("v1").unwrap()[0].clone()),
+ make_route(actors_by_vehicle.get("v2").unwrap()[0].clone()),
+ make_route(actors_by_vehicle.get("v2").unwrap()[1].clone()),
+ ];
+
+ let unbalanced_routes = vec![
+ make_route(actors_by_vehicle.get("v1").unwrap()[0].clone()),
+ make_route(actors_by_vehicle.get("v1").unwrap()[1].clone()),
+ make_route(actors_by_vehicle.get("v2").unwrap()[0].clone()),
+ ];
+
+ let mut balanced_ctx_builder = TestInsertionContextBuilder::default();
+ balanced_ctx_builder.with_fleet(fleet.clone());
+ balanced_ctx_builder.with_routes(balanced_routes);
+ let balanced_ctx = balanced_ctx_builder.build();
+
+ let mut unbalanced_ctx_builder = TestInsertionContextBuilder::default();
+ unbalanced_ctx_builder.with_fleet(fleet);
+ unbalanced_ctx_builder.with_routes(unbalanced_routes);
+ let unbalanced_ctx = unbalanced_ctx_builder.build();
+
+ let feature = create_balance_shifts_feature("balance").unwrap();
+ let goal = GoalContextBuilder::with_features(&[feature]).and_then(|builder| builder.build()).unwrap();
+
+ assert_eq!(goal.total_order(&balanced_ctx, &unbalanced_ctx), Ordering::Less);
+}
diff --git a/vrp-core/tests/unit/construction/features/job_time_limits_test.rs b/vrp-core/tests/unit/construction/features/job_time_limits_test.rs
new file mode 100644
index 000000000..7dd674668
--- /dev/null
+++ b/vrp-core/tests/unit/construction/features/job_time_limits_test.rs
@@ -0,0 +1,336 @@
+use crate::construction::features::*;
+use crate::helpers::construction::heuristics::TestInsertionContextBuilder;
+use crate::helpers::models::problem::*;
+use crate::helpers::models::solution::*;
+use crate::models::common::{Schedule, TimeWindow};
+use crate::models::problem::{JobTimeConstraints, JobTimeConstraintsDimension};
+use crate::models::solution::{Activity, Place};
+
+const VIOLATION_CODE: ViolationCode = ViolationCode(1);
+
+fn create_feature() -> Feature {
+ create_job_time_limits_feature(
+ "job_time_limits",
+ TestTransportCost::new_shared(),
+ TestActivityCost::new_shared(),
+ VIOLATION_CODE,
+ )
+ .unwrap()
+}
+
+fn create_fleet_with_job_time_constraints(id: &str, earliest_first: Option, latest_last: Option) -> Fleet {
+ let mut builder = TestVehicleBuilder::default();
+ builder.id(id);
+ builder.dimens_mut().set_job_time_constraints(JobTimeConstraints { earliest_first, latest_last });
+
+ FleetBuilder::default().add_driver(test_driver()).add_vehicle(builder.build()).build()
+}
+
+/// Creates a depot-like activity (no job) for testing
+fn create_depot_activity(location: usize, departure: f64) -> Activity {
+ Activity {
+ place: Place { idx: 0, location, duration: 0.0, time: TimeWindow::new(0.0, 1000.0) },
+ schedule: Schedule::new(departure, departure),
+ job: None,
+ commute: None,
+ }
+}
+
+mod earliest_first_constraint {
+ use super::*;
+
+ #[test]
+ fn allows_job_when_arrival_is_after_earliest_first() {
+ // Vehicle can depart at 0, earliest_first is 5
+ // Job at location 10 means arrival at 10 (distance = time)
+ // 10 > 5, so should be allowed
+ let fleet = create_fleet_with_job_time_constraints("v1", Some(5.0), None);
+ let solution_ctx = TestInsertionContextBuilder::default().build().solution;
+ let route_ctx = RouteContextBuilder::default()
+ .with_route(RouteBuilder::default().with_vehicle(&fleet, "v1").build())
+ .build();
+ let feature = create_feature();
+
+ let result = feature.constraint.unwrap().evaluate(&MoveContext::activity(
+ &solution_ctx,
+ &route_ctx,
+ &ActivityContext {
+ index: 0,
+ prev: &create_depot_activity(0, 0.0), // Start depot
+ target: &ActivityBuilder::with_location_and_tw(10, TimeWindow::new(0.0, 100.0)).build(),
+ next: Some(&create_depot_activity(0, 20.0)), // End depot
+ },
+ ));
+
+ assert_eq!(result, None);
+ }
+
+ #[test]
+ fn allows_job_when_can_wait_until_earliest_first() {
+ // Vehicle departs at 0, earliest_first is 15
+ // Job at location 10 means arrival at 10
+ // 10 < 15, but job time window extends to 100, so can wait
+ let fleet = create_fleet_with_job_time_constraints("v1", Some(15.0), None);
+ let solution_ctx = TestInsertionContextBuilder::default().build().solution;
+ let route_ctx = RouteContextBuilder::default()
+ .with_route(RouteBuilder::default().with_vehicle(&fleet, "v1").build())
+ .build();
+ let feature = create_feature();
+
+ let result = feature.constraint.unwrap().evaluate(&MoveContext::activity(
+ &solution_ctx,
+ &route_ctx,
+ &ActivityContext {
+ index: 0,
+ prev: &create_depot_activity(0, 0.0), // Start depot
+ target: &ActivityBuilder::with_location_and_tw(10, TimeWindow::new(0.0, 100.0)).build(),
+ next: Some(&create_depot_activity(0, 30.0)), // End depot
+ },
+ ));
+
+ assert_eq!(result, None);
+ }
+
+ #[test]
+ fn rejects_job_when_time_window_ends_before_earliest_first() {
+ // Vehicle departs at 0, earliest_first is 15
+ // Job at location 10 means arrival at 10
+ // Job time window ends at 12, which is before earliest_first (15)
+ // Cannot wait until earliest_first
+ let fleet = create_fleet_with_job_time_constraints("v1", Some(15.0), None);
+ let solution_ctx = TestInsertionContextBuilder::default().build().solution;
+ let route_ctx = RouteContextBuilder::default()
+ .with_route(RouteBuilder::default().with_vehicle(&fleet, "v1").build())
+ .build();
+ let feature = create_feature();
+
+ let result = feature.constraint.unwrap().evaluate(&MoveContext::activity(
+ &solution_ctx,
+ &route_ctx,
+ &ActivityContext {
+ index: 0,
+ prev: &create_depot_activity(0, 0.0), // Start depot
+ target: &ActivityBuilder::with_location_and_tw(10, TimeWindow::new(0.0, 12.0)).build(),
+ next: Some(&create_depot_activity(0, 30.0)), // End depot
+ },
+ ));
+
+ assert_eq!(result, ConstraintViolation::skip(VIOLATION_CODE));
+ }
+}
+
+mod latest_last_constraint {
+ use super::*;
+
+ #[test]
+ fn allows_job_when_departure_is_before_latest_last() {
+ // Job at location 10, service time is default (0 from TestActivityCost)
+ // Arrival at 10, departure at 10
+ // latest_last is 20, so 10 <= 20, should be allowed
+ let fleet = create_fleet_with_job_time_constraints("v1", None, Some(20.0));
+ let solution_ctx = TestInsertionContextBuilder::default().build().solution;
+ let route_ctx = RouteContextBuilder::default()
+ .with_route(RouteBuilder::default().with_vehicle(&fleet, "v1").build())
+ .build();
+ let feature = create_feature();
+
+ let result = feature.constraint.unwrap().evaluate(&MoveContext::activity(
+ &solution_ctx,
+ &route_ctx,
+ &ActivityContext {
+ index: 0,
+ prev: &create_depot_activity(0, 0.0), // Start depot
+ target: &ActivityBuilder::with_location_and_tw(10, TimeWindow::new(0.0, 100.0)).build(),
+ next: None, // Inserting at the end (will be last job)
+ },
+ ));
+
+ assert_eq!(result, None);
+ }
+
+ #[test]
+ fn rejects_job_when_departure_exceeds_latest_last() {
+ // Job at location 50, arrival at 50, departure at 50 (no service time)
+ // latest_last is 20, so departure 50 > 20, should be rejected
+ let fleet = create_fleet_with_job_time_constraints("v1", None, Some(20.0));
+ let solution_ctx = TestInsertionContextBuilder::default().build().solution;
+ let route_ctx = RouteContextBuilder::default()
+ .with_route(RouteBuilder::default().with_vehicle(&fleet, "v1").build())
+ .build();
+ let feature = create_feature();
+
+ let result = feature.constraint.unwrap().evaluate(&MoveContext::activity(
+ &solution_ctx,
+ &route_ctx,
+ &ActivityContext {
+ index: 0,
+ prev: &create_depot_activity(0, 0.0), // Start depot
+ target: &ActivityBuilder::with_location_and_tw(50, TimeWindow::new(0.0, 100.0)).build(),
+ next: None, // Inserting at the end (will be last job)
+ },
+ ));
+
+ assert_eq!(result, ConstraintViolation::skip(VIOLATION_CODE));
+ }
+
+ #[test]
+ fn rejects_job_when_duration_causes_departure_after_latest_last() {
+ // Job at location 10, arrival at 10 (which is BEFORE latest_last of 15)
+ // Service duration is 10, so departure = 10 + 10 = 20
+ // latest_last is 15, so departure 20 > 15, should be rejected
+ let fleet = create_fleet_with_job_time_constraints("v1", None, Some(15.0));
+ let solution_ctx = TestInsertionContextBuilder::default().build().solution;
+ let route_ctx = RouteContextBuilder::default()
+ .with_route(RouteBuilder::default().with_vehicle(&fleet, "v1").build())
+ .build();
+ let feature = create_feature();
+
+ let result = feature.constraint.unwrap().evaluate(&MoveContext::activity(
+ &solution_ctx,
+ &route_ctx,
+ &ActivityContext {
+ index: 0,
+ prev: &create_depot_activity(0, 0.0), // Start depot
+ // Arrival at 10, duration 10, departure at 20
+ target: &ActivityBuilder::with_location_tw_and_duration(10, TimeWindow::new(0.0, 100.0), 10.0).build(),
+ next: None, // Inserting at the end (will be last job)
+ },
+ ));
+
+ assert_eq!(result, ConstraintViolation::skip(VIOLATION_CODE));
+ }
+
+ #[test]
+ fn does_not_apply_when_inserting_before_another_job() {
+ // When inserting before another job, latest_last doesn't apply to the inserted job
+ // because it won't be the last job
+ let fleet = create_fleet_with_job_time_constraints("v1", None, Some(20.0));
+ let solution_ctx = TestInsertionContextBuilder::default().build().solution;
+ let route_ctx = RouteContextBuilder::default()
+ .with_route(RouteBuilder::default().with_vehicle(&fleet, "v1").build())
+ .build();
+ let feature = create_feature();
+
+ let result = feature.constraint.unwrap().evaluate(&MoveContext::activity(
+ &solution_ctx,
+ &route_ctx,
+ &ActivityContext {
+ index: 0,
+ prev: &create_depot_activity(0, 0.0), // Start depot
+ target: &ActivityBuilder::with_location_and_tw(50, TimeWindow::new(0.0, 100.0)).build(),
+ // Next is another job activity (has job), so latest_last doesn't apply to target
+ next: Some(&ActivityBuilder::with_location_and_tw(60, TimeWindow::new(0.0, 100.0)).build()),
+ },
+ ));
+
+ // Should pass because this is not the last job (next is a job, not a depot)
+ assert_eq!(result, None);
+ }
+}
+
+mod combined_constraints {
+ use super::*;
+
+ #[test]
+ fn applies_both_constraints_for_single_job_route() {
+ // When there's only one job, it's both first and last
+ // earliest_first = 5, latest_last = 20
+ // Job at location 10, arrival = 10 (>5), departure = 10 (<20)
+ let fleet = create_fleet_with_job_time_constraints("v1", Some(5.0), Some(20.0));
+ let solution_ctx = TestInsertionContextBuilder::default().build().solution;
+ let route_ctx = RouteContextBuilder::default()
+ .with_route(RouteBuilder::default().with_vehicle(&fleet, "v1").build())
+ .build();
+ let feature = create_feature();
+
+ let result = feature.constraint.unwrap().evaluate(&MoveContext::activity(
+ &solution_ctx,
+ &route_ctx,
+ &ActivityContext {
+ index: 0,
+ prev: &create_depot_activity(0, 0.0), // Start depot
+ target: &ActivityBuilder::with_location_and_tw(10, TimeWindow::new(0.0, 100.0)).build(),
+ next: None, // Last job (also first job since only one)
+ },
+ ));
+
+ assert_eq!(result, None);
+ }
+
+ #[test]
+ fn rejects_when_earliest_first_violated_even_with_valid_latest_last() {
+ // earliest_first = 15, but job time window ends at 12
+ // Arrival at 10, cannot wait until 15 because TW ends at 12
+ let fleet = create_fleet_with_job_time_constraints("v1", Some(15.0), Some(100.0));
+ let solution_ctx = TestInsertionContextBuilder::default().build().solution;
+ let route_ctx = RouteContextBuilder::default()
+ .with_route(RouteBuilder::default().with_vehicle(&fleet, "v1").build())
+ .build();
+ let feature = create_feature();
+
+ let result = feature.constraint.unwrap().evaluate(&MoveContext::activity(
+ &solution_ctx,
+ &route_ctx,
+ &ActivityContext {
+ index: 0,
+ prev: &create_depot_activity(0, 0.0), // Start depot
+ target: &ActivityBuilder::with_location_and_tw(10, TimeWindow::new(0.0, 12.0)).build(),
+ next: None,
+ },
+ ));
+
+ assert_eq!(result, ConstraintViolation::skip(VIOLATION_CODE));
+ }
+}
+
+mod no_constraints {
+ use super::*;
+
+ #[test]
+ fn allows_any_job_when_no_constraints_set() {
+ // Vehicle has job time constraints dimension but both are None
+ let fleet = create_fleet_with_job_time_constraints("v1", None, None);
+ let solution_ctx = TestInsertionContextBuilder::default().build().solution;
+ let route_ctx = RouteContextBuilder::default()
+ .with_route(RouteBuilder::default().with_vehicle(&fleet, "v1").build())
+ .build();
+ let feature = create_feature();
+
+ let result = feature.constraint.unwrap().evaluate(&MoveContext::activity(
+ &solution_ctx,
+ &route_ctx,
+ &ActivityContext {
+ index: 0,
+ prev: &create_depot_activity(0, 0.0), // Start depot
+ target: &ActivityBuilder::with_location_and_tw(100, TimeWindow::new(0.0, 5.0)).build(),
+ next: None,
+ },
+ ));
+
+ assert_eq!(result, None);
+ }
+
+ #[test]
+ fn allows_any_job_when_vehicle_has_no_dimension() {
+ // Use standard fleet without job time constraints dimension
+ let fleet = test_fleet();
+ let solution_ctx = TestInsertionContextBuilder::default().build().solution;
+ let route_ctx = RouteContextBuilder::default()
+ .with_route(RouteBuilder::default().with_vehicle(&fleet, "v1").build())
+ .build();
+ let feature = create_feature();
+
+ let result = feature.constraint.unwrap().evaluate(&MoveContext::activity(
+ &solution_ctx,
+ &route_ctx,
+ &ActivityContext {
+ index: 0,
+ prev: &create_depot_activity(0, 0.0), // Start depot
+ target: &ActivityBuilder::with_location_and_tw(100, TimeWindow::new(0.0, 5.0)).build(),
+ next: None,
+ },
+ ));
+
+ assert_eq!(result, None);
+ }
+}
diff --git a/vrp-core/tests/unit/construction/features/tour_limits_test.rs b/vrp-core/tests/unit/construction/features/tour_limits_test.rs
index 52d549e0c..720ddc7f1 100644
--- a/vrp-core/tests/unit/construction/features/tour_limits_test.rs
+++ b/vrp-core/tests/unit/construction/features/tour_limits_test.rs
@@ -56,6 +56,100 @@ mod activity {
}
}
+mod min_activity {
+ use super::*;
+ use crate::helpers::construction::heuristics::TestInsertionContextBuilder;
+
+ #[test]
+ fn can_create_min_activity_limit_feature() {
+ let feature = create_min_activity_limit_feature("min_activity_limit", Arc::new(|_| Some(3)));
+ assert!(feature.is_ok());
+ let feature = feature.unwrap();
+ // Now only has objective, no constraint
+ assert!(feature.objective.is_some());
+ }
+
+ #[test]
+ fn min_activity_objective_calculates_penalty_correctly() {
+ // Route with 1 activity when minimum is 3 should have penalty of 2
+ let insertion_ctx = TestInsertionContextBuilder::default()
+ .with_routes(vec![
+ RouteContextBuilder::default()
+ .with_route(
+ RouteBuilder::default()
+ .with_vehicle(&test_fleet(), "v1")
+ .add_activities((0..1).map(|idx| ActivityBuilder::with_location(idx).build()))
+ .build(),
+ )
+ .build(),
+ ])
+ .build();
+
+ let objective = create_min_activity_limit_feature(
+ "min_activity_limit",
+ Arc::new(|_| Some(3)), // minimum 3, route has 1
+ )
+ .unwrap()
+ .objective
+ .unwrap();
+
+ let fitness = objective.fitness(&insertion_ctx);
+
+ // Penalty should be (3 - 1) = 2
+ assert!((fitness - 2.0).abs() < f64::EPSILON);
+ }
+
+ #[test]
+ fn min_activity_objective_returns_zero_when_satisfied() {
+ // Route with 3 activities when minimum is 3 should have zero penalty
+ let insertion_ctx = TestInsertionContextBuilder::default()
+ .with_routes(vec![
+ RouteContextBuilder::default()
+ .with_route(
+ RouteBuilder::default()
+ .with_vehicle(&test_fleet(), "v1")
+ .add_activities((0..3).map(|idx| ActivityBuilder::with_location(idx).build()))
+ .build(),
+ )
+ .build(),
+ ])
+ .build();
+
+ let objective = create_min_activity_limit_feature(
+ "min_activity_limit",
+ Arc::new(|_| Some(3)), // minimum 3, route has 3
+ )
+ .unwrap()
+ .objective
+ .unwrap();
+
+ let fitness = objective.fitness(&insertion_ctx);
+
+ // No penalty when constraint is satisfied
+ assert!((fitness - 0.0).abs() < f64::EPSILON);
+ }
+
+ #[test]
+ fn min_activity_objective_ignores_empty_routes() {
+ // Empty routes should not be penalized
+ let insertion_ctx = TestInsertionContextBuilder::default()
+ .with_routes(vec![
+ RouteContextBuilder::default()
+ .with_route(RouteBuilder::default().with_vehicle(&test_fleet(), "v1").build())
+ .build(),
+ ])
+ .build();
+
+ let objective =
+ create_min_activity_limit_feature("min_activity_limit", Arc::new(|_| Some(3))).unwrap().objective.unwrap();
+
+ let fitness = objective.fitness(&insertion_ctx);
+
+ // No penalty for empty routes
+ assert!((fitness - 0.0).abs() < f64::EPSILON);
+ }
+}
+
mod traveling {
use super::*;
use crate::construction::enablers::{TotalDistanceTourState, TotalDurationTourState};
diff --git a/vrp-core/tests/unit/construction/features/vehicle_distance_test.rs b/vrp-core/tests/unit/construction/features/vehicle_distance_test.rs
new file mode 100644
index 000000000..89fcb9df2
--- /dev/null
+++ b/vrp-core/tests/unit/construction/features/vehicle_distance_test.rs
@@ -0,0 +1,323 @@
+use crate::construction::features::VehicleDistanceFeatureBuilder;
+use crate::construction::heuristics::MoveContext;
+use crate::helpers::construction::heuristics::TestInsertionContextBuilder;
+use crate::helpers::models::problem::{TestSingleBuilder, TestTransportCost, TestVehicleBuilder, test_driver};
+use crate::helpers::models::solution::{ActivityBuilder, RouteBuilder, RouteContextBuilder};
+use crate::models::common::{TimeInterval, TimeWindow};
+use crate::models::problem::{Actor, ActorDetail, Job, VehiclePlace};
+use std::sync::Arc;
+
+fn create_actor_at(location: usize) -> Arc {
+ let vehicle = TestVehicleBuilder::default()
+ .id(&format!("v_{location}"))
+ .details(vec![crate::models::problem::VehicleDetail {
+ start: Some(VehiclePlace { location, time: TimeInterval { earliest: Some(0.0), latest: None } }),
+ end: Some(VehiclePlace { location, time: TimeInterval { earliest: None, latest: Some(1000.0) } }),
+ }])
+ .build();
+
+ Arc::new(Actor {
+ vehicle: Arc::new(vehicle),
+ driver: Arc::new(test_driver()),
+ detail: ActorDetail {
+ start: Some(VehiclePlace { location, time: TimeInterval { earliest: Some(0.0), latest: None } }),
+ end: Some(VehiclePlace { location, time: TimeInterval { earliest: None, latest: Some(1000.0) } }),
+ time: TimeWindow { start: 0.0, end: 1000.0 },
+ },
+ })
+}
+
+fn create_test_feature(actors: Vec>) -> crate::models::Feature {
+ VehicleDistanceFeatureBuilder::new("test_vehicle_distance")
+ .set_transport(TestTransportCost::new_shared())
+ .set_actors(actors)
+ .set_compatibility_fn(|_, _| true)
+ .build()
+ .unwrap()
+}
+
+// ============================================================================
+// Builder Tests
+// ============================================================================
+
+#[test]
+fn can_create_feature_with_all_required_parameters() {
+ let actors = vec![create_actor_at(0)];
+ let result = VehicleDistanceFeatureBuilder::new("test")
+ .set_transport(TestTransportCost::new_shared())
+ .set_actors(actors)
+ .set_compatibility_fn(|_, _| true)
+ .build();
+
+ assert!(result.is_ok());
+ let feature = result.unwrap();
+ assert!(feature.objective.is_some());
+ assert!(feature.state.is_some());
+}
+
+#[test]
+fn can_return_error_when_transport_not_set() {
+ let actors = vec![create_actor_at(0)];
+ let result =
+ VehicleDistanceFeatureBuilder::new("test").set_actors(actors).set_compatibility_fn(|_, _| true).build();
+
+ assert!(result.is_err());
+ assert!(result.unwrap_err().to_string().contains("transport"));
+}
+
+#[test]
+fn can_return_error_when_actors_not_set() {
+ let result = VehicleDistanceFeatureBuilder::new("test")
+ .set_transport(TestTransportCost::new_shared())
+ .set_compatibility_fn(|_, _| true)
+ .build();
+
+ assert!(result.is_err());
+ assert!(result.unwrap_err().to_string().contains("actors"));
+}
+
+#[test]
+fn can_return_error_when_compatibility_fn_not_set() {
+ let actors = vec![create_actor_at(0)];
+ let result = VehicleDistanceFeatureBuilder::new("test")
+ .set_transport(TestTransportCost::new_shared())
+ .set_actors(actors)
+ .build();
+
+ assert!(result.is_err());
+ assert!(result.unwrap_err().to_string().contains("compatibility_fn"));
+}
+
+// ============================================================================
+// Fitness Tests - verify penalty calculations
+// ============================================================================
+
+#[test]
+fn can_return_zero_fitness_when_job_on_nearest_vehicle() {
+ // Vehicle at 0, job at 5. Only one vehicle, so assigned == nearest.
+ let actor = create_actor_at(0);
+ let actors = vec![actor.clone()];
+ let feature = create_test_feature(actors);
+ let objective = feature.objective.unwrap();
+
+ let job = TestSingleBuilder::default().location(Some(5)).build_shared();
+ let route_ctx = RouteContextBuilder::default()
+ .with_route(
+ RouteBuilder::default()
+ .with_start(ActivityBuilder::with_location(0).job(None).build())
+ .with_end(ActivityBuilder::with_location(0).job(None).build())
+ .add_activity(ActivityBuilder::with_location(5).job(Some(job)).build())
+ .build(),
+ )
+ .build();
+ let insertion_ctx = TestInsertionContextBuilder::default().with_routes(vec![route_ctx]).build();
+
+ let fitness = objective.fitness(&insertion_ctx);
+ assert_eq!(fitness, 0.0);
+}
+
+#[test]
+fn can_return_penalty_when_job_on_farther_vehicle() {
+ // Two vehicles: v0 at 0, v1 at 100. Job at 5, assigned to v1 (at 100).
+ // dist(job=5, assigned=100) = 95
+ // dist(job=5, nearest=0) = 5
+ // penalty = 95 - 5 = 90
+ let actor_0 = create_actor_at(0);
+ let actor_100 = create_actor_at(100);
+ let actors = vec![actor_0, actor_100.clone()];
+ let feature = create_test_feature(actors);
+ let objective = feature.objective.unwrap();
+
+ let job = TestSingleBuilder::default().location(Some(5)).build_shared();
+ // Build route using actor_100 (vehicle starting at 100)
+ let route = crate::models::solution::Route {
+ actor: actor_100,
+ tour: {
+ let mut tour = crate::models::solution::Tour::default();
+ tour.set_start(ActivityBuilder::with_location(100).job(None).build());
+ tour.set_end(ActivityBuilder::with_location(100).job(None).build());
+ tour.insert_last(ActivityBuilder::with_location(5).job(Some(job)).build());
+ tour
+ },
+ };
+ let route_ctx = crate::construction::heuristics::RouteContext::new_with_state(
+ route,
+ crate::construction::heuristics::RouteState::default(),
+ );
+ let insertion_ctx = TestInsertionContextBuilder::default().with_routes(vec![route_ctx]).build();
+
+ let fitness = objective.fitness(&insertion_ctx);
+ assert_eq!(fitness, 90.0);
+}
+
+#[test]
+fn can_return_zero_fitness_for_empty_route() {
+ let actors = vec![create_actor_at(0)];
+ let feature = create_test_feature(actors);
+ let objective = feature.objective.unwrap();
+ let route_ctx = RouteContextBuilder::default().build();
+ let insertion_ctx = TestInsertionContextBuilder::default().with_routes(vec![route_ctx]).build();
+
+ let fitness = objective.fitness(&insertion_ctx);
+ assert_eq!(fitness, 0.0);
+}
+
+#[test]
+fn can_sum_penalties_across_multiple_jobs() {
+ // Two vehicles: at 0 and at 100. Two jobs at 5 and 10, both assigned to v100.
+ // job@5: dist(5,100)=95, nearest=dist(5,0)=5, penalty=90
+ // job@10: dist(10,100)=90, nearest=dist(10,0)=10, penalty=80
+ // total = 170
+ let actor_0 = create_actor_at(0);
+ let actor_100 = create_actor_at(100);
+ let actors = vec![actor_0, actor_100.clone()];
+ let feature = create_test_feature(actors);
+ let objective = feature.objective.unwrap();
+
+ let job1 = TestSingleBuilder::default().location(Some(5)).build_shared();
+ let job2 = TestSingleBuilder::default().location(Some(10)).build_shared();
+ let route = crate::models::solution::Route {
+ actor: actor_100,
+ tour: {
+ let mut tour = crate::models::solution::Tour::default();
+ tour.set_start(ActivityBuilder::with_location(100).job(None).build());
+ tour.set_end(ActivityBuilder::with_location(100).job(None).build());
+ tour.insert_last(ActivityBuilder::with_location(5).job(Some(job1)).build());
+ tour.insert_last(ActivityBuilder::with_location(10).job(Some(job2)).build());
+ tour
+ },
+ };
+ let route_ctx = crate::construction::heuristics::RouteContext::new_with_state(
+ route,
+ crate::construction::heuristics::RouteState::default(),
+ );
+ let insertion_ctx = TestInsertionContextBuilder::default().with_routes(vec![route_ctx]).build();
+
+ let fitness = objective.fitness(&insertion_ctx);
+ assert_eq!(fitness, 170.0);
+}
+
+// ============================================================================
+// Estimate Tests - verify construction-time guidance
+// ============================================================================
+
+#[test]
+fn can_estimate_zero_when_inserting_into_nearest_vehicle() {
+ // Two vehicles: at 0 and at 100. Inserting job at 5 into v0's route.
+ // dist(5, 0) = 5 (assigned), dist(5, 0) = 5 (nearest) -> penalty = 0
+ let actor_0 = create_actor_at(0);
+ let actor_100 = create_actor_at(100);
+ let actors = vec![actor_0.clone(), actor_100];
+ let feature = create_test_feature(actors);
+ let objective = feature.objective.unwrap();
+
+ let job = Job::Single(TestSingleBuilder::default().location(Some(5)).build_shared());
+ let route = crate::models::solution::Route {
+ actor: actor_0,
+ tour: {
+ let mut tour = crate::models::solution::Tour::default();
+ tour.set_start(ActivityBuilder::with_location(0).job(None).build());
+ tour.set_end(ActivityBuilder::with_location(0).job(None).build());
+ tour
+ },
+ };
+ let route_ctx = crate::construction::heuristics::RouteContext::new_with_state(
+ route,
+ crate::construction::heuristics::RouteState::default(),
+ );
+ let insertion_ctx = TestInsertionContextBuilder::default().build();
+
+ let estimate = objective.estimate(&MoveContext::route(&insertion_ctx.solution, &route_ctx, &job));
+ assert_eq!(estimate, 0.0);
+}
+
+#[test]
+fn can_estimate_penalty_when_inserting_into_farther_vehicle() {
+ // Two vehicles: at 0 and at 100. Inserting job at 5 into v100's route.
+ // dist(5, 100) = 95 (assigned), dist(5, 0) = 5 (nearest) -> penalty = 90
+ let actor_0 = create_actor_at(0);
+ let actor_100 = create_actor_at(100);
+ let actors = vec![actor_0, actor_100.clone()];
+ let feature = create_test_feature(actors);
+ let objective = feature.objective.unwrap();
+
+ let job = Job::Single(TestSingleBuilder::default().location(Some(5)).build_shared());
+ let route = crate::models::solution::Route {
+ actor: actor_100,
+ tour: {
+ let mut tour = crate::models::solution::Tour::default();
+ tour.set_start(ActivityBuilder::with_location(100).job(None).build());
+ tour.set_end(ActivityBuilder::with_location(100).job(None).build());
+ tour
+ },
+ };
+ let route_ctx = crate::construction::heuristics::RouteContext::new_with_state(
+ route,
+ crate::construction::heuristics::RouteState::default(),
+ );
+ let insertion_ctx = TestInsertionContextBuilder::default().build();
+
+ let estimate = objective.estimate(&MoveContext::route(&insertion_ctx.solution, &route_ctx, &job));
+ assert_eq!(estimate, 90.0);
+}
+
+// ============================================================================
+// Comparison Tests
+// ============================================================================
+
+#[test]
+fn can_prefer_route_with_jobs_near_vehicle_start() {
+ // Two vehicles at 0 and 100. Two routes:
+ // Route A: v0 with job at 5 (near start) -> penalty 0
+ // Route B: v0 with job at 95 (far from start, near v100) -> penalty = 95 - 5 = 90
+ // Route A should have lower fitness.
+ let actor_0 = create_actor_at(0);
+ let actor_100 = create_actor_at(100);
+ let actors = vec![actor_0.clone(), actor_100];
+ let feature_a = create_test_feature(actors.clone());
+ let feature_b = create_test_feature(actors);
+ let obj_a = feature_a.objective.unwrap();
+ let obj_b = feature_b.objective.unwrap();
+
+ // Route A: job at 5, on v0
+ let job_near = TestSingleBuilder::default().location(Some(5)).build_shared();
+ let route_a = crate::models::solution::Route {
+ actor: actor_0.clone(),
+ tour: {
+ let mut tour = crate::models::solution::Tour::default();
+ tour.set_start(ActivityBuilder::with_location(0).job(None).build());
+ tour.set_end(ActivityBuilder::with_location(0).job(None).build());
+ tour.insert_last(ActivityBuilder::with_location(5).job(Some(job_near)).build());
+ tour
+ },
+ };
+ let route_ctx_a = crate::construction::heuristics::RouteContext::new_with_state(
+ route_a,
+ crate::construction::heuristics::RouteState::default(),
+ );
+ let ctx_a = TestInsertionContextBuilder::default().with_routes(vec![route_ctx_a]).build();
+ let fitness_a = obj_a.fitness(&ctx_a);
+
+ // Route B: job at 95, on v0 (far from v0's start, closer to v100)
+ let job_far = TestSingleBuilder::default().location(Some(95)).build_shared();
+ let route_b = crate::models::solution::Route {
+ actor: actor_0,
+ tour: {
+ let mut tour = crate::models::solution::Tour::default();
+ tour.set_start(ActivityBuilder::with_location(0).job(None).build());
+ tour.set_end(ActivityBuilder::with_location(0).job(None).build());
+ tour.insert_last(ActivityBuilder::with_location(95).job(Some(job_far)).build());
+ tour
+ },
+ };
+ let route_ctx_b = crate::construction::heuristics::RouteContext::new_with_state(
+ route_b,
+ crate::construction::heuristics::RouteState::default(),
+ );
+ let ctx_b = TestInsertionContextBuilder::default().with_routes(vec![route_ctx_b]).build();
+ let fitness_b = obj_b.fitness(&ctx_b);
+
+ assert_eq!(fitness_a, 0.0);
+ assert_eq!(fitness_b, 90.0);
+ assert!(fitness_a < fitness_b);
+}
diff --git a/vrp-core/tests/unit/construction/features/vehicle_shifts_test.rs b/vrp-core/tests/unit/construction/features/vehicle_shifts_test.rs
new file mode 100644
index 000000000..c7a3b82ed
--- /dev/null
+++ b/vrp-core/tests/unit/construction/features/vehicle_shifts_test.rs
@@ -0,0 +1,126 @@
+use super::*;
+use crate::construction::heuristics::{RegistryContext, RouteContext};
+use crate::helpers::models::domain::{TestGoalContextBuilder, test_random};
+use crate::helpers::models::problem::{FleetBuilder, TestSingleBuilder, test_driver, test_vehicle_with_id};
+use crate::helpers::models::solution::{ActivityBuilder, RouteBuilder, RouteContextBuilder};
+use crate::models::problem::Fleet;
+use crate::models::solution::Registry;
+use std::collections::{HashMap, HashSet};
+
+const VIOLATION_CODE: ViolationCode = ViolationCode(42);
+
+#[test]
+fn can_collect_missing_vehicle_ids() {
+ let fleet = create_test_fleet(&["v1", "v2"]);
+ let mut solution_ctx = create_solution_ctx(&fleet, vec![("v1", 1), ("v2", 0)]);
+ let feature = create_feature(vec![("v1", 1, false), ("v2", 1, false)]);
+
+ feature.state.unwrap().accept_solution_state(&mut solution_ctx);
+
+ let summary = solution_ctx.state.get_vehicle_shift_summary().unwrap();
+ let expected = HashSet::from(["v2".to_string()]);
+
+ assert_eq!(summary.missing_vehicle_ids, expected);
+}
+
+#[test]
+fn can_block_insertions_on_satisfied_routes_when_missing_exists() {
+ let fleet = create_test_fleet(&["v1", "v2"]);
+ let mut solution_ctx = create_solution_ctx(&fleet, vec![("v1", 1), ("v2", 0)]);
+ let feature = create_feature(vec![("v1", 1, false), ("v2", 1, false)]);
+ let constraint = feature.constraint.unwrap();
+ feature.state.unwrap().accept_solution_state(&mut solution_ctx);
+ let job = Job::Single(TestSingleBuilder::default().build_shared());
+
+ let route_v1 = get_route_ctx(&solution_ctx, "v1");
+ let violation = constraint.evaluate(&MoveContext::route(&solution_ctx, route_v1, &job));
+ assert_eq!(violation, Some(ConstraintViolation { code: VIOLATION_CODE, stopped: false }));
+
+ let route_v2 = get_route_ctx(&solution_ctx, "v2");
+ let violation = constraint.evaluate(&MoveContext::route(&solution_ctx, route_v2, &job));
+ assert_eq!(violation, None);
+}
+
+#[test]
+fn allows_insertions_when_all_requirements_met() {
+ let fleet = create_test_fleet(&["v1", "v2"]);
+ let mut solution_ctx = create_solution_ctx(&fleet, vec![("v1", 1), ("v2", 1)]);
+ let feature = create_feature(vec![("v1", 1, false), ("v2", 1, false)]);
+ let constraint = feature.constraint.unwrap();
+ feature.state.unwrap().accept_solution_state(&mut solution_ctx);
+ let job = Job::Single(TestSingleBuilder::default().build_shared());
+
+ let route_v1 = get_route_ctx(&solution_ctx, "v1");
+ let violation = constraint.evaluate(&MoveContext::route(&solution_ctx, route_v1, &job));
+ assert_eq!(violation, None);
+}
+
+#[test]
+fn can_allow_zero_usage() {
+ let fleet = create_test_fleet(&["v1"]);
+ let mut solution_ctx = create_solution_ctx(&fleet, vec![("v1", 0)]);
+ let feature = create_feature(vec![("v1", 1, true)]);
+ feature.state.unwrap().accept_solution_state(&mut solution_ctx);
+
+ let summary = solution_ctx.state.get_vehicle_shift_summary().unwrap();
+ assert!(summary.missing_vehicle_ids.is_empty());
+}
+
+fn create_feature(requirements: Vec<(&str, usize, bool)>) -> Feature {
+ let requirements = requirements
+ .into_iter()
+ .map(|(id, value, allow_zero)| {
+ (id.to_string(), MinShiftRequirement { minimum: value, allow_zero_usage: allow_zero })
+ })
+ .collect::>();
+
+ MinVehicleShiftsFeatureBuilder::new("min_shifts")
+ .with_violation_code(VIOLATION_CODE)
+ .with_requirements(requirements)
+ .build()
+ .unwrap()
+}
+
+fn create_solution_ctx(fleet: &Fleet, vehicle_jobs: Vec<(&str, usize)>) -> SolutionContext {
+ let routes = vehicle_jobs
+ .into_iter()
+ .map(|(vehicle_id, job_count)| {
+ let mut route_builder = RouteBuilder::default();
+ route_builder.with_vehicle(fleet, vehicle_id);
+ if job_count > 0 {
+ let activities = (0..job_count).map(|_| ActivityBuilder::default().build()).collect::>();
+ route_builder.add_activities(activities);
+ }
+
+ RouteContextBuilder::default().with_route(route_builder.build()).build()
+ })
+ .collect();
+
+ SolutionContext {
+ required: vec![],
+ ignored: vec![],
+ unassigned: Default::default(),
+ locked: Default::default(),
+ routes,
+ registry: RegistryContext::new(&TestGoalContextBuilder::default().build(), Registry::new(fleet, test_random())),
+ state: Default::default(),
+ }
+}
+
+fn create_test_fleet(vehicle_ids: &[&str]) -> Fleet {
+ let mut builder = FleetBuilder::default();
+ builder.add_driver(test_driver());
+ vehicle_ids.iter().for_each(|vehicle_id| {
+ builder.add_vehicle(test_vehicle_with_id(vehicle_id));
+ });
+
+ builder.build()
+}
+
+fn get_route_ctx<'a>(solution_ctx: &'a SolutionContext, vehicle_id: &str) -> &'a RouteContext {
+ solution_ctx
+ .routes
+ .iter()
+ .find(|route_ctx| route_ctx.route().actor.vehicle.dimens.get_vehicle_id().unwrap() == vehicle_id)
+ .unwrap()
+}
diff --git a/vrp-pragmatic/src/checker/breaks.rs b/vrp-pragmatic/src/checker/breaks.rs
index cd258ca19..2ec48aa50 100644
--- a/vrp-pragmatic/src/checker/breaks.rs
+++ b/vrp-pragmatic/src/checker/breaks.rs
@@ -3,8 +3,10 @@
mod breaks_test;
use super::*;
+use crate::format::problem::RouteCostSpan as FmtRouteCostSpan;
use crate::utils::combine_error_results;
use std::iter::once;
+use vrp_core::models::common::Timestamp;
use vrp_core::prelude::GenericResult;
use vrp_core::utils::GenericError;
@@ -16,6 +18,8 @@ pub fn check_breaks(context: &CheckerContext) -> Result<(), Vec> {
fn check_break_assignment(context: &CheckerContext) -> GenericResult<()> {
context.solution.tours.iter().try_for_each(|tour| {
let vehicle_shift = context.get_vehicle_shift(tour)?;
+ let cost_span = context.get_vehicle(&tour.vehicle_id).ok().and_then(|v| v.costs.span.as_ref());
+
let actual_break_count = tour
.stops
.iter()
@@ -31,7 +35,7 @@ fn check_break_assignment(context: &CheckerContext) -> GenericResult<()> {
|acc, (from_loc, (from, to), (break_activity, vehicle_break))| {
// check time
let visit_time = get_time_window(stop, break_activity);
- let break_time_window = get_break_time_window(tour, &vehicle_break)?;
+ let break_time_window = get_break_time_window(tour, &vehicle_break, cost_span)?;
if !visit_time.intersects(&break_time_window) {
return Err(format!(
"break visit time '{visit_time:?}' is invalid: expected is in '{break_time_window:?}'",
@@ -91,7 +95,8 @@ fn check_break_assignment(context: &CheckerContext) -> GenericResult<()> {
let expected_break_count =
vehicle_shift.breaks.iter().flat_map(|breaks| breaks.iter()).fold(0, |acc, vehicle_break| {
- let break_tw = get_break_time_window(tour, vehicle_break).expect("cannot get break time windows");
+ let break_tw =
+ get_break_time_window(tour, vehicle_break, cost_span).expect("cannot get break time windows");
let should_assign = match vehicle_break {
VehicleBreak::Optional { policy, .. } => {
@@ -160,14 +165,26 @@ fn as_leg_info_with_break<'a>(
None
}
-/// Gets break time window.
-pub(crate) fn get_break_time_window(tour: &Tour, vehicle_break: &VehicleBreak) -> GenericResult {
+/// Gets break time window, using the RouteCostSpan to determine the anchor for offset breaks.
+pub(crate) fn get_break_time_window(
+ tour: &Tour,
+ vehicle_break: &VehicleBreak,
+ cost_span: Option<&FmtRouteCostSpan>,
+) -> GenericResult {
let departure = tour
.stops
.first()
.map(|stop| parse_time(&stop.schedule().departure))
.ok_or_else(|| format!("cannot get departure time for tour: '{}'", tour.vehicle_id))?;
+ // Compute the offset anchor based on RouteCostSpan
+ let offset_anchor = match cost_span {
+ Some(FmtRouteCostSpan::FirstJobToDepot | FmtRouteCostSpan::FirstJobToLastJob) => {
+ get_first_job_arrival(tour).unwrap_or(departure)
+ }
+ _ => departure,
+ };
+
match vehicle_break {
VehicleBreak::Optional { time: VehicleOptionalBreakTime::TimeWindow(tw), .. } => Ok(parse_time_window(tw)),
VehicleBreak::Optional { time: VehicleOptionalBreakTime::TimeOffset(offset), .. } => {
@@ -180,7 +197,7 @@ pub(crate) fn get_break_time_window(tour: &Tour, vehicle_break: &VehicleBreak) -
VehicleBreak::Required { time, duration } => {
let (start, end) = match time {
VehicleRequiredBreakTime::OffsetTime { earliest, latest } => {
- (departure + *earliest, departure + *latest)
+ (offset_anchor + *earliest, offset_anchor + *latest)
}
VehicleRequiredBreakTime::ExactTime { earliest, latest } => (parse_time(earliest), parse_time(latest)),
};
@@ -190,6 +207,24 @@ pub(crate) fn get_break_time_window(tour: &Tour, vehicle_break: &VehicleBreak) -
}
}
+/// Gets the arrival time of the first job activity in the tour.
+fn get_first_job_arrival(tour: &Tour) -> Option {
+ // The first stop is departure, so first job is the second stop (or first non-departure activity)
+ tour.stops
+ .iter()
+ .flat_map(|stop| stop.activities().iter())
+ .find(|a| !matches!(a.activity_type.as_str(), "departure" | "arrival"))
+ .and_then(|_| {
+ // Find the stop that contains the first job activity and get its arrival
+ tour.stops
+ .iter()
+ .find(|stop| {
+ stop.activities().iter().any(|a| !matches!(a.activity_type.as_str(), "departure" | "arrival"))
+ })
+ .map(|stop| parse_time(&stop.schedule().arrival))
+ })
+}
+
fn get_break_violation_count(solution: &Solution, tour: &Tour) -> usize {
solution.violations.as_ref().map_or(0, |violations| {
violations
diff --git a/vrp-pragmatic/src/checker/limits.rs b/vrp-pragmatic/src/checker/limits.rs
index 13b34e92a..f928941b9 100644
--- a/vrp-pragmatic/src/checker/limits.rs
+++ b/vrp-pragmatic/src/checker/limits.rs
@@ -50,6 +50,21 @@ fn check_shift_limits(context: &CheckerContext) -> GenericResult<()> {
).into())
}
}
+
+ if let Some(min_tour_size_limit) = limits.min_tour_size {
+ let shift = context.get_vehicle_shift(tour)?;
+
+ let extra_activities = if shift.end.is_some() { 2 } else { 1 };
+ let tour_activities = tour.stops.iter().flat_map(|stop| stop.activities()).count();
+ let tour_activities = tour_activities.saturating_sub(extra_activities);
+
+ if tour_activities < min_tour_size_limit {
+ return Err(format!(
+ "min tour size limit violation, expected: not less than {}, got: {}, vehicle id '{}', shift index: {}",
+ min_tour_size_limit, tour_activities, tour.vehicle_id, tour.shift_index
+ ).into())
+ }
+ }
}
Ok(())
diff --git a/vrp-pragmatic/src/checker/mod.rs b/vrp-pragmatic/src/checker/mod.rs
index 9072d91b3..559236571 100644
--- a/vrp-pragmatic/src/checker/mod.rs
+++ b/vrp-pragmatic/src/checker/mod.rs
@@ -179,17 +179,19 @@ impl CheckerContext {
)
}
- "break" => shift
- .breaks
- .as_ref()
- .and_then(|breaks| {
- breaks
- .iter()
- // TODO: would be nice to propagate the error
- .find(|b| get_break_time_window(tour, b).map(|tw| tw.intersects(&time)).unwrap_or(false))
- })
- .map(|b| ActivityType::Break(b.clone()))
- .ok_or_else(|| format!("cannot find break for tour '{}'", tour.vehicle_id).into()),
+ "break" => {
+ let cost_span = self.get_vehicle(&tour.vehicle_id).ok().and_then(|v| v.costs.span.as_ref());
+ shift
+ .breaks
+ .as_ref()
+ .and_then(|breaks| {
+ breaks.iter().find(|b| {
+ get_break_time_window(tour, b, cost_span).map(|tw| tw.intersects(&time)).unwrap_or(false)
+ })
+ })
+ .map(|b| ActivityType::Break(b.clone()))
+ .ok_or_else(|| format!("cannot find break for tour '{}'", tour.vehicle_id).into())
+ }
"reload" => shift
.reloads
diff --git a/vrp-pragmatic/src/format/dimensions.rs b/vrp-pragmatic/src/format/dimensions.rs
index a842da055..fa3349426 100644
--- a/vrp-pragmatic/src/format/dimensions.rs
+++ b/vrp-pragmatic/src/format/dimensions.rs
@@ -11,12 +11,16 @@ custom_dimension!(pub ShiftIndex typeof usize);
custom_dimension!(pub TourSize typeof usize);
+custom_dimension!(pub MinTourSize typeof usize);
+
custom_dimension!(pub PlaceTags typeof Vec<(usize, String)>);
custom_dimension!(pub JobOrder typeof i32);
custom_dimension!(pub JobValue typeof Float);
+custom_dimension!(pub JobDueDate typeof Float);
+
custom_dimension!(pub JobType typeof String);
custom_dimension!(pub BreakPolicy typeof BreakPolicy);
diff --git a/vrp-pragmatic/src/format/mod.rs b/vrp-pragmatic/src/format/mod.rs
index 38aa83f5d..7c5a40755 100644
--- a/vrp-pragmatic/src/format/mod.rs
+++ b/vrp-pragmatic/src/format/mod.rs
@@ -195,6 +195,9 @@ const GROUP_CONSTRAINT_CODE: ViolationCode = ViolationCode(12);
const COMPATIBILITY_CONSTRAINT_CODE: ViolationCode = ViolationCode(13);
const RELOAD_RESOURCE_CONSTRAINT_CODE: ViolationCode = ViolationCode(14);
const RECHARGE_CONSTRAINT_CODE: ViolationCode = ViolationCode(15);
+const MIN_VEHICLE_SHIFTS_CONSTRAINT_CODE: ViolationCode = ViolationCode(16);
+const MIN_TOUR_SIZE_CONSTRAINT_CODE: ViolationCode = ViolationCode(17);
+const JOB_TIME_CONSTRAINT_CODE: ViolationCode = ViolationCode(18);
/// An job id to job index.
pub type JobIndex = HashMap;
diff --git a/vrp-pragmatic/src/format/problem/fleet_reader.rs b/vrp-pragmatic/src/format/problem/fleet_reader.rs
index f9468f14b..cde198196 100644
--- a/vrp-pragmatic/src/format/problem/fleet_reader.rs
+++ b/vrp-pragmatic/src/format/problem/fleet_reader.rs
@@ -11,6 +11,7 @@ use std::collections::HashSet;
use vrp_core::construction::enablers::create_typed_actor_groups;
use vrp_core::construction::features::{VehicleCapacityDimension, VehicleSkillsDimension};
use vrp_core::models::common::*;
+use vrp_core::models::problem::RouteCostSpanDimension;
use vrp_core::models::problem::*;
pub(super) fn get_profile_index_map(api_problem: &ApiProblem) -> HashMap {
@@ -112,6 +113,7 @@ pub(super) fn read_fleet(api_problem: &ApiProblem, props: &ProblemProperties, co
let profile = Profile::new(index, vehicle.profile.scale);
let tour_size = vehicle.limits.as_ref().and_then(|l| l.tour_size);
+ let min_tour_size = vehicle.limits.as_ref().and_then(|l| l.min_tour_size);
for (shift_index, shift) in vehicle.shifts.iter().enumerate() {
let start = {
@@ -150,6 +152,10 @@ pub(super) fn read_fleet(api_problem: &ApiProblem, props: &ProblemProperties, co
dimens.set_tour_size(tour_size);
}
+ if let Some(min_tour_size) = min_tour_size {
+ dimens.set_min_tour_size(min_tour_size);
+ }
+
if props.has_multi_dimen_capacity {
dimens.set_vehicle_capacity(MultiDimLoad::new(vehicle.capacity.clone()));
} else {
@@ -160,6 +166,32 @@ pub(super) fn read_fleet(api_problem: &ApiProblem, props: &ProblemProperties, co
dimens.set_vehicle_skills(skills.iter().cloned().collect::>());
}
+ if let Some(span) = vehicle.costs.span.as_ref() {
+ let core_span = match span {
+ crate::format::problem::model::RouteCostSpan::DepotToDepot => {
+ vrp_core::models::problem::RouteCostSpan::DepotToDepot
+ }
+ crate::format::problem::model::RouteCostSpan::DepotToLastJob => {
+ vrp_core::models::problem::RouteCostSpan::DepotToLastJob
+ }
+ crate::format::problem::model::RouteCostSpan::FirstJobToDepot => {
+ vrp_core::models::problem::RouteCostSpan::FirstJobToDepot
+ }
+ crate::format::problem::model::RouteCostSpan::FirstJobToLastJob => {
+ vrp_core::models::problem::RouteCostSpan::FirstJobToLastJob
+ }
+ };
+ dimens.set_route_cost_span(core_span);
+ }
+
+ if let Some(job_times) = shift.job_times.as_ref() {
+ let core_job_times = vrp_core::models::problem::JobTimeConstraints {
+ earliest_first: job_times.earliest_first.as_ref().map(|t| parse_time(t)),
+ latest_last: job_times.latest_last.as_ref().map(|t| parse_time(t)),
+ };
+ dimens.set_job_time_constraints(core_job_times);
+ }
+
vehicles.push(Arc::new(Vehicle {
profile: profile.clone(),
costs: costs.clone(),
diff --git a/vrp-pragmatic/src/format/problem/goal_reader.rs b/vrp-pragmatic/src/format/problem/goal_reader.rs
index 1724b7efe..37f64ac55 100644
--- a/vrp-pragmatic/src/format/problem/goal_reader.rs
+++ b/vrp-pragmatic/src/format/problem/goal_reader.rs
@@ -5,7 +5,7 @@ use vrp_core::construction::clustering::vicinity::ClusterInfoDimension;
use vrp_core::construction::enablers::FeatureCombinator;
use vrp_core::construction::features::*;
use vrp_core::models::common::{Demand, LoadOps, MultiDimLoad, SingleDimLoad};
-use vrp_core::models::problem::{Actor, Single, TransportCost};
+use vrp_core::models::problem::{Actor, Job as CoreJob, Single, TransportCost};
use vrp_core::models::solution::Route;
use vrp_core::models::{Feature, FeatureObjective, GoalBuilder, GoalContext, GoalContextBuilder};
use vrp_core::rosomaxa::evolution::objectives::dominance_order;
@@ -34,6 +34,15 @@ pub(super) fn create_goal_context(
)?)
}
+ if props.has_job_time_constraints {
+ features.push(create_job_time_limits_feature(
+ "job_time_limits",
+ blocks.transport.clone(),
+ blocks.activity.clone(),
+ JOB_TIME_CONSTRAINT_CODE,
+ )?)
+ }
+
if props.has_breaks {
features.push(create_optional_break_feature("break")?)
}
@@ -75,6 +84,12 @@ pub(super) fn create_goal_context(
)?);
}
+ if props.has_min_vehicle_shifts
+ && let Some(feature) = get_min_vehicle_shifts_feature("min_vehicle_shifts", api_problem)?
+ {
+ features.push(feature);
+ }
+
GoalContextBuilder::with_features(&features)?.set_main_goal(goal_builder.build()?).build()
}
@@ -149,6 +164,13 @@ fn get_objective_feature_layer(
}),
ViolationCode::unknown(),
),
+ Objective::BalanceShifts { saturation, weight } => {
+ const DEFAULT_VARIANCE_SATURATION: Float = 0.05;
+ let saturation = saturation.unwrap_or(DEFAULT_VARIANCE_SATURATION).max(1e-6);
+ let weight = weight.unwrap_or(1.).max(0.);
+ let penalty = Arc::new(move |variance: Float| weight * variance / (variance + saturation));
+ create_balance_shifts_feature_with_penalty("balance_shifts", penalty)
+ }
Objective::MinimizeUnassigned { breaks } => MinimizeUnassignedBuilder::new("min_unassigned")
.set_job_estimator({
let break_value = *breaks;
@@ -198,7 +220,47 @@ fn get_objective_feature_layer(
create_tour_compactness_feature("tour_compact", blocks.jobs.clone(), *job_radius)
}
Objective::TourOrder => create_tour_order_soft_feature("tour_order", get_tour_order_fn()),
+ Objective::MinimizeTourSizeViolation => create_min_activity_limit_feature(
+ "min_tour_size_objective",
+ Arc::new(|actor| actor.vehicle.dimens.get_min_tour_size().copied()),
+ ),
Objective::FastService => get_fast_service_feature("fast_service", blocks),
+ Objective::MinimizeOverdue => MinimizeOverdueBuilder::new("min_overdue")
+ .set_job_due_date_fn(|job| {
+ // For Multi jobs, find the earliest due date among all tasks
+ // For Single jobs, just get the due date directly
+ match job {
+ CoreJob::Single(single) => single.dimens.get_job_due_date().copied(),
+ CoreJob::Multi(multi) => multi
+ .jobs
+ .iter()
+ .filter_map(|single| single.dimens.get_job_due_date().copied())
+ .min_by(|a, b| a.total_cmp(b)),
+ }
+ })
+ .set_scheduled_date_fn(|route_ctx| route_ctx.route().actor.detail.time.start)
+ .set_unassigned_penalty_fn(|job| {
+ // High penalty for unassigned jobs that have a due date
+ let has_due_date = match job {
+ CoreJob::Single(single) => single.dimens.get_job_due_date().is_some(),
+ CoreJob::Multi(multi) => multi.jobs.iter().any(|single| single.dimens.get_job_due_date().is_some()),
+ };
+ if has_due_date { 10000.0 } else { 0.0 }
+ })
+ .build(),
+ Objective::MinimizeVehicleDistance => VehicleDistanceFeatureBuilder::new("min_vehicle_distance")
+ .set_transport(blocks.transport.clone())
+ .set_actors(blocks.fleet.actors.clone())
+ .set_compatibility_fn(|job, actor| {
+ if let Some(job_skills) = job.dimens().get_job_skills() {
+ let vehicle_skills = actor.vehicle.dimens.get_vehicle_skills();
+ if !is_job_skills_compatible(job_skills, &vehicle_skills) {
+ return false;
+ }
+ }
+ true
+ })
+ .build(),
Objective::HierarchicalAreas { levels } => get_hierarchical_areas_feature(blocks, *levels),
Objective::MultiObjective { objectives, strategy: composition_type } => {
let features = objectives
@@ -450,6 +512,33 @@ fn get_tour_limit_feature(
)
}
+fn get_min_vehicle_shifts_feature(name: &str, api_problem: &ApiProblem) -> GenericResult> {
+ let requirements = api_problem
+ .fleet
+ .vehicles
+ .iter()
+ .filter_map(|vehicle| vehicle.min_shifts.as_ref().map(|value| (vehicle, value.clone())))
+ .flat_map(|(vehicle, min_shifts)| {
+ vehicle.vehicle_ids.iter().map(move |vehicle_id| {
+ (
+ vehicle_id.clone(),
+ MinShiftRequirement { minimum: min_shifts.value, allow_zero_usage: min_shifts.allow_zero_usage },
+ )
+ })
+ })
+ .collect::>();
+
+ if requirements.is_empty() {
+ return Ok(None);
+ }
+
+ MinVehicleShiftsFeatureBuilder::new(name)
+ .with_violation_code(MIN_VEHICLE_SHIFTS_CONSTRAINT_CODE)
+ .with_requirements(requirements)
+ .build()
+ .map(Some)
+}
+
fn get_recharge_feature(
name: &str,
api_problem: &ApiProblem,
@@ -583,3 +672,55 @@ fn get_tour_order_fn() -> TourOrderFn {
})
}))
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn create_problem_with_min_shifts(min_shifts: Option) -> ApiProblem {
+ ApiProblem {
+ plan: Plan { jobs: vec![], relations: None, clustering: None },
+ fleet: Fleet {
+ vehicles: vec![VehicleType {
+ type_id: "vehicle_type".to_string(),
+ vehicle_ids: vec!["vehicle_1".to_string()],
+ profile: VehicleProfile { matrix: "car".to_string(), scale: None },
+ costs: VehicleCosts { fixed: Some(0.), distance: 1., time: 1., span: None },
+ shifts: vec![VehicleShift {
+ start: ShiftStart {
+ earliest: "1970-01-01T00:00:00Z".to_string(),
+ latest: None,
+ location: Location::new_coordinate(0., 0.),
+ },
+ end: None,
+ breaks: None,
+ reloads: None,
+ recharges: None,
+ job_times: None,
+ }],
+ capacity: vec![1],
+ skills: None,
+ limits: None,
+ min_shifts,
+ }],
+ profiles: vec![MatrixProfile { name: "car".to_string(), speed: None }],
+ resources: None,
+ },
+ objectives: None,
+ }
+ }
+
+ #[test]
+ fn creates_min_vehicle_shift_feature_when_needed() {
+ let problem = create_problem_with_min_shifts(Some(VehicleMinShifts { value: 1, allow_zero_usage: false }));
+ let feature = get_min_vehicle_shifts_feature("min_vehicle_shifts", &problem).unwrap();
+
+ assert!(feature.is_some());
+ }
+
+ #[test]
+ fn returns_none_when_no_requirements() {
+ let problem = create_problem_with_min_shifts(None);
+ assert!(get_min_vehicle_shifts_feature("min_vehicle_shifts", &problem).unwrap().is_none());
+ }
+}
diff --git a/vrp-pragmatic/src/format/problem/job_reader.rs b/vrp-pragmatic/src/format/problem/job_reader.rs
index 4dbcd80a4..ac7af0c5d 100644
--- a/vrp-pragmatic/src/format/problem/job_reader.rs
+++ b/vrp-pragmatic/src/format/problem/job_reader.rs
@@ -1,7 +1,9 @@
use crate::format::coord_index::CoordIndex;
+use crate::format::dimensions::JobDueDateDimension;
use crate::format::problem::JobSkills as ApiJobSkills;
use crate::format::problem::*;
use crate::format::{JobIndex, Location};
+use crate::parse_time;
use crate::utils::VariableJobPermutation;
use std::collections::HashMap;
use std::sync::Arc;
@@ -134,7 +136,15 @@ fn read_required_jobs(
.map(|p| (Some(p.location.clone()), p.duration, parse_times(&p.times), p.tag.clone()))
.collect();
- get_single_with_dimens(places, demand, &task.order, activity_type, has_multi_dimens, coord_index)
+ get_single_with_dimens(
+ places,
+ demand,
+ &task.order,
+ &task.due_date,
+ activity_type,
+ has_multi_dimens,
+ coord_index,
+ )
};
api_problem.plan.jobs.iter().for_each(|job| {
@@ -386,6 +396,7 @@ fn get_single_with_dimens(
places: Vec,
demand: Demand,
order: &Option,
+ due_date: &Option,
activity_type: &str,
has_multi_dimens: bool,
coord_index: &CoordIndex,
@@ -407,6 +418,10 @@ fn get_single_with_dimens(
dimens.set_job_order(*order);
}
+ if let Some(due_date) = due_date {
+ dimens.set_job_due_date(parse_time(due_date));
+ }
+
single
}
diff --git a/vrp-pragmatic/src/format/problem/mod.rs b/vrp-pragmatic/src/format/problem/mod.rs
index 114500a61..457670779 100644
--- a/vrp-pragmatic/src/format/problem/mod.rs
+++ b/vrp-pragmatic/src/format/problem/mod.rs
@@ -109,6 +109,8 @@ struct ProblemProperties {
has_compatibility: bool,
has_tour_size_limits: bool,
has_tour_travel_limits: bool,
+ has_job_time_constraints: bool,
+ has_min_vehicle_shifts: bool,
}
/// Keeps track of materialized problem building blocks.
diff --git a/vrp-pragmatic/src/format/problem/model.rs b/vrp-pragmatic/src/format/problem/model.rs
index 341c09a87..c5611fb00 100644
--- a/vrp-pragmatic/src/format/problem/model.rs
+++ b/vrp-pragmatic/src/format/problem/model.rs
@@ -80,6 +80,9 @@ pub struct JobTask {
/// An order, bigger value - later assignment in the route.
#[serde(skip_serializing_if = "Option::is_none")]
pub order: Option,
+ /// A due date for the task in RFC3339 format. Used for minimize-overdue objective.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub due_date: Option,
}
/// A customer job model. Actual tasks of the job specified by list of pickups and deliveries
@@ -231,6 +234,21 @@ pub struct Plan {
// region Fleet
+/// Specifies which portion of the route to include in cost calculations.
+#[derive(Clone, Deserialize, Debug, Serialize, Default)]
+#[serde(rename_all = "kebab-case")]
+pub enum RouteCostSpan {
+ /// Full round trip: depot to depot (default).
+ #[default]
+ DepotToDepot,
+ /// Outbound only: depot to last job.
+ DepotToLastJob,
+ /// Return only: first job to depot.
+ FirstJobToDepot,
+ /// Jobs only: first job to last job.
+ FirstJobToLastJob,
+}
+
/// Specifies vehicle costs.
#[derive(Clone, Deserialize, Debug, Serialize)]
pub struct VehicleCosts {
@@ -243,6 +261,11 @@ pub struct VehicleCosts {
/// Cost per time unit.
pub time: Float,
+
+ /// Specifies which portion of the route to include in cost calculations.
+ /// Defaults to depot-to-depot for full round trip costs.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub span: Option,
}
/// Specifies vehicle shift start.
@@ -276,8 +299,22 @@ pub struct ShiftEnd {
pub location: Location,
}
+/// Time constraints for jobs within a shift.
+/// Controls when the first job can start and when the last job must finish.
+#[derive(Clone, Deserialize, Debug, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct JobTimeConstraints {
+ /// Earliest allowed arrival at first job (RFC3339 format).
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub earliest_first: Option,
+ /// Latest allowed departure from last job (RFC3339 format).
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub latest_last: Option,
+}
+
/// Specifies vehicle shift.
#[derive(Clone, Deserialize, Debug, Serialize)]
+#[serde(rename_all = "camelCase")]
pub struct VehicleShift {
/// Vehicle shift start.
pub start: ShiftStart,
@@ -298,6 +335,10 @@ pub struct VehicleShift {
/// Vehicle recharge stations information.
#[serde(skip_serializing_if = "Option::is_none")]
pub recharges: Option,
+
+ /// Time constraints for the first and last jobs in this shift.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub job_times: Option,
}
/// Specifies a place where vehicle can load or unload cargo.
@@ -356,6 +397,11 @@ pub struct VehicleLimits {
/// No job activities restrictions when omitted.
#[serde(skip_serializing_if = "Option::is_none")]
pub tour_size: Option,
+
+ /// Min amount of job activities.
+ /// No minimum job activities restrictions when omitted.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub min_tour_size: Option,
}
/// Vehicle optional break time variant.
@@ -465,6 +511,21 @@ pub struct VehicleType {
/// Vehicle limits.
#[serde(skip_serializing_if = "Option::is_none")]
pub limits: Option,
+
+ /// Specifies a minimum amount of shifts each vehicle id of this type should serve.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub min_shifts: Option,
+}
+
+/// Specifies minimum shift usage requirement per vehicle.
+#[derive(Clone, Deserialize, Debug, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct VehicleMinShifts {
+ /// Minimum number of shifts that should be used.
+ pub value: usize,
+ /// Whether zero usage is allowed without violating the minimum. Default false.
+ #[serde(default)]
+ pub allow_zero_usage: bool,
}
/// Specifies a vehicle profile.
@@ -572,6 +633,17 @@ pub enum Objective {
/// An objective to balance duration across all tours.
BalanceDuration,
+ /// An objective to balance shifts across all vehicles.
+ BalanceShifts {
+ /// Controls how quickly the penalty grows as variance increases.
+ /// Lower values make even small imbalances costly. Default is 0.05.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ saturation: Option,
+ /// Scales the resulting penalty (default 1.0). Allows making shift balance more/less important.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ weight: Option,
+ },
+
/// An objective to control how tours are built.
CompactTour {
/// Specifies radius of neighbourhood. Min is 1.
@@ -581,9 +653,20 @@ pub enum Objective {
/// An objective to control order of job activities in the tour.
TourOrder,
+ /// An objective to minimize tour size violations (routes with fewer activities than min_tour_size).
+ /// Only relevant when vehicles have min_tour_size limits defined.
+ MinimizeTourSizeViolation,
+
/// An objective to prefer jobs to be served as soon as possible.
FastService,
+ /// An objective to minimize total overdue days for scheduled jobs.
+ MinimizeOverdue,
+
+ /// An objective to minimize the distance from jobs to their assigned vehicle,
+ /// compared to the nearest compatible vehicle in the fleet.
+ MinimizeVehicleDistance,
+
/// An objective to consider hierarchy of areas while serving jobs.
HierarchicalAreas {
/// Number of levels in area hierarchy.
diff --git a/vrp-pragmatic/src/format/problem/problem_reader.rs b/vrp-pragmatic/src/format/problem/problem_reader.rs
index b1aaf2628..9f00bb264 100644
--- a/vrp-pragmatic/src/format/problem/problem_reader.rs
+++ b/vrp-pragmatic/src/format/problem/problem_reader.rs
@@ -156,6 +156,11 @@ fn get_problem_properties(api_problem: &ApiProblem, matrices: &[Matrix]) -> Prob
.iter()
.any(|v| v.limits.as_ref().is_some_and(|l| l.max_duration.or(l.max_distance).is_some()));
+ let has_min_vehicle_shifts = api_problem.fleet.vehicles.iter().any(|vehicle| vehicle.min_shifts.is_some());
+
+ let has_job_time_constraints =
+ api_problem.fleet.vehicles.iter().any(|v| v.shifts.iter().any(|s| s.job_times.is_some()));
+
ProblemProperties {
has_multi_dimen_capacity,
has_breaks,
@@ -169,6 +174,8 @@ fn get_problem_properties(api_problem: &ApiProblem, matrices: &[Matrix]) -> Prob
has_compatibility,
has_tour_size_limits,
has_tour_travel_limits,
+ has_job_time_constraints,
+ has_min_vehicle_shifts,
}
}
diff --git a/vrp-pragmatic/src/format/solution/activity_matcher.rs b/vrp-pragmatic/src/format/solution/activity_matcher.rs
index c5e46c998..e3d7c740e 100644
--- a/vrp-pragmatic/src/format/solution/activity_matcher.rs
+++ b/vrp-pragmatic/src/format/solution/activity_matcher.rs
@@ -104,11 +104,13 @@ pub(crate) fn try_match_break_activity(
let route_start_time = get_route_start_time(tour)?;
let activity_time = get_activity_time(activity, stop_schedule);
+ // Filter to the specific vehicle type and shift for this tour
problem
.fleet
.vehicles
.iter()
- .flat_map(|vehicle| vehicle.shifts.iter())
+ .filter(|vehicle| vehicle.vehicle_ids.contains(&tour.vehicle_id))
+ .flat_map(|vehicle| vehicle.shifts.get(tour.shift_index).into_iter())
.flat_map(|shift| shift.breaks.iter())
.flat_map(|brs| brs.iter())
.filter_map(|br| match br {
diff --git a/vrp-pragmatic/src/format/solution/break_writer.rs b/vrp-pragmatic/src/format/solution/break_writer.rs
index 97df5a22c..e8b3d3e73 100644
--- a/vrp-pragmatic/src/format/solution/break_writer.rs
+++ b/vrp-pragmatic/src/format/solution/break_writer.rs
@@ -1,6 +1,6 @@
use super::*;
use std::cmp::Ordering;
-use vrp_core::construction::enablers::ReservedTimesIndex;
+use vrp_core::construction::enablers::{ReservedTimesIndex, get_offset_anchor};
use vrp_core::models::common::{Cost, TimeWindow};
use vrp_core::models::solution::Route;
use vrp_core::prelude::Float;
@@ -18,11 +18,13 @@ pub(super) fn insert_reserved_times_as_breaks(
.map(|(start, end)| TimeWindow::new(start.schedule.departure, end.schedule.arrival))
.expect("empty tour");
+ let offset_anchor = get_offset_anchor(route);
+
reserved_times_index
.get(&route.actor)
.iter()
.flat_map(|times| times.iter())
- .map(|reserved_time| reserved_time.to_reserved_time_window(shift_time.start))
+ .map(|reserved_time| reserved_time.to_reserved_time_window(offset_anchor))
.map(|rt| (TimeWindow::new(rt.time.end, rt.time.end + rt.duration), rt))
.filter(|(reserved_tw, _)| shift_time.intersects(reserved_tw))
.for_each(|(reserved_tw, reserved_time)| {
@@ -34,7 +36,7 @@ pub(super) fn insert_reserved_times_as_breaks(
if travel_tw.intersects_exclusive(&reserved_tw) {
// NOTE: should be moved to the last activity on previous stop by post-processing
- return if reserved_time.time.start < travel_tw.start {
+ return if reserved_tw.start < travel_tw.start {
let break_tw = TimeWindow::new(travel_tw.start - reserved_tw.duration(), travel_tw.start);
Some(BreakInsertion::TransitBreakMoved { leg_idx, break_tw })
} else {
@@ -63,17 +65,32 @@ pub(super) fn insert_reserved_times_as_breaks(
let break_time = reserved_time.duration as i64;
let break_cost = break_time as Float * route.actor.vehicle.costs.per_service_time;
- for (stop_idx, stop) in tour.stops.iter_mut().enumerate() {
+ if let Some(BreakInsertion::TransitBreakMoved { leg_idx, .. }) = &break_info {
+ // NOTE: when break was moved to the previous stop, its time window may not
+ // intersect the original reserved_tw (especially with wide offset ranges).
+ // Directly use the stop at leg_idx instead of searching by reserved_tw.
+ let stop = &mut tour.stops[*leg_idx];
let stop_tw =
TimeWindow::new(parse_time(&stop.schedule().arrival), parse_time(&stop.schedule().departure));
-
- if stop_tw.intersects_exclusive(&reserved_tw) {
- insert_break(
- (stop, stop_tw, stop_idx),
- (break_time, break_cost, break_info.clone()),
- &reserved_tw,
- &mut tour.statistic,
- )
+ insert_break(
+ (stop, stop_tw, *leg_idx),
+ (break_time, break_cost, break_info.clone()),
+ &reserved_tw,
+ &mut tour.statistic,
+ );
+ } else {
+ for (stop_idx, stop) in tour.stops.iter_mut().enumerate() {
+ let stop_tw =
+ TimeWindow::new(parse_time(&stop.schedule().arrival), parse_time(&stop.schedule().departure));
+
+ if stop_tw.intersects_exclusive(&reserved_tw) {
+ insert_break(
+ (stop, stop_tw, stop_idx),
+ (break_time, break_cost, break_info.clone()),
+ &reserved_tw,
+ &mut tour.statistic,
+ )
+ }
}
}
@@ -95,6 +112,10 @@ fn insert_break(
.iter()
.enumerate()
.filter_map(|(activity_idx, activity)| {
+ if activity.activity_type == "break" {
+ return None;
+ }
+
let activity_tw = activity.time.as_ref().map_or(stop_tw.clone(), |interval| {
TimeWindow::new(parse_time(&interval.start), parse_time(&interval.end))
});
@@ -104,6 +125,20 @@ fn insert_break(
.next()
.unwrap_or(stop.activities().len());
+ let activity_time = match &break_insertion {
+ Some(BreakInsertion::TransitBreakMoved { break_tw, leg_idx }) if *leg_idx == stop_idx => {
+ statistic.cost -= break_cost;
+ statistic.times.driving -= break_time;
+ break_tw
+ }
+ _ => reserved_tw,
+ };
+ let activity_time = if matches!(stop, Stop::Point(_)) {
+ align_break_to_activity_boundary(stop.activities(), break_idx, &stop_tw, activity_time)
+ } else {
+ activity_time.clone()
+ };
+
let activities = match stop {
Stop::Point(point) => {
statistic.cost += break_cost;
@@ -115,15 +150,6 @@ fn insert_break(
}
};
- let activity_time = match &break_insertion {
- Some(BreakInsertion::TransitBreakMoved { break_tw, leg_idx }) if *leg_idx == stop_idx => {
- statistic.cost -= break_cost;
- statistic.times.driving -= break_time;
- break_tw
- }
- _ => reserved_tw,
- };
-
activities.insert(
break_idx,
ApiActivity {
@@ -140,10 +166,10 @@ fn insert_break(
if let Some(time) = &mut activity.time {
let start = parse_time(&time.start);
let end = parse_time(&time.end);
- let overlap = TimeWindow::new(start, end).overlapping(reserved_tw);
+ let overlap = TimeWindow::new(start, end).overlapping(&activity_time);
- if let Some(overlap) = overlap {
- let extra_time = reserved_tw.end - overlap.end + overlap.duration();
+ if let Some(overlap) = overlap.filter(|overlap| overlap.duration() > 0.) {
+ let extra_time = activity_time.end - overlap.end + overlap.duration();
time.end = format_time(end + extra_time);
}
}
@@ -157,6 +183,54 @@ fn insert_break(
})
}
+fn align_break_to_activity_boundary(
+ activities: &[ApiActivity],
+ break_idx: usize,
+ stop_tw: &TimeWindow,
+ break_tw: &TimeWindow,
+) -> TimeWindow {
+ let has_overlap_with_job = activities.iter().any(|activity| {
+ if activity.activity_type == "break" {
+ return false;
+ }
+
+ activity.time.as_ref().is_some_and(|time| {
+ let activity_tw = TimeWindow::new(parse_time(&time.start), parse_time(&time.end));
+ activity_tw.overlapping(break_tw).is_some_and(|overlap| overlap.duration() > 0.)
+ })
+ });
+
+ if !has_overlap_with_job {
+ return break_tw.clone();
+ }
+
+ let duration = break_tw.duration();
+
+ let from_previous =
+ break_idx.checked_sub(1).and_then(|activity_idx| activities.get(activity_idx)).and_then(|activity| {
+ activity.time.as_ref().and_then(|time| {
+ let start = parse_time(&time.end).max(stop_tw.start);
+ let end = start + duration;
+ (end <= stop_tw.end).then_some(TimeWindow::new(start, end))
+ })
+ });
+
+ if let Some(aligned) = from_previous {
+ return aligned;
+ }
+
+ activities
+ .get(break_idx)
+ .and_then(|activity| {
+ activity.time.as_ref().and_then(|time| {
+ let end = parse_time(&time.start).min(stop_tw.end);
+ let start = end - duration;
+ (start >= stop_tw.start).then_some(TimeWindow::new(start, end))
+ })
+ })
+ .unwrap_or_else(|| break_tw.clone())
+}
+
#[derive(Clone)]
enum BreakInsertion {
TransitBreakUsed { leg_idx: usize, load: Vec },
diff --git a/vrp-pragmatic/src/format/solution/mod.rs b/vrp-pragmatic/src/format/solution/mod.rs
index 825195cab..2a5a725f7 100644
--- a/vrp-pragmatic/src/format/solution/mod.rs
+++ b/vrp-pragmatic/src/format/solution/mod.rs
@@ -97,6 +97,13 @@ fn map_code_reason(code: ViolationCode) -> (&'static str, &'static str) {
("RELOAD_RESOURCE_CONSTRAINT", "cannot be assigned due to reload resource constraint")
}
RECHARGE_CONSTRAINT_CODE => ("RECHARGE_CONSTRAINT_CODE", "cannot be assigned due to recharge constraint"),
+ MIN_VEHICLE_SHIFTS_CONSTRAINT_CODE => {
+ ("MIN_SHIFT_CONSTRAINT", "cannot be assigned due to minimum shift requirement")
+ }
+ MIN_TOUR_SIZE_CONSTRAINT_CODE => {
+ ("MIN_TOUR_SIZE_CONSTRAINT", "cannot be assigned due to min tour size constraint of vehicle")
+ }
+ JOB_TIME_CONSTRAINT_CODE => ("JOB_TIME_CONSTRAINT", "cannot be assigned due to shift job time constraints"),
_ => ("NO_REASON_FOUND", "unknown"),
}
}
@@ -118,6 +125,9 @@ fn map_reason_code(reason: &str) -> ViolationCode {
"COMPATIBILITY_CONSTRAINT" => COMPATIBILITY_CONSTRAINT_CODE,
"RELOAD_RESOURCE_CONSTRAINT" => RELOAD_RESOURCE_CONSTRAINT_CODE,
"RECHARGE_CONSTRAINT_CODE" => RECHARGE_CONSTRAINT_CODE,
+ "MIN_SHIFT_CONSTRAINT" => MIN_VEHICLE_SHIFTS_CONSTRAINT_CODE,
+ "MIN_TOUR_SIZE_CONSTRAINT" => MIN_TOUR_SIZE_CONSTRAINT_CODE,
+ "JOB_TIME_CONSTRAINT" => JOB_TIME_CONSTRAINT_CODE,
_ => ViolationCode::unknown(),
}
}
diff --git a/vrp-pragmatic/src/validation/objectives.rs b/vrp-pragmatic/src/validation/objectives.rs
index 6ab1011b6..a8f8dda88 100644
--- a/vrp-pragmatic/src/validation/objectives.rs
+++ b/vrp-pragmatic/src/validation/objectives.rs
@@ -166,6 +166,38 @@ fn check_e1607_jobs_with_value_but_no_objective(
}
}
+/// Checks that min tour size objective is specified when some vehicles have min_tour_size property set.
+fn check_e1608_vehicles_with_min_tour_size_but_no_objective(
+ ctx: &ValidationContext,
+ objectives: &[&Objective],
+) -> Result<(), FormatError> {
+ if objectives.is_empty() {
+ return Ok(());
+ }
+
+ let has_no_min_tour_size_objective =
+ !get_objectives_flattened(objectives).any(|objective| matches!(objective, MinimizeTourSizeViolation));
+ let has_vehicles_with_min_tour_size = ctx
+ .problem
+ .fleet
+ .vehicles
+ .iter()
+ .filter_map(|v| v.limits.as_ref())
+ .filter_map(|l| l.min_tour_size)
+ .any(|size| size > 0);
+
+ if has_no_min_tour_size_objective && has_vehicles_with_min_tour_size {
+ Err(FormatError::new(
+ "E1608".to_string(),
+ "missing min tour size objective".to_string(),
+ "specify 'minimize-tour-size-violation' objective, remove objectives property or remove min_tour_size from vehicles"
+ .to_string(),
+ ))
+ } else {
+ Ok(())
+ }
+}
+
fn get_objectives<'a>(ctx: &'a ValidationContext) -> Option> {
ctx.problem.objectives.as_ref().map(|objectives| objectives.iter().collect())
}
@@ -188,6 +220,7 @@ pub fn validate_objectives(ctx: &ValidationContext) -> Result<(), MultiFormatErr
check_e1605_check_positive_value_and_order(ctx),
check_e1606_check_multiple_cost_objectives(&objectives),
check_e1607_jobs_with_value_but_no_objective(ctx, &objectives),
+ check_e1608_vehicles_with_min_tour_size_but_no_objective(ctx, &objectives),
])
.map_err(From::from)
} else {
diff --git a/vrp-pragmatic/src/validation/vehicles.rs b/vrp-pragmatic/src/validation/vehicles.rs
index c70458a39..4c38aad75 100644
--- a/vrp-pragmatic/src/validation/vehicles.rs
+++ b/vrp-pragmatic/src/validation/vehicles.rs
@@ -3,9 +3,9 @@
mod vehicles_test;
use super::*;
+use crate::parse_time_safe;
use crate::utils::combine_error_results;
use crate::validation::common::get_time_windows;
-use crate::{parse_time, parse_time_safe};
use std::collections::HashSet;
use vrp_core::models::common::TimeWindow;
@@ -73,19 +73,27 @@ fn check_e1303_vehicle_breaks_time_is_correct(ctx: &ValidationContext) -> Result
.breaks
.as_ref()
.map(|breaks| {
+ // OffsetTime breaks: only structural validation (no absolute time computation
+ // against shift start, since the actual anchor is unknown at validation time)
+ let offset_valid = breaks.iter().all(|b| match b {
+ VehicleBreak::Required {
+ time: VehicleRequiredBreakTime::OffsetTime { earliest, latest },
+ duration,
+ } => *earliest >= 0. && *latest >= 0. && *earliest <= *latest && *duration > 0.,
+ _ => true,
+ });
+
+ if !offset_valid {
+ return false;
+ }
+
+ // ExactTime and optional breaks: validate against shift time windows as before
let tws = breaks
.iter()
.filter_map(|b| match b {
VehicleBreak::Optional { time: VehicleOptionalBreakTime::TimeWindow(tw), .. } => {
Some(get_time_window_from_vec(tw))
}
- VehicleBreak::Required {
- time: VehicleRequiredBreakTime::OffsetTime { earliest, latest },
- duration,
- } => {
- let departure = parse_time(&shift.start.earliest);
- Some(Some(TimeWindow::new(departure + *earliest, departure + *latest + *duration)))
- }
VehicleBreak::Required {
time: VehicleRequiredBreakTime::ExactTime { earliest, latest },
duration,
@@ -171,44 +179,6 @@ fn check_e1306_vehicle_has_no_zero_costs(ctx: &ValidationContext) -> Result<(),
}
}
-fn check_e1307_vehicle_offset_break_rescheduling(ctx: &ValidationContext) -> Result<(), FormatError> {
- let type_ids = get_invalid_type_ids(
- ctx,
- Box::new(|_, shift, _| {
- shift
- .breaks
- .as_ref()
- .map(|breaks| {
- let has_time_offset = breaks.iter().any(|br| {
- matches!(
- br,
- VehicleBreak::Required { time: VehicleRequiredBreakTime::OffsetTime { .. }, .. }
- | VehicleBreak::Optional { time: VehicleOptionalBreakTime::TimeOffset { .. }, .. }
- )
- });
- let has_rescheduling =
- shift.start.latest.as_ref().is_none_or(|latest| *latest != shift.start.earliest);
-
- !(has_time_offset && has_rescheduling)
- })
- .unwrap_or(true)
- }),
- );
-
- if type_ids.is_empty() {
- Ok(())
- } else {
- Err(FormatError::new(
- "E1307".to_string(),
- "time offset interval for break is used with departure rescheduling".to_string(),
- format!(
- "when time offset is used, start.latest should be set equal to start.earliest in the shift, check vehicle type ids: '{}'",
- type_ids.join(", ")
- ),
- ))
- }
-}
-
fn check_e1308_vehicle_reload_resources(ctx: &ValidationContext) -> Result<(), FormatError> {
let reload_resource_ids = ctx
.problem
@@ -299,7 +269,6 @@ pub fn validate_vehicles(ctx: &ValidationContext) -> Result<(), MultiFormatError
check_e1303_vehicle_breaks_time_is_correct(ctx),
check_e1304_vehicle_reload_time_is_correct(ctx),
check_e1306_vehicle_has_no_zero_costs(ctx),
- check_e1307_vehicle_offset_break_rescheduling(ctx),
check_e1308_vehicle_reload_resources(ctx),
])
.map_err(From::from)
diff --git a/vrp-pragmatic/tests/discovery/property/generated_with_breaks.txt b/vrp-pragmatic/tests/discovery/property/generated_with_breaks.txt
new file mode 100644
index 000000000..f25b49def
--- /dev/null
+++ b/vrp-pragmatic/tests/discovery/property/generated_with_breaks.txt
@@ -0,0 +1,17 @@
+# Seeds for failure cases proptest has generated in the past. It is
+# automatically read and these particular cases re-run before any
+# novel cases are generated.
+#
+# It is recommended to check this file in to source control so that
+# everyone who runs the test benefits from these saved cases.
+cc 6e31956d9fde120e15183c11b33efc809857a9cd5c43d00a00c7cca69e8a8a8b # shrinks to problem = Problem { plan: Plan { jobs: [Job { id: "b77a206e-e9b1-45cd-9c8d-0e3c19c1336f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.538988554999776, lng: 13.394171064215355 }, duration: 6.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "2d9d83d4-4477-4a61-8c40-1fb6cade0a7c", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.43469561633663, lng: 13.490457763374465 }, duration: 7.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "6760a179-f155-4780-8fc1-932001dd5f94", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.47407030309759, lng: 13.480561267856356 }, duration: 7.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "ae3e3659-113b-48ab-ab4a-c8cac3637758", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.53361134171157, lng: 13.510959120641076 }, duration: 3.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "58756b8c-afc7-4793-9e88-ce0676abf87b", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.574841395650196, lng: 13.430134893027402 }, duration: 8.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "e48a54f7-28b3-4c5b-bfd3-fbbd238a1226", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.494613032732495, lng: 13.421702998572403 }, duration: 9.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "ed65e02c-5404-4873-87a5-a18cc2203cc4", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.57372810060165, lng: 13.365673914999148 }, duration: 5.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "d23e557a-40fe-4e01-aded-61ff51c88efb", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.540659407448274, lng: 13.462438515566612 }, duration: 2.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "b283bbe5-610e-469f-844a-baedf1bcb0d6", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.48532309699349, lng: 13.449854925391785 }, duration: 8.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "dbc3d120-29fe-45fe-b8d3-9fbc0bd58e19", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.52837110295899, lng: 13.515006452883016 }, duration: 5.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "34a29eb8-91df-4eb4-846b-c42704312d77", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.528888096619625, lng: 13.462182632908753 }, duration: 2.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "878ba351-b28b-467b-87cb-553f910ed7ac", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.48650086225862, lng: 13.245454730943923 }, duration: 5.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "c8f83cbc-1796-40a6-910c-9b4182698ffa", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.53153998097877, lng: 13.553696619274293 }, duration: 1.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "4d807217-2801-4328-8eef-2a25d78feef1", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.59351423879879, lng: 13.522031579153207 }, duration: 8.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "7c14570d-773b-4040-92aa-53e76915ec52", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.58203410539741, lng: 13.379219192712544 }, duration: 9.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "96fd66b5-da0e-467e-803a-dac17d2db87b", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.436213026061736, lng: 13.54423221289806 }, duration: 3.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "de2fa84e-ecff-4d39-b09b-1ce34228b435", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.45399947586963, lng: 13.577799239615313 }, duration: 3.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "8672d540-df2b-4e46-97a0-a5203a2d0b01", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.4326941760432, lng: 13.571152898817367 }, duration: 8.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "5b2f76ff-328f-408f-aee6-e9b571c8ba4d", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.56538604923567, lng: 13.336636456435796 }, duration: 9.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "0b12bf95-ae48-4b2b-95f1-9b0f20ba36c2", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.44197820232572, lng: 13.424100233892766 }, duration: 7.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "dce62fbf-11f4-45ce-b2bf-3b77c1debc53", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.4738355028971, lng: 13.451173968374558 }, duration: 2.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "578a995f-b07b-48b0-a2c4-3dcf9f05239d", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.52886963997549, lng: 13.34420344092887 }, duration: 5.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "bb4a600d-b8e8-4529-9e31-b6d438047598", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.51299953859036, lng: 13.256978522524955 }, duration: 3.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "e8ddc973-e41a-454c-9afb-c0738c63db08", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.547199070829116, lng: 13.421784057789807 }, duration: 4.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "0f02d519-f45f-421b-91d9-b206ff274aa5", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.531798624466404, lng: 13.40982711114751 }, duration: 2.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "e93057fa-6f02-4bef-a4f4-b230593489d9", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.44054437452306, lng: 13.527774333889155 }, duration: 2.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "934be770-652f-459f-a6f2-ae86292b15ba", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.44670290382051, lng: 13.57527133189805 }, duration: 6.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "a38fc2c6-630f-49f1-ba0a-d62b034b9998", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.51250030679359, lng: 13.441971920692566 }, duration: 1.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "8ff85882-21a1-47e7-915a-a7fae1b0842f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.55122080104003, lng: 13.553211216100586 }, duration: 4.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "4c777fdc-2aba-4eb9-8b08-a9b5b3589f21", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.49899400341926, lng: 13.438333379629393 }, duration: 8.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "67bd3869-1c15-4024-80c1-161fe25c0088", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.563684562765154, lng: 13.245305053933162 }, duration: 3.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "ec7c053b-0bbe-48a3-9a64-b516fb62697b", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.54527825840168, lng: 13.232939250222046 }, duration: 7.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "f19d9360-8bcd-4635-bf2b-46ec7d2d8aa7", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.54523946108856, lng: 13.44808934100953 }, duration: 3.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "83dc6d9a-cc22-469b-b253-897937ee87b6", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.46624574701078, lng: 13.591897205192753 }, duration: 9.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "1329ab6c-481d-43de-a9b4-9423e82c628a", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.541267384505495, lng: 13.476118931400444 }, duration: 6.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "00f2686b-1a93-4ed0-9bcb-19352131d62e", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.57613534322221, lng: 13.273434367469454 }, duration: 4.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "adaa4e41-aed1-449e-b8c4-a4b51709f6c6", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.514052363905556, lng: 13.579594357176253 }, duration: 6.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "9691ef64-bbdf-461e-9140-e3f53e415759", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.48787205377354, lng: 13.3701664018665 }, duration: 8.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "bba79e67-3fc2-47b0-820f-4a0922c6b3ea", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.49349792560699, lng: 13.33603606488985 }, duration: 1.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "60f8dd78-0648-4daf-b6b3-d4872c432f16", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.57846330153393, lng: 13.261297484056687 }, duration: 5.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "8b7d9000-3f1e-44c4-8c4e-0bcb2fa9f4f2", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.49645043985017, lng: 13.448602680005264 }, duration: 8.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "dec082ea-2262-4889-963f-df7f2ee20bae", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.45217609360086, lng: 13.4518150397623 }, duration: 9.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "f3d63621-e1ef-4ae2-8c75-5c543628d7bb", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.54138584366291, lng: 13.485768479221091 }, duration: 9.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "e7765419-8a19-47df-91b6-30ee45ad8159", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.55798992838127, lng: 13.303849476632323 }, duration: 7.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "77c55ba8-24f1-4df3-b43e-7dcdc6475751", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.47588585869974, lng: 13.250850301903446 }, duration: 1.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "e6fe2431-fbfc-413a-913d-f078ce48a6a5", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.4685372095073, lng: 13.40226305398881 }, duration: 6.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "25d33cba-0264-4f92-8a3e-7cb2c0d56e3f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.51126571690904, lng: 13.44000655360524 }, duration: 2.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "80e4daf4-69d7-4592-a505-947c5f1eb2ac", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.54360751158137, lng: 13.582374257359495 }, duration: 2.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "60163230-82ee-4ad3-bd4c-e193d8ff5568", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.51204167017678, lng: 13.312293611551086 }, duration: 6.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "c392218f-8399-4840-8b5f-de671c2feea2", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.49721770636632, lng: 13.35447939918559 }, duration: 7.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "9835723e-918f-46fe-9475-1a1495497799", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.55856633325322, lng: 13.550532433283012 }, duration: 4.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "00d10428-1a29-4425-a63c-33799af65b64", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.49483405432941, lng: 13.294263104704443 }, duration: 7.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "3d32341e-05ca-494c-bb4e-fdf175d9e3dc", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.48309360084576, lng: 13.273644353758348 }, duration: 2.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "a93b52a6-55a5-43f8-b2d3-c50b27969185", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.44779036630319, lng: 13.293824744496565 }, duration: 1.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "e16393bf-bd9e-47d3-84f5-49bcde74a1e0", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.55130762122905, lng: 13.437716259478952 }, duration: 6.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "89917c55-d238-444a-85fc-d679fa87a155", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.44435408708289, lng: 13.3773716232696 }, duration: 3.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "8e12de63-331d-433a-82dd-b06fe2d2f7e3", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.43048669602292, lng: 13.589206795069135 }, duration: 3.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "85e1df9f-183e-4852-9642-91226a8c52a6", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.53003862627003, lng: 13.496235650159244 }, duration: 8.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "412b0ce7-7809-4305-b871-70fdd54971be", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.440813130120915, lng: 13.541475857418128 }, duration: 6.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "e4b04ff9-9d47-463a-b5d7-d9855997a7d7", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.57996014033966, lng: 13.431475705501501 }, duration: 4.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "796acd93-6be7-49fe-9df6-db942bb630d4", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.568432811510334, lng: 13.35384720300933 }, duration: 7.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "af906cdf-a998-4571-9f3a-9895b798c041", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.47991060287366, lng: 13.421408516985313 }, duration: 5.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "180fdb3e-3cbb-4b4d-916a-d7a5e520acad", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.50264978497095, lng: 13.285101387349615 }, duration: 9.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "e0d3cb53-be5f-453b-a15f-4cef3d01353a", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.49370753533861, lng: 13.384344650871736 }, duration: 1.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "442fd871-6c2c-4c15-b9ff-5c077f38c55b", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.53649699083927, lng: 13.42929832424187 }, duration: 9.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "3c6e4bdc-8e49-4259-aa13-c11be9bce160", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.54484626485402, lng: 13.553884277380863 }, duration: 5.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "96744d94-0e42-4745-8619-1ffc740e0699", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.57295470900415, lng: 13.545580940989804 }, duration: 4.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "56abed92-7c79-4baa-9d68-2674f15fb5d9", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.44959547798697, lng: 13.323859376007224 }, duration: 9.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "90e40c7d-d26a-4b88-b26b-2fc32c78659f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.53217620362142, lng: 13.454014961160746 }, duration: 8.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "ddc3be0c-db16-4bd9-ba4b-9b4c1926ed50", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.54728369747927, lng: 13.257829365743088 }, duration: 8.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "5c2c8975-2f90-4632-b119-29ffa444ec58", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.44073320402728, lng: 13.52796708365387 }, duration: 3.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "f7519827-7e23-44bf-989f-21463580ce15", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.428195209718034, lng: 13.44900920048565 }, duration: 8.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "0503f14a-a6de-4d25-a155-cc2b3efa121f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.42870577125016, lng: 13.395243620776933 }, duration: 5.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "2e723354-c5fa-4db3-af98-c33ed84470f7", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.57223630351569, lng: 13.342247435724113 }, duration: 5.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "777e9ad2-eff7-4035-a688-cbfa9e156ddb", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.46519833913668, lng: 13.3980390193097 }, duration: 8.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "c5fb251e-4e64-4ef1-a4ff-2decd293afe0", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.583763480218344, lng: 13.295162559011507 }, duration: 6.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "ac1a62b1-c423-4246-91a2-2d20882627ed", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.582688166783825, lng: 13.21864064574483 }, duration: 8.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "0b9bba05-d0fd-4d2f-a7d5-e6bbdf9091a3", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.567982367756315, lng: 13.566030203397967 }, duration: 2.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "81f36aa4-fbcd-41ad-bf48-d6a843aaf3a1", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.56402285646309, lng: 13.292272796882125 }, duration: 9.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "43e22dd7-c5fc-454a-8dd9-74e69d4e68dd", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.4607326529727, lng: 13.353815947239063 }, duration: 1.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "58ef37dd-65e7-4b5f-a594-ff1b8d2ed594", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.43291283313233, lng: 13.217755618941773 }, duration: 6.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "9bcbef0b-a3df-4dba-aca8-8a01e982dea2", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.495767639191655, lng: 13.38799691900038 }, duration: 5.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "01cdbb15-b27b-4158-a8b6-436435feaca4", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.48548543927216, lng: 13.348969705070798 }, duration: 2.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "1313b1da-6b8c-4428-895c-d0b2dd9e30c3", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.55037413108857, lng: 13.592289001171707 }, duration: 4.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "21587807-762e-49e3-a6c8-80b8e4ca8a49", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.45498725870821, lng: 13.522551476798293 }, duration: 9.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "862c9928-5590-4629-940a-7bf0bb6788a9", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.45055213652355, lng: 13.293848820258992 }, duration: 4.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "db7403dd-be84-47c3-8e18-c2e9d1914801", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.569213629774154, lng: 13.55133740361682 }, duration: 5.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "853cdbfa-a355-4aa1-97da-a979a6e027c2", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.532767563138705, lng: 13.457705958067985 }, duration: 8.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "0cab3cac-6675-4a86-b292-0a9638e1e7a5", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.496304196084154, lng: 13.442138985807889 }, duration: 8.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "01a17285-0b15-4d3e-9b49-74291f841300", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.52314823737531, lng: 13.295426845860181 }, duration: 8.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "50e73129-cb4f-478a-8b8d-1c478c241d25", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.515183789241505, lng: 13.516740032417651 }, duration: 6.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "4cb72b30-2f08-43f7-9b9a-6ddae50bdaf2", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.45284762568731, lng: 13.47084835020825 }, duration: 1.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "803e3044-fe2f-40e8-a06e-08a396dd2d7b", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.534146563287464, lng: 13.446720685216558 }, duration: 8.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "fa551d0b-db76-4ac7-b31a-52cc184e9405", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.49095485189454, lng: 13.256592563448926 }, duration: 1.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "ddfee833-87cc-4cb8-b29a-bccbff355e51", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.578821721344106, lng: 13.286874571268768 }, duration: 5.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "ea930e10-a704-41c2-b7f7-9319abcb6735", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.58619070003504, lng: 13.396124163748533 }, duration: 3.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "5920abbd-c976-4d35-ada6-f0bd750f12bf", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.46470707524205, lng: 13.426924688800518 }, duration: 6.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "2a4c27fa-97e0-45aa-b2d7-7936bf46145c", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.467566878510546, lng: 13.407006718354415 }, duration: 2.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "02eff787-685f-48fb-bc77-b9aabad240d2", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.4450250549485, lng: 13.377962093848636 }, duration: 4.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "0655bfdb-b7fd-47cd-9db4-783e69aeb967", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.49962333644365, lng: 13.298816468012687 }, duration: 9.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "c42cbde8-a4f3-4e93-a4f4-08e9276c57cf", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.49160781637281, lng: 13.491891751071886 }, duration: 8.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "0a5c9336-0c9b-4411-a4d3-9a3e71f41e0d", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.46031538525668, lng: 13.356334146218046 }, duration: 2.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "6421a5cc-8783-44eb-b954-b61a5fb2cd28", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.49783588953804, lng: 13.358162362607958 }, duration: 1.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "8884eff9-b37b-4fc0-9d0a-45d3237f886d", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.45649384571965, lng: 13.307231204508454 }, duration: 5.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "8a7cc5ba-168f-41b6-97b5-adf9ed1ee61a", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.45601728425319, lng: 13.493286714140744 }, duration: 7.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "8dc51df5-647d-4466-b729-abbb2b7522f6", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.58539913250217, lng: 13.345369179788785 }, duration: 8.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "2de521ba-9ebd-4572-843b-b1935ff5df08", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.53255555949437, lng: 13.483819013749205 }, duration: 4.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "106a3c74-6853-47ef-8db1-77bf3e1d4cb1", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.463314364849744, lng: 13.241461699498107 }, duration: 1.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "9a225847-a421-4ff6-b5c2-e74a5409b497", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.446098622789684, lng: 13.33321239735338 }, duration: 9.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "523ad239-652f-42a4-9166-5dfaa149bd6f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.5476745503787, lng: 13.4464677883206 }, duration: 1.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "164df5ab-912a-4521-90ba-c0d7cf5609fa", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.47891149789606, lng: 13.350998665744731 }, duration: 8.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "1ae8e884-b0b0-4d94-aba8-fa26c7290ce2", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.45669448128818, lng: 13.425825265082668 }, duration: 5.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "0496f576-2f97-4393-89d8-0a7e5fd739e2", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.51130877264465, lng: 13.405207983922 }, duration: 4.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "88f482e7-35b3-472b-a0f5-3fb7c4268fc0", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.5090951942607, lng: 13.452166676102067 }, duration: 1.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "883bb142-ee33-4401-b07f-5c4221ecdd17", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.42492132408168, lng: 13.56496958088482 }, duration: 2.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "a726b031-0ec1-4034-b9be-9ebb829ef597", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.441212549308055, lng: 13.42480233826529 }, duration: 6.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "1d99752d-1ac2-424a-9b53-ceaff07444d5", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.53656993795126, lng: 13.306452236405983 }, duration: 3.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "b8431cac-c68a-4d05-8a65-45564446f995", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.55509635405892, lng: 13.248756526533272 }, duration: 5.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "5dd1dbe3-afff-499e-9410-e14d9fdc0dc7", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.54469150439603, lng: 13.407927134079024 }, duration: 2.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "9f418aa7-bc3e-4432-aa90-54f53826f258", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.4370679657065, lng: 13.472845224064843 }, duration: 2.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "b6c2615f-04bf-4bd4-bbba-334254bd605a", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.481345109603176, lng: 13.57488593899773 }, duration: 2.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "e7ed6f82-31ce-41f2-acd6-11f501d26c28", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.550203158587664, lng: 13.332308312249333 }, duration: 5.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "83bff5c7-4d67-4c63-b26a-66ecb3b7375b", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.53676453021639, lng: 13.440957654801378 }, duration: 9.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "94ff93bf-96b1-417d-acfc-9c4f7ccba320", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.440414075180584, lng: 13.406083729060644 }, duration: 9.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "cb3f23cd-5a5d-4420-bb55-895f6f95338f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.53862410643122, lng: 13.403692679980214 }, duration: 6.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "0d8c4cdd-a8af-45ce-882b-76f9faf71649", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.58201466889436, lng: 13.511356345475143 }, duration: 5.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "77bb5b29-eed7-4d15-a04f-d8ca2558c7e0", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.57067221221036, lng: 13.327417022305887 }, duration: 4.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "cd6ea807-1109-4b53-aef7-152c61095e26", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.55438094463705, lng: 13.562127603387887 }, duration: 2.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "969973aa-3c09-49ca-8213-2ed73bea939a", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.46016811325727, lng: 13.283941940620782 }, duration: 1.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "5c3d061e-5fdf-47ea-8c40-9079614e6c6e", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.482033904402435, lng: 13.354409559560494 }, duration: 8.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "a1af5e6c-b562-4d40-bb33-04556dbb157f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.459060995004165, lng: 13.303213697819027 }, duration: 2.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "14969b69-0094-47dd-a6fe-a4602c331a46", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.531183100230535, lng: 13.556343547335567 }, duration: 7.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "25f58be8-7bd8-4a9b-93cc-b663eb1ca0ee", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.559284160828255, lng: 13.33117865265016 }, duration: 3.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "b888d39c-a538-45c6-8e3c-6d55d3e5b525", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.52989899260824, lng: 13.248902550381018 }, duration: 9.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "6539371b-a854-4376-826b-0a17f0e4965e", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.53005804208648, lng: 13.361220914213794 }, duration: 8.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "6eeb4c65-fea9-48ad-b1c4-56866b94171e", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.521279493613136, lng: 13.326814687166301 }, duration: 2.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "aae0ea2b-19ad-418b-9f79-db60a487ec8e", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.498695473187766, lng: 13.33362105626098 }, duration: 3.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "f4a6c7a0-26ea-45ed-b685-1841606fb257", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.55475629870741, lng: 13.297037690212472 }, duration: 3.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "cbc1d43b-ea0f-4899-8ed8-c738963bad72", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.47679529518342, lng: 13.255208422887621 }, duration: 5.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "9c3c9933-3058-436e-b3e8-7db7710b308e", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.54388981299108, lng: 13.438852932534731 }, duration: 8.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "77374321-57db-4f7a-a1ae-0977789d7c39", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.56080519865974, lng: 13.347166509306897 }, duration: 6.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "a0bf76f8-9330-4c6d-9529-937442bc717d", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.547381088367736, lng: 13.3407888341539 }, duration: 5.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "42cfcc37-7d21-4de7-a836-4463c9f514d9", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.58830218982775, lng: 13.343911177433206 }, duration: 6.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "deb13383-0995-4064-8a1e-93198ac2e333", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.44096674936214, lng: 13.5739800410373 }, duration: 1.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "df432b48-2996-4538-b1c8-bba9dc39c76b", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.51613173444303, lng: 13.288341274490133 }, duration: 9.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "794e4738-6a49-47ec-ab2e-dde15b557b8f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.54186962398254, lng: 13.462381237321413 }, duration: 6.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "06c082c3-2e8e-48ef-8398-812da14a1344", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.49209794880127, lng: 13.387431749962007 }, duration: 5.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "2bafb2af-eeb0-43a3-ab03-3baed2751306", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.47956221556207, lng: 13.220304827490917 }, duration: 3.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "ff58b53a-52bc-4ba3-a235-6cd28b123709", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.58189677617132, lng: 13.562832152864154 }, duration: 2.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "50492d1d-f6b8-4e55-bedf-266544a3b6b2", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.47338087407438, lng: 13.40345824751322 }, duration: 5.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "08aa69bf-889b-48b7-bf1d-5d4ba04e3193", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.585717727140576, lng: 13.550630627235362 }, duration: 7.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "1bffcb18-b3fa-4cbc-883b-56dd5092c5b4", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.5140658700889, lng: 13.380426521890762 }, duration: 3.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "94688c93-22e9-48de-ae35-2ffebda78b16", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.539734907032425, lng: 13.276391310943838 }, duration: 2.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "099c23f2-b3c9-4279-a579-9cfb2667d44b", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.440857394051626, lng: 13.572086473814837 }, duration: 2.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "aafaeb8e-1612-4951-b13d-02c0df37868f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.44896029664281, lng: 13.341582562156994 }, duration: 1.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "d6cdfe18-30cb-4ac6-9ebd-60336ae3061f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.486914331389116, lng: 13.272234947791672 }, duration: 3.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "513e69d3-6a6d-4ee6-b28c-e744a9eac898", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.54318089474043, lng: 13.332925808532243 }, duration: 1.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "8762422e-3551-4c43-9d3f-12ed9bb0517a", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.53385494883482, lng: 13.592449737796176 }, duration: 5.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "8ee0af93-1eff-481c-8249-222b42310fdf", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.528992977120126, lng: 13.374199577707973 }, duration: 4.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "df6b4b07-cfea-4ea9-95e3-65f93dab37b1", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.45915279963482, lng: 13.23207460677081 }, duration: 2.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "6dd6c106-ad87-4582-9715-2cdca565b865", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.57745506644664, lng: 13.542157452746839 }, duration: 2.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "44da8696-c21a-4035-9de4-b56e342275cd", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.54778134811768, lng: 13.539259002391699 }, duration: 2.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "df490b5a-a1e7-4594-b295-327c236c07b8", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.53555332553019, lng: 13.507779317469861 }, duration: 9.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "92ce5fed-6805-491f-b1f4-4e23e4aa313c", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.4396383481888, lng: 13.31313421757028 }, duration: 1.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "a06c2d71-44bb-4aae-b01f-63dddac87846", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.572802991113846, lng: 13.40556682720577 }, duration: 9.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "af900c1f-61af-44b4-9cb1-c5ad2969dce8", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.43328045612902, lng: 13.246411216811008 }, duration: 4.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "de9add15-3fde-408b-9b93-73eb0c723ab8", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.455963849265345, lng: 13.44312749644016 }, duration: 3.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "cab60cb4-7df1-47b4-9e37-8b40d07355a6", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.537917706584935, lng: 13.58093071511715 }, duration: 1.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "478d8e5d-730b-4e40-bb07-be138c61fc08", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.540243740839905, lng: 13.390209590428906 }, duration: 8.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "94388f79-7b4c-4ba9-acb9-929647fa5b8a", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.51384695395234, lng: 13.38324108784404 }, duration: 7.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "35a68599-9d15-4659-a6cb-d1970e2c5907", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.536436919046935, lng: 13.578778450398527 }, duration: 8.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "b6515da8-d39a-4277-bd9a-7f290a440bce", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.57388933940643, lng: 13.592570579929678 }, duration: 6.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "495550fd-9617-4710-bf24-0520494ad7b6", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.46366226070118, lng: 13.485509968301043 }, duration: 5.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "74890130-cc91-4970-8e3f-ceff765ad9d8", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.56940991362257, lng: 13.584007931591886 }, duration: 4.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "efb58663-63e9-433b-844a-d9755cfe951d", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.52698316318844, lng: 13.422953292464591 }, duration: 3.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "3d557994-09fb-473c-8d79-10417cb85f65", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.546307928585755, lng: 13.478294452638025 }, duration: 4.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "b155c74a-4cff-4d1a-ac84-e634f6c7b83a", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.47611818515231, lng: 13.219402482520632 }, duration: 5.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "8245aeea-33fc-4263-b294-63bcb6a77bf3", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.46749280666004, lng: 13.395711391778667 }, duration: 5.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "b351dacc-18ba-4f69-b3c3-a952d13d71c5", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.53550860984654, lng: 13.39367554068401 }, duration: 6.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "93fe0f14-f1b0-47c1-9967-111f301168b8", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.440176176947254, lng: 13.532269304796472 }, duration: 9.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "ffca8e16-67e3-45ba-b629-0220ee035c3e", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.45984341578757, lng: 13.594857452151945 }, duration: 7.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "5dfb7fad-8d20-42e3-a982-53f71cc56de5", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.54012041361669, lng: 13.26961239032952 }, duration: 6.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "65b7a04b-4fdc-4a78-b2a2-a4df6663126a", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.4720083995417, lng: 13.348248964815875 }, duration: 2.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "9efe6b5b-e0ce-4d21-9f27-cd19087cc571", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.5133970018286, lng: 13.580797179311293 }, duration: 4.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "cca64368-e3ff-4bb5-8970-1d072ceaecdd", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.5064081882435, lng: 13.335333899756888 }, duration: 1.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "835f2ab9-8dbe-451f-bdd5-8d010d3d4e7c", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.482346203551515, lng: 13.40082192079835 }, duration: 3.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "c012b104-65b7-4926-9388-bcac76cedf97", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.59118964257993, lng: 13.287963385844279 }, duration: 6.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "bfc04aac-7166-417a-845b-a6217752ae7c", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.43402485666114, lng: 13.563967088118671 }, duration: 2.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "dfe4dfa9-6b8e-4da6-b543-5cca56945f2f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.452442154603766, lng: 13.278993641072738 }, duration: 3.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "6d94beb4-e975-46b5-aa3d-134ca6367f4d", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.48773153443073, lng: 13.413160866173106 }, duration: 4.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "28cd557d-773b-48c1-a21b-73964d24d634", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.547772397437384, lng: 13.506787913786688 }, duration: 4.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "4b172733-aaca-410e-932d-2466ed515839", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.57028097606339, lng: 13.322861405544385 }, duration: 5.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "50c65b82-22b0-4953-918d-e62b25d0312c", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.56234641723677, lng: 13.249871181699318 }, duration: 2.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "2bb61db6-943e-4886-8bef-0aa95261adb0", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.48614110182912, lng: 13.304308418676934 }, duration: 1.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "d9bb098b-d044-49f2-9aaa-589be195ba56", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.47722555672548, lng: 13.4054254268118 }, duration: 6.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "d7eec2d7-e2ce-4e98-8b8b-5bf6a3be7696", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.4894767366615, lng: 13.325621196594781 }, duration: 2.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "c76785bf-cfce-4d1c-975f-4bb841959605", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.470079294622714, lng: 13.490201067577273 }, duration: 4.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "1f210e9d-f241-4bf0-8929-4bf628549cba", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.48915787682694, lng: 13.498039513974941 }, duration: 9.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "8087ab8a-e698-4028-8eb0-8a9cbf492f31", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.504336727093055, lng: 13.492782866359715 }, duration: 9.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "ac609c1c-a83c-4728-8033-cd15acdab219", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.49613223605223, lng: 13.546518453611807 }, duration: 1.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "47e6112a-102d-4aed-8cb2-cc1c4abbb247", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.58533776524385, lng: 13.522089564982446 }, duration: 1.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "bc7bf628-3d5f-4e52-a301-7ca35aef90df", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.50296207433479, lng: 13.346113421475186 }, duration: 9.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "3d8b80e4-b391-4d9a-9d35-0c2d11cd4213", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.5639280263376, lng: 13.326896095627818 }, duration: 7.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "d55b25db-9eb5-4dc8-8576-0ca6c09faadc", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.589550263336896, lng: 13.226189897089991 }, duration: 6.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "083a8edf-0e2a-42c6-9a1f-56d16faa67ef", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.54972795011843, lng: 13.555408061201028 }, duration: 6.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "2d0ab6ab-4950-40e5-b936-888cac261983", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.43158375243407, lng: 13.563109612688878 }, duration: 6.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "7bb13c04-9e7f-4b98-bad3-9ea7ae14c6f6", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.522048738481026, lng: 13.236142067037601 }, duration: 6.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "871ba006-a40a-4b5f-9267-96c041baecac", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.4597130570776, lng: 13.522138849268192 }, duration: 8.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "1a142df1-2a69-4ba4-a8c3-a98773ac8829", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.50997638829733, lng: 13.427082746825077 }, duration: 2.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "b3a98918-7114-4e97-80e2-a8397148b083", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.546413253600605, lng: 13.543082255719852 }, duration: 2.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }], relations: None, clustering: None }, fleet: Fleet { vehicles: [VehicleType { type_id: "f13f8c76-a6ae-4df0-bf30-78b6d9ed4235", vehicle_ids: ["f13f8c76-a6ae-4df0-bf30-78b6d9ed4235_1", "f13f8c76-a6ae-4df0-bf30-78b6d9ed4235_2", "f13f8c76-a6ae-4df0-bf30-78b6d9ed4235_3"], profile: VehicleProfile { matrix: "car", scale: None }, costs: VehicleCosts { fixed: Some(30.0), distance: 0.0015, time: 0.005, span: None }, shifts: [VehicleShift { start: ShiftStart { earliest: "2020-07-04T09:00:00Z", latest: Some("2020-07-04T09:00:00Z"), location: Coordinate { lat: 52.46644836504377, lng: 13.401890745253167 } }, end: Some(ShiftEnd { earliest: None, latest: "2020-07-04T18:00:00Z", location: Coordinate { lat: 52.46644836504377, lng: 13.401890745253167 } }), breaks: Some([Optional { time: TimeOffset([13778.0, 14807.0]), places: [VehicleOptionalBreakPlace { duration: 69.0, location: None, tag: None }], policy: None }]), reloads: None, recharges: None, job_times: None }], capacity: [47], skills: None, limits: None, min_shifts: None }, VehicleType { type_id: "1b9c73f0-5943-42bb-b55f-fa566f8d8a73", vehicle_ids: ["1b9c73f0-5943-42bb-b55f-fa566f8d8a73_1", "1b9c73f0-5943-42bb-b55f-fa566f8d8a73_2", "1b9c73f0-5943-42bb-b55f-fa566f8d8a73_3"], profile: VehicleProfile { matrix: "car", scale: None }, costs: VehicleCosts { fixed: Some(20.0), distance: 0.002, time: 0.003, span: None }, shifts: [VehicleShift { start: ShiftStart { earliest: "2020-07-04T09:00:00Z", latest: Some("2020-07-04T09:00:00Z"), location: Coordinate { lat: 52.50196568589801, lng: 13.558172577326951 } }, end: Some(ShiftEnd { earliest: None, latest: "2020-07-04T18:00:00Z", location: Coordinate { lat: 52.50196568589801, lng: 13.558172577326951 } }), breaks: Some([Optional { time: TimeOffset([5520.0, 6181.0]), places: [VehicleOptionalBreakPlace { duration: 11.0, location: Some(Coordinate { lat: 52.576860059801, lng: 13.268198230359719 }), tag: None }], policy: None }]), reloads: None, recharges: None, job_times: None }], capacity: [31], skills: None, limits: None, min_shifts: None }], profiles: [MatrixProfile { name: "car", speed: None }], resources: None }, objectives: None }
+cc f9e291559ecc6d1ef2bef060d2a254876842bc0104dbbf291882da0f35f02a2f # shrinks to problem = Problem { plan: Plan { jobs: [Job { id: "7dc08940-104a-4e2b-9510-d60aef96be5b", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.473041086648635, lng: 13.393412910970302 }, duration: 9.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "b9318557-615f-4e86-b9d4-615495f9cc7e", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.47084365180268, lng: 13.321970011147043 }, duration: 4.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "ba1a4e34-3944-4440-a871-a0666bcb3aa1", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.44002209084763, lng: 13.512711618318722 }, duration: 3.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "a0a4fda3-5a0e-4bd8-be2c-7c70a2a71afc", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.553774590428056, lng: 13.288797010006633 }, duration: 4.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "3b73653f-63df-4fdb-8abe-0472342f9e3d", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.42905870463015, lng: 13.55792888421158 }, duration: 2.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "69040893-8106-49ab-8652-fc0728da047d", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.436524035684265, lng: 13.24404979789138 }, duration: 8.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "1fe1661d-96c0-4aba-8004-0f9e1273a235", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.57906575193932, lng: 13.374425990658168 }, duration: 9.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "3db1cfb0-663d-4e5d-b1bb-6df44436b560", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.5183670980797, lng: 13.384144826990664 }, duration: 9.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "71bad6fb-8553-49df-9bd8-e591e7790442", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.50522259624702, lng: 13.532406791984938 }, duration: 1.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "b62f39ec-0bea-4579-811c-aa5158201386", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.58778943552134, lng: 13.37236261044958 }, duration: 6.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "f3c77ee7-dd3c-4c62-93d8-94440ef8759d", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.50472313127957, lng: 13.373152644606511 }, duration: 3.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "48eed1dd-a283-43b2-b02a-51a9d978dd71", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.47261030479377, lng: 13.231615903654516 }, duration: 7.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "266c44bc-f1ea-437f-a816-9f9678ae8e43", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.44562067940165, lng: 13.236541440499984 }, duration: 7.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "755f90d2-3c1a-44f7-b4a4-62f7eb3b7f76", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.57639498385679, lng: 13.469669595376605 }, duration: 9.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "0fb32b24-fc29-4941-8eb8-e6905ab81da0", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.57297506609125, lng: 13.586076017006533 }, duration: 2.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "281f2397-c6eb-4ec2-b573-8d9adcc2db6f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.4841521502621, lng: 13.451040438993713 }, duration: 3.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "1eed9709-bb07-44fe-aaee-5262b0e37f9d", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.545200162492876, lng: 13.516520037008275 }, duration: 3.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "cd6cb76c-d713-4528-bd57-b41061b67ab6", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.43900781052958, lng: 13.301993900896914 }, duration: 6.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "53e5b16f-a993-42e8-9f35-0340877a7a8a", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.49238124597424, lng: 13.446701639740088 }, duration: 5.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "ee598aae-c55e-4c35-aecd-66eef4716724", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.56236248540754, lng: 13.4259997573967 }, duration: 1.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "c30b586d-6dad-42d6-ba92-37c7c5c6637e", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.517902297730856, lng: 13.31287262538185 }, duration: 4.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "45653a9c-718a-40cf-9b98-750ff6a4d5cc", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.5817424979823, lng: 13.588045304730429 }, duration: 7.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "408190ea-19cc-4ab4-8e78-78a7b7acb752", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.56063813656303, lng: 13.362261522712357 }, duration: 1.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "faaa1d9b-a922-4047-9bc8-24ca92b36000", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.55370263879013, lng: 13.370558838094755 }, duration: 2.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "266b2744-a2a4-41ae-af37-781736fe5459", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.529049926046866, lng: 13.569754082673029 }, duration: 4.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "89739e15-682f-4003-b826-f6f7ffa584f7", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.43597993743977, lng: 13.425657429594922 }, duration: 4.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "14eaa783-c217-4f77-a9d4-b549f0c36c34", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.48735823165926, lng: 13.275933325449639 }, duration: 3.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "bf36ed78-9e0d-4370-aaa8-6eb6c4ef930d", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.485936642449886, lng: 13.540578818057192 }, duration: 7.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "0d456c77-ac82-45b5-9115-0154d466d507", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.43172208394491, lng: 13.379977748495662 }, duration: 1.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "aaf30bf4-195e-411f-ae1a-bb7944fc5c15", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.442688778966584, lng: 13.232423800130775 }, duration: 8.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "fb0437b4-b1ec-4b82-a859-f1b4098c71b0", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.54521826864851, lng: 13.416552547456158 }, duration: 6.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "ebee59d7-aa4c-4a65-8f10-36c412d80653", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.56306155944408, lng: 13.494501768871167 }, duration: 4.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "d5764c40-eb5c-4baa-9b2f-f913c62788f5", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.50563036265164, lng: 13.291866845339477 }, duration: 8.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "f0044156-b382-41a0-bdcb-4600dcbb620c", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.42632527250704, lng: 13.395853489984747 }, duration: 4.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "7dd64624-cd87-4771-8ac5-9fcc195db468", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.49258921099793, lng: 13.29610752483297 }, duration: 7.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "b7114e2b-b09f-4597-a004-4bf13e359084", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.573065213874244, lng: 13.324514021678784 }, duration: 9.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "445558ef-7284-413f-8798-dc70ed99c7ec", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.47330811443622, lng: 13.34694334418335 }, duration: 9.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "d40d83cf-227b-4459-9ed0-4b810c13e979", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.529313469744245, lng: 13.474884019005136 }, duration: 1.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "33dade8e-39d2-4f83-adb5-c13e944c7f3a", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.53354381700989, lng: 13.423011770778356 }, duration: 4.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "e96e910d-3aee-4359-8c5d-b5c8d8f3e493", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.501040197517796, lng: 13.330237927357745 }, duration: 2.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "56d51496-908d-419d-ac4a-c9abca6d800f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.52438915220155, lng: 13.460964824778488 }, duration: 8.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "f43ea697-68c1-4666-8b98-0e3f66398167", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.45314978958264, lng: 13.356412965432177 }, duration: 1.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "fd718336-8c25-4104-88c0-d8e31c773658", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.569935795355114, lng: 13.478022531528723 }, duration: 3.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "7a2ab349-9373-4b57-ab99-5f968d6cf72e", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.440399708604915, lng: 13.44735824481886 }, duration: 9.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "7208fb58-b290-4a7c-94fb-8039da171050", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.591950745034616, lng: 13.480713581650159 }, duration: 2.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "f5bc179f-2a0e-4211-adcf-4f83a6265b7a", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.48608055412091, lng: 13.297081407873549 }, duration: 8.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "3ab44165-7ddb-40ca-a834-44f9d6e4d3b4", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.53134441406069, lng: 13.266627191732477 }, duration: 7.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "ca6daac2-d855-4b78-b6ab-03215016baf3", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.451946521931774, lng: 13.514778079003268 }, duration: 4.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "1bea644a-b4c4-4ca4-a582-e047bbbe2027", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.45026676641414, lng: 13.222912791876109 }, duration: 8.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "8923c127-53f7-46c2-826a-521758702fef", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.57914320225788, lng: 13.543383958340177 }, duration: 6.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "a4e362ea-d916-4f47-8306-9036448ef2cf", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.473177339980936, lng: 13.29851719405926 }, duration: 5.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "9944b228-613a-4aa2-9acc-a74703f7bd97", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.58252145028346, lng: 13.526341248969599 }, duration: 2.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "b9e1c6be-7031-43e0-a137-dfdece8ce52b", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.49916388311676, lng: 13.479778561999698 }, duration: 4.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "a16241bb-d5a3-47ad-bc42-b563acc27532", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.47058326507756, lng: 13.296427748870693 }, duration: 5.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "72ee229a-0a85-4a59-843a-4a4598ed8947", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.48841370508134, lng: 13.548875128246218 }, duration: 5.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "037648a1-fc64-4b18-80f5-0daef23a93c3", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.432699741936574, lng: 13.389974275390143 }, duration: 7.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "31880f2d-b98e-419b-a204-aafd449a30bc", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.446919207262816, lng: 13.407850751489667 }, duration: 9.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "e7fdf77f-5f9a-4aaf-b1e0-21e5953f5af6", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.5093254629489, lng: 13.50768835436565 }, duration: 2.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "ecfa7860-de52-4d35-bff3-52ef094b33cf", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.47806768957937, lng: 13.285195396592691 }, duration: 6.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "cb04c44e-3419-490f-93b0-b0b8dd6db213", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.489123931045604, lng: 13.364848020825582 }, duration: 3.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "b6a3c686-4aa2-4c9a-8548-9de45efc532f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.565860115392255, lng: 13.545564446603898 }, duration: 6.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "4ce6eb84-d3a5-4887-96f7-ff7d973a1b26", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.51470618264471, lng: 13.561371559134887 }, duration: 4.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "c5c9294e-e862-46f8-941e-0671b3cbc301", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.52577578309586, lng: 13.382408512637687 }, duration: 1.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "5699d0a9-fe01-4927-97b3-5ad6c6fcad04", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.52460501837997, lng: 13.519688540248316 }, duration: 9.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "9ed04c36-73da-4e3c-ad59-af77f3dd18fa", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.589046959183285, lng: 13.250987494748818 }, duration: 5.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "1467b884-dc0a-4db5-a757-d141d012334e", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.43561398256824, lng: 13.466039930489314 }, duration: 9.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "0184b6e7-3146-4658-98a7-9552bfa2e830", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.592082078849344, lng: 13.285000237002698 }, duration: 7.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }], relations: None, clustering: None }, fleet: Fleet { vehicles: [VehicleType { type_id: "d160a6f2-295d-47fd-9d48-4a2288506f15", vehicle_ids: ["d160a6f2-295d-47fd-9d48-4a2288506f15_1", "d160a6f2-295d-47fd-9d48-4a2288506f15_2"], profile: VehicleProfile { matrix: "car", scale: None }, costs: VehicleCosts { fixed: Some(30.0), distance: 0.0015, time: 0.005, span: None }, shifts: [VehicleShift { start: ShiftStart { earliest: "2020-07-04T09:00:00Z", latest: Some("2020-07-04T09:00:00Z"), location: Coordinate { lat: 52.532329480232185, lng: 13.587172616249967 } }, end: Some(ShiftEnd { earliest: None, latest: "2020-07-04T18:00:00Z", location: Coordinate { lat: 52.532329480232185, lng: 13.587172616249967 } }), breaks: Some([Required { time: ExactTime { earliest: "2020-07-04T10:07:35Z", latest: "2020-07-04T10:07:36Z" }, duration: 2656.0 }]), reloads: None, recharges: None, job_times: None }], capacity: [44], skills: None, limits: None, min_shifts: None }], profiles: [MatrixProfile { name: "car", speed: None }], resources: None }, objectives: None }
+cc 8be2a96ffefcaa0a7995577c167750a5240398f7a0eca00802e047959b085f16 # shrinks to problem = Problem { plan: Plan { jobs: [Job { id: "b295e8fa-aec1-429e-90ab-fbdafb0a2130", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.486953104185865, lng: 13.561024561827534 }, duration: 5.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "54a559fb-aacb-427e-891a-ffdff35a4f50", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.557427799122685, lng: 13.345254246602071 }, duration: 7.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "094012aa-37ec-49b6-9895-df233451d1a4", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.431676441831684, lng: 13.553244797080223 }, duration: 1.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "bb864480-eb9b-4ad5-aaaa-493f6839c3e7", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.516512658260304, lng: 13.569624752741582 }, duration: 3.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "f21bfd87-4366-442a-a30e-ca8e9fbac423", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.49810738762818, lng: 13.447631397378196 }, duration: 3.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "5b575e97-859b-410b-8b51-4c19fcc47989", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.52045885425346, lng: 13.243126953081195 }, duration: 8.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "745961f4-6997-4a1b-8eca-788c453654f6", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.53403576182338, lng: 13.47692706042089 }, duration: 1.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "2e41c308-2cef-4cc6-8257-fd09aa258992", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.52386144611341, lng: 13.469324996992414 }, duration: 1.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "014fcbba-4261-43e0-a0ec-d7d3972bd7e7", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.466804448712615, lng: 13.271934330520581 }, duration: 8.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "0ca76861-bed5-4eb7-b4fe-65aa2a882dad", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.454351144449426, lng: 13.28727283189494 }, duration: 1.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "084750de-89a6-4001-bf2f-db4d90938e4f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.549694690633025, lng: 13.449351463852521 }, duration: 9.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "a1040abe-0fe5-49c4-a303-bf422d6b5d43", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.591045780611296, lng: 13.518660725150703 }, duration: 1.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "d7c3ebb1-7983-4ed5-a0e6-c094d296df8c", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.43583580952312, lng: 13.56696827946083 }, duration: 7.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "0dd9fb61-5bbb-4370-8afa-ba466656eed2", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.586647827280856, lng: 13.335534197545607 }, duration: 8.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "d69dcc24-fe7b-4cb0-ad4e-ee48c20b459a", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.533136910498186, lng: 13.282286613076167 }, duration: 8.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "3fc734a4-fe53-46f6-8596-46e7b418db17", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.49453700750613, lng: 13.421815713686081 }, duration: 2.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "1674216f-04ed-425b-bccd-d24956e28dbd", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.457621847106054, lng: 13.361124751712412 }, duration: 5.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "103e668f-cb88-4458-a5ea-26459878df3b", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.446855200707, lng: 13.30818321864331 }, duration: 3.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "50971ce9-a3d6-443b-9e94-22a36e19dec8", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.56728083649838, lng: 13.483137947641094 }, duration: 2.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "a4adea62-5cc3-4c8f-b96e-914c4609a92b", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.57981996062938, lng: 13.322835618226126 }, duration: 2.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "b8864b7e-a15b-49fe-95e0-7c8968174829", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.46612281682343, lng: 13.491893003312347 }, duration: 7.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "135fe680-e53d-4d5a-9f48-eca14b4cc470", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.43802111087568, lng: 13.481385671321195 }, duration: 1.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "7a8ba8b3-886a-4cbf-830d-f0b266560ae2", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.515914919547754, lng: 13.276130819890966 }, duration: 7.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "b9bca7af-308e-43c5-9a14-d56d2c814022", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.53658558620783, lng: 13.31937374546347 }, duration: 4.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "d9057d69-c6ca-46a3-b082-f9b039fec18e", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.44196164446899, lng: 13.431895823532836 }, duration: 9.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "962e874a-ffeb-4f23-bd54-cb89c3e59b6c", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.47264436793881, lng: 13.405144268067454 }, duration: 1.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "25b45328-0052-470c-b200-c8f9c9dc380e", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.442963786582816, lng: 13.589049120230547 }, duration: 8.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "1153257e-ce61-4430-9791-7ed9d0f4763d", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.43597883260041, lng: 13.459995121267298 }, duration: 5.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "801ab6c1-2940-473e-b953-574b8a2cf443", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.48940304179968, lng: 13.272571870094348 }, duration: 1.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "204f764a-0edd-443c-baa0-6e869cb0faf8", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.53568645692666, lng: 13.544270810936991 }, duration: 7.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "1971ea4f-f747-48b6-8bb5-fe5f7df241da", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.45087917656169, lng: 13.44793341237729 }, duration: 4.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "97ea0381-c566-4c09-954a-e5feeb72de29", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.55150688521086, lng: 13.52084837324932 }, duration: 9.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "a77495c4-1a80-4399-90a1-b11f7343747f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.52602153479888, lng: 13.296200618997116 }, duration: 5.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "eaa7cdc4-2142-4675-bbdf-08835892f6fa", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.56487726468955, lng: 13.562001006185548 }, duration: 4.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "ec510b58-40df-4945-ac74-6d936b39baa0", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.462094303639866, lng: 13.358583184370751 }, duration: 1.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "3963c7da-7f85-43f7-8b1e-fd287967b6b4", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.554094518492015, lng: 13.571857619214613 }, duration: 2.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "39ff97d8-46e2-4b55-bc45-3ac2cf0cde7a", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.429985217281725, lng: 13.388946275779725 }, duration: 1.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "1325297e-a1e8-4acf-ab55-60754fd00f9a", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.431621986475605, lng: 13.46911558888015 }, duration: 5.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "4f9f0bff-a5bb-4b6c-90af-a6145f7812d5", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.509900492929866, lng: 13.529296144252756 }, duration: 2.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "4ddbed51-504b-4564-8766-d03d2b09586b", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.43479453793578, lng: 13.223578075649536 }, duration: 7.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "003aea2d-2d83-44f3-8a67-c61db1a73284", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.48870452019681, lng: 13.291650379211758 }, duration: 8.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }], relations: None, clustering: None }, fleet: Fleet { vehicles: [VehicleType { type_id: "1b2f00dd-a149-4730-ab3c-6efc6fae2cd2", vehicle_ids: ["1b2f00dd-a149-4730-ab3c-6efc6fae2cd2_1", "1b2f00dd-a149-4730-ab3c-6efc6fae2cd2_2", "1b2f00dd-a149-4730-ab3c-6efc6fae2cd2_3"], profile: VehicleProfile { matrix: "car", scale: None }, costs: VehicleCosts { fixed: Some(30.0), distance: 0.0015, time: 0.005, span: None }, shifts: [VehicleShift { start: ShiftStart { earliest: "2020-07-04T09:00:00Z", latest: Some("2020-07-04T09:00:00Z"), location: Coordinate { lat: 52.53633468761949, lng: 13.469271231787173 } }, end: Some(ShiftEnd { earliest: None, latest: "2020-07-04T18:00:00Z", location: Coordinate { lat: 52.53633468761949, lng: 13.469271231787173 } }), breaks: Some([Optional { time: TimeOffset([11555.0, 12432.0]), places: [VehicleOptionalBreakPlace { duration: 18.0, location: Some(Coordinate { lat: 52.53594073425772, lng: 13.50351195997104 }), tag: None }], policy: None }]), reloads: None, recharges: None, job_times: None }], capacity: [31], skills: None, limits: None, min_shifts: None }, VehicleType { type_id: "703e78d9-1dd2-4402-bd20-a031536bc8ae", vehicle_ids: ["703e78d9-1dd2-4402-bd20-a031536bc8ae_1", "703e78d9-1dd2-4402-bd20-a031536bc8ae_2"], profile: VehicleProfile { matrix: "car", scale: None }, costs: VehicleCosts { fixed: Some(20.0), distance: 0.002, time: 0.003, span: None }, shifts: [VehicleShift { start: ShiftStart { earliest: "2020-07-04T09:00:00Z", latest: Some("2020-07-04T09:00:00Z"), location: Coordinate { lat: 52.46305213923088, lng: 13.282137060276076 } }, end: Some(ShiftEnd { earliest: None, latest: "2020-07-04T18:00:00Z", location: Coordinate { lat: 52.46305213923088, lng: 13.282137060276076 } }), breaks: Some([Optional { time: TimeOffset([5173.0, 5844.0]), places: [VehicleOptionalBreakPlace { duration: 42.0, location: Some(Coordinate { lat: 52.47678687840374, lng: 13.529931975715298 }), tag: None }], policy: None }]), reloads: None, recharges: None, job_times: None }], capacity: [40], skills: None, limits: None, min_shifts: None }, VehicleType { type_id: "f1d25f2a-701d-481d-bdf2-99dea38f96cf", vehicle_ids: ["f1d25f2a-701d-481d-bdf2-99dea38f96cf_1", "f1d25f2a-701d-481d-bdf2-99dea38f96cf_2"], profile: VehicleProfile { matrix: "car", scale: None }, costs: VehicleCosts { fixed: Some(20.0), distance: 0.002, time: 0.003, span: None }, shifts: [VehicleShift { start: ShiftStart { earliest: "2020-07-04T09:00:00Z", latest: Some("2020-07-04T09:00:00Z"), location: Coordinate { lat: 52.44040468195033, lng: 13.224510355707949 } }, end: Some(ShiftEnd { earliest: None, latest: "2020-07-04T18:00:00Z", location: Coordinate { lat: 52.44040468195033, lng: 13.224510355707949 } }), breaks: Some([Optional { time: TimeWindow(["2020-07-04T13:00:00Z", "2020-07-04T15:00:00Z"]), places: [VehicleOptionalBreakPlace { duration: 30.0, location: Some(Coordinate { lat: 52.4447434021244, lng: 13.534792132789535 }), tag: None }], policy: None }]), reloads: None, recharges: None, job_times: None }], capacity: [36], skills: None, limits: None, min_shifts: None }], profiles: [MatrixProfile { name: "car", speed: None }], resources: None }, objectives: None }
+cc 98fccc14667073e24d5cb364042d3a440d0d61b95e682a6331208659bf95230a # shrinks to problem = Problem { plan: Plan { jobs: [Job { id: "664d4b2e-299d-4fb6-b84b-928c2ee52421", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.44418460987773, lng: 13.445977838858301 }, duration: 9.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "c555d703-625a-40c6-abda-68a4eb603a7f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.55322500284135, lng: 13.559547248921502 }, duration: 1.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "81ddbc10-66dc-45c0-b139-9223f2882e2a", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.57418595613651, lng: 13.24175316241901 }, duration: 2.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "664f00bb-0540-48cc-af5a-343670e581be", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.45772064482617, lng: 13.559403782053897 }, duration: 3.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "a5369170-e054-4338-a2b5-04f82a7e3582", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.52491390539944, lng: 13.538542249067632 }, duration: 4.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "c7aad991-a632-44a0-b3b1-a1b76135e331", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.47324985272417, lng: 13.473928779257454 }, duration: 3.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "04091f3f-4416-46ff-9a6d-61183107cb7c", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.57963398128164, lng: 13.56583915268176 }, duration: 8.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "5de72ceb-b968-4fed-8186-b483af60c4c3", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.55249316644722, lng: 13.348603228684693 }, duration: 4.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "dd081c47-3ab7-4f5e-b945-2fa9047cc81d", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.58509545811544, lng: 13.547366680610704 }, duration: 8.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "95f8ed69-c561-47c9-b030-31dfd6bbe027", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.52148234309863, lng: 13.35379896470347 }, duration: 2.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "794b7468-8ad4-4026-a2ae-fd3856b09261", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.45722274734229, lng: 13.342285512140785 }, duration: 9.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "070fb850-441b-4ce4-8ab7-eae22ac35f04", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.49001649344307, lng: 13.319808443404186 }, duration: 9.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "007cd733-a3bf-43c8-8815-37c0eb2349cc", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.43121275873318, lng: 13.523782723494953 }, duration: 8.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "f4e600b2-a2b6-40f2-8798-1275bc9a7508", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.480207319688, lng: 13.310953794868073 }, duration: 6.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "bcf44009-7906-4550-a377-c632c4fcfd5f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.45037174308171, lng: 13.467363191594318 }, duration: 2.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "8817e755-5e49-456d-8d83-ddf502802d9d", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.5879488845627, lng: 13.394537091956641 }, duration: 2.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "9da1e817-dd3a-4561-a734-91f603c2d5b9", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.4243504580123, lng: 13.287187791349105 }, duration: 4.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "004de5c8-b3c8-4fca-9508-d219b4c6fecc", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.47964677608287, lng: 13.295845974047428 }, duration: 7.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "326be34b-e379-405b-a249-7cae98bbda5f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.547098293376, lng: 13.58583352672208 }, duration: 4.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "a08eb516-f4e4-43a3-b6ca-cd930c37c379", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.49257813717986, lng: 13.271046641575968 }, duration: 3.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "3a10cb87-c1c3-4270-81d4-ea309aead611", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.460221531245644, lng: 13.320390385686805 }, duration: 8.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "d3d0b24b-6a4c-48d7-bda3-3a7998198c92", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.58339582134225, lng: 13.362071506921291 }, duration: 1.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "410cbbaf-e1dc-47b1-8da3-0488108a4937", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.46839138227835, lng: 13.318872830259021 }, duration: 7.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "bdafd1d0-15c2-4fdc-abaf-6962d7414489", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.50371851300001, lng: 13.571836903967657 }, duration: 5.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "76699e1a-8c00-4d69-9eb9-e2e0ab5d20f7", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.55172642024452, lng: 13.300033217494411 }, duration: 4.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "7e0de4b8-a53b-466a-8533-527755e00177", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.446986809341446, lng: 13.52574718589523 }, duration: 1.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "222b122f-9bee-464e-84b9-0c1ed64a8bd2", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.57570357864288, lng: 13.321137145874367 }, duration: 5.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "514fe316-91b4-4759-a66d-3527af661304", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.47548571860302, lng: 13.449063013510546 }, duration: 3.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "cf66813c-8f85-43dc-a8da-19cfe465194f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.42744903107648, lng: 13.465024742405145 }, duration: 5.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "dd94c7df-ad79-41a0-94bd-391926d99515", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.52296792777305, lng: 13.219357101845732 }, duration: 2.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "49874989-be23-491c-9867-0e5be67c1505", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.48314582531678, lng: 13.404901295060071 }, duration: 5.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "ba6fff90-97af-4019-a39e-6c7b43935039", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.4260114487118, lng: 13.522203884013376 }, duration: 7.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "ab5f5987-373f-421c-8da6-26fb1029c92c", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.481221141284514, lng: 13.44083024042531 }, duration: 6.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "fc380287-d9d9-499a-846e-18ba78631609", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.452860343619065, lng: 13.385582006852818 }, duration: 5.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "b94a90c7-4f19-4572-882b-8159b2729fed", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.58370122789077, lng: 13.573913265370871 }, duration: 2.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "73a126de-7bfa-452f-a6d5-8070b776d6a0", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.512450265960695, lng: 13.55365937541387 }, duration: 3.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "e21d0a4a-56e3-4c91-ba41-8409d4b20fc6", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.5373525228078, lng: 13.26175318756936 }, duration: 3.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "da1d3d67-2d0e-4f7d-8572-9e5ec1e8c80a", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.49100696382359, lng: 13.593478507393755 }, duration: 2.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "50057bd0-691c-4741-9548-c4435e3facfc", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.4865638641106, lng: 13.453257406135563 }, duration: 4.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "cd978943-745c-454e-bdb9-8a2a5f85cf95", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.44495359974663, lng: 13.548926236200996 }, duration: 4.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "142cfa90-ed71-46fa-95bc-08336ba437bf", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.51872189359819, lng: 13.285389811370441 }, duration: 9.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "5bf43a33-5009-423c-b7da-66aa7512dbc8", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.42935435922719, lng: 13.513454694868376 }, duration: 4.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "5a5dd095-d129-4637-9edf-41b9dacca51e", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.479885403096254, lng: 13.445981964445474 }, duration: 7.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "5d7beae0-4976-4beb-8d3d-680a582c68db", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.48758895426152, lng: 13.358379283275786 }, duration: 7.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "c5230336-8bc0-4f60-8655-707965a898a3", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.451244175529595, lng: 13.231480986732656 }, duration: 8.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "3b0bd29b-9396-4655-b756-1e7734724e30", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.54646629467548, lng: 13.359347448379593 }, duration: 2.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "b5e3a191-b9f5-4d7c-b8ce-a48207330264", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.43441082238736, lng: 13.43847203002988 }, duration: 6.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "46a00595-95c0-4001-a9cf-698d3cc4cdf9", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.5333116847194, lng: 13.384632535655117 }, duration: 1.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "d48626b6-71da-4a90-9ebd-56dc1097a35e", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.48742610453203, lng: 13.576326613084165 }, duration: 4.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "231c8a58-57ae-4917-850d-b1cf2809afdf", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.57503668284466, lng: 13.274119614514875 }, duration: 5.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "9eaa534e-3e31-48d7-8f21-6d8b486d1ea7", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.47898161748369, lng: 13.345020133012326 }, duration: 6.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "3afe2a65-9265-4a24-9952-05baa5b66bf3", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.43730579637051, lng: 13.517815283760552 }, duration: 4.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "e2a9c65b-8a92-4365-85dc-244e0b61725f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.58508256600894, lng: 13.377640188556384 }, duration: 3.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "3c5fffda-178e-4b17-a7f6-56658d47532f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.56612597197098, lng: 13.541848934872247 }, duration: 1.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "50b584a8-497c-4d34-ae37-22a96820c0e4", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.480683192746056, lng: 13.588154067979803 }, duration: 2.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "8724a255-7012-48eb-b789-70b7e3696f4a", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.445282326201536, lng: 13.280875926893307 }, duration: 4.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "7e4763ff-2b45-4ef9-86a9-8c82cd842e55", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.45367154420382, lng: 13.351804745548089 }, duration: 4.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "836c0a35-ed1c-4ae8-923a-ab4a2a81f5b3", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.42727248632929, lng: 13.569380791906989 }, duration: 1.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "2bac75f2-dc54-4326-aa89-faf6fd0800ce", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.42532693667911, lng: 13.32809707208384 }, duration: 9.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "fa101ff1-ae15-4271-8282-69b7b448e6e2", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.50265572316235, lng: 13.315419600817153 }, duration: 5.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "27a0261d-7e04-40bb-a52f-437261ccd218", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.45646433840729, lng: 13.564008898624285 }, duration: 1.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "78358f75-59dd-42fc-b698-d2fd3ff45e14", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.44450952043492, lng: 13.336721455228645 }, duration: 2.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "f7840f06-9ebd-4ee3-ad20-d08b55862524", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.46396570641827, lng: 13.294721635719949 }, duration: 8.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "94ef6ca1-84ec-4e2a-b98d-843417d5d6ff", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.47969799713151, lng: 13.248723767036378 }, duration: 5.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "d6013689-6b44-449c-bf9a-9329f8521306", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.430076802856036, lng: 13.430967100161988 }, duration: 1.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "f4853e88-0c27-4a1a-ae2d-0c129d072eb3", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.56995858781359, lng: 13.388636338580547 }, duration: 2.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "28977be6-dbb5-4d53-97bb-a686a1dab56d", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.566189316005875, lng: 13.530657134610376 }, duration: 5.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "039c3d78-1a1e-470d-a4ad-a83a8c997b17", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.457433816939535, lng: 13.307640072345247 }, duration: 7.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "c60956cf-72e6-42ab-8466-c2703a44e640", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.54947199962397, lng: 13.321530266179197 }, duration: 9.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "987c1540-2b3e-402b-b2dd-9a2d5bb78745", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.45123298756491, lng: 13.353271591187744 }, duration: 7.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "1b76ff51-00ae-4e6f-9fcf-3e8845e0360c", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.448893317658936, lng: 13.242415735177447 }, duration: 6.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "cb953947-7b48-47a1-9abf-c13f0817c6dd", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.48033751544455, lng: 13.328014888243493 }, duration: 9.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "bde71120-43da-4906-8e6b-b91ce7edd945", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.51651448453383, lng: 13.50607249686771 }, duration: 5.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "9cdc416c-77cb-41b6-baa8-02c3b33ffe33", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.53397232813673, lng: 13.29511818542456 }, duration: 3.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "7a855ec2-74b8-4652-a776-dda19615b630", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.48177737375512, lng: 13.382039307154239 }, duration: 9.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "bada747d-ebaf-4710-8312-297f5ff4dc60", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.574677247802335, lng: 13.328180125282598 }, duration: 3.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "a2dda0ae-fc83-450b-8a0d-1885b367fa02", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.53766872242129, lng: 13.564820799885043 }, duration: 5.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "b3e4e1e4-4293-4c09-8fc3-5bdbcd024c9c", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.51646169116841, lng: 13.327831247157027 }, duration: 6.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "b1e638b5-5d60-4154-8f24-6e425f7c530d", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.50898418567914, lng: 13.306089977815246 }, duration: 5.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "1f19d5f4-fcd3-4138-98ed-86f1a796f545", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.54427753190411, lng: 13.575359386244676 }, duration: 4.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "5e1aa74c-5e18-406c-8367-010809673147", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.43644796995659, lng: 13.502644760453112 }, duration: 7.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "c2c29d3d-ac39-4e39-b138-7d996bc94aba", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.4967905801139, lng: 13.469866977271336 }, duration: 5.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "489ce574-fa5c-4de0-abac-16def9359abc", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.525169725212216, lng: 13.305743710346642 }, duration: 3.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "79ee6d8f-5867-4686-938b-19eb95079686", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.575836490199876, lng: 13.439548022954545 }, duration: 1.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "630833ae-d991-4523-a3c8-3b98c571e11f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.56257261286016, lng: 13.329524272671614 }, duration: 1.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "6b9df9f2-b43f-48a2-a05f-fe53d007cd1d", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.492666172456964, lng: 13.265378894497502 }, duration: 8.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "ad8c7d92-1a72-443c-94a9-b5933dc2741e", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.463579150276594, lng: 13.429340321552877 }, duration: 3.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "75adc69a-ebba-4de5-b66d-7273b49de110", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.52442657625314, lng: 13.517209961282235 }, duration: 9.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "ffc93815-17bd-49c8-a185-6d82189787db", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.52783946955706, lng: 13.378244414782896 }, duration: 8.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "6b909ecc-025c-4816-a9d5-b578a1891cfb", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.4770893249816, lng: 13.328751124443585 }, duration: 7.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "1ad5c041-6881-421f-ab6a-8a4c0d091bdc", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.56595901657335, lng: 13.444757100778368 }, duration: 5.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "4fc7aad1-4dfc-4ec4-8885-d8426b8aba23", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.57677222476066, lng: 13.336094090424616 }, duration: 3.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "312a891e-c56a-4ee9-b173-6c3dc80b7d3b", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.489472213176406, lng: 13.528645295496204 }, duration: 8.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "bfa8ea89-1c3f-460a-bfe6-66b2c04b8eb2", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.453026759559, lng: 13.305108949778035 }, duration: 6.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "13fa6057-1bdf-412f-b301-1ab9d82daab7", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.579049063099546, lng: 13.258371367006509 }, duration: 3.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "69f54976-735c-44ff-94ae-e40e367bfa48", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.47304622515708, lng: 13.263325167199056 }, duration: 1.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "1669105e-1d77-4efd-8a6d-c68d7de5e8f5", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.5300680106584, lng: 13.40804671764052 }, duration: 6.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "4a91cf58-b62d-4712-a216-330cd496079f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.46515041686424, lng: 13.554517083448907 }, duration: 2.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "6e8baaf3-cb14-4644-b42f-26c3df326386", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.47307668959643, lng: 13.582467440109042 }, duration: 5.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "7046eb41-42ea-4a64-a60b-f08a44509fcf", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.434702921781565, lng: 13.539388347965426 }, duration: 9.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "8a8ee939-062c-4cb7-acf4-e3e2918f5de7", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.42978702914164, lng: 13.335914267302293 }, duration: 4.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "fb329b68-1427-4ea9-beb9-e6fbb38397bb", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.503847107691314, lng: 13.452387163803255 }, duration: 1.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "f24dd34d-b15c-4f99-9b97-819d8f69646e", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.507170912939145, lng: 13.27010965252276 }, duration: 2.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "0fd60af2-86af-4906-b849-0d1554e53c16", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.50596309595814, lng: 13.437850044731402 }, duration: 9.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "b08cebb2-c819-4d6f-b1d6-3c77929c0aaf", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.4862604011969, lng: 13.225722230393469 }, duration: 6.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "b7e7e483-7a93-42bd-99c7-bd5d4b1a0a86", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.58409339194098, lng: 13.261245221383794 }, duration: 4.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "73c39c38-9a0e-46de-be46-90cb8ce55a8b", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.554282626266435, lng: 13.232391775768045 }, duration: 9.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "218a3594-c4ea-40e2-83fa-22ad2d257ea4", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.49791474146677, lng: 13.230219634724222 }, duration: 6.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "db1e4fa3-6bc4-4a24-9b8e-0ada00b071b6", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.586489227625954, lng: 13.290170835682611 }, duration: 2.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "57b31297-66ca-4627-8e77-d29c5f4036a7", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.58420439746396, lng: 13.564466371853754 }, duration: 1.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "586783d8-af93-4d0b-9652-cf951a992405", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.588534944027984, lng: 13.289212501264068 }, duration: 3.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "2bafeaec-7d64-4c93-82a0-18fb456c9bc4", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.55991883571875, lng: 13.263771588981434 }, duration: 5.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "0861df09-b31a-4a78-966f-72f0b47baed6", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.477711696804725, lng: 13.498324634506288 }, duration: 2.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "b1ad401e-5e2c-401d-ad13-859d23302341", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.52671839066392, lng: 13.531441222927093 }, duration: 6.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "e2ef2a45-a48f-4f96-8ab2-a9c020d9b23e", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.58976571077067, lng: 13.319095694517246 }, duration: 6.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "ca09c8ca-fe72-4ee2-9138-eb91d463d8bc", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.51187175338785, lng: 13.430218358325941 }, duration: 2.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "cbb487c9-96b8-4598-8627-c6879af1ed37", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.541360927846014, lng: 13.555886022543717 }, duration: 9.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "6cae8a28-cf8a-42e9-abe3-c44fe73f19dc", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.51100117733669, lng: 13.392819014502457 }, duration: 7.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "dc4d8221-6617-48be-a636-5c380fd2ea49", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.49822616522383, lng: 13.333031055200781 }, duration: 6.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "b4f94cf7-0402-472b-9864-d49809356052", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.55245432367412, lng: 13.371877300217484 }, duration: 7.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "54e90cd7-65bb-4257-aae8-e5707517b20c", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.48705403758826, lng: 13.585911424948417 }, duration: 4.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "848c301d-e9d2-458a-a3d7-f6065bf59bf0", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.5840072316375, lng: 13.49460199368934 }, duration: 4.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "5fa4de7e-233d-4474-b931-8a75be2db7be", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.49081817453084, lng: 13.394168165075147 }, duration: 7.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "de0430dc-e773-4920-aa2d-ab6c02d30b7a", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.4784936903927, lng: 13.510303392782767 }, duration: 2.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "e7137a80-07ce-45f0-9d88-9069a3c066b0", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.59277746275365, lng: 13.311480077482194 }, duration: 8.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "4e8c686e-2f85-4d33-93a0-ab40db22452c", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.56972876171516, lng: 13.291968734544207 }, duration: 5.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "b552f76a-7378-4950-85e7-d95ab1edb3b1", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.582697809147874, lng: 13.365680773293832 }, duration: 7.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "4e3838f9-6d1f-4276-8572-8b6883989766", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.46753086770457, lng: 13.489172737225514 }, duration: 7.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "b74f5043-80a2-4bbd-9139-88441b4bb39e", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.47307164061433, lng: 13.516492378723298 }, duration: 9.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "7f15bf98-e172-4f3a-97e9-1b37ce207a1e", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.574030730573114, lng: 13.412505124733098 }, duration: 4.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "d6e1497e-483d-4fc9-86f8-7117d99df967", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.461800984962125, lng: 13.501360480986525 }, duration: 8.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "042888ab-d304-45ca-96bc-155bb951229d", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.54731387195531, lng: 13.518758442467233 }, duration: 6.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "f3ae549c-ef7e-4e06-b287-42d80bf6c1ae", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.46052028294311, lng: 13.331237354347648 }, duration: 9.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "a31a18f1-17de-471b-a04e-cc7d41031160", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.47954591114776, lng: 13.514274279894197 }, duration: 7.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "f9b673e9-e86b-47c6-80b5-8992109d7e6f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.5149266114185, lng: 13.26602970002951 }, duration: 9.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "7c4695f7-9bb8-4191-b645-71328ca39540", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.49255196972856, lng: 13.58977704603514 }, duration: 5.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "14c44924-7c41-4d48-be42-8fbaa355c170", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.586152611649574, lng: 13.246114464638929 }, duration: 1.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "d45b221e-cb7e-44c0-919f-abf0e7a035e3", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.43342767661759, lng: 13.49067068151916 }, duration: 5.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "7afd5013-5c7a-4cb8-b865-a710091168e4", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.52505171957411, lng: 13.586315937049203 }, duration: 8.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "9104f28d-3ed3-4a2d-aa26-bdc6b20b8e0c", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.49854241398211, lng: 13.245634441867221 }, duration: 6.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "0622a9f1-dd1c-412b-8107-6edb9c80b5b0", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.50258482987289, lng: 13.360339257880561 }, duration: 7.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "8859de97-dbf6-4c88-9a6d-afa7ec9c19d9", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.577788923313335, lng: 13.447454874114637 }, duration: 9.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "0a1af0b4-56fa-487b-89b1-40d5fd9faf82", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.53878786508347, lng: 13.338836702385604 }, duration: 5.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "1d1407d7-ef8a-4f40-979b-efb5fcb013cf", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.53893424311592, lng: 13.45441086329057 }, duration: 5.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "3037d16b-2e5a-4d51-966f-75894f734cb8", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.46047345435608, lng: 13.446755424993357 }, duration: 8.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "9f121cc6-13fe-4d24-a7a7-00d8cf2dccc5", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.48842778133907, lng: 13.593074398108303 }, duration: 6.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "d957ff3d-6f77-4326-a73e-0c076dc47165", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.42902904695536, lng: 13.523013450647316 }, duration: 9.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "d720194b-55f0-4a12-878a-6cd26cedff53", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.519629471233806, lng: 13.583292087786257 }, duration: 5.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "d90c9a0e-a3df-4c80-bae3-ad29cc7c3455", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.534599163777806, lng: 13.306742399781468 }, duration: 9.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "45b893dd-1c77-4978-9a89-19ee4a59917e", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.47206570335138, lng: 13.475943412350006 }, duration: 7.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "50491ad9-e604-4729-b26d-56974be1b368", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.48487309611562, lng: 13.52865438272355 }, duration: 3.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "5c52aeb4-eccb-4c61-abd6-556f05171c43", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.50068335108318, lng: 13.41318460934654 }, duration: 7.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "54a6ddff-d157-4fea-b6d4-8cabc4fb0a3e", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.46786160887342, lng: 13.249321199552886 }, duration: 3.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "00f72fb9-a988-41ea-8150-f62e0acc96f3", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.49400541117457, lng: 13.237130021675522 }, duration: 6.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "883ec707-199d-40f2-84e4-8d00612f6546", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.47025857794244, lng: 13.56267934934915 }, duration: 4.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "38755978-425d-4ad4-ad64-12f365c76318", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.48270482789289, lng: 13.489692218220107 }, duration: 9.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "cf78a768-2cfa-4541-917c-e0b29f167faa", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.42980037767111, lng: 13.219895162034591 }, duration: 6.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "8f158466-126a-4b65-9a73-97c5cd7e7f02", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.55916528993291, lng: 13.411208343779666 }, duration: 8.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "e7e20d6b-6807-4d8c-b5bd-8682b6206b17", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.55134275943254, lng: 13.423788676201347 }, duration: 6.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "f7c494b3-ea9c-49b6-8600-0ec04944d114", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.51311466506583, lng: 13.390927857591372 }, duration: 8.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "82c4da4f-9326-4e4d-9ee1-d32c8313800f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.48325100490651, lng: 13.347481544297588 }, duration: 8.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "1882197e-0e45-4be3-9029-9057ae3b0bb3", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.55772125979835, lng: 13.286733984100534 }, duration: 7.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "6bc819f7-8be9-4924-9a9c-f39f1fcc2e3c", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.474941409913505, lng: 13.266855565453541 }, duration: 9.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "5e3dd635-6a5e-4697-83ff-1f78b9e31b94", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.56709657513478, lng: 13.398144986293676 }, duration: 4.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "0c10cbc4-1510-4b74-83f4-45333ff0de8f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.45271001324661, lng: 13.338706629028865 }, duration: 4.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "854bb135-923e-477a-8431-6f5da0135921", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.511030823692096, lng: 13.218894160984469 }, duration: 9.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "c22ae642-083d-415b-9cfe-699b4887e2aa", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.53450758297474, lng: 13.48711095427638 }, duration: 7.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "4b057f73-fb86-4042-a9f5-7aae087b4a0e", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.431422717827715, lng: 13.50813567812832 }, duration: 4.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "e1d26fff-5d33-42df-9798-f574aaaf5657", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.51126939458414, lng: 13.557899948715914 }, duration: 7.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "124bde1a-1d26-49a2-b25c-5ef82a7219be", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.51758068254245, lng: 13.46171403827469 }, duration: 1.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "42321413-918e-4ff6-996e-c6f09ffc3a0c", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.45864857592488, lng: 13.533637794784047 }, duration: 8.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "b7df686e-63bf-45e6-a952-6c37bfd5c528", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.5546635937754, lng: 13.402311566735786 }, duration: 2.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }], relations: None, clustering: None }, fleet: Fleet { vehicles: [VehicleType { type_id: "ee30fb51-0682-479c-8868-fc2bd8d7fa9e", vehicle_ids: ["ee30fb51-0682-479c-8868-fc2bd8d7fa9e_1", "ee30fb51-0682-479c-8868-fc2bd8d7fa9e_2", "ee30fb51-0682-479c-8868-fc2bd8d7fa9e_3"], profile: VehicleProfile { matrix: "car", scale: None }, costs: VehicleCosts { fixed: Some(20.0), distance: 0.002, time: 0.003, span: None }, shifts: [VehicleShift { start: ShiftStart { earliest: "2020-07-04T09:00:00Z", latest: Some("2020-07-04T09:00:00Z"), location: Coordinate { lat: 52.473067493093396, lng: 13.359957452773127 } }, end: Some(ShiftEnd { earliest: None, latest: "2020-07-04T18:00:00Z", location: Coordinate { lat: 52.473067493093396, lng: 13.359957452773127 } }), breaks: Some([Optional { time: TimeOffset([4857.0, 6013.0]), places: [VehicleOptionalBreakPlace { duration: 97.0, location: Some(Coordinate { lat: 52.57138551651986, lng: 13.381473504749222 }), tag: None }], policy: None }]), reloads: None, recharges: None, job_times: None }], capacity: [39], skills: None, limits: None, min_shifts: None }, VehicleType { type_id: "176c7c86-e2fb-4cf7-a0c5-7b9cf62dd1db", vehicle_ids: ["176c7c86-e2fb-4cf7-a0c5-7b9cf62dd1db_1", "176c7c86-e2fb-4cf7-a0c5-7b9cf62dd1db_2"], profile: VehicleProfile { matrix: "car", scale: None }, costs: VehicleCosts { fixed: Some(30.0), distance: 0.0015, time: 0.005, span: None }, shifts: [VehicleShift { start: ShiftStart { earliest: "2020-07-04T09:00:00Z", latest: Some("2020-07-04T09:00:00Z"), location: Coordinate { lat: 52.50681867806829, lng: 13.454483276138166 } }, end: Some(ShiftEnd { earliest: None, latest: "2020-07-04T18:00:00Z", location: Coordinate { lat: 52.50681867806829, lng: 13.454483276138166 } }), breaks: Some([Optional { time: TimeWindow(["2020-07-04T11:00:00Z", "2020-07-04T15:00:00Z"]), places: [VehicleOptionalBreakPlace { duration: 70.0, location: Some(Coordinate { lat: 52.5383730293492, lng: 13.257344086373854 }), tag: None }], policy: None }]), reloads: None, recharges: None, job_times: None }], capacity: [43], skills: None, limits: None, min_shifts: None }], profiles: [MatrixProfile { name: "car", speed: None }], resources: None }, objectives: None }
+cc a0629626109e1a34b99f32518c2ea1242a5a3607ea59c997cb36fbe2acf6315e # shrinks to problem = Problem { plan: Plan { jobs: [Job { id: "91a79010-a473-4439-93b9-1b62a19ffe22", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.481867960343386, lng: 13.515310581455218 }, duration: 5.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "f50ca405-f968-4dca-8145-7ee3bcba94b1", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.4412421308758, lng: 13.53830065835765 }, duration: 1.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "5b22f07a-9a4f-475f-b3cf-c785290b7498", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.436289638975545, lng: 13.446114728375571 }, duration: 7.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "eb3691d9-a083-4cdd-8394-a6dde6b19afd", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.5542427873238, lng: 13.332238080642973 }, duration: 5.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "f20374da-a615-4d1d-8550-0c0d067c9859", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.57474973650454, lng: 13.300744293953533 }, duration: 3.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "63384b61-2774-4706-acaa-db29fd77d637", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.42790586370594, lng: 13.41437033450191 }, duration: 7.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "3b8c4516-b7c0-4a23-97eb-eb34f38bf9cc", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.523660954326, lng: 13.387912879621808 }, duration: 9.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "db21002e-9b7c-46ce-81fe-2d783ecf03ee", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.56073161657427, lng: 13.407116746549566 }, duration: 3.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "1765a832-1106-499c-9c83-1d7e11506043", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.593076578458295, lng: 13.542053392092862 }, duration: 4.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "fe252dcd-ece6-4bdd-85d3-41a939e50df0", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.50606938197855, lng: 13.22071917916111 }, duration: 3.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "87c95fc9-4d20-4cfe-8a97-49cd2bfc5314", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.44599726830543, lng: 13.314235070871153 }, duration: 8.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "85383dc8-0d0a-4af4-bc3b-98ca8c9a9173", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.54046607314568, lng: 13.491043448286964 }, duration: 9.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "f553413a-458b-45e8-8649-62bc69ebfe50", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.546703013905756, lng: 13.456733797050509 }, duration: 8.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "a53c9e12-a25f-4a56-8333-ee41db250d6c", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.42525460551186, lng: 13.480339000195066 }, duration: 5.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "9aef4f77-3396-4f5a-a11a-2a332ed70f13", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.57975569252548, lng: 13.490082046027108 }, duration: 7.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "df1be46b-0066-43bb-9e66-a27d45c80ecc", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.468363786516484, lng: 13.494318795370635 }, duration: 5.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "d423c5c1-4b2a-4f1d-a285-c1eaaa1df6cb", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.510520278507144, lng: 13.521323683836618 }, duration: 2.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "71bc635e-3297-46cf-a2a5-d4973da8567f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.502352725780405, lng: 13.221022076004976 }, duration: 6.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "447d9cd4-0dfe-47fa-87cf-6a7aa93a5113", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.57599017308078, lng: 13.53203565273009 }, duration: 7.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "6a14840c-089f-4c66-838c-719ba0293df8", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.472411252225285, lng: 13.336104283033155 }, duration: 7.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "a615ab28-ff95-4689-9d0b-b4f5673c2f2f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.525137012298615, lng: 13.34089499714848 }, duration: 9.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "dbd4ba11-d377-4fe5-ba58-4fbce92d10ee", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.49711633912417, lng: 13.530036387654889 }, duration: 8.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "fe84f67d-712b-4abd-88f1-f108c0760192", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.466757156195754, lng: 13.51340072260042 }, duration: 3.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "7fb85ef0-cde0-4616-8798-acf617fbea53", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.53318789472442, lng: 13.461036106499373 }, duration: 2.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "c5d46a91-7bb1-4d8e-9a16-636e22eee4f1", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.44318959372823, lng: 13.27553979253979 }, duration: 7.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "070d1943-171a-438b-9361-fd9ec247b1da", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.57137095262976, lng: 13.357099896491132 }, duration: 8.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "59c7cf34-5e29-44e9-bd26-defc54c1ee31", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.56647136115006, lng: 13.503358541553995 }, duration: 4.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "9555b748-d15e-4b8d-be9f-f8c2209a17e5", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.57244573060305, lng: 13.24192068637507 }, duration: 6.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "f508a65f-7712-4afa-8782-108e44f32660", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.48940646629219, lng: 13.29065733811078 }, duration: 3.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "119af672-7c0d-4c39-a1e8-131c4cc29320", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.43709317696653, lng: 13.587418730221797 }, duration: 3.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "db6ec78c-806c-483c-8072-a1c73ed406ee", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.48262890357365, lng: 13.548427993968515 }, duration: 3.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "f9b3925b-76fb-4cad-b604-212a05db5d0c", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.552448149121716, lng: 13.422072171193731 }, duration: 3.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "6b5003f5-5aff-4bdb-9704-2c08720124a4", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.4748260110319, lng: 13.588304267958241 }, duration: 1.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "e3ec17a0-b09a-4b56-9853-773c896dcd8f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.49905194503727, lng: 13.590342563850966 }, duration: 2.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "11515a28-e652-447f-9782-7f4fa1215d88", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.49859830211323, lng: 13.317946717513005 }, duration: 3.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "255d4d55-c3e0-4b59-9b48-b4519a37cc5d", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.51133202375177, lng: 13.590307994255438 }, duration: 4.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "e2fc725a-7fe2-4052-ac5f-3eee632c54d9", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.498307604762246, lng: 13.334202996197488 }, duration: 6.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "82be2172-f57c-44df-b5d8-1249c5279097", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.520564681926196, lng: 13.354488064652553 }, duration: 3.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "248dcfa9-65c7-41f5-a883-ad7ec74ba3c0", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.527954879623486, lng: 13.302730782612201 }, duration: 1.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "b2253374-0610-479e-a096-2ca30c8b99e5", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.48166632639194, lng: 13.447963055734327 }, duration: 7.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "24f76d5a-5770-4478-96ab-913e8cf6a118", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.483491587788265, lng: 13.232082589769698 }, duration: 1.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "b8c0e66a-ebbd-4b0e-8898-87ea6960f1a0", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.49188424500328, lng: 13.271782533510887 }, duration: 4.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "931d819b-15e1-4310-b689-8a94abf83feb", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.46685331554595, lng: 13.350209414107738 }, duration: 4.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "a1d98da1-ae84-4654-9525-c77f5c86dbeb", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.536127072242614, lng: 13.53381621100803 }, duration: 5.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "bec30cf1-4da6-4409-91ec-7254fa6eb6ac", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.56127951178136, lng: 13.516400212378816 }, duration: 7.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "459d6ab8-89e7-4363-8dbc-9d321730180b", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.50863655983515, lng: 13.541046412825677 }, duration: 9.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "c4463918-006c-4ffc-9a38-f54b41aa4d44", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.486639955648485, lng: 13.563152391895512 }, duration: 3.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "c43ab883-0e35-4cd9-98db-51c60b493a2a", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.57635834541592, lng: 13.220241409284453 }, duration: 9.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "081a9598-4cff-4aa3-a211-1a969d281194", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.50754684124822, lng: 13.548307069187672 }, duration: 1.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "fb405864-8ca2-460f-9b44-505c734c4139", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.43278731281283, lng: 13.596247840237366 }, duration: 2.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "0ee0c31d-40d7-476e-91b5-c82c1fd8f8ab", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.52382819167495, lng: 13.254471044901743 }, duration: 3.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "a14e266a-768f-4dd3-8f27-db9519d59c69", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.50079338703029, lng: 13.281718635519088 }, duration: 1.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "e2ecdbd3-59f1-4968-a3fa-2f545db6b272", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.5062578507037, lng: 13.313703948216878 }, duration: 5.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "0c762ebe-fefb-4bc2-b635-4b41e1e4c0af", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.57533724938679, lng: 13.294037852093368 }, duration: 2.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "ed0d9bb7-af56-4096-b512-8f5fd0262c25", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.52384823790682, lng: 13.260263449966379 }, duration: 4.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "b392e706-f5a3-4bbb-8c95-5b78bb1a9a43", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.553643259368584, lng: 13.277555411920265 }, duration: 4.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "d4ea15bf-bf28-40a9-9b70-3675aed39139", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.560431622878156, lng: 13.401557384018224 }, duration: 4.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "f7eee27a-ceb7-479e-aa13-1c3f7e9dde28", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.47795409356496, lng: 13.350652292236374 }, duration: 7.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "f41240b0-6d0c-4c4c-9e9a-3852b0e0cadd", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.56634187421733, lng: 13.58408702528746 }, duration: 7.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "721ff840-2fa8-4f11-8397-f24eebad319b", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.46884846669925, lng: 13.268573010724108 }, duration: 9.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "3477c603-a84b-4cdf-9038-0fe6537a7a5c", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.480343608605445, lng: 13.392942436714957 }, duration: 5.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "eb3a52fb-841c-4403-bd87-55605c63b9d6", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.4837097489511, lng: 13.579213955551493 }, duration: 7.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "12d372df-758a-4877-9f13-ab4d52f6ff38", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.576887332845196, lng: 13.238143337008696 }, duration: 9.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "b5a55cad-cade-4e21-a3b6-d42e0bc5bfdb", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.56357539600657, lng: 13.501554708410902 }, duration: 5.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "a6b1d51f-0508-4d8e-9c45-7a13d44c82af", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.52666802772873, lng: 13.52001647222994 }, duration: 3.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "01a90ca3-6546-4b6a-b611-333d6f37cc34", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.54892848412336, lng: 13.297145286983818 }, duration: 3.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "9cf03e71-f85d-4040-ad06-be211a192eb4", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.52002199652532, lng: 13.510342217127855 }, duration: 2.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "4551e705-75bd-48c6-9aa9-8c50af1a7960", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.51531529528068, lng: 13.449939526135578 }, duration: 3.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "5c574505-853e-422a-a674-ee5925b50bb4", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.512138262466515, lng: 13.525089294173208 }, duration: 2.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "a3c794c9-4d15-4a3d-976c-0cad76701a52", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.589594204107414, lng: 13.240711952119714 }, duration: 2.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "3dbc11d5-de64-48b9-b5c3-ccda44736496", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.536015543243536, lng: 13.319641866382305 }, duration: 9.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "5b613b21-4403-4f8d-b16b-43eb0f8e8f07", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.51581968784528, lng: 13.324946884673626 }, duration: 5.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "67a35728-06d4-4ede-abd2-92637aa67226", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.52234028728468, lng: 13.593147176060537 }, duration: 7.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "619b0998-8d4e-4729-8ed8-74408ca62eb5", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.433402456274, lng: 13.434166323429938 }, duration: 2.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "c0d37dc8-586d-4adf-bcb9-4191584177e9", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.56830058553471, lng: 13.53978876137663 }, duration: 9.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "1376bc2b-e9d6-4534-b49a-f0d4c731621e", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.537811975556416, lng: 13.584897186109787 }, duration: 8.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "50a62271-13b9-4494-8b05-4101c15cfa91", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.473549374160385, lng: 13.526129325073112 }, duration: 3.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "3d18d563-44a0-40a5-b979-22758fa0e4fe", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.53417010986227, lng: 13.51502456062986 }, duration: 3.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "d4319e68-30f5-415f-885c-7b235cfdb107", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.54755781003602, lng: 13.297954756798426 }, duration: 2.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "a54c0875-9440-405f-9bf7-aa3944402d12", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.51738374396765, lng: 13.340441759906259 }, duration: 8.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "41d4a127-6fd4-4683-8f5e-2d58445ac7c2", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.48274195585463, lng: 13.495630449660485 }, duration: 6.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "e13abe50-e9fd-433e-80ba-8dda3b6c4764", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.43729488557146, lng: 13.544573694841203 }, duration: 4.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "0b0425d1-d991-4dd8-ada6-c55d92132e57", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.550646590533944, lng: 13.352533299650139 }, duration: 1.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "7245cd02-39c3-4f06-a2c9-917242dfd794", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.555814075450755, lng: 13.452843700786293 }, duration: 7.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "5345e26e-24d7-40de-b04b-ffa38b4d4f8b", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.521668699752254, lng: 13.495458544664245 }, duration: 5.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "a2412761-1679-4248-b915-66f5cfb6c96b", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.53289392782365, lng: 13.32353420505743 }, duration: 4.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "d5086bc8-ddce-4483-9da0-22e18b5c808c", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.574509348881, lng: 13.417023552427962 }, duration: 6.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "74fd8003-d528-460b-a284-2766d420768b", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.58301989805598, lng: 13.55869937440246 }, duration: 4.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "65975619-eb66-4415-ab91-b849124a5fef", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.52882321670953, lng: 13.227544170586471 }, duration: 6.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "d3241ffa-4c5d-49d5-96ac-4fc833224be6", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.464017505861435, lng: 13.457802110471055 }, duration: 1.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "b8a01499-95a6-4c03-8da1-e724301dc51c", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.45233628781423, lng: 13.412524530988698 }, duration: 4.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "92541326-5ff9-4884-a33c-812471efa7a6", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.576413056300645, lng: 13.422756858259495 }, duration: 5.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "437f5a93-3977-47a8-8cfe-abb8947eac5b", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.42505699626766, lng: 13.347494674140501 }, duration: 1.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "195806ad-744f-4c2e-951e-6996717b2ac2", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.57537251098861, lng: 13.249436377868749 }, duration: 6.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "c9ab4978-c0fa-44ac-b6cd-ac881d330a0e", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.50966991774061, lng: 13.42203021326235 }, duration: 9.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "e0dd14e0-414c-4885-8752-cb590cd08f31", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.49012249829684, lng: 13.507987966718614 }, duration: 2.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "0c8bb1a0-66c0-4c87-871f-ce50b29a4fe5", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.4973107350308, lng: 13.383277567915469 }, duration: 4.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "415c15a8-2b87-4cc1-922e-8f0059723678", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.44115311457616, lng: 13.3201376228479 }, duration: 4.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "c9c92e19-8bd1-421f-817f-7b81f07b74ac", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.49652272060089, lng: 13.596427944827099 }, duration: 1.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "5582fb05-ce1d-4dbe-939b-0f9ac030a798", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.52154033071739, lng: 13.241027335622777 }, duration: 6.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "754a064e-327a-4668-999c-bcb1dc49c2eb", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.59138279064522, lng: 13.42686671128343 }, duration: 1.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "899008b7-1a66-4129-9cf0-f1f156c89be2", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.583505511944246, lng: 13.22487805037664 }, duration: 8.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "3600e6d8-63af-4c16-a130-2ffdfff5856a", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.53007570561231, lng: 13.430722484289346 }, duration: 5.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "1094835c-daa2-4833-bc45-ab4c3351fd7c", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.43008813431707, lng: 13.571799760245257 }, duration: 4.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "75fc171f-19d8-4336-94a0-0f566bef83a5", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.42564572325702, lng: 13.30451567207205 }, duration: 5.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "ffcfaa78-a0b5-4c95-90d1-8691518ea4d2", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.50142233862298, lng: 13.262790025747043 }, duration: 7.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "5abd7a66-f428-4a7a-95f3-5ae2a8fc4db2", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.445378213363746, lng: 13.53948600564784 }, duration: 1.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "9989bb16-c67e-4e9a-a5fb-7c54b8425f7e", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.50538125530761, lng: 13.419622058370905 }, duration: 7.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "07bc4ddf-2478-41a7-bdfa-6f4592fb8014", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.52323495261066, lng: 13.215942834327395 }, duration: 4.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "955be6ca-7a9b-47fe-8245-17686c73ef25", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.433157727879724, lng: 13.588000036795945 }, duration: 3.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "4970ff54-a9af-4d60-8906-4c5868288cbc", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.55519011099536, lng: 13.265629725030937 }, duration: 1.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "01ac4514-429e-4fe5-a786-8136ecb9e534", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.562990061251, lng: 13.421388453664354 }, duration: 1.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "4f660a21-bf32-462d-ba7c-58870cb72271", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.44261148908299, lng: 13.457227689503439 }, duration: 2.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "edaa6898-1b12-48ae-b2d9-a9cb19d09579", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.499927119205296, lng: 13.345662441952767 }, duration: 4.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "49f6a012-dfd0-4dc8-b883-ac8524088315", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.45394329714663, lng: 13.318631158425184 }, duration: 1.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "5e2edb64-6587-4380-a56d-e849570bde71", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.54750155283299, lng: 13.289895361745302 }, duration: 5.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "ea6713fc-c628-4c26-8228-e7b4cc110e61", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.57699537762936, lng: 13.490108604268029 }, duration: 6.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "b30d8723-daf1-4018-b414-c1e112bdaa98", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.57537056450471, lng: 13.517842194079753 }, duration: 9.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "099b2b2e-40d9-45de-bb0b-7778496b47db", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.58800569157715, lng: 13.58154256206416 }, duration: 6.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "25148179-dff2-431e-a690-6874e4f99547", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.58717012803812, lng: 13.564204242595093 }, duration: 2.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "458eca84-49b3-4236-89bf-4464d0cd870f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.54857005898802, lng: 13.23988575483169 }, duration: 6.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "cf4ae19a-04b5-4ede-85fe-d91382873973", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.48852610546587, lng: 13.492683831310046 }, duration: 7.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "a6b10838-95f8-435a-bb49-59ccdc0a9f50", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.59351912940879, lng: 13.554888793094195 }, duration: 4.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "b6555799-02e3-4150-9956-adfc0b0e94ed", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.50135786038147, lng: 13.56543921418967 }, duration: 3.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "9dee43b2-14d2-4e01-bccb-60f20d83a471", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.46883074990379, lng: 13.235162715146025 }, duration: 5.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "beafc0e1-0557-4e01-a845-ffce5489499c", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.48081804563212, lng: 13.556009465174538 }, duration: 6.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "7d802c26-d6f5-4e72-a49b-9fb2bd3c5169", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.477466823025956, lng: 13.215017898230665 }, duration: 9.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "c8a3b4b0-1291-4859-89fe-d2e42910f811", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.47664054157485, lng: 13.4010792354045 }, duration: 3.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "778efb28-cd51-4c27-b394-648f21e410dc", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.531767169405356, lng: 13.385698725725053 }, duration: 2.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "3d45405c-40f1-48ab-b021-a0d9a864b5b8", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.48676353132126, lng: 13.412937494596113 }, duration: 9.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "10ad01cd-a2ac-406e-a0db-166fd47754ab", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.55439663365847, lng: 13.24671146698928 }, duration: 5.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "9afd7dbd-17d1-4900-82ae-de2c88c22c6d", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.583359910219684, lng: 13.565539633121414 }, duration: 1.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "224f2462-ae64-439f-9899-4771859ce579", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.48849622592438, lng: 13.245659871005488 }, duration: 6.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "caa1daea-c651-4d38-9778-342dc0f078be", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.430753986037274, lng: 13.335968668417728 }, duration: 4.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "fd60c1c6-a7d1-484f-9c82-cb50365c5fa5", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.485957953241275, lng: 13.33616536565339 }, duration: 4.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "42a10f08-4c84-4ace-a75f-4dcdb8c18b90", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.47839112953693, lng: 13.434152991430707 }, duration: 6.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "5949d503-2e71-4260-a225-0f9e887ff969", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.537271633789096, lng: 13.557047812087768 }, duration: 9.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "067e1525-86f8-44bf-80f5-461ae0dfcb87", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.51675669184376, lng: 13.403458703984061 }, duration: 4.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "dec88a72-ece4-4918-ae1e-d150592fd6c4", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.54510891949871, lng: 13.319486478738975 }, duration: 3.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "ce726652-de2f-4876-ac17-e4305e989653", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.49636529016367, lng: 13.500692422943716 }, duration: 7.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "150a71dd-b8a0-47d8-945a-65b01770f1ba", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.482922113762555, lng: 13.582373736802452 }, duration: 2.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "7bffae3d-e160-4e4c-8e93-464383fc5006", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.52284116437998, lng: 13.590484825407161 }, duration: 6.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }], relations: None, clustering: None }, fleet: Fleet { vehicles: [VehicleType { type_id: "abaa228d-6f49-4d63-963d-0553efa6cba1", vehicle_ids: ["abaa228d-6f49-4d63-963d-0553efa6cba1_1", "abaa228d-6f49-4d63-963d-0553efa6cba1_2", "abaa228d-6f49-4d63-963d-0553efa6cba1_3"], profile: VehicleProfile { matrix: "car", scale: None }, costs: VehicleCosts { fixed: Some(20.0), distance: 0.002, time: 0.003, span: None }, shifts: [VehicleShift { start: ShiftStart { earliest: "2020-07-04T09:00:00Z", latest: Some("2020-07-04T09:00:00Z"), location: Coordinate { lat: 52.42428912797891, lng: 13.294335331604398 } }, end: Some(ShiftEnd { earliest: None, latest: "2020-07-04T18:00:00Z", location: Coordinate { lat: 52.42428912797891, lng: 13.294335331604398 } }), breaks: Some([Required { time: ExactTime { earliest: "2020-07-04T10:00:47Z", latest: "2020-07-04T10:00:48Z" }, duration: 2417.0 }]), reloads: None, recharges: None, job_times: None }], capacity: [49], skills: None, limits: None, min_shifts: None }], profiles: [MatrixProfile { name: "car", speed: None }], resources: None }, objectives: None }
+cc 1699692ecd60036a36f1a7c8e8090b6bc5624743d8e22a8802b4ce3be653ff18 # shrinks to problem = Problem { plan: Plan { jobs: [Job { id: "de46a717-af98-40e5-955f-abb6b186115c", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.447650204564376, lng: 13.498329725421659 }, duration: 5.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "eec9b38d-eb97-45f9-97df-b43eea05a936", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.530180277734146, lng: 13.254974210173472 }, duration: 1.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "2c6d67f2-13ad-4800-a517-0f2d7839236a", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.45758772666898, lng: 13.331768537657988 }, duration: 9.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "599f01e3-f391-4ef7-a2da-7b5221b9d3d9", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.55731748296904, lng: 13.374329311994273 }, duration: 1.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "23826b51-215a-4f23-91b5-2251a1af4516", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.444473045489374, lng: 13.465858016584466 }, duration: 2.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "2ef88af9-f459-4d38-af89-4c43b370eaea", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.482375105262875, lng: 13.38384898273188 }, duration: 6.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "d46243b3-8689-49cd-8af8-d110998d222b", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.527024833465745, lng: 13.495936711432034 }, duration: 3.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "e335b0aa-da9b-4533-bce1-950b2975c2fe", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.55915014407684, lng: 13.281843057157042 }, duration: 6.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "e6fbc6c4-2cd9-4d2e-987c-b4ed112c3b00", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.440479653982216, lng: 13.444447555665091 }, duration: 8.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "18b47bb1-0024-45f3-8530-3537f2614885", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.51033457846878, lng: 13.304591614344337 }, duration: 3.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "5df97763-abca-493f-9cf6-31489712e0b8", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.560025272188234, lng: 13.333415973290524 }, duration: 7.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "34ca1b28-5c5b-4e91-a59d-e67b5e9d8130", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.59175417882865, lng: 13.326265638455903 }, duration: 9.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "bf269a03-62b4-46f2-9b3a-18c22821c24c", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.565648739964004, lng: 13.418351517555767 }, duration: 8.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "38e91266-3ed5-4d1d-b289-68630314a4ba", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.547045824930066, lng: 13.271401620689874 }, duration: 7.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "05634022-afc2-46d7-bbfc-a4280fcb3f3b", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.54203565557975, lng: 13.355280755461155 }, duration: 4.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "d0378a19-6fc3-4e4e-9054-118067e780f6", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.48298049483484, lng: 13.263844353266014 }, duration: 1.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "4b355a86-afc9-49b4-b677-6da9fa540691", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.53624180508589, lng: 13.375168260282775 }, duration: 8.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "7a7d776a-7ab1-490a-a187-af3606b9196b", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.55993416848799, lng: 13.25111996832651 }, duration: 2.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "e09c598e-3bff-495a-ac55-dec8587a21ad", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.47568294165767, lng: 13.429897151780432 }, duration: 4.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "a7b76f27-1882-45e7-b41c-882b15994a90", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.522544804632794, lng: 13.227198559528285 }, duration: 2.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "05cd83b0-1944-47c0-9c7e-f61e6b839463", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.57976272644969, lng: 13.263931267647521 }, duration: 5.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "33125f3e-24ec-490b-b537-c63d3932b9e5", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.44911904047268, lng: 13.347790852414805 }, duration: 7.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "78cd49a9-fb50-4d96-9833-2a2ac7fd0770", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.52110533486909, lng: 13.352303778707332 }, duration: 5.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "fcf5c410-5c75-4174-97f0-76d760757852", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.43913712222253, lng: 13.374090372699747 }, duration: 8.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "9b8aaae5-03ab-4b0c-af40-0634ccfbca18", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.465814878065366, lng: 13.511848222005172 }, duration: 4.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "65eecf81-d9e0-4463-8ea7-bc8d4abc57ba", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.54455652574385, lng: 13.233450225949191 }, duration: 5.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "0745d04b-9bae-43b0-b014-298687af8f27", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.44893704458404, lng: 13.32617966229477 }, duration: 7.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "0af1158a-a90b-43df-8a24-12d08907a4c3", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.54422138863102, lng: 13.431996070494645 }, duration: 3.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "5197be8e-8b71-4afb-965b-0cc628ddf011", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.553568814495165, lng: 13.401786640570027 }, duration: 2.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "570a0f76-427c-4cd7-a266-a17fe24f03ea", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.44542638000675, lng: 13.455599418081981 }, duration: 8.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "ec7717cf-b307-4e9e-a223-ef5b6c38d16b", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.46481817814646, lng: 13.482069428162701 }, duration: 5.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "2af3f388-623c-44d1-b58b-8a8d1e0d2c3f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.586105477008005, lng: 13.308031000767942 }, duration: 2.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "8247764a-07a4-4764-94d5-3cb5966b08e5", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.486349790378675, lng: 13.418779470710284 }, duration: 3.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "04208f99-fc2d-4b4c-9f63-cf0854681db8", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.554952740657264, lng: 13.256964452831982 }, duration: 6.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "0849a286-3f80-4a72-802d-c5daff1bf3ee", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.53211436679658, lng: 13.334138709490638 }, duration: 4.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "cbb483b9-bec5-4df4-8475-af2ac8f682ec", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.530293192183514, lng: 13.352481351581115 }, duration: 6.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "81c8956e-a856-4db3-8b43-f2e45e6d2291", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.428957905881994, lng: 13.278353200555916 }, duration: 9.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "2b8ed2c1-ba53-40b6-8a4b-1662ed470db9", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.519982628563774, lng: 13.588903868822458 }, duration: 3.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "818b6a66-091d-43b2-b553-2f27bb8c88a1", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.54127923083359, lng: 13.56548700080369 }, duration: 2.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "b143f1ac-254a-4d36-b75a-954dd95b337a", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.51478894018975, lng: 13.322962906166179 }, duration: 3.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "958bcd31-030c-40b3-804a-0c8a2701abb1", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.57451602049463, lng: 13.448379647564119 }, duration: 5.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "c3f215e1-befd-4625-954e-8c677ce1038d", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.50964645792423, lng: 13.481944846642111 }, duration: 1.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "7d0669e3-e778-4401-afcb-3a228e94e222", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.426227160820304, lng: 13.37119831757004 }, duration: 9.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "5f7b0df0-3f71-4252-8ea5-28f6e4ae4d1f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.454736470825445, lng: 13.374072669358616 }, duration: 5.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "db2b63c1-9a71-4c2b-a48e-1e7e687c6fc0", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.52999490099986, lng: 13.502767769712992 }, duration: 1.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "f1ec229b-0d71-465d-8369-899f84cce8d5", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.562580026289645, lng: 13.3287922288725 }, duration: 3.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "c31f5438-d2f3-4dc6-aaac-29b81febe528", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.57080940270471, lng: 13.250887564722605 }, duration: 1.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "4754cf55-dbbf-434f-b28f-9dbd860e5bc4", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.48642910484786, lng: 13.552874754467739 }, duration: 1.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "cf63c3f4-7568-460d-84c9-a915d86947c0", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.49694855529579, lng: 13.345262878374724 }, duration: 6.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "03f2de79-7d65-453d-8a84-e3df88df5562", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.473861558659124, lng: 13.48877481212796 }, duration: 5.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "6cf3e337-7d56-464c-86c4-a14c8bc8b94a", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.54439628075913, lng: 13.427444547784965 }, duration: 7.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "0d4b19ce-c3fa-4af2-b3a7-067e007212c1", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.47426756085194, lng: 13.332100540911515 }, duration: 9.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "07e2104c-68ba-4560-a3b6-b99de7d837c7", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.503738954523335, lng: 13.23251495086919 }, duration: 6.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "f8f431cb-0e5c-4a9a-bdec-4d1e7f3c0023", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.561160708951654, lng: 13.296494027096626 }, duration: 7.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "ab6249f3-488c-476a-84a5-8ec0d8292371", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.521637841076746, lng: 13.293248370738493 }, duration: 5.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "5d6d08a5-6d86-45bb-9ae0-8b05baec46eb", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.44257223859547, lng: 13.560780168040207 }, duration: 2.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "d68709aa-e7b8-44dc-afb4-6231dafa8128", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.56217878348294, lng: 13.420670248226651 }, duration: 9.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "29760dc4-62f4-4e47-af30-490a9ba83ffa", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.429607527438456, lng: 13.388515332714748 }, duration: 7.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "2f2f22e9-0920-4c9d-84be-b957183201df", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.582555421514094, lng: 13.364892648814818 }, duration: 5.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "b3b7e524-bc61-473e-94a2-84d3a096d4af", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.47584896109524, lng: 13.57703207732508 }, duration: 1.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "a7e7b477-8dbe-4958-b7b1-53a03f3234bf", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.475563098737005, lng: 13.51357400722752 }, duration: 8.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "ca7ce87a-c6da-4e25-8329-251f51f6011b", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.485743578885476, lng: 13.338193257096078 }, duration: 2.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "2182dbee-0507-4bd5-9b17-d4b7f5dd0dc8", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.44158065490212, lng: 13.58783830231801 }, duration: 3.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "3b24fa87-d893-4fd0-94db-384d62572173", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.42897667443369, lng: 13.235484269397395 }, duration: 6.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "2715a1d0-123f-47be-88e5-d6158bf66bee", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.56879730678837, lng: 13.596237369732796 }, duration: 9.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "c66a5d74-233a-434f-9387-c9f8221a6d32", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.583056855134885, lng: 13.426902191486038 }, duration: 1.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "04e58603-e093-412b-88b4-991e43e37666", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.588445176063445, lng: 13.453005248679856 }, duration: 6.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "2d92f475-1638-4b3d-a4fe-5a31799e24d7", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.58232912934057, lng: 13.502991179317373 }, duration: 9.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "c56ba694-86f8-4845-86f9-1d2b5848e6d9", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.51202639159968, lng: 13.440562776110546 }, duration: 9.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "c71688df-42a2-4fcd-9f6f-8cb7f7a6c773", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.5349228699331, lng: 13.531866106279965 }, duration: 8.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "84202988-d08f-46d4-ad60-14af5f851547", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.572392353025755, lng: 13.571476966388133 }, duration: 3.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "220eb1c0-1327-4e6b-a960-1d526139c3fd", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.554174619308355, lng: 13.334635477756832 }, duration: 9.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "7a36233e-5aaf-46f7-8d99-e3d5fba66a0a", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.557414523626825, lng: 13.233981218558373 }, duration: 5.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "c60ee292-6cba-4274-a7d8-7e468fffe1ad", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.434249426461065, lng: 13.330783034759838 }, duration: 4.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "4489925d-461e-4f2b-9b5f-611db639f91b", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.444723414452426, lng: 13.505715793918258 }, duration: 8.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "a09a9642-ff8f-4b15-a8fa-5b231b3f912d", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.48485406767818, lng: 13.328002355107888 }, duration: 7.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "05e9d3f3-48ef-4fe8-b3b0-1c6dd81da11f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.45350643530821, lng: 13.51406396525912 }, duration: 3.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "5f937c60-e24e-4e5f-abe0-48d1dc3bb552", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.533758611682245, lng: 13.548428702438448 }, duration: 1.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "adae7852-1778-474f-a75d-2938633fe630", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.5294323714758, lng: 13.395777098997357 }, duration: 4.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "7818bb71-7adf-42e7-9c1e-0e7f19446fa6", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.46998457799882, lng: 13.440813850755681 }, duration: 2.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "92d008a9-8cdc-41bb-ba9f-eff5827f1a2f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.47499316466678, lng: 13.418870369302452 }, duration: 8.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "f5b493a5-14f6-47c3-9c53-c5a4c850699e", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.544016170617176, lng: 13.585166726911657 }, duration: 2.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "f8a11e7a-7eab-4bc0-a4d6-160b7160c333", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.5256202063319, lng: 13.28842424544607 }, duration: 5.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "61b8e0a7-df8e-4673-864e-4f67cd2a0d8d", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.43445300592451, lng: 13.309443320865997 }, duration: 8.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "20b5f4f2-1b8d-441b-a049-2dfe190b4b76", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.429537059084545, lng: 13.44729603928947 }, duration: 5.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "fab42349-47e4-4de6-82df-14900e6bd3d6", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.42792972115441, lng: 13.588697369021878 }, duration: 4.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "9d9259fc-b0fe-4126-a5bc-ea38024a7afc", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.46465083868914, lng: 13.30102787086179 }, duration: 7.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "0cb9086b-4cc8-4afd-80b3-6fb83fae174f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.44399398277094, lng: 13.46377020991278 }, duration: 6.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "91c86c6f-eb87-436f-9d8b-cc98ab53b449", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.47558553116906, lng: 13.445770463575359 }, duration: 3.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "1ca67385-3a18-4e3f-b6ac-66b926b2caec", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.55175935045085, lng: 13.283959617332078 }, duration: 3.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "81a402d1-5b8b-4e04-9722-70dbd9acf486", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.59018621644774, lng: 13.589642605669045 }, duration: 9.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "771d22c8-87e0-41b3-9a05-3b735ff37b1a", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.54522699676943, lng: 13.362224922197155 }, duration: 2.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "9e7a0b2e-83d0-452b-8a37-261855adbc33", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.445362524385935, lng: 13.238718767305857 }, duration: 2.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "e419f8da-cb26-45c2-a0a5-79a35db991d0", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.55212081257352, lng: 13.53350572541648 }, duration: 7.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "4430bdfd-d484-46a2-9575-586cc2386ebd", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.43316195929203, lng: 13.43149884022416 }, duration: 1.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "6ff9bc1f-08f4-4b4e-a8e0-df1c8a2bb19e", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.438400500904855, lng: 13.304061069057807 }, duration: 1.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "bda83c96-054f-418f-a190-efb02e690b28", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.539823655889485, lng: 13.319658963563787 }, duration: 1.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "afce190f-b078-45ef-a464-0c75812ac52c", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.52486828235193, lng: 13.514554573462492 }, duration: 5.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "c70add25-6830-40e8-9f6f-c03f9c5d80f1", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.52277087291523, lng: 13.418923156020423 }, duration: 6.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "b5dc171b-957a-42a4-9023-bb2ae2fab860", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.448236351233795, lng: 13.24738399999562 }, duration: 5.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "1a357790-a379-4885-8562-1cea74c77005", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.43548039829525, lng: 13.266955794023422 }, duration: 2.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "14de2f91-63cc-4ea0-82f9-fa3d5475dc71", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.45825200244391, lng: 13.382583985233051 }, duration: 2.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "15858215-416d-4d16-8113-2687f74ca4e1", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.445550614201096, lng: 13.50495033450584 }, duration: 3.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "d2cc3c84-911c-46af-8e5a-e86cd3eb15a9", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.49284672340736, lng: 13.345730796072013 }, duration: 4.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "6ff2b362-ab1c-4c3d-9443-86120a01503c", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.53409115649474, lng: 13.584585962381741 }, duration: 3.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "802ad7a3-ce84-489f-968b-d8a5d3fde8f5", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.46888713374967, lng: 13.501701793086989 }, duration: 1.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "78da2fa0-5739-4045-a37a-2a105001f205", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.57192438518218, lng: 13.38125473482487 }, duration: 5.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "df5a5e93-9bbe-4f8d-95f5-5bafbb9a5cff", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.45527153446966, lng: 13.507571752470486 }, duration: 4.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "77a73631-3d01-49e7-938d-9bdb7785d3da", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.50860359233061, lng: 13.290255813765974 }, duration: 9.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "37f52091-eef4-458b-b8b9-1daac7c8bb77", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.53232513485462, lng: 13.224001890804699 }, duration: 8.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "6a509331-419c-4a8e-a4aa-e75d3d4f6c1a", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.580504505502645, lng: 13.532622468102712 }, duration: 2.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "b86087da-0040-4b53-8166-10d534443c6b", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.428899940491185, lng: 13.515751066780494 }, duration: 8.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "981c9e79-f18a-4dd4-9793-df0500a71b9c", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.59096262933644, lng: 13.462992766108052 }, duration: 6.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "86125ee7-8650-4131-b08f-4772067073d3", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.50221432543172, lng: 13.555140680176056 }, duration: 6.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "13098e35-2549-4829-bf5c-d4374966fc24", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.53154797283799, lng: 13.380581825380395 }, duration: 3.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "247eeb9f-bcca-40a4-898b-77c77fb465b0", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.54429903295455, lng: 13.42901530722731 }, duration: 5.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "11e39415-039a-423e-8f16-c09e67577175", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.50812533573073, lng: 13.268105153472042 }, duration: 7.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "38b24f8f-14aa-4ae4-b3a6-5280a951ae08", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.4320917861356, lng: 13.236431394327912 }, duration: 2.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "6a5a6bbf-8e02-42ee-a333-ea629d645997", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.5077718687463, lng: 13.515069487585068 }, duration: 8.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "30361b97-7c20-4028-8367-45b631beeca6", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.463385546915546, lng: 13.384760590487149 }, duration: 9.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "b0272cec-22fb-47c9-a545-f69f12dcf7e9", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.44744313887178, lng: 13.534912262545571 }, duration: 1.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "e114c9bb-866c-4803-84f5-7953e73499f1", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.59015185789208, lng: 13.445793181513443 }, duration: 9.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "f280cfde-0d92-4960-a75c-5581685a2a53", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.49525268639518, lng: 13.408772332222577 }, duration: 5.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "af9ddf4f-379e-4ddb-b545-fd54cf5ac5fd", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.43890875826017, lng: 13.254517304570433 }, duration: 8.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "45026624-b917-4547-9899-3cbbe27a353d", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.58665164690947, lng: 13.50485442475567 }, duration: 6.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "96ded7fd-3a62-4443-ab52-43a8e27c0e09", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.55102396212264, lng: 13.291169936582895 }, duration: 8.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "8c4fd6a6-a65a-4ad2-aaa8-2c99e14737e2", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.49028986561523, lng: 13.368121272969676 }, duration: 5.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "6c3eb299-d74e-4fcb-b09e-90282162f8dc", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.55869980047896, lng: 13.381001538580264 }, duration: 2.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "01438924-a91c-4048-9f97-c865b7234e1e", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.58242583255426, lng: 13.424024931174126 }, duration: 8.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "a04f6653-b434-405a-893d-20bb015a9b28", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.42408405408318, lng: 13.410100532514091 }, duration: 9.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "5b4243f2-023a-4260-ac52-e11d3cae35fd", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.53915360732873, lng: 13.284549673297757 }, duration: 9.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "4343c1cb-a89d-4997-abac-b854d6c37745", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.47870577375783, lng: 13.591543831059107 }, duration: 3.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "441afb8e-4bc7-47fd-8bf9-661030055a8a", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.546047722078825, lng: 13.476651851477996 }, duration: 9.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "e14426bd-a01f-40fb-bdde-d9ae662cb125", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.555667694102134, lng: 13.274429666503758 }, duration: 1.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "b01f157c-dd5a-40f2-827d-02aa0c0f77c3", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.54051543529075, lng: 13.22239971418508 }, duration: 1.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "31cd8755-469a-4333-8954-c16d12275750", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.56735738712238, lng: 13.568929597481816 }, duration: 3.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "75347edc-dac3-4bbe-9b5d-14a40022e151", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.57709328526965, lng: 13.286355675930084 }, duration: 3.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "e3df2a0d-4177-4f33-a22d-7f480141bf60", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.57137776157539, lng: 13.43306974910318 }, duration: 8.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "7c70f0f7-0a35-4180-b3c3-8df3d0ea423f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.45889164891661, lng: 13.394172090881627 }, duration: 9.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "d2b41632-0ca8-4999-b918-64e84619bec8", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.50639886765689, lng: 13.588759905705462 }, duration: 1.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "ee3257eb-76a5-4ba1-8241-13d100fe97dc", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.487627860716294, lng: 13.43790476131986 }, duration: 9.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "630073b9-389b-42ed-808b-ab846abdf872", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.51293787759646, lng: 13.24520715459765 }, duration: 1.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "9b347662-cda4-4a82-8b10-38500b49ab84", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.470605815643246, lng: 13.451558648600088 }, duration: 6.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "276536bb-1743-4c2f-b943-630d5898dc4b", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.480343548539025, lng: 13.44151965378926 }, duration: 7.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "2aeda3d3-c672-4de0-a9cb-9d9e1a581196", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.528162814892234, lng: 13.461657958541654 }, duration: 9.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "02432b8c-6846-48e3-b3a7-f97ed00bd507", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.54486680508886, lng: 13.228079736590146 }, duration: 8.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "ce6043ae-eb3a-46f9-99fc-502a6d74f067", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.4401353367575, lng: 13.26990357216738 }, duration: 1.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "5d6d1f28-df89-4044-837a-f192b2bd8ac2", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.588753646099704, lng: 13.565873423130352 }, duration: 4.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "e5c75619-d10f-494c-9100-dcc6ebb06cdd", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.432706335795125, lng: 13.456545183311565 }, duration: 3.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "45203a6f-2718-4bd2-8919-18e166571acf", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.43022262677754, lng: 13.320715544314792 }, duration: 5.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "a9df3014-e34a-4d7f-924a-a53c2cc26041", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.53287611569656, lng: 13.278281531893185 }, duration: 4.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "c1f1adbb-40c8-47d1-8393-26fcbd1cfe19", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.569702052852776, lng: 13.24463887294892 }, duration: 4.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "cd6f703d-49f7-4f15-8a60-9afdebf2107e", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.591464020880665, lng: 13.510585285219635 }, duration: 4.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "9baf23be-4521-4154-8ff0-102bd72b9926", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.42982275489383, lng: 13.31276157040735 }, duration: 9.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "6f32c1a4-20d6-4701-84f7-ca40912ea635", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.550229687064665, lng: 13.580494320508334 }, duration: 1.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "bec43f8c-cc07-47bc-a2e5-14141ac7dacc", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.57031836514887, lng: 13.321853608114758 }, duration: 6.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "301df478-8798-49e2-8396-8c3e9e54aa89", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.50222423936231, lng: 13.5392176706279 }, duration: 5.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "80e781ec-4cbd-4780-82ea-ef5cc1afa28a", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.5535861643843, lng: 13.288087095041506 }, duration: 3.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "b7d6c4b9-ad0b-4511-bc43-3558e21a8e97", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.46601593404098, lng: 13.32878471576825 }, duration: 5.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "268be9f3-b1c3-4bee-a75c-0ca245018d79", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.50867533429348, lng: 13.379334395278022 }, duration: 4.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "8d4b00ac-e793-4756-bfe5-6f4dc6541014", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.55634428071969, lng: 13.415755065592974 }, duration: 1.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "595d5e4f-5076-46dd-bb53-a6ff158abedd", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.44909363886323, lng: 13.409332537528641 }, duration: 4.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "0670abe5-ecd2-4783-a1fc-35c7c8b10c39", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.45127912920764, lng: 13.223947584874704 }, duration: 7.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "7b622337-3782-49dc-b542-b4c1d5060a3a", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.50284895640776, lng: 13.493449854850162 }, duration: 4.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "474c4bab-0830-43f7-84de-d7981dff380e", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.581933847359046, lng: 13.255132682017463 }, duration: 7.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "67e4525b-d895-4d02-bdad-97a1583a1b43", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.427031471706925, lng: 13.519243345400847 }, duration: 8.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "a5715f0d-b89b-4a42-9ee2-db224ea45dd8", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.451867174967575, lng: 13.35392129905765 }, duration: 2.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "69b71470-9917-4181-aaaa-bf4d2474bb62", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.51670854866352, lng: 13.264554325407122 }, duration: 3.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "3569b4eb-b168-4358-bb92-5ed8d9ea4c1c", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.571306194810106, lng: 13.540478827935068 }, duration: 1.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "a3ecec4a-a00b-40fc-89bf-280aa259d4da", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.571503021324986, lng: 13.47425521564663 }, duration: 2.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }], relations: None, clustering: None }, fleet: Fleet { vehicles: [VehicleType { type_id: "72bf5459-1d6d-4e0c-9e20-fad5bf43b46e", vehicle_ids: ["72bf5459-1d6d-4e0c-9e20-fad5bf43b46e_1", "72bf5459-1d6d-4e0c-9e20-fad5bf43b46e_2", "72bf5459-1d6d-4e0c-9e20-fad5bf43b46e_3"], profile: VehicleProfile { matrix: "car", scale: None }, costs: VehicleCosts { fixed: Some(30.0), distance: 0.0015, time: 0.005, span: None }, shifts: [VehicleShift { start: ShiftStart { earliest: "2020-07-04T09:00:00Z", latest: Some("2020-07-04T09:00:00Z"), location: Coordinate { lat: 52.45973476077087, lng: 13.545244671066003 } }, end: Some(ShiftEnd { earliest: None, latest: "2020-07-04T18:00:00Z", location: Coordinate { lat: 52.45973476077087, lng: 13.545244671066003 } }), breaks: Some([Optional { time: TimeOffset([5597.0, 6719.0]), places: [VehicleOptionalBreakPlace { duration: 24.0, location: Some(Coordinate { lat: 52.43907683238865, lng: 13.365717044514959 }), tag: None }], policy: None }]), reloads: None, recharges: None, job_times: None }], capacity: [34], skills: None, limits: None, min_shifts: None }], profiles: [MatrixProfile { name: "car", speed: None }], resources: None }, objectives: None }
+cc 94dd2fac865a93fcc85d807ad314d02ece2a37855b4aa0a26c04eae3f03f3981 # shrinks to problem = Problem { plan: Plan { jobs: [Job { id: "155e338f-c11c-4a44-8a81-3b1fd78fe4d3", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.474559134677214, lng: 13.398853146297467 }, duration: 7.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "58c49ea9-4798-4bb3-a6d3-1c84ed10f19c", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.4657321161798, lng: 13.296558301477063 }, duration: 7.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "22d4b64c-86fe-42c2-a915-2e376ec38a7e", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.544093122662574, lng: 13.40903547640542 }, duration: 3.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "820d485a-4d3d-4c14-8b68-1803b4136ca5", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.47172175068895, lng: 13.220165618050677 }, duration: 3.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "2cf2a9bc-206a-4c35-a05f-80993afffc32", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.4623855483617, lng: 13.542517925529927 }, duration: 3.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "caecb981-cdc5-48c5-bbe1-6baeaba187f8", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.59243338193474, lng: 13.390468113388948 }, duration: 3.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "bede2b1b-ccac-466e-a8b2-c4dc7bbf55c3", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.44303506437899, lng: 13.488833551602335 }, duration: 4.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "c06583e3-1aae-43a6-a74a-e7028a4f2679", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.46157881694266, lng: 13.509555092158259 }, duration: 6.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "1faae26d-0f56-41d8-80d2-0e05f7a0e84a", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.504509018678455, lng: 13.523991907159354 }, duration: 3.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "0b86e3a7-20fa-4439-8640-db8b40c21aec", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.43441590783998, lng: 13.37010335548603 }, duration: 9.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "38594bf8-0fab-4530-8798-e2e98a61688a", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.45115389936743, lng: 13.27728194443155 }, duration: 8.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "e0b135a2-9bc7-4604-a873-158faca84460", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.45089260969449, lng: 13.445995093957148 }, duration: 6.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "5ab5b1b0-71d8-4832-9cba-e08aa8355d57", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.54194593657285, lng: 13.30073749127706 }, duration: 7.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "edf5d56c-67a4-4b80-a44d-004af7be8a3e", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.51091233818449, lng: 13.506248282586881 }, duration: 1.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "b3ce6c80-bc59-40ac-9f0d-c221fa3f2f89", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.58673743515357, lng: 13.575418229599986 }, duration: 3.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "291d0f31-a1b0-4412-ad52-6e639454cbfc", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.52744218299286, lng: 13.458575307632197 }, duration: 1.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "96f94e2c-8756-4b9a-9be7-9512ef90fec8", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.488040787466666, lng: 13.409517094201322 }, duration: 1.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "2d30d5c7-8a3f-49c1-a485-4a780adc8cb1", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.549817600581754, lng: 13.34843137204637 }, duration: 8.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "6c2d459d-1833-40c2-8905-5ecfc52d7817", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.49511536434449, lng: 13.52646234303235 }, duration: 8.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "0312983d-ac4c-4972-998c-9be59b8e1b20", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.50540222303969, lng: 13.521325955548852 }, duration: 8.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "5174848d-5c3b-47ba-a89c-d464e0194b5f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.48255413181206, lng: 13.56641935887193 }, duration: 4.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "31795e3f-0c44-4d6e-b7bd-e5faed6d82b9", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.456037716601315, lng: 13.320209456256784 }, duration: 5.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "16b85f4e-d669-43fa-a433-3c1ab5574137", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.54908413623294, lng: 13.350362286981035 }, duration: 5.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "61fc73cb-f667-4027-82be-eab0495f315f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.482298379708745, lng: 13.594972868337834 }, duration: 5.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "3bc9a2ef-a6c2-40a1-8ab7-ff5b9076b2b6", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.57913587155296, lng: 13.435682567857437 }, duration: 2.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "2b2b4d97-ef61-4047-bee6-2dcd44cc20a9", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.444365630477726, lng: 13.541949575941324 }, duration: 4.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "28226c78-2c02-4a3e-bee7-56ea9ccbd2a6", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.4292683830361, lng: 13.244152347240153 }, duration: 7.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "ceb62f31-b855-41b5-9454-13195b117ec9", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.47608549196223, lng: 13.240539437312048 }, duration: 2.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "6dc40df8-12e1-42e8-bbaa-97db228ad894", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.42679015669843, lng: 13.503759381796486 }, duration: 8.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "bd7951cb-8d26-4022-8e67-a6d80429e8b6", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.54596323522235, lng: 13.21642583691555 }, duration: 3.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "85f62cca-b7d4-44d1-aac9-d79e19155a4e", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.52227303258286, lng: 13.370765900897698 }, duration: 3.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "07fc8d22-adcc-4af3-8f61-d38fc1543c85", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.48169615398642, lng: 13.55748663922583 }, duration: 2.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "afe8a4be-a786-4948-831a-36dfcbd413f8", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.425472838404076, lng: 13.280176171805186 }, duration: 1.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "396dd753-dc2a-49fd-a003-f140310f4822", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.42995645102395, lng: 13.56462068002746 }, duration: 3.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "7d3536ff-2f5d-444f-ba0e-9c17cb91f55d", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.46377103764277, lng: 13.510270405696247 }, duration: 4.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "ae9162f8-3dd0-4c57-a7cf-d6219ad9943f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.52722120804016, lng: 13.388768685829916 }, duration: 3.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "18dbe27d-31e3-4935-b6a8-9a416f2293e4", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.42839304279535, lng: 13.46339458429352 }, duration: 4.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "8d1e6207-ddd7-442c-9038-37bc2e87548d", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.49323044724849, lng: 13.467041913634441 }, duration: 4.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "58a49acb-bb88-40fa-838f-b1d199d950c5", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.56463627044338, lng: 13.415437273395948 }, duration: 3.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "96c3cbb1-06e3-4326-be65-1bee3d32bdd6", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.426181608050605, lng: 13.425292486711578 }, duration: 5.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "1d155c6c-b5f2-42a8-ab64-6c38340c81d4", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.48411091463025, lng: 13.328578496613444 }, duration: 1.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "a8811528-54d6-4088-87a4-2ba403970228", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.56034231634126, lng: 13.456463611078847 }, duration: 6.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "a5991866-c931-4e61-bf10-c3be541ddbde", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.57915537802015, lng: 13.329118090403389 }, duration: 6.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "53323dc6-2bcf-438f-9984-95a34822f492", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.4698308840938, lng: 13.341678236338831 }, duration: 3.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "31be873c-c9de-46e4-8e8f-b28f849e091c", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.49608460830737, lng: 13.41880183937149 }, duration: 1.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "e80e97dd-238f-4757-808b-ad80f3cf6970", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.55202068033335, lng: 13.396359693322873 }, duration: 6.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }], relations: None, clustering: None }, fleet: Fleet { vehicles: [VehicleType { type_id: "84ec8266-0eab-4235-b265-9b6e4aa9a8d9", vehicle_ids: ["84ec8266-0eab-4235-b265-9b6e4aa9a8d9_1", "84ec8266-0eab-4235-b265-9b6e4aa9a8d9_2", "84ec8266-0eab-4235-b265-9b6e4aa9a8d9_3"], profile: VehicleProfile { matrix: "car", scale: None }, costs: VehicleCosts { fixed: Some(30.0), distance: 0.0015, time: 0.005, span: None }, shifts: [VehicleShift { start: ShiftStart { earliest: "2020-07-04T09:00:00Z", latest: Some("2020-07-04T09:00:00Z"), location: Coordinate { lat: 52.43268028014136, lng: 13.281144730368917 } }, end: Some(ShiftEnd { earliest: None, latest: "2020-07-04T18:00:00Z", location: Coordinate { lat: 52.43268028014136, lng: 13.281144730368917 } }), breaks: Some([Required { time: OffsetTime { earliest: 7276.0, latest: 7286.0 }, duration: 1655.0 }]), reloads: None, recharges: None, job_times: None }], capacity: [42], skills: None, limits: None, min_shifts: None }, VehicleType { type_id: "36edf95d-a32d-472f-8459-c3b6a906fe1b", vehicle_ids: ["36edf95d-a32d-472f-8459-c3b6a906fe1b_1", "36edf95d-a32d-472f-8459-c3b6a906fe1b_2", "36edf95d-a32d-472f-8459-c3b6a906fe1b_3"], profile: VehicleProfile { matrix: "car", scale: None }, costs: VehicleCosts { fixed: Some(30.0), distance: 0.0015, time: 0.005, span: None }, shifts: [VehicleShift { start: ShiftStart { earliest: "2020-07-04T09:00:00Z", latest: Some("2020-07-04T09:00:00Z"), location: Coordinate { lat: 52.44025038204385, lng: 13.330718067786913 } }, end: Some(ShiftEnd { earliest: None, latest: "2020-07-04T18:00:00Z", location: Coordinate { lat: 52.44025038204385, lng: 13.330718067786913 } }), breaks: Some([Required { time: OffsetTime { earliest: 5052.0, latest: 5062.0 }, duration: 532.0 }]), reloads: None, recharges: None, job_times: None }], capacity: [40], skills: None, limits: None, min_shifts: None }], profiles: [MatrixProfile { name: "car", speed: None }], resources: None }, objectives: None }
+cc 5e42012c3e223245982befeac1df0c438a8f4353a33d1ae2a98a18eb23fd9953 # shrinks to problem = Problem { plan: Plan { jobs: [Job { id: "826fe620-cde6-430b-a750-7753474188ed", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.4956830571904, lng: 13.220361121542927 }, duration: 2.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "ed0011c0-61c0-41d1-a2f0-c0018e635f87", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.5290976252691, lng: 13.252262555287464 }, duration: 2.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "c63a1753-471f-49d1-bcbe-804f8a425c34", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.53725119502138, lng: 13.499224385422208 }, duration: 5.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "fc606012-5ab2-49f4-9c4a-3f3f183c512c", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.49647862398169, lng: 13.22816614815501 }, duration: 7.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "fc1b5af7-48c8-4967-bcd8-bafbd92cfb37", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.55501624287509, lng: 13.326171962175945 }, duration: 3.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "7a59acba-7676-41f0-a2df-f421c51e9776", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.582217136131796, lng: 13.512470702643899 }, duration: 8.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "4e5aed5a-cd6f-45d8-9b67-87ad64809b71", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.43963128531451, lng: 13.585909755597843 }, duration: 6.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "f1bfbceb-68f5-45d7-aaa6-a3f487a5e633", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.58656499546066, lng: 13.449546803838617 }, duration: 2.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "9280a126-1277-4c2c-a82b-70e203f4ed98", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.43298629569137, lng: 13.356325760259518 }, duration: 1.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "a15d37bf-2773-4689-ba36-57596d3cdde0", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.55348387532306, lng: 13.390558215347468 }, duration: 5.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "54f595b5-f991-4cd6-9bd1-020550fdd1dd", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.48005093260635, lng: 13.38850020026242 }, duration: 2.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "93d96e98-bd85-4d35-a148-5789cc8043fd", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.553571845644946, lng: 13.563282459864269 }, duration: 8.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "e0872430-fc70-427f-8471-46a1d6236698", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.439088988334944, lng: 13.457488810066451 }, duration: 5.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "47e5374d-ca34-47ca-a590-0390dbebf1d9", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.53533412280426, lng: 13.480385792057877 }, duration: 8.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "36be86fd-1d3e-4567-9e0e-169db06fa85c", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.56945345428819, lng: 13.467853139769277 }, duration: 7.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "da3bea65-4a70-4a1a-96a9-d61e4137a0ed", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.537743831074025, lng: 13.317542183493778 }, duration: 4.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "67ad6f65-d400-49fc-8fdb-fb3c27779d88", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.524292178619575, lng: 13.240768918518512 }, duration: 7.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "8182fb5e-a831-44c4-87c5-85502a613ac9", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.58506524769368, lng: 13.31312929517216 }, duration: 6.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "0d7c28d4-eae2-4168-9dc6-9887888c96be", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.493403828861275, lng: 13.560830118699256 }, duration: 5.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "621c1a18-ffc0-45f2-ac1d-7da97f67b181", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.42907610982545, lng: 13.49322085357528 }, duration: 4.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "7b6a21ed-b790-4c73-90ba-17eb9dc925bf", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.425839828809714, lng: 13.31499023550089 }, duration: 2.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "d6f73407-77f6-47f1-a579-fc078f19c9db", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.517738550380145, lng: 13.571899606349202 }, duration: 2.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "56db18e7-ee4f-47f2-b889-89f97167f41f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.4872444883959, lng: 13.428537017227645 }, duration: 8.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "39903f8a-5fe1-40ec-bdff-b8149a61eb2a", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.587052547076816, lng: 13.410031641619474 }, duration: 5.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "0493392a-c7a9-444e-980a-fb6bc214b006", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.54966509206905, lng: 13.400842166593836 }, duration: 5.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "358e805e-decf-4fa8-8820-67765b4ba269", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.534403931603116, lng: 13.497003580273324 }, duration: 5.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "64b8d358-3922-492a-92ad-a58e7728b499", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.526457410475935, lng: 13.349203911695037 }, duration: 6.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "174b9a85-61c5-43a5-9f72-90626f027efb", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.471825523643396, lng: 13.214865907985349 }, duration: 6.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "cbe372aa-c8ad-4c0b-8bff-9af536587d72", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.43227000575288, lng: 13.218471799639605 }, duration: 9.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "201504b1-6fac-499d-80b2-0046a5943f42", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.44505101160084, lng: 13.44749323420409 }, duration: 2.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "88f413d1-a964-4d2b-af59-6d68ced00b8e", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.59293925129785, lng: 13.520136786834673 }, duration: 3.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "55e66a1e-c296-4566-96c4-3f8556bbe1c1", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.43146363447461, lng: 13.396575838944466 }, duration: 5.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "bb200f1c-0abc-486c-b699-5e4525ebab07", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.55060174733469, lng: 13.370980697180096 }, duration: 6.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "24c83638-341c-4132-aaae-a0daa92293f8", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.57462828869128, lng: 13.371537729264723 }, duration: 4.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "83c6a68d-3350-4464-9f28-09873ea8420e", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.483235183923085, lng: 13.546856448161636 }, duration: 9.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "6fd9ad61-0983-4b9c-908a-272f7606eb0e", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.461366248371434, lng: 13.417981361596187 }, duration: 6.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "bb2ebf79-fb81-4652-b5ca-936366830d9b", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.433313649831, lng: 13.368720956340345 }, duration: 6.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "e31c7a36-5abe-4346-ad2c-11980f9c2c99", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.52875930214337, lng: 13.413885219395395 }, duration: 2.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "5b800c1b-7413-4a62-946b-52a1fb991890", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.52842030784828, lng: 13.346915918832645 }, duration: 3.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "6d85168a-ab59-404f-b0af-fb82a2f67830", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.48796618541804, lng: 13.57592689865129 }, duration: 9.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "1517cc2b-6ff4-479f-b65e-86fe581e30cc", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.506793117491846, lng: 13.229916869353906 }, duration: 5.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "765d31d6-a94c-4991-b710-e4c77e378642", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.556585109948536, lng: 13.520534432652335 }, duration: 7.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "df5242fd-f9fa-4bc5-91c8-55ef9007a5b5", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.551168120951196, lng: 13.427440512469758 }, duration: 2.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "fb246257-8b99-40ab-a325-dd86a491c45e", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.54588465598947, lng: 13.56179381527092 }, duration: 6.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "0d27965e-d587-440c-ade4-56e14a21c55e", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.582130259436255, lng: 13.451810734599187 }, duration: 2.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "d10fc160-a38d-4d71-afea-ae47f3601ff2", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.59133690402662, lng: 13.432358162566516 }, duration: 3.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "d5e28af7-c4d3-419a-ad4b-e0933c01c8ad", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.52790688403257, lng: 13.245346087349406 }, duration: 5.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "4be4e304-1e2e-4417-82c0-9c531f3cb59b", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.554281480694556, lng: 13.560827630744484 }, duration: 7.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "116e6f61-7641-4447-be8f-14020f8f44eb", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.45615923757627, lng: 13.37676488188056 }, duration: 7.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "691f7eeb-0069-4c78-b5ab-a1a28f10e386", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.44147749194526, lng: 13.294531944581877 }, duration: 5.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "1f139ad3-1688-4de1-8b51-11da6964ad7a", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.52543229201517, lng: 13.364641859486332 }, duration: 6.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "78e7a4df-6239-4de9-bc59-19449f279b8d", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.473537210344794, lng: 13.433985287385832 }, duration: 2.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "8a7b56d0-e0f6-4a6a-ad75-fcdcf5c175d1", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.574025287052415, lng: 13.241093193003632 }, duration: 4.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "de886746-6021-4eee-a92a-8b541fb5170a", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.46467831973891, lng: 13.34540498861492 }, duration: 9.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "a0e71f81-4f68-43b0-b025-3ce2db74cebe", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.53878781449112, lng: 13.585580044586221 }, duration: 8.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "93b24028-c78d-42c0-9bcd-d4cd80ca9c2b", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.561061883833226, lng: 13.313181078322472 }, duration: 4.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "ba468c99-401c-494f-b5cd-9945603cc7c8", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.430309724756874, lng: 13.589787051627777 }, duration: 3.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "30f52d61-0aa8-49d8-8c9f-3313d28b0851", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.47366945071049, lng: 13.226415768568263 }, duration: 5.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "60806c99-de10-48fc-8622-2263bdffac19", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.58224366686516, lng: 13.472096160262959 }, duration: 5.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "5fe79182-1254-4a06-ac0f-2bdbe4063077", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.46335727438428, lng: 13.224503759726467 }, duration: 3.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "8b912470-f2c6-434a-8565-788848a7009d", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.5231591027105, lng: 13.587647658812518 }, duration: 3.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "c68bdcb7-7031-4938-9233-c20c67ec8fa3", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.460303941151956, lng: 13.290588251377843 }, duration: 2.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "b1b5a953-fe9f-4fcd-9044-84a01863f693", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.486227512413116, lng: 13.340159002502652 }, duration: 5.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "626c78c2-a534-4807-956d-815cadb6e718", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.57696561523725, lng: 13.217806696264603 }, duration: 3.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "936d9f52-5564-4937-ad4c-d9593359d09a", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.523022802128, lng: 13.565564530019945 }, duration: 8.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "04055cc1-0dbb-4281-a6cd-ece4d115b421", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.57807544008803, lng: 13.511921386672016 }, duration: 8.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "ab6162bd-1bdb-4bea-bf2a-da3f2ab1409c", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.576947442085334, lng: 13.273033948086633 }, duration: 9.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "b1ae5a3d-ea10-42f4-ba9e-26abee805169", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.46445905688678, lng: 13.326544395032098 }, duration: 5.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "711db01b-74bb-43e0-9379-d87007a23af1", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.501599244544565, lng: 13.434266311673882 }, duration: 8.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "c5c640ec-0844-4970-a2dc-3fa9962600b4", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.44904994746494, lng: 13.226937986088535 }, duration: 3.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "3a805de0-0a40-4978-982a-002eb46d9fc9", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.435380948664104, lng: 13.311671391215738 }, duration: 3.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "6dc9d8fb-0ef1-4773-9df5-8d7367f859a1", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.51186388635609, lng: 13.551351566496843 }, duration: 3.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "347fde89-9aa7-4c74-ac98-94a2fbda5a50", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.52790287144636, lng: 13.48948593828069 }, duration: 6.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "c7ed490f-0bab-4fd6-8be0-195878c9e7da", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.54928423104826, lng: 13.51424323185075 }, duration: 2.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "f4200d96-4fad-4e98-980c-a7749169a6d7", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.4388641525266, lng: 13.531594056174207 }, duration: 5.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "6faf6a28-230a-4e15-949d-a5218401a66f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.43707224365035, lng: 13.32710084349339 }, duration: 2.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "d265a977-6b09-476a-a74e-8e809201af46", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.44072036617677, lng: 13.474886264084565 }, duration: 1.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "2f617bde-a23d-4a06-bad5-71dc750f1d97", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.440262460146755, lng: 13.26295841607324 }, duration: 9.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "2c8ec0cb-f894-4145-a0e2-b432d3d1e277", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.576785029476966, lng: 13.494578297934963 }, duration: 1.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "57e6a485-d580-4105-9a2d-ed298dfbef5f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.513975118462, lng: 13.467416055627925 }, duration: 6.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "dfab1ec5-e758-415c-9f3b-8e345da40c87", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.51749977550084, lng: 13.4608886697685 }, duration: 3.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "3538b8d4-d612-4fdc-88da-1cadff3da3ef", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.429489915435035, lng: 13.445812259590864 }, duration: 2.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "c6e63990-e6fc-480d-bae4-55a9ae9799cb", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.44812726230034, lng: 13.34325697964438 }, duration: 7.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "95a895a9-7687-4420-ae91-8351fd201a1f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.52638325447115, lng: 13.369940639430617 }, duration: 4.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "756730a0-03c6-4ea4-9a0f-df5a68d820cc", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.514498604846985, lng: 13.589000026906847 }, duration: 7.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "b0bf6dc9-3116-4c7c-b054-e44325ea7997", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.59255921745685, lng: 13.340072969162026 }, duration: 2.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "dfc7c10e-da26-48b1-9da2-77738def4f30", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.57836795514375, lng: 13.282263506124028 }, duration: 4.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "82a390c5-9236-4f86-97cb-7f571b325d46", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.43752316167672, lng: 13.3576847212127 }, duration: 5.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "f4cf4a61-5dfc-4555-8aa7-58abccbf7268", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.535525838368564, lng: 13.363842038916705 }, duration: 3.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "fb01fd40-fafd-44f0-8118-d7ea9f5e211e", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.44920927586304, lng: 13.50104688720275 }, duration: 8.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "fcfa0fd3-4c81-4971-96f5-f8265834e49c", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.572624054944775, lng: 13.447649996120356 }, duration: 8.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "1dfa84c8-1102-4786-9ff5-d568ddc9a131", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.50469022788219, lng: 13.570578690502852 }, duration: 2.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "65c4acc3-98a8-40d2-a0e0-39e1a51e5e3b", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.543843837608065, lng: 13.262309425763021 }, duration: 5.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "329bf07e-1be8-4670-b7f4-26df0839cb70", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.49682960821704, lng: 13.554718293387644 }, duration: 5.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "5be28df9-f77f-4987-a1f6-85a4e17a22bd", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.5424667908735, lng: 13.466198041372525 }, duration: 4.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "74521264-b07c-45bd-9e75-ff4491f711a9", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.48914836151938, lng: 13.259599985350008 }, duration: 7.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "e6610d12-b930-41c3-8771-a8a0191f4e14", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.54938847771349, lng: 13.257941493767328 }, duration: 3.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "da029efb-0e79-4db6-85ce-9758635b3dc3", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.45476172997255, lng: 13.310664955018431 }, duration: 4.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "44e540e9-2630-4bb8-8ceb-3bff17f26c03", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.5934844645573, lng: 13.281377333834195 }, duration: 2.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "120eda86-4a1f-42a7-a3d4-96c6b1f5a349", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.45144653260114, lng: 13.563120490139985 }, duration: 3.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "5ec51b30-4259-4c08-99ff-e641afe21af3", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.471640830422785, lng: 13.26268568034167 }, duration: 5.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "6e1ca0df-2539-45b6-9c72-7167afe62351", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.50844405025136, lng: 13.364615787396385 }, duration: 9.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "2ae7a916-e160-4e28-aa38-d80d2cdff1a4", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.44380536859367, lng: 13.508376364891127 }, duration: 3.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "e7553888-aa67-4f68-a054-ab55bfcb4a2f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.5468045922072, lng: 13.585389197745002 }, duration: 3.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "1e6607cc-1cff-4710-93cd-6a9d3d491bb4", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.46153224658127, lng: 13.48634525466487 }, duration: 1.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "a4b54130-2a2d-4488-bff7-fdbe3e366866", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.442341570264134, lng: 13.239331608070417 }, duration: 4.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "8a90bb6e-5697-464c-aeb9-2dece586254f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.54685339130178, lng: 13.506281102976288 }, duration: 7.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "fe46a32a-d6d7-4e7d-bf7f-78a97e94af8f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.473786312551134, lng: 13.596579052158406 }, duration: 3.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "8f567027-313c-4f9a-ba6b-58ba5eaef72d", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.56323341920995, lng: 13.246320960650804 }, duration: 6.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "499184c3-dc37-41e0-b49e-0b60778bc495", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.562899456092886, lng: 13.293334408048649 }, duration: 7.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "65c8cf7f-4dd1-4e9c-a0b8-de00cdb8d380", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.44864280981295, lng: 13.425971729496181 }, duration: 8.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "92f5d449-a773-480d-ae7b-6ed7cf48f7a5", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.51132385517064, lng: 13.328597984642196 }, duration: 3.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "f208a544-94ef-4f44-bbbf-ce627a2fce9a", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.562375498192935, lng: 13.421345349202737 }, duration: 7.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "bbd68318-afc6-43cf-a5df-44e8f150aac7", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.49414617747451, lng: 13.580284030335447 }, duration: 8.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "77e49390-8baf-456b-b4e2-fce18888f535", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.577547813157004, lng: 13.320310539769153 }, duration: 3.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "091cc72b-506d-4955-b79a-1a1710d90752", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.587765031749804, lng: 13.264172860067022 }, duration: 6.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "5198486b-9216-4ff0-85e5-433e1889aae9", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.44415541226153, lng: 13.344359100153614 }, duration: 3.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }], relations: None, clustering: None }, fleet: Fleet { vehicles: [VehicleType { type_id: "01aa3802-cc13-4381-9162-78ff1957554f", vehicle_ids: ["01aa3802-cc13-4381-9162-78ff1957554f_1", "01aa3802-cc13-4381-9162-78ff1957554f_2", "01aa3802-cc13-4381-9162-78ff1957554f_3"], profile: VehicleProfile { matrix: "car", scale: None }, costs: VehicleCosts { fixed: Some(30.0), distance: 0.0015, time: 0.005, span: None }, shifts: [VehicleShift { start: ShiftStart { earliest: "2020-07-04T09:00:00Z", latest: Some("2020-07-04T09:00:00Z"), location: Coordinate { lat: 52.427364405133645, lng: 13.505140863025977 } }, end: Some(ShiftEnd { earliest: None, latest: "2020-07-04T18:00:00Z", location: Coordinate { lat: 52.427364405133645, lng: 13.505140863025977 } }), breaks: Some([Optional { time: TimeOffset([9664.0, 11115.0]), places: [VehicleOptionalBreakPlace { duration: 24.0, location: None, tag: None }], policy: None }]), reloads: None, recharges: None, job_times: None }], capacity: [44], skills: None, limits: None, min_shifts: None }, VehicleType { type_id: "d172f659-d260-4574-99a1-214d9a3be1e4", vehicle_ids: ["d172f659-d260-4574-99a1-214d9a3be1e4_1", "d172f659-d260-4574-99a1-214d9a3be1e4_2"], profile: VehicleProfile { matrix: "car", scale: None }, costs: VehicleCosts { fixed: Some(30.0), distance: 0.0015, time: 0.005, span: None }, shifts: [VehicleShift { start: ShiftStart { earliest: "2020-07-04T09:00:00Z", latest: Some("2020-07-04T09:00:00Z"), location: Coordinate { lat: 52.479391257660076, lng: 13.53775256480862 } }, end: Some(ShiftEnd { earliest: None, latest: "2020-07-04T18:00:00Z", location: Coordinate { lat: 52.479391257660076, lng: 13.53775256480862 } }), breaks: Some([Optional { time: TimeOffset([5155.0, 6715.0]), places: [VehicleOptionalBreakPlace { duration: 84.0, location: None, tag: None }], policy: None }]), reloads: None, recharges: None, job_times: None }], capacity: [44], skills: None, limits: None, min_shifts: None }], profiles: [MatrixProfile { name: "car", speed: None }], resources: None }, objectives: None }
+cc fa9b38d0a532255ae4da35aa129cfd620da3d34a85ae78f7c209667167e7795b # shrinks to problem = Problem { plan: Plan { jobs: [Job { id: "7d1941d5-dd4a-4b95-a09b-7b4c95a2dd5c", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.436559712559344, lng: 13.44626908014057 }, duration: 9.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "4bf5c7e2-fd44-48dc-b1b5-e98b04ca49b2", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.551203283950436, lng: 13.440043782858611 }, duration: 1.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "13addb0f-0ce8-4698-87f3-650213bfc036", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.55152702201738, lng: 13.338364692522509 }, duration: 8.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "d9cbb910-70b3-4e3e-bd37-5c94798421ab", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.50463555890694, lng: 13.221173284137114 }, duration: 9.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "07d169e4-3fc2-47ea-bc49-85515e016cd5", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.57580344396131, lng: 13.47860707265018 }, duration: 4.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "b0a3a9ec-73c8-4fe1-8dbb-6c4afdb87ebc", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.48352683907001, lng: 13.529843649717893 }, duration: 9.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "614b08fa-77db-4467-b9f0-fd68952ab348", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.567237221282696, lng: 13.230730542417362 }, duration: 6.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "f518af8f-9b6e-43b9-aea0-439c56cb309b", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.493822935768875, lng: 13.421688914642028 }, duration: 5.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "72fe32f7-fc24-4be8-9d73-0efd42b5238d", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.55827604321322, lng: 13.512906825342258 }, duration: 8.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "b2597a11-6ae4-485e-ab23-1dcc3d92c3bf", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.507020941100905, lng: 13.490255175517394 }, duration: 3.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "f0509ee0-5e47-46fa-8831-e2ff3a07de70", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.504113484304554, lng: 13.37722302122535 }, duration: 9.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "54f70238-7444-4d80-95cd-8d7a7d6c7be6", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.433630970035146, lng: 13.481576313300584 }, duration: 4.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "a81e49f2-226c-4521-be19-a51cf75f6e7f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.55565111974251, lng: 13.419985572321734 }, duration: 2.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "65b83f00-3c40-488e-b43d-54403f434e39", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.43586577795592, lng: 13.529185415243267 }, duration: 2.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "ab006cc9-db83-47e1-b0e5-4d3293578cd9", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.44740704567263, lng: 13.439089505242343 }, duration: 4.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "83938e65-d69a-4772-a0be-559a53aa016a", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.56941168548236, lng: 13.420799447514296 }, duration: 2.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "3957b753-c943-400c-8270-d2e0ee61d9ca", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.44450889911224, lng: 13.556961988677253 }, duration: 9.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "ca73a66b-ef74-4262-b1bb-456d4c6477e6", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.50716685202195, lng: 13.3518947978143 }, duration: 9.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "012e995e-4da7-487d-9215-086c4ceb6b50", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.51427329064696, lng: 13.584453800177354 }, duration: 5.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "8db76567-bca6-4841-a9a4-89590d3ac506", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.465769101628, lng: 13.327542843584261 }, duration: 6.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "2f617e28-97f4-4224-9de5-52c40f2b70ef", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.470317182591224, lng: 13.405061854809569 }, duration: 9.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "d09c1023-781f-4634-a2f0-d6e8f500eed6", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.57942893250574, lng: 13.348169201737639 }, duration: 4.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "60beca16-3a19-4603-8e68-c4e01a7f06a2", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.56311675616047, lng: 13.270485726861688 }, duration: 9.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "44503372-555b-4d91-a889-97b88dcdcf7c", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.45720756063484, lng: 13.222222881231707 }, duration: 7.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "52517585-fc28-49ca-be47-0ffc64d8b54e", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.509873335341545, lng: 13.590740545341479 }, duration: 3.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "f5128fbb-19ef-4899-8d14-ee23f3f8bbb8", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.47061943885503, lng: 13.253536051753906 }, duration: 4.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "3c331932-8ae9-4310-b220-52bcd9ad113d", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.56373261355958, lng: 13.370095148654501 }, duration: 3.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "a125c336-8bd3-4f44-badc-c3b2d2fc6884", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.57022525719234, lng: 13.465726963719897 }, duration: 4.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "15c05ec4-44b8-412e-8c38-ae88a9164f44", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.56931128515663, lng: 13.511676750532077 }, duration: 5.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "e60e8796-166a-4462-bc4a-10b5806f6620", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.55959844713937, lng: 13.583653270546147 }, duration: 2.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "a1c82eaf-dfdb-4c15-9c64-0be20f8b79c5", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.547388658223355, lng: 13.589466728885325 }, duration: 9.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "010c30a0-fefd-4128-a398-1421d13bf1f5", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.57916783200197, lng: 13.269796960219773 }, duration: 4.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "258fcbf6-a0ba-4744-9d5c-07f49e5cd824", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.557657441863164, lng: 13.250062721989655 }, duration: 2.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "6e181071-bb02-4ff1-bf81-5dd58dd45782", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.53226296873273, lng: 13.224715819697922 }, duration: 8.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "29769f0e-c1a8-48c3-8190-48c2f6e54338", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.478641186066106, lng: 13.553908632190371 }, duration: 2.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "4920981a-9ec3-47fd-85a7-0f95c58989dc", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.45153518659282, lng: 13.524246359172237 }, duration: 8.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "c73464ca-1456-49b4-979f-382bf70bb500", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.559151579615, lng: 13.52724125032329 }, duration: 9.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "1738e23a-4a44-4900-b080-4f83514d435c", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.560371633217486, lng: 13.367443899064254 }, duration: 9.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "242f0ae1-2911-4ea7-9179-5f2cc2de7676", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.51197719520246, lng: 13.44941527972524 }, duration: 8.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "46fb5303-34cf-4dc0-af66-7caf2af9afb0", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.55985314884123, lng: 13.34090970222512 }, duration: 4.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "c0330047-d427-42e7-8b4f-f836bf044ab3", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.485973382803735, lng: 13.24786473021797 }, duration: 7.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "3d0127e5-6a8f-42ca-a279-62823aeb235a", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.580468370822445, lng: 13.293489592495344 }, duration: 5.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "7e7a2d02-84f9-4517-b243-3bce37f7fe77", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.509831547839426, lng: 13.414550833578392 }, duration: 2.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "678c8b94-a1fd-46a3-bc93-be5473f7f16c", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.554523999502216, lng: 13.287452498319656 }, duration: 7.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "0ab36ba6-56ad-4542-b52c-c7dccd6f580b", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.54184138156112, lng: 13.476895125959253 }, duration: 1.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "5c4e2135-c06d-47f3-a006-965160732a52", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.58609434699699, lng: 13.463647047430669 }, duration: 3.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "a6e0c8a6-59dd-46a1-90a6-42060dd476d4", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.510785462877216, lng: 13.330386487082134 }, duration: 1.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "899bd836-5b2e-4748-a780-6a8c64a99bd7", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.5228700447718, lng: 13.485853534297432 }, duration: 2.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "ce5b2b81-76bd-4212-b44d-a100a7deb13c", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.454210505228346, lng: 13.285514760316728 }, duration: 2.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "2088fc71-9a70-46d8-b130-658ca6471fd4", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.55484457689969, lng: 13.323529732582626 }, duration: 4.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "2e9ae0f8-3c10-47e4-ae6d-a34a19e35fb1", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.42682064375289, lng: 13.402838497440998 }, duration: 4.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "6293194d-a71d-4b52-9aff-709cf4bc6ea0", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.4528676274495, lng: 13.224489048768348 }, duration: 5.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "b08e83fc-49f4-487c-84ad-99d5a61ac454", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.4739571410518, lng: 13.527877231844116 }, duration: 7.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "aa9e65c8-7c3c-4482-999a-2f769226a682", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.57001926169496, lng: 13.420773592496305 }, duration: 7.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "694bb780-d109-4cf3-89c9-961079ff0980", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.50939712262257, lng: 13.457001484714882 }, duration: 9.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "01aae912-2ed7-47ee-adf6-ba52d8d66384", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.45554561791293, lng: 13.298895877617483 }, duration: 7.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "4a038658-91bb-4fd7-81e3-bcf795166d08", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.47347945355404, lng: 13.305801994515424 }, duration: 3.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "0ca4ea77-e3a8-47bc-92a3-7f2bd42871e4", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.5806277109677, lng: 13.314885296935081 }, duration: 1.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "9ac5a354-bdd5-460a-a19d-da40b19a92bb", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.504836880673636, lng: 13.437156976696299 }, duration: 3.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "6d8b5eb3-68d9-46ec-a3b7-afe7fee66c06", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.535015850950685, lng: 13.25205665716618 }, duration: 8.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "c36a1d7b-edfe-4640-81cd-38cb91c84420", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.483191195808196, lng: 13.388324624235874 }, duration: 1.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "dbe17be9-f76e-425f-b57c-21290eaa6e6e", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.45873358106861, lng: 13.49192160751955 }, duration: 3.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "a4ca89b7-7832-49d4-9545-f27c89ca4e89", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.549855785324404, lng: 13.383414605058558 }, duration: 3.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "8505fdd4-373a-4c87-8d33-2d18dfc132ae", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.440343757584024, lng: 13.579716686104492 }, duration: 4.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "ba65139a-8ceb-49e0-b63a-2df4c803c359", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.49660825352338, lng: 13.584791380473641 }, duration: 2.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "32b2857f-a506-4d37-a2f0-bc3f5e6e6533", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.47928262897012, lng: 13.590557375357726 }, duration: 1.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "ac037478-b713-413c-8f48-7a1b43872e6d", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.52373247955751, lng: 13.526723279425736 }, duration: 9.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "c9e73349-49c2-4d04-93ab-83ef494154d4", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.53673945249396, lng: 13.342193844228118 }, duration: 8.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "c9f3bb72-3627-4ea8-86b7-c4663dccd9b7", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.48176168176222, lng: 13.455519633391827 }, duration: 4.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "98e4307c-79b2-4a68-a0cd-8507dc1e2247", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.46839855068231, lng: 13.27288304505762 }, duration: 4.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "d9ce374a-50e4-4b15-abe8-853626125983", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.454567808388674, lng: 13.398520763785596 }, duration: 5.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "32a38b34-a6fb-4894-82fa-26a5c073453a", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.5178153237356, lng: 13.556344469275196 }, duration: 3.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "1ab87c6c-fd96-46f9-9081-ad46caf85708", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.54995018397718, lng: 13.564864923679247 }, duration: 1.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "ed8f4479-47df-4f97-b88a-3515f64a6322", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.50948346776199, lng: 13.355239908456833 }, duration: 2.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "ccc672fb-24bf-4514-8a80-329755c20096", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.56899452426809, lng: 13.381896676590024 }, duration: 4.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "f7c4d9ae-4e53-445d-8966-e72b12867138", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.485542888758836, lng: 13.50570183812208 }, duration: 2.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "8209753e-6077-491f-b8e1-33859df3c72e", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.44243568132832, lng: 13.551563785517425 }, duration: 9.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "082b53b8-a544-45fd-a2c4-aa44117c69c9", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.468043656557384, lng: 13.472278704734764 }, duration: 4.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "2de86b84-d097-49ef-b3c5-744983c13eff", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.52847868448557, lng: 13.541367230776217 }, duration: 6.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "5afe863e-6a34-436d-abe0-19dce143e62a", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.5409651190929, lng: 13.463811003348464 }, duration: 4.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "da54f957-851b-46a7-b2fb-eec84e5e62f8", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.5929616737426, lng: 13.553926657384606 }, duration: 6.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "06cf185f-2978-4f7e-8f43-76ca3cb41600", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.54378251252502, lng: 13.526443407853971 }, duration: 2.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "af1d6147-9b90-4844-9dcf-7a9fe5b1e89d", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.45339332484311, lng: 13.502570141962599 }, duration: 8.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "2def025b-1280-42a6-ab9c-16aedd0cf236", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.44450967966371, lng: 13.31299221235376 }, duration: 4.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "c7e93988-ccd0-4e88-84d4-851c31a606a2", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.4715430730388, lng: 13.50029474583756 }, duration: 6.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "b6a3100f-64a3-4274-ad17-aa460ae87854", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.52773468824927, lng: 13.32956944303461 }, duration: 9.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "12fd374d-3b64-4e2f-93dd-b267b98b7e63", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.58688839125437, lng: 13.53555926097789 }, duration: 7.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "c40583a7-b182-49d2-8581-ede62d5185d1", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.495873888721654, lng: 13.305003133683007 }, duration: 4.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "38ac02e1-79d5-44ee-aa98-c8b43dfdac54", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.56108796305832, lng: 13.248145803572262 }, duration: 9.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "c81040a6-8704-4bcb-b67d-459d661f60c5", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.45497860539378, lng: 13.58509480575765 }, duration: 3.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "7f824841-4da2-4940-b418-0a2483795ccb", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.470829568417734, lng: 13.216199595772414 }, duration: 3.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "bb30cf82-068a-48b7-a4e4-f8df08f55dd2", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.56026241241874, lng: 13.507018219732847 }, duration: 5.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "3e43b0d5-3889-4962-9f1e-cd86bd771133", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.52122451556535, lng: 13.579358633779865 }, duration: 7.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "a067cb9f-322c-4089-9ba6-e2e0a39d8f37", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.5535616364974, lng: 13.44509401057692 }, duration: 8.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "1f1c5e15-ec36-4be4-8adb-403f83148159", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.54572850449269, lng: 13.302572538436086 }, duration: 2.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "4a882219-6b49-4f85-a660-5a6bc56e1e4e", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.437784490151515, lng: 13.230663137662285 }, duration: 2.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "63c6f413-7e27-4d68-9c2e-7e2ad9352d27", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.49812017695, lng: 13.315537211040747 }, duration: 3.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "706adf39-6035-47c3-a050-88b79bfbccb1", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.56593418717684, lng: 13.246290988254758 }, duration: 1.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "b5e831ff-8bd7-471f-91cb-f4a66aee38e7", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.47483831431515, lng: 13.318237496926418 }, duration: 5.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "0426933a-82a0-40c3-a25f-0b74608961de", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.51211451406029, lng: 13.392394796684847 }, duration: 1.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "728352e4-7b36-4525-b0e6-298466121536", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.528404752314756, lng: 13.297577674602373 }, duration: 3.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "0e9fd8a2-c379-423c-b3d8-7d3715edd497", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.54914207607318, lng: 13.488781817167482 }, duration: 3.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "30d7edc7-b4b1-4efa-bb46-63a22b3e1886", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.487380943976056, lng: 13.331367233504187 }, duration: 1.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "1cf91752-4fed-4088-880f-ccef6b4efaa3", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.45108087267874, lng: 13.407236331871722 }, duration: 8.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "42f40e6e-b1ce-4e54-9cb5-b7101aa858f8", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.563282255349655, lng: 13.557054734564023 }, duration: 4.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "27e7c9eb-7acf-4cfe-8e87-4bd4cb8b6ced", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.45598121990181, lng: 13.357488652172986 }, duration: 9.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "ea5018a1-c46e-4183-bd3d-fc38750361eb", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.59102630960705, lng: 13.292258992068165 }, duration: 3.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "87f3733d-3181-4c27-aec4-c9b95cbfee61", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.45154217454813, lng: 13.222150866900037 }, duration: 3.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "a8164999-e76a-41d0-951e-ec9243944eb9", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.577560597738305, lng: 13.500269007108717 }, duration: 6.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "179c9ee8-e9ff-400c-81e1-6bfd3e3cf64f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.505498639028254, lng: 13.545811625206115 }, duration: 3.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "8b856ce9-7a5d-4e39-af7c-6a01c96e3149", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.49944113834835, lng: 13.378662686008003 }, duration: 3.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "a74b9530-3f34-4f51-9ee6-a0bb164ec6cd", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.518211626776065, lng: 13.261928094879977 }, duration: 6.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "cbffef52-7624-4bfd-8142-f6b8b08bf4ca", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.52400007824917, lng: 13.52275349121059 }, duration: 5.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "10f8fb65-5d71-45b4-939f-7fe560e9f679", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.51000676944894, lng: 13.34136595856356 }, duration: 1.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "1538a574-a25e-47f1-bb05-f33283705d3f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.53867621965807, lng: 13.265426994855401 }, duration: 7.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "a8253243-bb3f-40e0-a582-b3f2b86ac519", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.4645235435386, lng: 13.42253426964283 }, duration: 7.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "76fb4c54-476e-4db8-a8ab-be39c27d0257", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.58455952979207, lng: 13.238252484862313 }, duration: 4.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "574d604c-a20d-49d8-802c-28f920de1869", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.59345542995976, lng: 13.326651866764902 }, duration: 3.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "1087b65f-29a7-4a61-bfdc-3db64190391c", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.50794072945912, lng: 13.565904801322151 }, duration: 4.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "5913365a-9f0b-4e49-93f3-bb743de8d3bd", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.467134764183164, lng: 13.311502580912045 }, duration: 4.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }], relations: None, clustering: None }, fleet: Fleet { vehicles: [VehicleType { type_id: "896ec545-19f0-47d2-b2e6-18fd7cc2bea4", vehicle_ids: ["896ec545-19f0-47d2-b2e6-18fd7cc2bea4_1", "896ec545-19f0-47d2-b2e6-18fd7cc2bea4_2"], profile: VehicleProfile { matrix: "car", scale: None }, costs: VehicleCosts { fixed: Some(20.0), distance: 0.002, time: 0.003, span: None }, shifts: [VehicleShift { start: ShiftStart { earliest: "2020-07-04T09:00:00Z", latest: Some("2020-07-04T09:00:00Z"), location: Coordinate { lat: 52.44975811328428, lng: 13.50986137594233 } }, end: Some(ShiftEnd { earliest: None, latest: "2020-07-04T18:00:00Z", location: Coordinate { lat: 52.44975811328428, lng: 13.50986137594233 } }), breaks: Some([Optional { time: TimeWindow(["2020-07-04T13:00:00Z", "2020-07-04T15:00:00Z"]), places: [VehicleOptionalBreakPlace { duration: 10.0, location: None, tag: None }], policy: None }]), reloads: None, recharges: None, job_times: None }], capacity: [34], skills: None, limits: None, min_shifts: None }, VehicleType { type_id: "35b6738b-5579-4aed-88df-93d18ac020f1", vehicle_ids: ["35b6738b-5579-4aed-88df-93d18ac020f1_1", "35b6738b-5579-4aed-88df-93d18ac020f1_2"], profile: VehicleProfile { matrix: "car", scale: None }, costs: VehicleCosts { fixed: Some(30.0), distance: 0.0015, time: 0.005, span: None }, shifts: [VehicleShift { start: ShiftStart { earliest: "2020-07-04T09:00:00Z", latest: Some("2020-07-04T09:00:00Z"), location: Coordinate { lat: 52.46847524227856, lng: 13.22143191462543 } }, end: Some(ShiftEnd { earliest: None, latest: "2020-07-04T18:00:00Z", location: Coordinate { lat: 52.46847524227856, lng: 13.22143191462543 } }), breaks: Some([Optional { time: TimeOffset([4691.0, 6126.0]), places: [VehicleOptionalBreakPlace { duration: 12.0, location: Some(Coordinate { lat: 52.473040034907676, lng: 13.391874000144147 }), tag: None }], policy: None }]), reloads: None, recharges: None, job_times: None }], capacity: [48], skills: None, limits: None, min_shifts: None }, VehicleType { type_id: "1ef41691-2f5c-4df5-839e-60ada6021251", vehicle_ids: ["1ef41691-2f5c-4df5-839e-60ada6021251_1", "1ef41691-2f5c-4df5-839e-60ada6021251_2", "1ef41691-2f5c-4df5-839e-60ada6021251_3"], profile: VehicleProfile { matrix: "car", scale: None }, costs: VehicleCosts { fixed: Some(30.0), distance: 0.0015, time: 0.005, span: None }, shifts: [VehicleShift { start: ShiftStart { earliest: "2020-07-04T09:00:00Z", latest: Some("2020-07-04T09:00:00Z"), location: Coordinate { lat: 52.56847586444386, lng: 13.555306341412106 } }, end: Some(ShiftEnd { earliest: None, latest: "2020-07-04T18:00:00Z", location: Coordinate { lat: 52.56847586444386, lng: 13.555306341412106 } }), breaks: Some([Optional { time: TimeOffset([4762.0, 6368.0]), places: [VehicleOptionalBreakPlace { duration: 96.0, location: Some(Coordinate { lat: 52.4341134818349, lng: 13.269324523493152 }), tag: None }], policy: None }]), reloads: None, recharges: None, job_times: None }], capacity: [46], skills: None, limits: None, min_shifts: None }], profiles: [MatrixProfile { name: "car", speed: None }], resources: None }, objectives: None }
+cc 51b0f2d033b41f39d2b663a70a1a7c22eca197997e666af09e2ada1a00ea650f # shrinks to problem = Problem { plan: Plan { jobs: [Job { id: "efeaa9e4-f829-4f8b-a5b8-97f556bab4c7", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.50198422863931, lng: 13.335723772104254 }, duration: 2.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "c195536b-3fdd-43a3-914f-7a303a9fb1e6", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.590643387681084, lng: 13.343877277774169 }, duration: 3.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "ac1934a4-9653-4a81-a386-8b8dd1a8192c", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.44263305130879, lng: 13.307532498040134 }, duration: 9.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "e012e34e-a3c1-43f7-8881-f93085946fb8", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.4598231063312, lng: 13.532253493421475 }, duration: 4.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "f1d5226f-8d1a-4c4d-9aa1-94a239ebe700", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.467322394678696, lng: 13.530629126331608 }, duration: 5.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "b9113b8f-9278-46b1-9cf6-cd1e37920730", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.58220521282851, lng: 13.332727459317294 }, duration: 5.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "297f2876-f26b-4f03-acdf-694409d53f99", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.512677742464035, lng: 13.356563638711558 }, duration: 8.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "e28b1c35-22ff-41ce-a206-46e5e4ac401f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.448825063331235, lng: 13.216640885313483 }, duration: 7.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "f3d1a8bf-fe1c-4d39-8c28-a83c6d20fd41", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.43183539900072, lng: 13.45176345099564 }, duration: 2.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "1fdcaa89-9688-4c11-8423-c88b5d5fca23", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.55560896308439, lng: 13.410598676454898 }, duration: 9.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "5fbab409-c938-4f91-ae1f-7869e22f8734", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.576663158210465, lng: 13.564966167516731 }, duration: 3.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "b669e50c-0f1e-43ed-97d9-5d6bb8ac4f16", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.57826832323129, lng: 13.4013098433344 }, duration: 8.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "4f003b52-aa7c-472f-a1cf-ffb88c7aaf34", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.4491777860849, lng: 13.329638449363573 }, duration: 5.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "2ba5dc64-89b7-49e6-903a-bafe3d8340fc", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.43911497991156, lng: 13.54960982980911 }, duration: 4.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "707e0694-7b5a-4883-ac9c-817bd649e1c2", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.43379442077513, lng: 13.5254180811627 }, duration: 8.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "71c4cf93-93da-47f4-820b-fefba16fecad", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.426630570291415, lng: 13.25193397710655 }, duration: 8.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "502ae531-eeeb-4489-acfb-d7b0f6b064af", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.51754041262216, lng: 13.263890311855471 }, duration: 1.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "19b04875-f01e-4a8d-b9b9-c74d77615171", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.569970015539276, lng: 13.34194571731119 }, duration: 2.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "d3060b8e-56ea-4ef1-9f84-66230050193a", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.501222495869456, lng: 13.230751936688904 }, duration: 5.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "46d7d2da-0a81-4028-b676-37d09432b65e", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.55227320017594, lng: 13.569622372618978 }, duration: 9.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "e7d76d0e-6eed-45dd-9c29-871c00a787ca", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.535043680405416, lng: 13.552318349850916 }, duration: 1.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "84bd4720-636b-4d44-aa04-a0d22b6a3ae0", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.499405425487495, lng: 13.434228136459769 }, duration: 1.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "8b7fb2f0-2fe0-4e9d-97c6-893dfd7d424c", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.42930899910961, lng: 13.416111386155503 }, duration: 6.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "d8858376-4288-4721-9bba-bf30411c1a46", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.43708450045798, lng: 13.324692258938045 }, duration: 2.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "080dbf38-2a57-4700-83e8-35b9bb4f3c7a", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.49259717803115, lng: 13.25489096163635 }, duration: 8.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "8c88fbc5-4dc7-4787-9a67-a13cbda1f1ae", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.57956069909216, lng: 13.415008928159997 }, duration: 4.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "98e089f7-c02b-4300-b67d-a564f669a105", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.57690752807255, lng: 13.37610505613635 }, duration: 5.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "339087c3-a6a5-44e5-99ae-cf879bc06e13", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.46946564303721, lng: 13.292309901440984 }, duration: 3.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "ae046d1c-ec42-477b-afcd-3f48be4c21be", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.465109373604, lng: 13.254634237702602 }, duration: 8.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "b8cd1785-1758-4001-9732-1a9942d1e4f5", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.58304177424391, lng: 13.444922491072626 }, duration: 9.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "777582a2-8bac-4337-9a57-002e906819cb", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.509124028768895, lng: 13.312734475985026 }, duration: 9.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "b17b905a-4eca-409e-a1ca-2255b543f39d", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.581411697262936, lng: 13.4552712018904 }, duration: 6.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "99d6be21-9075-474f-9380-4c679f4ba88e", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.427740596341934, lng: 13.390241061966973 }, duration: 6.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "8f9403ad-e5c8-4da6-bac5-88d96ed3e34f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.455227431409746, lng: 13.293452429323144 }, duration: 9.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "c5ab06c5-35df-4793-897b-6ccb9cb16356", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.55721343431099, lng: 13.356568028089399 }, duration: 5.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "2b88c9d4-45d4-4782-ad20-aadf39787f56", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.5387137385455, lng: 13.431780361234267 }, duration: 4.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "f6e46092-ade5-4eed-812c-2373145f09ca", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.43858819309698, lng: 13.336967046983835 }, duration: 9.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "49f3de0b-b94f-4664-95c4-f973d769fb8b", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.55330204325613, lng: 13.534480394361397 }, duration: 6.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "8840566f-fec1-4943-8494-a8ae09123bc1", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.591598069636945, lng: 13.489163458571358 }, duration: 6.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "72177310-a9e7-482d-aa1c-9d8c7df3d65e", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.45772304527027, lng: 13.328691385178987 }, duration: 2.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "bb073754-c8f7-4850-ae8d-70e7f7589dd4", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.461808651974394, lng: 13.270868140104229 }, duration: 7.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "cc85bf66-cefe-459a-87fc-5d20f43fb948", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.538124390965066, lng: 13.42569305765484 }, duration: 9.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "4d5be9a4-c1a9-4074-8703-10b6c02d1015", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.4376280621566, lng: 13.234915740497758 }, duration: 8.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "43c78312-08d7-44bf-b485-feb1bfff02d6", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.55177647514301, lng: 13.493063617838667 }, duration: 6.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "1755e43e-aff3-427d-8a99-df824bc4b6b6", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.448015056273064, lng: 13.3625786686109 }, duration: 4.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "4afe1175-7ccc-446b-bbc5-48e6d5aab694", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.53977301513012, lng: 13.23461827593372 }, duration: 2.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "7af7623c-47a3-4c98-a566-466660a47ce4", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.5054520820687, lng: 13.517653596205067 }, duration: 3.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "0746ea93-9b16-42b2-9fa6-9c2444330b13", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.453489125405085, lng: 13.397890861753174 }, duration: 4.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "ba387635-5908-412b-b463-a8ac147e816e", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.467816889917444, lng: 13.519924875750071 }, duration: 4.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "fac60adc-a7b4-42e1-a2c9-e382884c3c35", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.584581458181574, lng: 13.353794112680928 }, duration: 3.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "612e1153-7c5f-4029-90ec-3cb8f0fe5419", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.550423869634805, lng: 13.533403658714839 }, duration: 1.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "78b78f05-c7e0-4df4-82d2-85613cb00b2a", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.564245598696104, lng: 13.34314667722763 }, duration: 3.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "1372ee33-a290-4ebc-8d1f-e5aac9339d59", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.56313646517095, lng: 13.24305007840697 }, duration: 6.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "e397b980-7ee6-4888-adbb-8e9411fda8e1", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.5159401608345, lng: 13.572462470837221 }, duration: 6.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "2e5f963d-f25f-443a-a748-c5bbd24f52ae", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.48210071631328, lng: 13.29620839868563 }, duration: 2.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "999c5107-0825-4030-88f0-eb350948b73a", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.467354292601634, lng: 13.45569688581607 }, duration: 5.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "a97d97a9-cbef-4e97-91c8-252b0bb2356f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.439237154077205, lng: 13.224230924715856 }, duration: 3.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "63a17a2a-b9f5-4965-b25b-7da1d80356c6", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.47293968912351, lng: 13.423106099823697 }, duration: 8.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "8f3cf066-21a5-423b-8c2f-097ecdcf816f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.51862111777575, lng: 13.462337840642352 }, duration: 8.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "d3f7f873-f6b4-4d55-98db-535c5137ce5d", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.54628689749778, lng: 13.453432514476185 }, duration: 6.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "10132c39-44cb-4245-97b3-529a2a92f611", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.480211874221034, lng: 13.512701495944425 }, duration: 6.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "e41649ff-6e96-4b21-bed0-6f86f195c4bf", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.47357383402315, lng: 13.479223377020753 }, duration: 6.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "92fda335-e210-406c-a15c-b4e5102dfa66", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.570596753912476, lng: 13.26153901508034 }, duration: 3.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "889c1c9a-d9d4-4486-aa40-e853984f0352", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.48526340173824, lng: 13.569174741148947 }, duration: 3.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "dd05aed1-3666-4477-805d-25daf5805904", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.47478469189815, lng: 13.581535118400213 }, duration: 2.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "c3144f1b-bb6c-456d-99d5-3c6cac4bbcc6", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.4294385398349, lng: 13.564881707136205 }, duration: 5.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "1219e2af-3e62-4abf-9fa4-325d84a2c798", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.467870439713664, lng: 13.219968535618023 }, duration: 8.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "502a01fc-4640-43d4-824a-c573a448d242", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.466698110271146, lng: 13.262671525200505 }, duration: 2.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "cc42a37e-f4d2-4ad3-89b6-c39d4fda517c", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.53496800916804, lng: 13.298244796411636 }, duration: 2.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "1eaa814c-a506-49f4-8b22-a1bc1015f537", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.49414261130867, lng: 13.375392621796207 }, duration: 9.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "94141bb2-d421-466e-bac1-89762ba587bf", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.45727981808902, lng: 13.588068784499809 }, duration: 4.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "487306b7-3d24-4fa7-a385-be76464998a4", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.559421989952305, lng: 13.279989998842437 }, duration: 6.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "ea14d0dd-6aeb-4d4e-9cc5-522dbc0c2f62", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.542157868756256, lng: 13.221633510821526 }, duration: 3.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "ffcd4978-e24b-46b9-bc76-98da5c49f43b", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.57006672894607, lng: 13.3166301776125 }, duration: 9.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "2e491e6b-d05e-4068-b061-bcb5b35d493d", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.51124900886018, lng: 13.432781790063093 }, duration: 3.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "b279b54b-a6c2-4989-883e-479e167de5b6", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.49797899713559, lng: 13.262497404568384 }, duration: 2.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "a0f93b16-02a3-456b-bf6e-3ed7f7a63ae8", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.5375565013468, lng: 13.238582613190152 }, duration: 4.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "3cb728a5-8e2a-4e1f-88dd-a50e35abbe16", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.48005613077529, lng: 13.446004194016368 }, duration: 6.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "22f7b339-1abb-4a6a-89c8-a3a300297640", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.47569120859226, lng: 13.476568328908293 }, duration: 9.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "75c3759f-8216-4bd0-94d4-fa8d2d90f90a", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.49586744211548, lng: 13.361581826218117 }, duration: 4.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "37aca49d-1a79-4ffb-8c81-9a106d4bc008", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.462430481221055, lng: 13.229617801327814 }, duration: 9.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "661ad9ca-773b-405f-a589-df513f38dfb9", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.579855335514395, lng: 13.368433746574636 }, duration: 4.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "165c0815-be99-4c07-b2df-d98ed8138ed3", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.432920761379016, lng: 13.22422488121203 }, duration: 2.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "d9c05472-9156-4204-ab7f-c02e7b8b21b3", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.43234314422486, lng: 13.475038021299445 }, duration: 8.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "8e24ad6f-d632-406a-868b-42eeeededf0c", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.58961157228767, lng: 13.227984235364868 }, duration: 9.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "a29b5b19-525b-4fab-a3ba-2d78a1d38ef3", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.47435982405622, lng: 13.40894548092958 }, duration: 3.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "6f451805-9ec2-46cf-871e-7bd8b28774cf", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.555909517423025, lng: 13.441503326723078 }, duration: 2.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "ddb91439-88ce-4aa5-9dd0-8c2e71a462df", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.59094183641902, lng: 13.438135052539959 }, duration: 2.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "8fa37be0-23a5-449b-a1ba-78e87bfa2991", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.42641980855623, lng: 13.35910176188236 }, duration: 9.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "5503f7fa-b909-4937-be72-e3831936e3a1", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.57030168438357, lng: 13.38989597983953 }, duration: 5.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "cc36ab36-e97b-4edb-9ea7-acd10f044d94", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.44353373284969, lng: 13.329420614817902 }, duration: 4.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "43862736-df8d-4ef1-9fdd-09411d1e13d2", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.47305064786, lng: 13.277984834172662 }, duration: 8.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "a3e22a4d-35d2-4f86-b9c2-37eb235f0b93", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.58924818047862, lng: 13.483853539341817 }, duration: 7.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "576a893a-edbe-4c00-a0d2-15c405f262ef", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.5523347705084, lng: 13.582793554012909 }, duration: 3.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "499b0099-d5e1-40c5-a6fb-1c837bc34457", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.478586319895534, lng: 13.378169729362131 }, duration: 4.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "99c56b85-af74-413c-9eb2-ff388834d36b", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.54432595311518, lng: 13.43042711026809 }, duration: 2.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "fe63dfd0-b8f4-4029-86d1-e72a5ff15818", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.50221023168227, lng: 13.469547418929562 }, duration: 1.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "1d0eeaa4-f0b6-4c1d-89f2-927624cbce02", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.55945367775156, lng: 13.53267018710888 }, duration: 4.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "04a04f4a-7f82-4b01-bc29-2516f6d98108", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.537458525566336, lng: 13.468998808339384 }, duration: 3.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "ef1ccaff-3293-4f27-ab88-7c2d6767c0b2", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.45446208275413, lng: 13.568397740684336 }, duration: 7.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "ed5f65df-bb0e-4a8e-928a-e3f968540cb9", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.475760286041144, lng: 13.414163316692385 }, duration: 8.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "614ffd81-b0c2-427b-a12c-64d0ec3bef4f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.54107859438325, lng: 13.250928374611725 }, duration: 9.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "d58e2baa-5084-4ae4-b5a0-9cd4b8061632", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.58053048285574, lng: 13.257442221038419 }, duration: 4.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "4af108a2-0011-4a4a-8700-bb0e37d25939", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.437426635987734, lng: 13.221537914222832 }, duration: 8.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "c8563cb3-f3d3-4dd7-97c0-cfbcf585904a", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.501794578169, lng: 13.324807349260746 }, duration: 6.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "398cb8db-487b-4eb2-9425-07921c81e2b9", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.46618142579707, lng: 13.259001869279144 }, duration: 3.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "e98dcfbd-c2fe-4e1d-bacd-d803912b3529", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.5268043046167, lng: 13.314201378328315 }, duration: 7.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "9ee5feaa-b49f-4203-93f6-6ab999b230ae", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.48839839333745, lng: 13.293909518985295 }, duration: 3.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "8ad31fb5-8c57-4bb8-921b-34ea11d4760b", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.46653916090922, lng: 13.308493034738468 }, duration: 7.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "2a11dc5b-4418-4cb0-ab6a-10f3a81a8a95", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.445284099579716, lng: 13.515047419031788 }, duration: 4.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "301107b2-ff30-40f1-9953-f78794254ec6", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.51020779602209, lng: 13.422165246318626 }, duration: 6.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }], relations: None, clustering: None }, fleet: Fleet { vehicles: [VehicleType { type_id: "512f8193-b412-46fd-a613-229e365f0c71", vehicle_ids: ["512f8193-b412-46fd-a613-229e365f0c71_1", "512f8193-b412-46fd-a613-229e365f0c71_2"], profile: VehicleProfile { matrix: "car", scale: None }, costs: VehicleCosts { fixed: Some(30.0), distance: 0.0015, time: 0.005, span: None }, shifts: [VehicleShift { start: ShiftStart { earliest: "2020-07-04T09:00:00Z", latest: Some("2020-07-04T09:00:00Z"), location: Coordinate { lat: 52.49193651710959, lng: 13.316031113438967 } }, end: Some(ShiftEnd { earliest: None, latest: "2020-07-04T18:00:00Z", location: Coordinate { lat: 52.49193651710959, lng: 13.316031113438967 } }), breaks: Some([Required { time: ExactTime { earliest: "2020-07-04T10:38:30Z", latest: "2020-07-04T10:38:31Z" }, duration: 1287.0 }]), reloads: None, recharges: None, job_times: None }], capacity: [42], skills: None, limits: None, min_shifts: None }, VehicleType { type_id: "06a37ffa-1558-4c92-aeff-035d013cb47a", vehicle_ids: ["06a37ffa-1558-4c92-aeff-035d013cb47a_1", "06a37ffa-1558-4c92-aeff-035d013cb47a_2"], profile: VehicleProfile { matrix: "car", scale: None }, costs: VehicleCosts { fixed: Some(30.0), distance: 0.0015, time: 0.005, span: None }, shifts: [VehicleShift { start: ShiftStart { earliest: "2020-07-04T09:00:00Z", latest: Some("2020-07-04T09:00:00Z"), location: Coordinate { lat: 52.49243940592943, lng: 13.352431693220716 } }, end: Some(ShiftEnd { earliest: None, latest: "2020-07-04T18:00:00Z", location: Coordinate { lat: 52.49243940592943, lng: 13.352431693220716 } }), breaks: Some([Required { time: ExactTime { earliest: "2020-07-04T10:18:05Z", latest: "2020-07-04T10:18:06Z" }, duration: 1715.0 }]), reloads: None, recharges: None, job_times: None }], capacity: [46], skills: None, limits: None, min_shifts: None }], profiles: [MatrixProfile { name: "car", speed: None }], resources: None }, objectives: None }
+cc fb3a081395ec50249cd12a5ef92784a0b134b6f5779b50695202b5cc8d4b7896 # shrinks to problem = Problem { plan: Plan { jobs: [Job { id: "7f2c3d08-fddd-4ec5-a412-9d7734f8a721", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.43597584725234, lng: 13.337111683338934 }, duration: 1.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "03627838-b0e9-4fcf-936e-96bcdf8c04ba", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.426100079301406, lng: 13.582393769807771 }, duration: 2.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "cd7dbc50-df65-4b31-94a4-f9953971334d", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.51588056652537, lng: 13.468325533178916 }, duration: 9.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "31350a24-96e6-4fc1-aa1f-f02c8c3378d8", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.48650960260089, lng: 13.591060847245531 }, duration: 9.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "fcf6afa1-622e-476b-935a-d77673c3a695", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.54055361454735, lng: 13.376220488915704 }, duration: 5.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "abd3199f-b752-4374-a1e6-fcfb39b11d3e", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.49162087143096, lng: 13.25421179267848 }, duration: 8.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "79691226-4564-422e-95d9-9932291d4764", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.560760798387, lng: 13.50648729690338 }, duration: 5.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "d204ac85-8c70-4d35-93e6-8cbd640443a4", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.447425220961065, lng: 13.220645345598761 }, duration: 3.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "cb51c1cd-e5d0-42d6-8971-ed6f535280ee", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.507303029192784, lng: 13.592315401205218 }, duration: 7.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "4fbbb982-1e7d-4a58-a1b1-429ddbda8dfa", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.4905581155283, lng: 13.397500072727565 }, duration: 6.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "d3ec2bec-5ff0-474b-9433-663b6e7f17ac", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.53263194232921, lng: 13.349574145785253 }, duration: 6.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "65a39628-dc2d-4e42-be8a-b93cf050ec5a", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.429187856888696, lng: 13.426439361183593 }, duration: 7.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "ec84c2c2-7023-4d68-8718-6ff0407b2c4a", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.510076214024295, lng: 13.380435131751248 }, duration: 9.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "dce2d7f9-ad73-4f94-83b9-ec6a87a85e67", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.584956454485585, lng: 13.298117263275854 }, duration: 4.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "be65f77c-5df2-4a6d-abcc-7126852a3fa3", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.4995071318221, lng: 13.475330202838924 }, duration: 8.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "1035b736-cede-4c21-bc6c-b7cf8a8978f9", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.5025787715502, lng: 13.30530327987404 }, duration: 4.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "81ea1430-f6aa-48b6-a3bd-fabc10625b62", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.48987092592619, lng: 13.412624704771266 }, duration: 9.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "71b11865-31a5-41e1-ac1f-c83894e42e2d", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.4642224043791, lng: 13.409598536058388 }, duration: 3.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "f5f58197-1b52-43a0-bf7b-8c93a90f465d", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.44752576684859, lng: 13.541012430406045 }, duration: 3.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "8e24c7ae-0d33-49b2-99ba-f1098b543902", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.489539824127306, lng: 13.336173844433338 }, duration: 5.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "063e6df4-b4c9-47e2-a682-273893ff207a", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.55230768783244, lng: 13.328612252956633 }, duration: 8.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "55c40897-bfb4-490c-95c0-fa11fa6a52ac", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.49405907794189, lng: 13.469882089091769 }, duration: 3.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "fac0ec1a-f4f1-4a77-80e6-919fc5d6e0f2", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.563683433236775, lng: 13.354714726458797 }, duration: 6.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "6ba9c709-6224-496a-b87a-7f159e7c2be7", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.428346869213584, lng: 13.417816632028414 }, duration: 4.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "99dc5cb5-3ad7-441b-aaec-c53b392ef514", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.55844008619861, lng: 13.47099586307248 }, duration: 2.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "4fbbcdf5-ec6b-4e5b-aeea-7a8e841afdf0", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.54253249825354, lng: 13.429107608641974 }, duration: 6.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "05224664-d514-43f9-8531-fb8bf7125227", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.5043559830519, lng: 13.473362807093247 }, duration: 4.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "bda18b82-d37d-4973-b6e6-3f60972f574f", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.58571397090342, lng: 13.564124156143176 }, duration: 2.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "197d2ce3-5801-4b44-8ece-062f20fea847", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.54210492370325, lng: 13.46157558805475 }, duration: 8.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "bcb40fd0-934e-4a4f-9bbd-6f3e2026a2c5", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.48213422149534, lng: 13.357124615760737 }, duration: 7.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "b8891353-dcd1-46f5-995f-b9e7602cd051", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.46592020362436, lng: 13.488154555746984 }, duration: 1.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "5bdcb391-b4ce-4e39-a7c2-3d0296528fd3", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.50457387232987, lng: 13.235210191828664 }, duration: 5.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "febcdfcb-0a27-4747-a4ca-42f241aed161", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.56119290182213, lng: 13.288271794961068 }, duration: 3.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "d102adc0-5765-4b6e-85fd-af56de02ea19", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.43148702317684, lng: 13.259030521698762 }, duration: 6.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "e2f40d6c-6995-441d-9a6e-bef213824006", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.52958803200646, lng: 13.503728137476548 }, duration: 6.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "d528bca7-acc6-487f-b988-4cfa80c6aa64", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.5456916379178, lng: 13.445090834704821 }, duration: 6.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "7c2c6b22-a3e9-4645-ba58-646e616cfb91", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.59314365874293, lng: 13.249202309210288 }, duration: 8.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "a34306a5-36f7-478d-8ee3-455042b81e64", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.44454429402262, lng: 13.481303403736174 }, duration: 9.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "d9314179-e253-4cdc-bdb8-6de17c2f811d", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.47456004662994, lng: 13.283236164631253 }, duration: 6.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "7d8d6606-9257-4ad5-8e82-25a20e3a783c", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.57958765230363, lng: 13.550623861483382 }, duration: 4.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "fe0ad8c4-38da-45d0-824b-fae0b7308644", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.51970569594964, lng: 13.354474182484653 }, duration: 2.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "8fffbe83-afd3-4f44-89e1-cd1535a3066b", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.53381722139522, lng: 13.490021098804894 }, duration: 2.0, times: None, tag: None }], demand: Some([4]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "fff2c98f-fa7c-4b8e-ab9d-508afa0e550c", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.48118124341274, lng: 13.331782779719513 }, duration: 2.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "6a9417b6-f9df-4a4b-b261-993099dc403a", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.466906980037486, lng: 13.435244483543768 }, duration: 6.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "3719fc14-62f8-47c4-ba40-4b31bc8f5ef4", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.53849530860635, lng: 13.284129090159542 }, duration: 2.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "468a7795-1044-4e21-b0bf-16ee68aecf8e", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.51752019193257, lng: 13.361814067393787 }, duration: 3.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "7c76e89e-297b-45c3-acf0-754eba52c407", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.57050228757273, lng: 13.422918232216928 }, duration: 9.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "3216e931-1b19-481b-9c27-31fe824f7d75", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.47044624028106, lng: 13.56205941898365 }, duration: 6.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "af9c614a-628f-4c48-9551-b990b62784cd", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.47324698464907, lng: 13.59684156466157 }, duration: 1.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "e8bf5899-5003-436e-a544-506d6466a232", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.56593862141317, lng: 13.381039182535888 }, duration: 9.0, times: None, tag: None }], demand: Some([3]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "c553ef17-b274-4070-bcbd-b2944125e730", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.5170884835415, lng: 13.48222454354468 }, duration: 5.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "6a59f1ba-60d2-41ad-b839-51aff32f6d58", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.45937801604091, lng: 13.379180968451772 }, duration: 9.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "87527e75-8486-4c61-8b52-783f79d7d0f4", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.48374781005707, lng: 13.21704269292955 }, duration: 8.0, times: None, tag: None }], demand: Some([1]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }, Job { id: "07a03515-0001-4627-90fe-6292f4bc5372", pickups: None, deliveries: Some([JobTask { places: [JobPlace { location: Coordinate { lat: 52.46346643709553, lng: 13.44897307237314 }, duration: 4.0, times: None, tag: None }], demand: Some([2]), order: None, due_date: None }]), replacements: None, services: None, skills: None, value: None, group: None, compatibility: None }], relations: None, clustering: None }, fleet: Fleet { vehicles: [VehicleType { type_id: "fb239a61-a4a9-401b-aad5-12b33ac432d2", vehicle_ids: ["fb239a61-a4a9-401b-aad5-12b33ac432d2_1", "fb239a61-a4a9-401b-aad5-12b33ac432d2_2", "fb239a61-a4a9-401b-aad5-12b33ac432d2_3"], profile: VehicleProfile { matrix: "car", scale: None }, costs: VehicleCosts { fixed: Some(20.0), distance: 0.002, time: 0.003, span: None }, shifts: [VehicleShift { start: ShiftStart { earliest: "2020-07-04T09:00:00Z", latest: Some("2020-07-04T09:00:00Z"), location: Coordinate { lat: 52.56965073353027, lng: 13.483381177714296 } }, end: Some(ShiftEnd { earliest: None, latest: "2020-07-04T18:00:00Z", location: Coordinate { lat: 52.56965073353027, lng: 13.483381177714296 } }), breaks: Some([Required { time: OffsetTime { earliest: 12370.0, latest: 12380.0 }, duration: 1389.0 }]), reloads: None, recharges: None, job_times: None }], capacity: [41], skills: None, limits: None, min_shifts: None }, VehicleType { type_id: "09970de7-dd6d-4114-b67c-554abd1e7b07", vehicle_ids: ["09970de7-dd6d-4114-b67c-554abd1e7b07_1", "09970de7-dd6d-4114-b67c-554abd1e7b07_2"], profile: VehicleProfile { matrix: "car", scale: None }, costs: VehicleCosts { fixed: Some(30.0), distance: 0.0015, time: 0.005, span: None }, shifts: [VehicleShift { start: ShiftStart { earliest: "2020-07-04T09:00:00Z", latest: Some("2020-07-04T09:00:00Z"), location: Coordinate { lat: 52.46682443216909, lng: 13.527115860980679 } }, end: Some(ShiftEnd { earliest: None, latest: "2020-07-04T18:00:00Z", location: Coordinate { lat: 52.46682443216909, lng: 13.527115860980679 } }), breaks: Some([Required { time: OffsetTime { earliest: 5219.0, latest: 5229.0 }, duration: 230.0 }]), reloads: None, recharges: None, job_times: None }], capacity: [32], skills: None, limits: None, min_shifts: None }, VehicleType { type_id: "341751de-c88c-42db-9ccb-db3fc6e65729", vehicle_ids: ["341751de-c88c-42db-9ccb-db3fc6e65729_1", "341751de-c88c-42db-9ccb-db3fc6e65729_2"], profile: VehicleProfile { matrix: "car", scale: None }, costs: VehicleCosts { fixed: Some(30.0), distance: 0.0015, time: 0.005, span: None }, shifts: [VehicleShift { start: ShiftStart { earliest: "2020-07-04T09:00:00Z", latest: Some("2020-07-04T09:00:00Z"), location: Coordinate { lat: 52.54425183361275, lng: 13.419420287597076 } }, end: Some(ShiftEnd { earliest: None, latest: "2020-07-04T18:00:00Z", location: Coordinate { lat: 52.54425183361275, lng: 13.419420287597076 } }), breaks: Some([Required { time: ExactTime { earliest: "2020-07-04T11:13:04Z", latest: "2020-07-04T11:13:05Z" }, duration: 1440.0 }]), reloads: None, recharges: None, job_times: None }], capacity: [36], skills: None, limits: None, min_shifts: None }], profiles: [MatrixProfile { name: "car", speed: None }], resources: None }, objectives: None }
diff --git a/vrp-pragmatic/tests/features/breaks/interval_break_test.rs b/vrp-pragmatic/tests/features/breaks/interval_break_test.rs
index f2cbd6a86..bddd9580c 100644
--- a/vrp-pragmatic/tests/features/breaks/interval_break_test.rs
+++ b/vrp-pragmatic/tests/features/breaks/interval_break_test.rs
@@ -117,6 +117,7 @@ fn can_assign_interval_break_with_reload() {
..create_default_reload()
}]),
recharges: None,
+ job_times: None,
}],
capacity: vec![2],
..create_default_vehicle_type()
@@ -194,7 +195,6 @@ fn can_assign_interval_break_with_reload() {
}
#[test]
-#[ignore]
fn can_consider_departure_rescheduling() {
let problem = Problem {
plan: Plan {
@@ -225,6 +225,9 @@ fn can_consider_departure_rescheduling() {
let solution = solve_with_metaheuristic_and_iterations(problem, Some(vec![matrix]), 2000);
+ print!("{:#?}", solution);
+
+ assert!(solution.tours[0].stops[0].schedule().arrival != solution.tours[0].stops[0].schedule().departure);
assert!(solution.violations.is_none());
assert!(solution.unassigned.is_none());
}
diff --git a/vrp-pragmatic/tests/features/breaks/mod.rs b/vrp-pragmatic/tests/features/breaks/mod.rs
index d40a3c408..e8e85ff15 100644
--- a/vrp-pragmatic/tests/features/breaks/mod.rs
+++ b/vrp-pragmatic/tests/features/breaks/mod.rs
@@ -6,3 +6,4 @@ mod open_end_by_interval_break;
mod policy_break_test;
mod relation_break_test;
mod required_break;
+mod required_break_flexible_start;
diff --git a/vrp-pragmatic/tests/features/breaks/required_break.rs b/vrp-pragmatic/tests/features/breaks/required_break.rs
index 1219ef2ac..cd7be8f40 100644
--- a/vrp-pragmatic/tests/features/breaks/required_break.rs
+++ b/vrp-pragmatic/tests/features/breaks/required_break.rs
@@ -115,10 +115,10 @@ fn can_assign_break_during_activity() {
ActivityBuilder::delivery()
.job_id("job1")
.coordinate((5., 0.))
- .time_stamp(5., 10.)
+ .time_stamp(5., 8.)
.build()
)
- .activity(ActivityBuilder::break_type().time_stamp(7., 9.).build())
+ .activity(ActivityBuilder::break_type().time_stamp(8., 10.).build())
.build(),
StopBuilder::default()
.coordinate((0., 0.))
@@ -265,6 +265,7 @@ fn can_handle_required_break_with_infeasible_sequence_relation() {
}],
demand: None,
order: None,
+ due_date: None,
}]),
..create_job(index.to_string().as_str())
};
diff --git a/vrp-pragmatic/tests/features/breaks/required_break_flexible_start.rs b/vrp-pragmatic/tests/features/breaks/required_break_flexible_start.rs
new file mode 100644
index 000000000..5ec41abe8
--- /dev/null
+++ b/vrp-pragmatic/tests/features/breaks/required_break_flexible_start.rs
@@ -0,0 +1,1585 @@
+use crate::format::problem::*;
+use crate::format::solution::{Solution, Stop, Tour};
+use crate::format_time;
+use crate::helpers::*;
+use crate::parse_time;
+
+/// Tests that OffsetTime required breaks work correctly with flexible start times
+/// (shift.start.latest is None), verifying that departure rescheduling
+/// produces feasible solutions with breaks placed at the correct offset from the anchor.
+/// Collects all activity intervals (start, end, type, job_id) from a tour, flattened across stops.
+fn collect_activity_intervals(tour: &Tour) -> Vec<(f64, f64, String, String)> {
+ let mut intervals = Vec::new();
+ for stop in &tour.stops {
+ let schedule = stop.schedule();
+ let stop_arrival = parse_time(&schedule.arrival);
+ let stop_departure = parse_time(&schedule.departure);
+ let activities = stop.activities();
+
+ if activities.len() == 1 {
+ let a = &activities[0];
+ if let Some(time) = &a.time {
+ intervals.push((
+ parse_time(&time.start),
+ parse_time(&time.end),
+ a.activity_type.clone(),
+ a.job_id.clone(),
+ ));
+ } else {
+ intervals.push((stop_arrival, stop_departure, a.activity_type.clone(), a.job_id.clone()));
+ }
+ } else {
+ for a in activities {
+ if let Some(time) = &a.time {
+ intervals.push((
+ parse_time(&time.start),
+ parse_time(&time.end),
+ a.activity_type.clone(),
+ a.job_id.clone(),
+ ));
+ } else {
+ intervals.push((stop_arrival, stop_departure, a.activity_type.clone(), a.job_id.clone()));
+ }
+ }
+ }
+ }
+ intervals
+}
+
+/// Comprehensive validation of break placement and schedule consistency for a single tour.
+/// Checks:
+/// 1. Correct number of breaks with correct duration
+/// 2. Breaks don't overlap with job activities (cross-stop)
+/// 3. Stop schedule consistency (departure >= arrival, monotonic)
+/// 4. Activities within each stop are time-ordered and within stop bounds
+/// 5. Break time is within tour time bounds
+/// 6. Break doesn't have a location (required breaks are locationless)
+fn validate_tour_breaks_and_schedule(tour: &Tour, expected_break_count: usize, expected_break_duration: f64) {
+ let intervals = collect_activity_intervals(tour);
+
+ // 1. Break count and duration
+ let break_intervals: Vec<_> = intervals.iter().filter(|(_, _, typ, _)| typ == "break").collect();
+ assert_eq!(
+ break_intervals.len(),
+ expected_break_count,
+ "expected {expected_break_count} break(s), got {}\ntour stops: {}",
+ break_intervals.len(),
+ format_tour_debug(tour)
+ );
+
+ for (start, end, _, _) in &break_intervals {
+ let duration = end - start;
+ assert!(
+ (duration - expected_break_duration).abs() < 1.0,
+ "break duration mismatch: expected {expected_break_duration}, got {duration}\ntour: {}",
+ format_tour_debug(tour)
+ );
+ }
+
+ // 2. Breaks don't overlap with job activities at DIFFERENT stops
+ let non_break_job_intervals: Vec<_> =
+ intervals.iter().filter(|(_, _, typ, _)| typ != "break" && typ != "departure" && typ != "arrival").collect();
+
+ for (b_start, b_end, _, _) in &break_intervals {
+ for (a_start, a_end, a_type, a_id) in &non_break_job_intervals {
+ let same_stop = tour.stops.iter().any(|stop| {
+ let acts = stop.activities();
+ acts.iter().any(|a| a.activity_type == "break") && acts.iter().any(|a| a.job_id == **a_id)
+ });
+ if !same_stop {
+ let overlaps = b_start < a_end && a_start < b_end;
+ assert!(
+ !overlaps,
+ "break [{b_start}..{b_end}] overlaps with {a_type} '{a_id}' [{a_start}..{a_end}] at different stop\ntour: {}",
+ format_tour_debug(tour)
+ );
+ }
+ }
+ }
+
+ // 3. Stop schedule consistency
+ let mut prev_departure: Option = None;
+ for (i, stop) in tour.stops.iter().enumerate() {
+ let arr = parse_time(&stop.schedule().arrival);
+ let dep = parse_time(&stop.schedule().departure);
+ assert!(dep >= arr - 0.001, "stop {i}: departure ({dep}) < arrival ({arr})\ntour: {}", format_tour_debug(tour));
+ if let Some(prev_dep) = prev_departure {
+ assert!(
+ arr >= prev_dep - 0.001,
+ "stop {i}: arrival ({arr}) < previous departure ({prev_dep})\ntour: {}",
+ format_tour_debug(tour)
+ );
+ }
+ prev_departure = Some(dep);
+ }
+
+ // 4. Activities within each stop are time-ordered and within bounds
+ for (i, stop) in tour.stops.iter().enumerate() {
+ let stop_arr = parse_time(&stop.schedule().arrival);
+ let stop_dep = parse_time(&stop.schedule().departure);
+ let mut prev_act_start = f64::NEG_INFINITY;
+
+ for act in stop.activities() {
+ if let Some(time) = &act.time {
+ let act_start = parse_time(&time.start);
+ let act_end = parse_time(&time.end);
+ assert!(
+ act_end >= act_start - 0.001,
+ "stop {i}: activity '{}' ({}) has end ({act_end}) < start ({act_start})\ntour: {}",
+ act.job_id,
+ act.activity_type,
+ format_tour_debug(tour)
+ );
+ assert!(
+ act_start >= stop_arr - 0.001,
+ "stop {i}: activity '{}' start ({act_start}) < stop arrival ({stop_arr})\ntour: {}",
+ act.job_id,
+ format_tour_debug(tour)
+ );
+ assert!(
+ act_end <= stop_dep + 0.001,
+ "stop {i}: activity '{}' end ({act_end}) > stop departure ({stop_dep})\ntour: {}",
+ act.job_id,
+ format_tour_debug(tour)
+ );
+ assert!(
+ act_start >= prev_act_start - 0.001,
+ "stop {i}: activity '{}' start ({act_start}) < previous activity start ({prev_act_start}) — not time-ordered\ntour: {}",
+ act.job_id,
+ format_tour_debug(tour)
+ );
+ prev_act_start = act_start;
+ }
+ }
+ }
+
+ // 5. Break time within tour bounds
+ let tour_start = parse_time(&tour.stops.first().unwrap().schedule().departure);
+ let tour_end = parse_time(&tour.stops.last().unwrap().schedule().arrival);
+ for (b_start, b_end, _, _) in &break_intervals {
+ assert!(
+ *b_start >= tour_start - 0.001 && *b_end <= tour_end + 0.001,
+ "break [{b_start}..{b_end}] outside tour time [{tour_start}..{tour_end}]\ntour: {}",
+ format_tour_debug(tour)
+ );
+ }
+
+ // 6. Break activities have no location
+ for stop in &tour.stops {
+ for act in stop.activities() {
+ if act.activity_type == "break" {
+ assert!(
+ act.location.is_none(),
+ "required break should have no location, but got {:?}\ntour: {}",
+ act.location,
+ format_tour_debug(tour)
+ );
+ }
+ }
+ }
+}
+
+/// Validates all tours in a solution.
+fn validate_solution_breaks(solution: &Solution, expected_break_count: usize, expected_break_duration: f64) {
+ assert!(!solution.tours.is_empty(), "expected at least one tour");
+ for tour in &solution.tours {
+ validate_tour_breaks_and_schedule(tour, expected_break_count, expected_break_duration);
+ }
+}
+
+/// Debug formatter for a tour — prints all stops with activities, times, and locations.
+fn format_tour_debug(tour: &Tour) -> String {
+ let mut lines = vec![format!("vehicle={} shift={}", tour.vehicle_id, tour.shift_index)];
+ for (i, stop) in tour.stops.iter().enumerate() {
+ let s = stop.schedule();
+ let loc = stop.location().map(|l| format!("{l:?}")).unwrap_or_default();
+ let acts: Vec<_> = stop
+ .activities()
+ .iter()
+ .map(|a| {
+ let t = a.time.as_ref().map(|t| format!("[{}..{}]", t.start, t.end)).unwrap_or_default();
+ format!(" {}({}) {}", a.job_id, a.activity_type, t)
+ })
+ .collect();
+ let stop_type = if matches!(stop, Stop::Transit(_)) { "T" } else { "P" };
+ lines.push(format!(" stop {i}{stop_type} {loc}: arr={} dep={}", s.arrival, s.departure));
+ for a in acts {
+ lines.push(format!(" {a}"));
+ }
+ }
+ lines.join("\n")
+}
+
+// =============================================================================
+// Basic scenarios
+// =============================================================================
+
+#[test]
+fn can_assign_offset_break_with_flexible_departure() {
+ let problem = Problem {
+ plan: Plan {
+ jobs: vec![create_delivery_job("job1", (5., 0.)), create_delivery_job("job2", (15., 0.))],
+ ..create_empty_plan()
+ },
+ fleet: Fleet {
+ vehicles: vec![VehicleType {
+ shifts: vec![VehicleShift {
+ end: Some(ShiftEnd { earliest: None, latest: format_time(100.), location: (0., 0.).to_loc() }),
+ breaks: Some(vec![VehicleBreak::Required {
+ time: VehicleRequiredBreakTime::OffsetTime { earliest: 7., latest: 7. },
+ duration: 2.,
+ }]),
+ ..create_default_vehicle_shift()
+ }],
+ ..create_default_vehicle_type()
+ }],
+ ..create_default_fleet()
+ },
+ ..create_empty_problem()
+ };
+ let matrix = create_matrix_from_problem(&problem);
+ let solution = solve_with_metaheuristic(problem, Some(vec![matrix]));
+
+ assert!(solution.violations.is_none(), "expected no violations");
+ assert!(solution.unassigned.is_none(), "expected all jobs assigned");
+ validate_solution_breaks(&solution, 1, 2.0);
+
+ let departure = parse_time(&solution.tours[0].stops[0].schedule().departure);
+ let intervals = collect_activity_intervals(&solution.tours[0]);
+ let (b_start, _, _, _) = intervals.iter().find(|(_, _, t, _)| t == "break").unwrap();
+ let offset = b_start - departure;
+ assert!((offset - 7.0).abs() < 1.0, "break offset from departure should be ~7, got {offset}");
+}
+
+#[test]
+fn can_assign_offset_break_with_wide_end_window_and_late_jobs() {
+ let problem = Problem {
+ plan: Plan {
+ jobs: vec![
+ create_delivery_job_with_times("job1", (5., 0.), vec![(30, 100)], 1.),
+ create_delivery_job_with_times("job2", (15., 0.), vec![(30, 100)], 1.),
+ ],
+ ..create_empty_plan()
+ },
+ fleet: Fleet {
+ vehicles: vec![VehicleType {
+ shifts: vec![VehicleShift {
+ end: Some(ShiftEnd { earliest: None, latest: format_time(200.), location: (0., 0.).to_loc() }),
+ breaks: Some(vec![VehicleBreak::Required {
+ time: VehicleRequiredBreakTime::OffsetTime { earliest: 7., latest: 7. },
+ duration: 2.,
+ }]),
+ ..create_default_vehicle_shift()
+ }],
+ ..create_default_vehicle_type()
+ }],
+ ..create_default_fleet()
+ },
+ ..create_empty_problem()
+ };
+ let matrix = create_matrix_from_problem(&problem);
+ let solution = solve_with_metaheuristic_and_iterations_without_check(problem, Some(vec![matrix]), 200);
+
+ assert!(solution.unassigned.is_none(), "expected all jobs assigned");
+ let departure = parse_time(&solution.tours[0].stops[0].schedule().departure);
+ assert!(departure > 0., "expected departure to be advanced past time 0, got {departure}");
+ validate_solution_breaks(&solution, 1, 2.0);
+}
+
+#[test]
+fn can_assign_offset_break_with_recede_departure() {
+ let problem = Problem {
+ plan: Plan {
+ jobs: vec![create_delivery_job("job1", (5., 0.)), create_delivery_job("job2", (15., 0.))],
+ ..create_empty_plan()
+ },
+ fleet: Fleet {
+ vehicles: vec![VehicleType {
+ shifts: vec![VehicleShift {
+ end: Some(ShiftEnd { earliest: None, latest: format_time(100.), location: (0., 0.).to_loc() }),
+ breaks: Some(vec![VehicleBreak::Required {
+ time: VehicleRequiredBreakTime::OffsetTime { earliest: 7., latest: 7. },
+ duration: 2.,
+ }]),
+ ..create_default_vehicle_shift()
+ }],
+ ..create_default_vehicle_type()
+ }],
+ ..create_default_fleet()
+ },
+ ..create_empty_problem()
+ };
+ let matrix = create_matrix_from_problem(&problem);
+ let solution = solve_with_metaheuristic(problem, Some(vec![matrix]));
+
+ assert!(solution.violations.is_none());
+ assert!(solution.unassigned.is_none());
+ validate_solution_breaks(&solution, 1, 2.0);
+}
+
+// =============================================================================
+// Mixed break types
+// =============================================================================
+
+#[test]
+fn can_handle_mixed_break_types_in_validation() {
+ let problem = Problem {
+ plan: Plan {
+ jobs: vec![
+ create_delivery_job("job1", (5., 0.)),
+ create_delivery_job("job2", (15., 0.)),
+ create_delivery_job("job3", (25., 0.)),
+ ],
+ ..create_empty_plan()
+ },
+ fleet: Fleet {
+ vehicles: vec![VehicleType {
+ shifts: vec![VehicleShift {
+ start: ShiftStart {
+ earliest: format_time(0.),
+ latest: Some(format_time(0.)),
+ location: (0., 0.).to_loc(),
+ },
+ breaks: Some(vec![
+ VehicleBreak::Required {
+ time: VehicleRequiredBreakTime::ExactTime {
+ earliest: format_time(7.),
+ latest: format_time(7.),
+ },
+ duration: 2.,
+ },
+ VehicleBreak::Required {
+ time: VehicleRequiredBreakTime::OffsetTime { earliest: 22., latest: 22. },
+ duration: 2.,
+ },
+ ]),
+ ..create_default_vehicle_shift()
+ }],
+ ..create_default_vehicle_type()
+ }],
+ ..create_default_fleet()
+ },
+ ..create_empty_problem()
+ };
+ let matrix = create_matrix_from_problem(&problem);
+ let solution = solve_with_metaheuristic_and_iterations_without_check(problem, Some(vec![matrix]), 200);
+
+ assert!(solution.unassigned.is_none());
+ validate_solution_breaks(&solution, 2, 2.0);
+
+ let intervals = collect_activity_intervals(&solution.tours[0]);
+ let mut break_starts: Vec =
+ intervals.iter().filter(|(_, _, t, _)| t == "break").map(|(s, _, _, _)| *s).collect();
+ break_starts.sort_by(|a, b| a.total_cmp(b));
+ assert_eq!(break_starts.len(), 2);
+ assert!((break_starts[0] - 7.0).abs() < 1.0, "first break should start at ~7, got {}", break_starts[0]);
+ assert!((break_starts[1] - 22.0).abs() < 1.0, "second break should start at ~22, got {}", break_starts[1]);
+}
+
+// =============================================================================
+// FirstJobToLastJob cost span
+// =============================================================================
+
+#[test]
+fn can_assign_offset_break_with_first_job_cost_span() {
+ let problem = Problem {
+ plan: Plan {
+ jobs: vec![create_delivery_job("job1", (10., 0.)), create_delivery_job("job2", (25., 0.))],
+ ..create_empty_plan()
+ },
+ fleet: Fleet {
+ vehicles: vec![VehicleType {
+ costs: VehicleCosts {
+ fixed: Some(10.),
+ distance: 1.,
+ time: 1.,
+ span: Some(RouteCostSpan::FirstJobToLastJob),
+ },
+ shifts: vec![VehicleShift {
+ start: ShiftStart {
+ earliest: format_time(0.),
+ latest: Some(format_time(0.)),
+ location: (0., 0.).to_loc(),
+ },
+ end: Some(ShiftEnd { earliest: None, latest: format_time(200.), location: (0., 0.).to_loc() }),
+ breaks: Some(vec![VehicleBreak::Required {
+ time: VehicleRequiredBreakTime::OffsetTime { earliest: 7., latest: 7. },
+ duration: 2.,
+ }]),
+ ..create_default_vehicle_shift()
+ }],
+ ..create_default_vehicle_type()
+ }],
+ ..create_default_fleet()
+ },
+ ..create_empty_problem()
+ };
+ let matrix = create_matrix_from_problem(&problem);
+ let solution = solve_with_metaheuristic_and_iterations_without_check(problem, Some(vec![matrix]), 200);
+
+ assert!(solution.unassigned.is_none());
+ validate_solution_breaks(&solution, 1, 2.0);
+
+ let intervals = collect_activity_intervals(&solution.tours[0]);
+ let first_job = intervals.iter().find(|(_, _, _, id)| id == "job1").expect("job1 missing");
+ let brk = intervals.iter().find(|(_, _, t, _)| t == "break").expect("break missing");
+ let offset_from_first_job = brk.0 - first_job.0;
+ assert!(
+ (offset_from_first_job - 7.0).abs() < 1.0,
+ "break offset from first job arrival ({}) should be ~7, got {offset_from_first_job} (break at {})",
+ first_job.0,
+ brk.0
+ );
+}
+
+#[test]
+fn can_assign_offset_break_with_first_job_span_and_range_offset() {
+ let problem = Problem {
+ plan: Plan {
+ jobs: vec![
+ create_delivery_job("job1", (10., 0.)),
+ create_delivery_job("job2", (20., 0.)),
+ create_delivery_job("job3", (30., 0.)),
+ ],
+ ..create_empty_plan()
+ },
+ fleet: Fleet {
+ vehicles: vec![VehicleType {
+ costs: VehicleCosts {
+ fixed: Some(10.),
+ distance: 1.,
+ time: 1.,
+ span: Some(RouteCostSpan::FirstJobToLastJob),
+ },
+ shifts: vec![VehicleShift {
+ start: ShiftStart {
+ earliest: format_time(0.),
+ latest: Some(format_time(0.)),
+ location: (0., 0.).to_loc(),
+ },
+ end: Some(ShiftEnd { earliest: None, latest: format_time(200.), location: (0., 0.).to_loc() }),
+ breaks: Some(vec![VehicleBreak::Required {
+ time: VehicleRequiredBreakTime::OffsetTime { earliest: 7., latest: 12. },
+ duration: 2.,
+ }]),
+ ..create_default_vehicle_shift()
+ }],
+ ..create_default_vehicle_type()
+ }],
+ ..create_default_fleet()
+ },
+ ..create_empty_problem()
+ };
+ let matrix = create_matrix_from_problem(&problem);
+ let solution = solve_with_metaheuristic_and_iterations_without_check(problem, Some(vec![matrix]), 200);
+
+ assert!(solution.unassigned.is_none());
+ validate_solution_breaks(&solution, 1, 2.0);
+
+ let intervals = collect_activity_intervals(&solution.tours[0]);
+ let first_route_job = intervals
+ .iter()
+ .find(|(_, _, typ, _)| typ != "departure" && typ != "arrival" && typ != "break")
+ .expect("no job in route");
+ let brk = intervals.iter().find(|(_, _, t, _)| t == "break").unwrap();
+ let offset = brk.0 - first_route_job.0;
+ assert!(
+ (6.0..=14.0).contains(&offset),
+ "break offset from first job arrival ({}) should be in [7..12], got {offset} (break at {})",
+ first_route_job.0,
+ brk.0
+ );
+}
+
+// =============================================================================
+// Wide offset range — the core bug scenario
+// =============================================================================
+
+#[test]
+fn can_assign_wide_range_offset_break_during_long_travel() {
+ // Time windows force ordering: depot→job1→job2→depot.
+ // Wide offset [4, 40]: break triggers at 40 during long travel job1→job2.
+ // Previously: avoid_reserved_time_when_driving incorrectly shifted departure (6 > 4),
+ // and break_writer failed to place the break due to TransitBreakMoved with no matching stop.
+ let problem = Problem {
+ plan: Plan {
+ jobs: vec![
+ create_delivery_job_with_times("job1", (5., 0.), vec![(0, 10)], 1.),
+ create_delivery_job_with_times("job2", (50., 0.), vec![(40, 100)], 1.),
+ ],
+ ..create_empty_plan()
+ },
+ fleet: Fleet {
+ vehicles: vec![VehicleType {
+ shifts: vec![VehicleShift {
+ start: ShiftStart {
+ earliest: format_time(0.),
+ latest: Some(format_time(0.)),
+ location: (0., 0.).to_loc(),
+ },
+ end: Some(ShiftEnd { earliest: None, latest: format_time(500.), location: (0., 0.).to_loc() }),
+ breaks: Some(vec![VehicleBreak::Required {
+ time: VehicleRequiredBreakTime::OffsetTime { earliest: 4., latest: 40. },
+ duration: 2.,
+ }]),
+ ..create_default_vehicle_shift()
+ }],
+ ..create_default_vehicle_type()
+ }],
+ ..create_default_fleet()
+ },
+ ..create_empty_problem()
+ };
+ let matrix = create_matrix_from_problem(&problem);
+ let solution = solve_with_metaheuristic_and_iterations_without_check(problem, Some(vec![matrix]), 200);
+
+ assert!(solution.unassigned.is_none());
+ validate_solution_breaks(&solution, 1, 2.0);
+}
+
+#[test]
+fn can_place_wide_offset_break_on_transit_leg_with_consistent_times() {
+ // Strict regression check for wide offset break placement:
+ // break must be placed on transit leg job1 -> job2 with coherent timing.
+ let problem = Problem {
+ plan: Plan {
+ jobs: vec![
+ create_delivery_job_with_times("job1", (5., 0.), vec![(0, 10)], 1.),
+ create_delivery_job_with_times("job2", (50., 0.), vec![(40, 100)], 1.),
+ ],
+ ..create_empty_plan()
+ },
+ fleet: Fleet {
+ vehicles: vec![VehicleType {
+ shifts: vec![VehicleShift {
+ start: ShiftStart {
+ earliest: format_time(0.),
+ latest: Some(format_time(0.)),
+ location: (0., 0.).to_loc(),
+ },
+ end: Some(ShiftEnd { earliest: None, latest: format_time(500.), location: (0., 0.).to_loc() }),
+ breaks: Some(vec![VehicleBreak::Required {
+ time: VehicleRequiredBreakTime::OffsetTime { earliest: 4., latest: 40. },
+ duration: 2.,
+ }]),
+ ..create_default_vehicle_shift()
+ }],
+ ..create_default_vehicle_type()
+ }],
+ ..create_default_fleet()
+ },
+ ..create_empty_problem()
+ };
+ let matrix = create_matrix_from_problem(&problem);
+ let solution = solve_with_metaheuristic_and_iterations_without_check(problem, Some(vec![matrix]), 200);
+
+ assert!(solution.unassigned.is_none(), "expected all jobs assigned");
+ assert_eq!(solution.tours.len(), 1, "expected one tour");
+
+ let tour = &solution.tours[0];
+ let debug = format_tour_debug(tour);
+
+ validate_tour_schedule_only(tour);
+ validate_no_break_job_overlap(tour);
+
+ let break_positions: Vec<_> = tour
+ .stops
+ .iter()
+ .enumerate()
+ .flat_map(|(stop_idx, stop)| {
+ stop.activities().iter().enumerate().filter_map(move |(act_idx, activity)| {
+ if activity.activity_type == "break" { Some((stop_idx, act_idx)) } else { None }
+ })
+ })
+ .collect();
+
+ assert_eq!(break_positions.len(), 1, "expected exactly one break\n{debug}");
+
+ let flat_order: Vec<_> =
+ tour.stops.iter().flat_map(|stop| stop.activities().iter().map(|activity| activity.job_id.clone())).collect();
+ assert_eq!(
+ flat_order,
+ vec!["departure", "job1", "break", "job2", "arrival"],
+ "unexpected flattened activity order\n{debug}"
+ );
+
+ let (break_stop_idx, break_activity_idx) = break_positions[0];
+ assert!(
+ break_stop_idx > 0 && break_stop_idx + 1 < tour.stops.len(),
+ "break stop should have previous and next stops\n{debug}"
+ );
+
+ let break_stop = &tour.stops[break_stop_idx];
+ assert!(matches!(break_stop, Stop::Transit(_)), "break should be attached to transit stop\n{debug}");
+ assert_eq!(break_stop.activities().len(), 1, "transit break stop should have a single break activity\n{debug}");
+
+ let prev_stop = &tour.stops[break_stop_idx - 1];
+ let next_stop = &tour.stops[break_stop_idx + 1];
+ assert!(
+ prev_stop.activities().iter().any(|activity| activity.job_id == "job1"),
+ "break previous stop should contain job1\n{debug}"
+ );
+ assert!(
+ next_stop.activities().iter().any(|activity| activity.job_id == "job2"),
+ "break next stop should contain job2\n{debug}"
+ );
+
+ let break_activity = &break_stop.activities()[break_activity_idx];
+ let stop_arrival = parse_time(&break_stop.schedule().arrival);
+ let stop_departure = parse_time(&break_stop.schedule().departure);
+ let (break_start, break_end) = break_activity
+ .time
+ .as_ref()
+ .map(|time| (parse_time(&time.start), parse_time(&time.end)))
+ .unwrap_or((stop_arrival, stop_departure));
+
+ assert!(
+ (break_start - stop_arrival).abs() < 1e-3 && (break_end - stop_departure).abs() < 1e-3,
+ "break activity interval should match transit stop interval\n{debug}"
+ );
+
+ let prev_departure = parse_time(&prev_stop.schedule().departure);
+ let next_arrival = parse_time(&next_stop.schedule().arrival);
+ assert!(
+ break_start >= prev_departure - 1e-3,
+ "break starts before previous stop departure: break_start={break_start}, prev_departure={prev_departure}\n{debug}"
+ );
+ assert!(
+ break_start < break_end - 1e-3,
+ "break interval is not strictly positive: [{break_start}..{break_end}]\n{debug}"
+ );
+ assert!(
+ break_end <= next_arrival + 1e-3,
+ "break ends after next stop arrival: break_end={break_end}, next_arrival={next_arrival}\n{debug}"
+ );
+
+ let departure = parse_time(&tour.stops[0].schedule().departure);
+ let offset = break_start - departure;
+ assert!((offset - 40.0).abs() <= 1.0, "break offset from tour departure should be near 40, got {offset}\n{debug}");
+}
+
+#[test]
+fn can_keep_job_activity_duration_when_break_starts_at_activity_end_on_same_stop() {
+ // Boundary regression: required break starts exactly when job1 activity ends.
+ // Break should be on the same point stop as job1 without extending job1 activity time.
+ let problem = Problem {
+ plan: Plan {
+ jobs: vec![
+ create_delivery_job_with_times("job1", (4., 0.), vec![(5, 10)], 1.),
+ create_delivery_job_with_times("job2", (12., 0.), vec![(20, 100)], 1.),
+ ],
+ ..create_empty_plan()
+ },
+ fleet: Fleet {
+ vehicles: vec![VehicleType {
+ shifts: vec![VehicleShift {
+ start: ShiftStart {
+ earliest: format_time(0.),
+ latest: Some(format_time(0.)),
+ location: (0., 0.).to_loc(),
+ },
+ end: Some(ShiftEnd { earliest: None, latest: format_time(300.), location: (0., 0.).to_loc() }),
+ breaks: Some(vec![VehicleBreak::Required {
+ time: VehicleRequiredBreakTime::OffsetTime { earliest: 5., latest: 6. },
+ duration: 2.,
+ }]),
+ ..create_default_vehicle_shift()
+ }],
+ ..create_default_vehicle_type()
+ }],
+ ..create_default_fleet()
+ },
+ ..create_empty_problem()
+ };
+ let matrix = create_matrix_from_problem(&problem);
+ let solution = solve_with_metaheuristic_and_iterations_without_check(problem, Some(vec![matrix]), 200);
+
+ assert!(solution.unassigned.is_none(), "expected all jobs assigned");
+ assert_eq!(solution.tours.len(), 1, "expected one tour");
+
+ let tour = &solution.tours[0];
+ let debug = format_tour_debug(tour);
+
+ let break_positions: Vec<_> = tour
+ .stops
+ .iter()
+ .enumerate()
+ .flat_map(|(stop_idx, stop)| {
+ stop.activities().iter().enumerate().filter_map(move |(act_idx, activity)| {
+ if activity.activity_type == "break" { Some((stop_idx, act_idx)) } else { None }
+ })
+ })
+ .collect();
+
+ assert_eq!(break_positions.len(), 1, "expected exactly one break\n{debug}");
+
+ let flat_order: Vec<_> =
+ tour.stops.iter().flat_map(|stop| stop.activities().iter().map(|activity| activity.job_id.clone())).collect();
+ assert_eq!(
+ flat_order,
+ vec!["departure", "job1", "break", "job2", "arrival"],
+ "unexpected flattened activity order\n{debug}"
+ );
+
+ let (break_stop_idx, break_activity_idx) = break_positions[0];
+ assert!(
+ break_stop_idx > 0 && break_stop_idx + 1 < tour.stops.len(),
+ "break stop should have previous and next stops\n{debug}"
+ );
+
+ let break_stop = &tour.stops[break_stop_idx];
+ assert!(
+ matches!(break_stop, Stop::Point(_)),
+ "break should be attached to point stop with job1 in this scenario\n{debug}"
+ );
+ assert!(
+ break_stop.activities().iter().any(|activity| activity.job_id == "job1"),
+ "break stop should contain job1\n{debug}"
+ );
+
+ let stop_arrival = parse_time(&break_stop.schedule().arrival);
+ let stop_departure = parse_time(&break_stop.schedule().departure);
+
+ let break_activity = &break_stop.activities()[break_activity_idx];
+ let (break_start, break_end) = break_activity
+ .time
+ .as_ref()
+ .map(|time| (parse_time(&time.start), parse_time(&time.end)))
+ .unwrap_or((stop_arrival, stop_departure));
+
+ let job1_activity = break_stop
+ .activities()
+ .iter()
+ .find(|activity| activity.job_id == "job1")
+ .expect("job1 activity should be on break stop");
+ let (job1_start, job1_end) = job1_activity
+ .time
+ .as_ref()
+ .map(|time| (parse_time(&time.start), parse_time(&time.end)))
+ .unwrap_or((stop_arrival, stop_departure));
+
+ assert!(
+ ((job1_end - job1_start) - 1.0).abs() < 1e-3,
+ "job1 activity duration should stay at 1, got {}\n{debug}",
+ job1_end - job1_start
+ );
+ assert!(
+ job1_end <= break_start + 1e-3,
+ "job1 end should not be after break start: job1_end={job1_end}, break_start={break_start}\n{debug}"
+ );
+ assert!(
+ break_start < break_end - 1e-3,
+ "break interval is not strictly positive: [{break_start}..{break_end}]\n{debug}"
+ );
+ assert!(
+ break_start >= stop_arrival - 1e-3 && break_end <= stop_departure + 1e-3,
+ "break interval should stay within stop bounds: stop=[{stop_arrival}..{stop_departure}], break=[{break_start}..{break_end}]\n{debug}"
+ );
+
+ let prev_departure = parse_time(&tour.stops[break_stop_idx - 1].schedule().departure);
+ let next_arrival = parse_time(&tour.stops[break_stop_idx + 1].schedule().arrival);
+ assert!(
+ prev_departure <= break_start + 1e-3,
+ "break starts before previous stop departure: prev_departure={prev_departure}, break_start={break_start}\n{debug}"
+ );
+ assert!(
+ break_end <= next_arrival + 1e-3,
+ "break ends after next stop arrival: break_end={break_end}, next_arrival={next_arrival}\n{debug}"
+ );
+
+ validate_tour_schedule_only(tour);
+ validate_no_break_job_overlap(tour);
+}
+
+#[test]
+fn can_align_required_break_to_job_boundary_when_reserved_time_hits_mid_activity() {
+ let problem = Problem {
+ plan: Plan { jobs: vec![create_delivery_job_with_duration("job1", (5., 0.), 3.)], ..create_empty_plan() },
+ fleet: Fleet {
+ vehicles: vec![VehicleType {
+ shifts: vec![VehicleShift {
+ start: ShiftStart {
+ earliest: format_time(0.),
+ latest: Some(format_time(0.)),
+ location: (0., 0.).to_loc(),
+ },
+ end: Some(ShiftEnd { earliest: None, latest: format_time(200.), location: (0., 0.).to_loc() }),
+ breaks: Some(vec![VehicleBreak::Required {
+ time: VehicleRequiredBreakTime::ExactTime {
+ earliest: format_time(7.),
+ latest: format_time(7.),
+ },
+ duration: 2.,
+ }]),
+ ..create_default_vehicle_shift()
+ }],
+ ..create_default_vehicle_type()
+ }],
+ ..create_default_fleet()
+ },
+ ..create_empty_problem()
+ };
+ let matrix = create_matrix_from_problem(&problem);
+ let solution = solve_with_metaheuristic(problem, Some(vec![matrix]));
+
+ assert!(solution.unassigned.is_none(), "expected all jobs assigned");
+ assert_eq!(solution.tours.len(), 1, "expected one tour");
+
+ let tour = &solution.tours[0];
+ let debug = format_tour_debug(tour);
+
+ let stop = tour
+ .stops
+ .iter()
+ .find(|stop| {
+ stop.activities().iter().any(|activity| activity.job_id == "job1")
+ && stop.activities().iter().any(|activity| activity.activity_type == "break")
+ })
+ .expect("expected job1 and break at same stop");
+
+ let stop_arrival = parse_time(&stop.schedule().arrival);
+ let stop_departure = parse_time(&stop.schedule().departure);
+
+ let job = stop.activities().iter().find(|activity| activity.job_id == "job1").expect("job1 activity");
+ let brk = stop.activities().iter().find(|activity| activity.activity_type == "break").expect("break activity");
+
+ let (job_start, job_end) = job
+ .time
+ .as_ref()
+ .map(|time| (parse_time(&time.start), parse_time(&time.end)))
+ .unwrap_or((stop_arrival, stop_departure));
+ let (break_start, break_end) = brk
+ .time
+ .as_ref()
+ .map(|time| (parse_time(&time.start), parse_time(&time.end)))
+ .unwrap_or((stop_arrival, stop_departure));
+
+ assert!(
+ ((job_end - job_start) - 3.0).abs() < 1e-3,
+ "job1 duration should stay equal to service duration, got {}\n{debug}",
+ job_end - job_start
+ );
+ assert!(
+ (break_start - job_end).abs() <= 1e-3 || (job_start - break_end).abs() <= 1e-3,
+ "break should start at job end or finish at job start, got job=[{job_start}..{job_end}], break=[{break_start}..{break_end}]\n{debug}"
+ );
+ assert!(
+ !(break_start < job_end - 1e-3 && job_start < break_end - 1e-3),
+ "break should not overlap job activity at same stop: job=[{job_start}..{job_end}], break=[{break_start}..{break_end}]\n{debug}"
+ );
+ assert!(
+ break_start >= stop_arrival - 1e-3 && break_end <= stop_departure + 1e-3,
+ "break should stay within stop bounds: stop=[{stop_arrival}..{stop_departure}], break=[{break_start}..{break_end}]\n{debug}"
+ );
+
+ validate_tour_schedule_only(tour);
+ validate_no_break_job_overlap(tour);
+}
+
+#[test]
+fn can_skip_required_break_when_it_starts_at_tour_end_boundary() {
+ let problem = Problem {
+ plan: Plan { jobs: vec![create_delivery_job("job1", (5., 0.))], ..create_empty_plan() },
+ fleet: Fleet {
+ vehicles: vec![VehicleType {
+ shifts: vec![VehicleShift {
+ start: ShiftStart {
+ earliest: format_time(0.),
+ latest: Some(format_time(0.)),
+ location: (0., 0.).to_loc(),
+ },
+ end: Some(ShiftEnd { earliest: None, latest: format_time(200.), location: (0., 0.).to_loc() }),
+ breaks: Some(vec![VehicleBreak::Required {
+ time: VehicleRequiredBreakTime::OffsetTime { earliest: 11., latest: 11. },
+ duration: 2.,
+ }]),
+ ..create_default_vehicle_shift()
+ }],
+ ..create_default_vehicle_type()
+ }],
+ ..create_default_fleet()
+ },
+ ..create_empty_problem()
+ };
+ let matrix = create_matrix_from_problem(&problem);
+ let solution = solve_with_metaheuristic(problem, Some(vec![matrix]));
+
+ assert!(solution.unassigned.is_none(), "expected all jobs assigned");
+ assert_eq!(solution.tours.len(), 1, "expected one tour");
+
+ let tour = &solution.tours[0];
+ let debug = format_tour_debug(tour);
+
+ let break_count = tour
+ .stops
+ .iter()
+ .flat_map(|stop| stop.activities().iter())
+ .filter(|activity| activity.activity_type == "break")
+ .count();
+ assert_eq!(break_count, 0, "required break should be skipped when it only touches tour end boundary\n{debug}");
+
+ let flat_order: Vec<_> =
+ tour.stops.iter().flat_map(|stop| stop.activities().iter().map(|activity| activity.job_id.clone())).collect();
+ assert_eq!(flat_order, vec!["departure", "job1", "arrival"], "unexpected flattened activity order\n{debug}");
+
+ let last_stop = tour.stops.last().expect("expected last stop");
+ let last_arrival = parse_time(&last_stop.schedule().arrival);
+ let last_departure = parse_time(&last_stop.schedule().departure);
+ assert!(
+ (last_departure - last_arrival).abs() <= 1e-3,
+ "last stop should not be stretched by boundary-touching break: arrival={last_arrival}, departure={last_departure}\n{debug}"
+ );
+
+ validate_tour_schedule_only(tour);
+ validate_no_break_job_overlap(tour);
+}
+
+#[test]
+fn can_assign_range_offset_break_without_wrong_departure_shift() {
+ let problem = Problem {
+ plan: Plan {
+ jobs: vec![
+ create_delivery_job("job1", (5., 0.)),
+ create_delivery_job("job2", (12., 0.)),
+ create_delivery_job("job3", (20., 0.)),
+ ],
+ ..create_empty_plan()
+ },
+ fleet: Fleet {
+ vehicles: vec![VehicleType {
+ shifts: vec![VehicleShift {
+ start: ShiftStart {
+ earliest: format_time(0.),
+ latest: Some(format_time(0.)),
+ location: (0., 0.).to_loc(),
+ },
+ end: Some(ShiftEnd { earliest: None, latest: format_time(200.), location: (0., 0.).to_loc() }),
+ breaks: Some(vec![VehicleBreak::Required {
+ time: VehicleRequiredBreakTime::OffsetTime { earliest: 4., latest: 12. },
+ duration: 2.,
+ }]),
+ ..create_default_vehicle_shift()
+ }],
+ ..create_default_vehicle_type()
+ }],
+ ..create_default_fleet()
+ },
+ ..create_empty_problem()
+ };
+ let matrix = create_matrix_from_problem(&problem);
+ let solution = solve_with_metaheuristic_and_iterations_without_check(problem, Some(vec![matrix]), 200);
+
+ assert!(solution.unassigned.is_none());
+ validate_solution_breaks(&solution, 1, 2.0);
+}
+
+// =============================================================================
+// Complex realistic scenarios
+// =============================================================================
+
+#[test]
+fn can_assign_break_with_many_closely_spaced_jobs_and_long_service() {
+ // 6 jobs along a line with varying service durations (some long).
+ // Break at offset [10, 15] with duration 3.
+ // Tests that break is placed correctly between dense job stops with long service times.
+ let problem = Problem {
+ plan: Plan {
+ jobs: vec![
+ create_delivery_job_with_duration("j1", (3., 0.), 2.),
+ create_delivery_job_with_duration("j2", (6., 0.), 4.),
+ create_delivery_job_with_duration("j3", (9., 0.), 1.),
+ create_delivery_job_with_duration("j4", (12., 0.), 3.),
+ create_delivery_job_with_duration("j5", (15., 0.), 2.),
+ create_delivery_job_with_duration("j6", (18., 0.), 1.),
+ ],
+ ..create_empty_plan()
+ },
+ fleet: Fleet {
+ vehicles: vec![VehicleType {
+ shifts: vec![VehicleShift {
+ start: ShiftStart {
+ earliest: format_time(0.),
+ latest: Some(format_time(0.)),
+ location: (0., 0.).to_loc(),
+ },
+ end: Some(ShiftEnd { earliest: None, latest: format_time(300.), location: (0., 0.).to_loc() }),
+ breaks: Some(vec![VehicleBreak::Required {
+ time: VehicleRequiredBreakTime::OffsetTime { earliest: 10., latest: 15. },
+ duration: 3.,
+ }]),
+ ..create_default_vehicle_shift()
+ }],
+ ..create_default_vehicle_type()
+ }],
+ ..create_default_fleet()
+ },
+ ..create_empty_problem()
+ };
+ let matrix = create_matrix_from_problem(&problem);
+ let solution = solve_with_metaheuristic_and_iterations_without_check(problem, Some(vec![matrix]), 200);
+
+ assert!(solution.unassigned.is_none(), "expected all 6 jobs assigned");
+ validate_solution_breaks(&solution, 1, 3.0);
+
+ // Verify offset is in expected range
+ let tour = &solution.tours[0];
+ let departure = parse_time(&tour.stops[0].schedule().departure);
+ let intervals = collect_activity_intervals(tour);
+ let brk = intervals.iter().find(|(_, _, t, _)| t == "break").unwrap();
+ let offset = brk.0 - departure;
+ assert!(
+ (9.0..=18.0).contains(&offset),
+ "break offset from departure should be in [10..15] range, got {offset}\ntour: {}",
+ format_tour_debug(tour)
+ );
+}
+
+#[test]
+fn can_assign_break_with_pickup_delivery_jobs() {
+ // Pickup-delivery pairs: pickup at one location, deliver at another.
+ // Break at offset [8, 12] with duration 2.
+ // Tests that break doesn't split a pickup from its delivery incorrectly,
+ // and that all schedule constraints hold.
+ let problem = Problem {
+ plan: Plan {
+ jobs: vec![
+ create_pickup_delivery_job("pd1", (5., 0.), (15., 0.)),
+ create_pickup_delivery_job("pd2", (8., 0.), (20., 0.)),
+ ],
+ ..create_empty_plan()
+ },
+ fleet: Fleet {
+ vehicles: vec![VehicleType {
+ shifts: vec![VehicleShift {
+ start: ShiftStart {
+ earliest: format_time(0.),
+ latest: Some(format_time(0.)),
+ location: (0., 0.).to_loc(),
+ },
+ end: Some(ShiftEnd { earliest: None, latest: format_time(300.), location: (0., 0.).to_loc() }),
+ breaks: Some(vec![VehicleBreak::Required {
+ time: VehicleRequiredBreakTime::OffsetTime { earliest: 8., latest: 12. },
+ duration: 2.,
+ }]),
+ ..create_default_vehicle_shift()
+ }],
+ ..create_default_vehicle_type()
+ }],
+ ..create_default_fleet()
+ },
+ ..create_empty_problem()
+ };
+ let matrix = create_matrix_from_problem(&problem);
+ let solution = solve_with_metaheuristic_and_iterations_without_check(problem, Some(vec![matrix]), 200);
+
+ assert!(solution.unassigned.is_none(), "expected all pickup-delivery jobs assigned");
+ validate_solution_breaks(&solution, 1, 2.0);
+}
+
+#[test]
+fn can_assign_break_with_tight_time_windows_and_long_break() {
+ // Jobs with time windows forcing a specific schedule.
+ // Break duration is relatively long (5 units).
+ // Tests that long breaks don't violate time window constraints or overlap with activities.
+ let problem = Problem {
+ plan: Plan {
+ jobs: vec![
+ create_delivery_job_with_times("j1", (5., 0.), vec![(4, 12)], 1.),
+ create_delivery_job_with_times("j2", (10., 0.), vec![(12, 30)], 1.),
+ create_delivery_job_with_times("j3", (15., 0.), vec![(20, 45)], 1.),
+ ],
+ ..create_empty_plan()
+ },
+ fleet: Fleet {
+ vehicles: vec![VehicleType {
+ shifts: vec![VehicleShift {
+ start: ShiftStart {
+ earliest: format_time(0.),
+ latest: Some(format_time(0.)),
+ location: (0., 0.).to_loc(),
+ },
+ end: Some(ShiftEnd { earliest: None, latest: format_time(300.), location: (0., 0.).to_loc() }),
+ breaks: Some(vec![VehicleBreak::Required {
+ time: VehicleRequiredBreakTime::OffsetTime { earliest: 8., latest: 12. },
+ duration: 5.,
+ }]),
+ ..create_default_vehicle_shift()
+ }],
+ ..create_default_vehicle_type()
+ }],
+ ..create_default_fleet()
+ },
+ ..create_empty_problem()
+ };
+ let matrix = create_matrix_from_problem(&problem);
+ let solution = solve_with_metaheuristic_and_iterations_without_check(problem, Some(vec![matrix]), 200);
+
+ assert!(solution.unassigned.is_none(), "expected all jobs assigned");
+ validate_solution_breaks(&solution, 1, 5.0);
+}
+
+#[test]
+fn can_assign_break_with_multiple_vehicles() {
+ // Two vehicles, each with their own break offset.
+ // 4 jobs spread out: each vehicle takes ~2 jobs.
+ // Tests that each vehicle gets its own break with correct offset.
+ let problem = Problem {
+ plan: Plan {
+ jobs: vec![
+ create_delivery_job("j1", (5., 5.)),
+ create_delivery_job("j2", (10., 5.)),
+ create_delivery_job("j3", (5., -5.)),
+ create_delivery_job("j4", (10., -5.)),
+ ],
+ ..create_empty_plan()
+ },
+ fleet: Fleet {
+ vehicles: vec![
+ VehicleType {
+ type_id: "v1_type".to_string(),
+ vehicle_ids: vec!["v1".to_string()],
+ shifts: vec![VehicleShift {
+ start: ShiftStart {
+ earliest: format_time(0.),
+ latest: Some(format_time(0.)),
+ location: (0., 0.).to_loc(),
+ },
+ end: Some(ShiftEnd { earliest: None, latest: format_time(300.), location: (0., 0.).to_loc() }),
+ breaks: Some(vec![VehicleBreak::Required {
+ time: VehicleRequiredBreakTime::OffsetTime { earliest: 8., latest: 8. },
+ duration: 2.,
+ }]),
+ ..create_default_vehicle_shift()
+ }],
+ capacity: vec![10],
+ ..create_default_vehicle_type()
+ },
+ VehicleType {
+ type_id: "v2_type".to_string(),
+ vehicle_ids: vec!["v2".to_string()],
+ shifts: vec![VehicleShift {
+ start: ShiftStart {
+ earliest: format_time(0.),
+ latest: Some(format_time(0.)),
+ location: (0., 0.).to_loc(),
+ },
+ end: Some(ShiftEnd { earliest: None, latest: format_time(300.), location: (0., 0.).to_loc() }),
+ breaks: Some(vec![VehicleBreak::Required {
+ time: VehicleRequiredBreakTime::OffsetTime { earliest: 10., latest: 10. },
+ duration: 3.,
+ }]),
+ ..create_default_vehicle_shift()
+ }],
+ capacity: vec![10],
+ ..create_default_vehicle_type()
+ },
+ ],
+ profiles: create_default_matrix_profiles(),
+ ..create_default_fleet()
+ },
+ ..create_empty_problem()
+ };
+ let matrix = create_matrix_from_problem(&problem);
+ let solution = solve_with_metaheuristic_and_iterations_without_check(problem, Some(vec![matrix]), 200);
+
+ assert!(solution.unassigned.is_none(), "expected all 4 jobs assigned");
+ assert!(!solution.tours.is_empty(), "expected at least 1 tour");
+
+ // Validate each tour independently — break count/duration varies by vehicle type
+ for tour in &solution.tours {
+ let expected_duration = if tour.vehicle_id == "v1" { 2.0 } else { 3.0 };
+ validate_tour_breaks_and_schedule(tour, 1, expected_duration);
+ }
+}
+
+#[test]
+fn can_assign_break_with_flexible_departure_and_many_jobs_clustered() {
+ // Flexible departure (no latest). 8 jobs clustered in two groups far apart.
+ // Group A at x~5..10, Group B at x~40..50. Break offset [15, 25] duration 3.
+ // The break should occur during the long travel between groups, not overlap with any job.
+ let problem = Problem {
+ plan: Plan {
+ jobs: vec![
+ create_delivery_job("a1", (5., 0.)),
+ create_delivery_job("a2", (7., 0.)),
+ create_delivery_job("a3", (8., 1.)),
+ create_delivery_job("a4", (10., 0.)),
+ create_delivery_job("b1", (40., 0.)),
+ create_delivery_job("b2", (43., 0.)),
+ create_delivery_job("b3", (45., 1.)),
+ create_delivery_job("b4", (48., 0.)),
+ ],
+ ..create_empty_plan()
+ },
+ fleet: Fleet {
+ vehicles: vec![VehicleType {
+ shifts: vec![VehicleShift {
+ end: Some(ShiftEnd { earliest: None, latest: format_time(500.), location: (0., 0.).to_loc() }),
+ breaks: Some(vec![VehicleBreak::Required {
+ time: VehicleRequiredBreakTime::OffsetTime { earliest: 15., latest: 25. },
+ duration: 3.,
+ }]),
+ ..create_default_vehicle_shift()
+ }],
+ ..create_default_vehicle_type()
+ }],
+ ..create_default_fleet()
+ },
+ ..create_empty_problem()
+ };
+ let matrix = create_matrix_from_problem(&problem);
+ let solution = solve_with_metaheuristic_and_iterations_without_check(problem, Some(vec![matrix]), 200);
+
+ assert!(solution.unassigned.is_none(), "expected all 8 jobs assigned");
+ validate_solution_breaks(&solution, 1, 3.0);
+}
+
+#[test]
+fn can_assign_break_with_first_job_span_flexible_departure_and_wide_offset() {
+ // The full combination: FirstJobToLastJob span + flexible departure + wide offset range.
+ // Departure is flexible, first job at (8,0). Anchor = first job arrival.
+ // Break offset [4, 10] relative to anchor (first job arrival), duration 2.
+ // 4 jobs along a line: 8, 15, 22, 30.
+ let problem = Problem {
+ plan: Plan {
+ jobs: vec![
+ create_delivery_job("j1", (8., 0.)),
+ create_delivery_job("j2", (15., 0.)),
+ create_delivery_job("j3", (22., 0.)),
+ create_delivery_job("j4", (30., 0.)),
+ ],
+ ..create_empty_plan()
+ },
+ fleet: Fleet {
+ vehicles: vec![VehicleType {
+ costs: VehicleCosts {
+ fixed: Some(10.),
+ distance: 1.,
+ time: 1.,
+ span: Some(RouteCostSpan::FirstJobToLastJob),
+ },
+ shifts: vec![VehicleShift {
+ end: Some(ShiftEnd { earliest: None, latest: format_time(500.), location: (0., 0.).to_loc() }),
+ breaks: Some(vec![VehicleBreak::Required {
+ time: VehicleRequiredBreakTime::OffsetTime { earliest: 4., latest: 10. },
+ duration: 2.,
+ }]),
+ ..create_default_vehicle_shift()
+ }],
+ ..create_default_vehicle_type()
+ }],
+ ..create_default_fleet()
+ },
+ ..create_empty_problem()
+ };
+ let matrix = create_matrix_from_problem(&problem);
+ let solution = solve_with_metaheuristic_and_iterations_without_check(problem, Some(vec![matrix]), 200);
+
+ assert!(solution.unassigned.is_none(), "expected all 4 jobs assigned");
+ validate_solution_breaks(&solution, 1, 2.0);
+
+ // Verify offset is relative to first job, not departure
+ let tour = &solution.tours[0];
+ let intervals = collect_activity_intervals(tour);
+ let first_route_job = intervals
+ .iter()
+ .find(|(_, _, typ, _)| typ != "departure" && typ != "arrival" && typ != "break")
+ .expect("no job in route");
+ let brk = intervals.iter().find(|(_, _, t, _)| t == "break").unwrap();
+ let offset = brk.0 - first_route_job.0;
+ assert!(
+ (3.0..=12.0).contains(&offset),
+ "break offset from first job ({}) should be in [4..10], got {offset} (break at {})\ntour: {}",
+ first_route_job.0,
+ brk.0,
+ format_tour_debug(tour)
+ );
+}
+
+#[test]
+fn can_assign_break_with_first_job_span_late_time_windows_and_wide_offset() {
+ // FirstJobToLastJob + late time windows + wide offset range [4, 20].
+ // Jobs available only after t=20, so departure must be advanced.
+ // Break should be relative to first job arrival, not departure.
+ let problem = Problem {
+ plan: Plan {
+ jobs: vec![
+ create_delivery_job_with_times("j1", (5., 0.), vec![(20, 50)], 1.),
+ create_delivery_job_with_times("j2", (12., 0.), vec![(25, 60)], 1.),
+ create_delivery_job_with_times("j3", (20., 0.), vec![(30, 70)], 1.),
+ ],
+ ..create_empty_plan()
+ },
+ fleet: Fleet {
+ vehicles: vec![VehicleType {
+ costs: VehicleCosts {
+ fixed: Some(10.),
+ distance: 1.,
+ time: 1.,
+ span: Some(RouteCostSpan::FirstJobToLastJob),
+ },
+ shifts: vec![VehicleShift {
+ end: Some(ShiftEnd { earliest: None, latest: format_time(500.), location: (0., 0.).to_loc() }),
+ breaks: Some(vec![VehicleBreak::Required {
+ time: VehicleRequiredBreakTime::OffsetTime { earliest: 4., latest: 20. },
+ duration: 2.,
+ }]),
+ ..create_default_vehicle_shift()
+ }],
+ ..create_default_vehicle_type()
+ }],
+ ..create_default_fleet()
+ },
+ ..create_empty_problem()
+ };
+ let matrix = create_matrix_from_problem(&problem);
+ let solution = solve_with_metaheuristic_and_iterations_without_check(problem, Some(vec![matrix]), 200);
+
+ assert!(solution.unassigned.is_none(), "expected all 3 jobs assigned");
+ validate_solution_breaks(&solution, 1, 2.0);
+
+ // Departure should have been advanced past 0
+ let departure = parse_time(&solution.tours[0].stops[0].schedule().departure);
+ assert!(departure > 0., "expected departure advanced for late time windows, got {departure}");
+}
+
+#[test]
+fn can_assign_break_with_jobs_requiring_long_service_times() {
+ // Jobs with long service durations (10, 15, 8 units). Break offset [20, 25] duration 3.
+ // Tests that break placed during or between long-service jobs doesn't overlap.
+ let problem = Problem {
+ plan: Plan {
+ jobs: vec![
+ create_delivery_job_with_duration("j1", (5., 0.), 10.),
+ create_delivery_job_with_duration("j2", (15., 0.), 15.),
+ create_delivery_job_with_duration("j3", (25., 0.), 8.),
+ ],
+ ..create_empty_plan()
+ },
+ fleet: Fleet {
+ vehicles: vec![VehicleType {
+ shifts: vec![VehicleShift {
+ start: ShiftStart {
+ earliest: format_time(0.),
+ latest: Some(format_time(0.)),
+ location: (0., 0.).to_loc(),
+ },
+ end: Some(ShiftEnd { earliest: None, latest: format_time(500.), location: (0., 0.).to_loc() }),
+ breaks: Some(vec![VehicleBreak::Required {
+ time: VehicleRequiredBreakTime::OffsetTime { earliest: 20., latest: 25. },
+ duration: 3.,
+ }]),
+ ..create_default_vehicle_shift()
+ }],
+ ..create_default_vehicle_type()
+ }],
+ ..create_default_fleet()
+ },
+ ..create_empty_problem()
+ };
+ let matrix = create_matrix_from_problem(&problem);
+ let solution = solve_with_metaheuristic_and_iterations_without_check(problem, Some(vec![matrix]), 200);
+
+ assert!(solution.unassigned.is_none());
+ validate_solution_breaks(&solution, 1, 3.0);
+}
+
+#[test]
+fn can_assign_two_offset_breaks_with_wide_ranges() {
+ // Two required breaks with wide offset ranges: [5, 15] and [25, 40].
+ // 5 jobs along a long route. Tests that both breaks are placed correctly
+ // without overlapping each other or any job activities.
+ let problem = Problem {
+ plan: Plan {
+ jobs: vec![
+ create_delivery_job("j1", (5., 0.)),
+ create_delivery_job("j2", (15., 0.)),
+ create_delivery_job("j3", (25., 0.)),
+ create_delivery_job("j4", (35., 0.)),
+ create_delivery_job("j5", (45., 0.)),
+ ],
+ ..create_empty_plan()
+ },
+ fleet: Fleet {
+ vehicles: vec![VehicleType {
+ shifts: vec![VehicleShift {
+ start: ShiftStart {
+ earliest: format_time(0.),
+ latest: Some(format_time(0.)),
+ location: (0., 0.).to_loc(),
+ },
+ end: Some(ShiftEnd { earliest: None, latest: format_time(500.), location: (0., 0.).to_loc() }),
+ breaks: Some(vec![
+ VehicleBreak::Required {
+ time: VehicleRequiredBreakTime::OffsetTime { earliest: 5., latest: 15. },
+ duration: 2.,
+ },
+ VehicleBreak::Required {
+ time: VehicleRequiredBreakTime::OffsetTime { earliest: 25., latest: 40. },
+ duration: 2.,
+ },
+ ]),
+ ..create_default_vehicle_shift()
+ }],
+ ..create_default_vehicle_type()
+ }],
+ ..create_default_fleet()
+ },
+ ..create_empty_problem()
+ };
+ let matrix = create_matrix_from_problem(&problem);
+ let solution = solve_with_metaheuristic_and_iterations_without_check(problem, Some(vec![matrix]), 200);
+
+ assert!(solution.unassigned.is_none(), "expected all 5 jobs assigned");
+ validate_solution_breaks(&solution, 2, 2.0);
+
+ // Verify the two breaks don't overlap each other
+ let intervals = collect_activity_intervals(&solution.tours[0]);
+ let breaks: Vec<_> = intervals.iter().filter(|(_, _, t, _)| t == "break").collect();
+ assert_eq!(breaks.len(), 2);
+ let (b1_start, b1_end, _, _) = breaks[0];
+ let (b2_start, b2_end, _, _) = breaks[1];
+ let overlaps = b1_start < b2_end && b2_start < b1_end;
+ assert!(
+ !overlaps,
+ "two breaks overlap: [{b1_start}..{b1_end}] and [{b2_start}..{b2_end}]\ntour: {}",
+ format_tour_debug(&solution.tours[0])
+ );
+}
+
+#[test]
+fn can_assign_exact_and_offset_breaks_with_many_jobs() {
+ // Mixed: one ExactTime break at t=10, one OffsetTime break at offset [30, 40].
+ // 6 jobs along a 60-unit route. Tests that both types coexist with many activities.
+ let problem = Problem {
+ plan: Plan {
+ jobs: vec![
+ create_delivery_job("j1", (5., 0.)),
+ create_delivery_job("j2", (12., 0.)),
+ create_delivery_job("j3", (20., 0.)),
+ create_delivery_job("j4", (30., 0.)),
+ create_delivery_job("j5", (42., 0.)),
+ create_delivery_job("j6", (55., 0.)),
+ ],
+ ..create_empty_plan()
+ },
+ fleet: Fleet {
+ vehicles: vec![VehicleType {
+ shifts: vec![VehicleShift {
+ start: ShiftStart {
+ earliest: format_time(0.),
+ latest: Some(format_time(0.)),
+ location: (0., 0.).to_loc(),
+ },
+ end: Some(ShiftEnd { earliest: None, latest: format_time(500.), location: (0., 0.).to_loc() }),
+ breaks: Some(vec![
+ VehicleBreak::Required {
+ time: VehicleRequiredBreakTime::ExactTime {
+ earliest: format_time(10.),
+ latest: format_time(10.),
+ },
+ duration: 2.,
+ },
+ VehicleBreak::Required {
+ time: VehicleRequiredBreakTime::OffsetTime { earliest: 30., latest: 40. },
+ duration: 3.,
+ },
+ ]),
+ ..create_default_vehicle_shift()
+ }],
+ ..create_default_vehicle_type()
+ }],
+ ..create_default_fleet()
+ },
+ ..create_empty_problem()
+ };
+ let matrix = create_matrix_from_problem(&problem);
+ let solution = solve_with_metaheuristic_and_iterations_without_check(problem, Some(vec![matrix]), 200);
+
+ assert!(solution.unassigned.is_none(), "expected all 6 jobs assigned");
+
+ let tour = &solution.tours[0];
+ let intervals = collect_activity_intervals(tour);
+ let breaks: Vec<_> = intervals.iter().filter(|(_, _, t, _)| t == "break").collect();
+ assert_eq!(breaks.len(), 2, "expected 2 breaks\ntour: {}", format_tour_debug(tour));
+
+ // Validate each break individually for duration
+ for (b_start, b_end, _, _) in &breaks {
+ let dur = b_end - b_start;
+ assert!((1.5..=3.5).contains(&dur), "unexpected break duration {dur}\ntour: {}", format_tour_debug(tour));
+ }
+
+ // Full validation (uses the longer break's duration for the uniform check — skip that, check manually)
+ // Instead validate schedule and overlap manually
+ validate_tour_schedule_only(tour);
+ validate_no_break_job_overlap(tour);
+}
+
+/// Validates stop schedule consistency only (no break count/duration check).
+fn validate_tour_schedule_only(tour: &Tour) {
+ let mut prev_departure: Option = None;
+ for (i, stop) in tour.stops.iter().enumerate() {
+ let arr = parse_time(&stop.schedule().arrival);
+ let dep = parse_time(&stop.schedule().departure);
+ assert!(dep >= arr - 0.001, "stop {i}: dep ({dep}) < arr ({arr})\ntour: {}", format_tour_debug(tour));
+ if let Some(prev_dep) = prev_departure {
+ assert!(
+ arr >= prev_dep - 0.001,
+ "stop {i}: arr ({arr}) < prev dep ({prev_dep})\ntour: {}",
+ format_tour_debug(tour)
+ );
+ }
+ prev_departure = Some(dep);
+
+ // Activities within stop should be time-ordered and within bounds
+ for act in stop.activities() {
+ if let Some(time) = &act.time {
+ let a_start = parse_time(&time.start);
+ let a_end = parse_time(&time.end);
+ assert!(
+ a_end >= a_start - 0.001,
+ "stop {i}: activity '{}' end < start\ntour: {}",
+ act.job_id,
+ format_tour_debug(tour)
+ );
+ assert!(
+ a_start >= arr - 0.001,
+ "stop {i}: activity '{}' start ({a_start}) < stop arr ({arr})\ntour: {}",
+ act.job_id,
+ format_tour_debug(tour)
+ );
+ assert!(
+ a_end <= dep + 0.001,
+ "stop {i}: activity '{}' end ({a_end}) > stop dep ({dep})\ntour: {}",
+ act.job_id,
+ format_tour_debug(tour)
+ );
+ }
+ }
+ }
+}
+
+/// Validates no cross-stop overlap between break activities and job activities.
+fn validate_no_break_job_overlap(tour: &Tour) {
+ let intervals = collect_activity_intervals(tour);
+ let breaks: Vec<_> = intervals.iter().filter(|(_, _, t, _)| t == "break").collect();
+ let jobs: Vec<_> =
+ intervals.iter().filter(|(_, _, t, _)| t != "break" && t != "departure" && t != "arrival").collect();
+
+ for (b_start, b_end, _, _) in &breaks {
+ for (a_start, a_end, a_type, a_id) in &jobs {
+ let same_stop = tour.stops.iter().any(|stop| {
+ let acts = stop.activities();
+ acts.iter().any(|a| a.activity_type == "break") && acts.iter().any(|a| a.job_id == **a_id)
+ });
+ if !same_stop {
+ let overlaps = b_start < a_end && a_start < b_end;
+ assert!(
+ !overlaps,
+ "break [{b_start}..{b_end}] overlaps {a_type} '{a_id}' [{a_start}..{a_end}]\ntour: {}",
+ format_tour_debug(tour)
+ );
+ }
+ }
+ }
+}
diff --git a/vrp-pragmatic/tests/features/fleet/balance_and_min_shifts.rs b/vrp-pragmatic/tests/features/fleet/balance_and_min_shifts.rs
new file mode 100644
index 000000000..a60e8c6de
--- /dev/null
+++ b/vrp-pragmatic/tests/features/fleet/balance_and_min_shifts.rs
@@ -0,0 +1,52 @@
+use crate::format::problem::*;
+use crate::helpers::*;
+
+fn build_problem(objectives: Option>, min_shifts: Option) -> (Problem, Matrix) {
+ let jobs = vec![
+ create_delivery_job("job1", (1., 0.)),
+ create_delivery_job("job2", (2., 0.)),
+ create_delivery_job("job3", (3., 0.)),
+ ];
+
+ let fleet = Fleet {
+ vehicles: vec![VehicleType {
+ type_id: "vehicle_type".to_string(),
+ vehicle_ids: vec!["vehicle_1".to_string(), "vehicle_2".to_string()],
+ profile: create_default_vehicle_profile(),
+ costs: VehicleCosts { fixed: Some(0.), distance: 1., time: 1., span: None },
+ shifts: vec![create_default_vehicle_shift()],
+ capacity: vec![10],
+ skills: None,
+ limits: None,
+ min_shifts,
+ }],
+ profiles: create_default_matrix_profiles(),
+ resources: None,
+ };
+
+ let mut problem = create_empty_problem();
+ problem.plan = Plan { jobs, relations: None, clustering: None };
+ problem.fleet = fleet;
+ problem.objectives = objectives;
+
+ let matrix = create_matrix_from_problem(&problem);
+
+ (problem, matrix)
+}
+
+#[test]
+fn min_vehicle_shifts_constraint_enforces_usage() {
+ let (problem_without_requirement, matrix) = build_problem(Some(vec![Objective::MinimizeCost]), None);
+ let (problem_with_requirement, _) = build_problem(
+ Some(vec![Objective::MinimizeCost]),
+ Some(VehicleMinShifts { value: 1, allow_zero_usage: false }),
+ );
+ let matrices = vec![matrix];
+
+ let solution_without_requirement =
+ solve_with_cheapest_insertion(problem_without_requirement, Some(matrices.clone()));
+ let solution_with_requirement = solve_with_cheapest_insertion(problem_with_requirement, Some(matrices));
+
+ assert_eq!(solution_without_requirement.tours.len(), 1);
+ assert_eq!(solution_with_requirement.tours.len(), 2);
+}
diff --git a/vrp-pragmatic/tests/features/fleet/mod.rs b/vrp-pragmatic/tests/features/fleet/mod.rs
index 48ff5510a..9636737a2 100644
--- a/vrp-pragmatic/tests/features/fleet/mod.rs
+++ b/vrp-pragmatic/tests/features/fleet/mod.rs
@@ -1,3 +1,4 @@
+mod balance_and_min_shifts;
mod basic_multi_shift;
mod basic_open_end;
mod multi_dimens;
diff --git a/vrp-pragmatic/tests/features/limits/job_times.rs b/vrp-pragmatic/tests/features/limits/job_times.rs
new file mode 100644
index 000000000..57c6d01a1
--- /dev/null
+++ b/vrp-pragmatic/tests/features/limits/job_times.rs
@@ -0,0 +1,623 @@
+use crate::format::problem::*;
+use crate::format::solution::*;
+use crate::format_time;
+use crate::helpers::*;
+
+fn create_vehicle_with_job_time_constraints(earliest_first: Option, latest_last: Option) -> VehicleType {
+ create_named_vehicle_with_job_time_constraints("my_vehicle", earliest_first, latest_last)
+}
+
+fn create_named_vehicle_with_job_time_constraints(
+ type_id: &str,
+ earliest_first: Option,
+ latest_last: Option,
+) -> VehicleType {
+ VehicleType {
+ type_id: type_id.to_string(),
+ vehicle_ids: vec![format!("{}_1", type_id)],
+ shifts: vec![VehicleShift {
+ start: ShiftStart { earliest: format_time(0.), latest: None, location: (0., 0.).to_loc() },
+ end: Some(ShiftEnd { earliest: None, latest: format_time(1000.), location: (0., 0.).to_loc() }),
+ breaks: None,
+ reloads: None,
+ recharges: None,
+ job_times: Some(JobTimeConstraints {
+ earliest_first: earliest_first.map(format_time),
+ latest_last: latest_last.map(format_time),
+ }),
+ }],
+ ..create_default_vehicle_type()
+ }
+}
+
+fn create_open_route_vehicle_with_job_time_constraints(
+ earliest_first: Option,
+ latest_last: Option,
+) -> VehicleType {
+ VehicleType {
+ shifts: vec![VehicleShift {
+ start: ShiftStart { earliest: format_time(0.), latest: None, location: (0., 0.).to_loc() },
+ end: None, // Open route - no return to depot
+ breaks: None,
+ reloads: None,
+ recharges: None,
+ job_times: Some(JobTimeConstraints {
+ earliest_first: earliest_first.map(format_time),
+ latest_last: latest_last.map(format_time),
+ }),
+ }],
+ ..create_default_vehicle_type()
+ }
+}
+
+#[test]
+fn can_reject_job_when_arrival_before_earliest_first() {
+ // Job is at location (5, 0), so arrival at 5 time units
+ // earliest_first is 10, so job cannot start before 10
+ // Job time window ends at 8, which is before earliest_first
+ // Therefore, the job should be unassigned
+ let problem = Problem {
+ plan: Plan {
+ jobs: vec![create_delivery_job_with_times("job1", (5., 0.), vec![(0, 8)], 1.)],
+ ..create_empty_plan()
+ },
+ fleet: Fleet {
+ vehicles: vec![create_vehicle_with_job_time_constraints(Some(10.), None)],
+ ..create_default_fleet()
+ },
+ ..create_empty_problem()
+ };
+ let matrix = create_matrix_from_problem(&problem);
+
+ let solution = solve_with_metaheuristic(problem, Some(vec![matrix]));
+
+ assert_eq!(
+ solution,
+ SolutionBuilder::default()
+ .unassigned(Some(vec![UnassignedJob {
+ job_id: "job1".to_string(),
+ reasons: vec![UnassignedJobReason {
+ code: "JOB_TIME_CONSTRAINT".to_string(),
+ description: "cannot be assigned due to shift job time constraints".to_string(),
+ details: None
+ }]
+ }]))
+ .build()
+ );
+}
+
+#[test]
+fn can_assign_job_when_arrival_after_earliest_first() {
+ // Job is at location (15, 0), so arrival at 15 time units
+ // earliest_first is 10, so job can start at 15 (which is after 10)
+ // Job should be assigned
+ let problem = Problem {
+ plan: Plan {
+ jobs: vec![create_delivery_job_with_times("job1", (15., 0.), vec![(0, 100)], 1.)],
+ ..create_empty_plan()
+ },
+ fleet: Fleet {
+ vehicles: vec![create_vehicle_with_job_time_constraints(Some(10.), None)],
+ ..create_default_fleet()
+ },
+ ..create_empty_problem()
+ };
+ let matrix = create_matrix_from_problem(&problem);
+
+ let solution = solve_with_metaheuristic(problem, Some(vec![matrix]));
+
+ assert!(solution.unassigned.is_none(), "Job should be assigned");
+ assert_eq!(solution.tours.len(), 1);
+ assert!(
+ solution.tours[0].stops.iter().any(|stop| { stop.activities().iter().any(|a| a.job_id == "job1") }),
+ "Tour should contain job1"
+ );
+}
+
+#[test]
+fn can_assign_job_when_time_window_allows_waiting() {
+ // Job is at location (5, 0), so arrival at 5 time units
+ // earliest_first is 10, but job time window extends to 100
+ // The job should be assigned because the time window allows waiting until earliest_first
+ // Note: The constraint checks feasibility; actual schedule timing may vary
+ let problem = Problem {
+ plan: Plan {
+ jobs: vec![create_delivery_job_with_times("job1", (5., 0.), vec![(0, 100)], 1.)],
+ ..create_empty_plan()
+ },
+ fleet: Fleet {
+ vehicles: vec![create_vehicle_with_job_time_constraints(Some(10.), None)],
+ ..create_default_fleet()
+ },
+ ..create_empty_problem()
+ };
+ let matrix = create_matrix_from_problem(&problem);
+
+ let solution = solve_with_metaheuristic(problem, Some(vec![matrix]));
+
+ // The key assertion: job should be assigned (not rejected) because the constraint
+ // recognizes that the job's time window (ends at 100) extends past earliest_first (10)
+ assert!(solution.unassigned.is_none(), "Job should be assigned because TW allows waiting");
+ assert_eq!(solution.tours.len(), 1);
+ assert!(
+ solution.tours[0].stops.iter().any(|stop| { stop.activities().iter().any(|a| a.job_id == "job1") }),
+ "Tour should contain job1"
+ );
+}
+
+#[test]
+fn can_reject_job_when_departure_after_latest_last() {
+ // Job is at location (50, 0), so arrival at 50 time units
+ // With service duration of 1, departure is at 51
+ // latest_last is 30, so departure 51 > 30
+ // Job should be unassigned
+ let problem = Problem {
+ plan: Plan {
+ jobs: vec![create_delivery_job_with_times("job1", (50., 0.), vec![(0, 100)], 1.)],
+ ..create_empty_plan()
+ },
+ fleet: Fleet {
+ vehicles: vec![create_vehicle_with_job_time_constraints(None, Some(30.))],
+ ..create_default_fleet()
+ },
+ ..create_empty_problem()
+ };
+ let matrix = create_matrix_from_problem(&problem);
+
+ let solution = solve_with_metaheuristic(problem, Some(vec![matrix]));
+
+ assert_eq!(
+ solution,
+ SolutionBuilder::default()
+ .unassigned(Some(vec![UnassignedJob {
+ job_id: "job1".to_string(),
+ reasons: vec![UnassignedJobReason {
+ code: "JOB_TIME_CONSTRAINT".to_string(),
+ description: "cannot be assigned due to shift job time constraints".to_string(),
+ details: None
+ }]
+ }]))
+ .build()
+ );
+}
+
+#[test]
+fn can_assign_job_when_departure_before_latest_last() {
+ // Job is at location (10, 0), so arrival at 10 time units
+ // With service duration of 1, departure is at 11
+ // latest_last is 30, so departure 11 < 30
+ // Job should be assigned
+ let problem = Problem {
+ plan: Plan {
+ jobs: vec![create_delivery_job_with_times("job1", (10., 0.), vec![(0, 100)], 1.)],
+ ..create_empty_plan()
+ },
+ fleet: Fleet {
+ vehicles: vec![create_vehicle_with_job_time_constraints(None, Some(30.))],
+ ..create_default_fleet()
+ },
+ ..create_empty_problem()
+ };
+ let matrix = create_matrix_from_problem(&problem);
+
+ let solution = solve_with_metaheuristic(problem, Some(vec![matrix]));
+
+ assert!(solution.unassigned.is_none(), "Job should be assigned");
+ assert_eq!(solution.tours.len(), 1);
+ assert!(
+ solution.tours[0].stops.iter().any(|stop| { stop.activities().iter().any(|a| a.job_id == "job1") }),
+ "Tour should contain job1"
+ );
+}
+
+#[test]
+fn can_apply_both_constraints_and_assign_job() {
+ // Job is at location (15, 0), so arrival at 15 time units
+ // earliest_first is 10 (arrival 15 >= 10, OK)
+ // With service duration of 1, departure is at 16
+ // latest_last is 30 (departure 16 <= 30, OK)
+ // Job should be assigned
+ let problem = Problem {
+ plan: Plan {
+ jobs: vec![create_delivery_job_with_times("job1", (15., 0.), vec![(0, 100)], 1.)],
+ ..create_empty_plan()
+ },
+ fleet: Fleet {
+ vehicles: vec![create_vehicle_with_job_time_constraints(Some(10.), Some(30.))],
+ ..create_default_fleet()
+ },
+ ..create_empty_problem()
+ };
+ let matrix = create_matrix_from_problem(&problem);
+
+ let solution = solve_with_metaheuristic(problem, Some(vec![matrix]));
+
+ assert!(solution.unassigned.is_none(), "Job should be assigned");
+ assert_eq!(solution.tours.len(), 1);
+}
+
+#[test]
+fn can_apply_both_constraints_and_reject_job() {
+ // Job is at location (5, 0), so arrival at 5 time units
+ // earliest_first is 10, job TW ends at 8
+ // Cannot wait until earliest_first because TW ends before that
+ // Job should be unassigned
+ let problem = Problem {
+ plan: Plan {
+ jobs: vec![create_delivery_job_with_times("job1", (5., 0.), vec![(0, 8)], 1.)],
+ ..create_empty_plan()
+ },
+ fleet: Fleet {
+ vehicles: vec![create_vehicle_with_job_time_constraints(Some(10.), Some(100.))],
+ ..create_default_fleet()
+ },
+ ..create_empty_problem()
+ };
+ let matrix = create_matrix_from_problem(&problem);
+
+ let solution = solve_with_metaheuristic(problem, Some(vec![matrix]));
+
+ assert!(solution.unassigned.is_some(), "Job should be unassigned");
+ assert_eq!(solution.unassigned.as_ref().unwrap()[0].job_id, "job1");
+}
+
+#[test]
+fn can_handle_multiple_jobs_with_constraints() {
+ // Three jobs:
+ // - job1 at (5, 0): arrival 5, can wait until 10 (earliest_first), departure ~11
+ // - job2 at (20, 0): arrival ~31 (from job1), departure ~32
+ // - job3 at (100, 0): arrival too late for latest_last (50)
+ //
+ // With earliest_first=10 and latest_last=50:
+ // - job1 and job2 should be assigned
+ // - job3 should be unassigned (departure would be > 50)
+ let problem = Problem {
+ plan: Plan {
+ jobs: vec![
+ create_delivery_job_with_times("job1", (5., 0.), vec![(0, 100)], 1.),
+ create_delivery_job_with_times("job2", (20., 0.), vec![(0, 100)], 1.),
+ create_delivery_job_with_times("job3", (100., 0.), vec![(0, 200)], 1.),
+ ],
+ ..create_empty_plan()
+ },
+ fleet: Fleet {
+ vehicles: vec![create_vehicle_with_job_time_constraints(Some(10.), Some(50.))],
+ ..create_default_fleet()
+ },
+ ..create_empty_problem()
+ };
+ let matrix = create_matrix_from_problem(&problem);
+
+ let solution = solve_with_metaheuristic(problem, Some(vec![matrix]));
+
+ // job3 should be unassigned
+ assert!(solution.unassigned.is_some(), "Should have unassigned jobs");
+ let unassigned_ids: Vec<_> = solution.unassigned.as_ref().unwrap().iter().map(|u| u.job_id.as_str()).collect();
+ assert!(unassigned_ids.contains(&"job3"), "job3 should be unassigned");
+
+ // job1 and job2 should be assigned
+ assert_eq!(solution.tours.len(), 1);
+ let assigned_jobs: Vec<_> =
+ solution.tours[0].stops.iter().flat_map(|stop| stop.activities().iter()).map(|a| a.job_id.as_str()).collect();
+ assert!(assigned_jobs.contains(&"job1"), "job1 should be assigned");
+ assert!(assigned_jobs.contains(&"job2"), "job2 should be assigned");
+}
+
+#[test]
+fn can_reject_job_when_duration_causes_departure_after_latest_last() {
+ // Job is at location (10, 0), so arrival at 10 time units
+ // latest_last is 15, so arrival (10) is BEFORE latest_last - seems OK
+ // BUT service duration is 10, meaning:
+ // - Service starts at 10, finishes at 20
+ // - Departure (20) > latest_last (15)
+ // Job should be unassigned because departure exceeds latest_last
+ let problem = Problem {
+ plan: Plan {
+ jobs: vec![create_delivery_job_with_times("job1", (10., 0.), vec![(0, 100)], 10.)],
+ ..create_empty_plan()
+ },
+ fleet: Fleet {
+ vehicles: vec![create_vehicle_with_job_time_constraints(None, Some(15.))],
+ ..create_default_fleet()
+ },
+ ..create_empty_problem()
+ };
+ let matrix = create_matrix_from_problem(&problem);
+
+ let solution = solve_with_metaheuristic(problem, Some(vec![matrix]));
+
+ // Job should be unassigned - even though arrival < latest_last,
+ // the service duration causes departure to exceed latest_last
+ assert!(solution.unassigned.is_some(), "Job should be unassigned");
+ assert_eq!(solution.unassigned.as_ref().unwrap()[0].job_id, "job1");
+}
+
+#[test]
+fn can_work_with_depot_to_depot_span() {
+ // Verify that job_times works independently of cost span
+ // Even with depot-to-depot costing, the job time constraints should apply
+ let problem = Problem {
+ plan: Plan {
+ jobs: vec![create_delivery_job_with_times("job1", (5., 0.), vec![(0, 8)], 1.)],
+ ..create_empty_plan()
+ },
+ fleet: Fleet {
+ vehicles: vec![VehicleType {
+ shifts: vec![VehicleShift {
+ start: ShiftStart { earliest: format_time(0.), latest: None, location: (0., 0.).to_loc() },
+ end: Some(ShiftEnd { earliest: None, latest: format_time(1000.), location: (0., 0.).to_loc() }),
+ breaks: None,
+ reloads: None,
+ recharges: None,
+ job_times: Some(JobTimeConstraints { earliest_first: Some(format_time(10.)), latest_last: None }),
+ }],
+ costs: VehicleCosts {
+ fixed: Some(10.),
+ distance: 1.,
+ time: 1.,
+ span: Some(RouteCostSpan::DepotToDepot), // Explicit depot-to-depot
+ },
+ ..create_default_vehicle_type()
+ }],
+ ..create_default_fleet()
+ },
+ ..create_empty_problem()
+ };
+ let matrix = create_matrix_from_problem(&problem);
+
+ let solution = solve_with_metaheuristic(problem, Some(vec![matrix]));
+
+ // Job should still be rejected due to job_times constraint
+ assert!(solution.unassigned.is_some(), "Job should be unassigned");
+ assert_eq!(solution.unassigned.as_ref().unwrap()[0].reasons[0].code, "JOB_TIME_CONSTRAINT");
+}
+
+#[test]
+fn can_apply_different_constraints_to_different_vehicles() {
+ // Two vehicles with different job_times constraints:
+ // - vehicle_strict: earliest_first=20, latest_last=25 (very strict)
+ // - vehicle_relaxed: earliest_first=5, latest_last=100 (relaxed)
+ //
+ // Job at (10, 0): arrival at 10, departure at 11
+ // - vehicle_strict: arrival 10 < earliest_first 20 AND can't wait (need to check TW)
+ // - vehicle_relaxed: arrival 10 > earliest_first 5, departure 11 < latest_last 100
+ //
+ // Job should be assigned to vehicle_relaxed
+ let problem = Problem {
+ plan: Plan {
+ jobs: vec![create_delivery_job_with_times("job1", (10., 0.), vec![(0, 15)], 1.)],
+ ..create_empty_plan()
+ },
+ fleet: Fleet {
+ vehicles: vec![
+ create_named_vehicle_with_job_time_constraints("vehicle_strict", Some(20.), Some(25.)),
+ create_named_vehicle_with_job_time_constraints("vehicle_relaxed", Some(5.), Some(100.)),
+ ],
+ ..create_default_fleet()
+ },
+ ..create_empty_problem()
+ };
+ let matrix = create_matrix_from_problem(&problem);
+
+ let solution = solve_with_metaheuristic(problem, Some(vec![matrix]));
+
+ assert!(solution.unassigned.is_none(), "Job should be assigned");
+ assert_eq!(solution.tours.len(), 1);
+ // Job should be assigned to vehicle_relaxed
+ assert!(
+ solution.tours[0].vehicle_id.contains("vehicle_relaxed"),
+ "Job should be assigned to vehicle_relaxed, but was assigned to {}",
+ solution.tours[0].vehicle_id
+ );
+}
+
+#[test]
+fn can_reject_job_when_waiting_causes_latest_last_violation() {
+ // This tests the interaction between earliest_first and latest_last
+ // Job at (5, 0): arrival at 5
+ // earliest_first=10: must wait until 10 to start
+ // Service duration=5: depart at 15
+ // latest_last=12: departure 15 > 12
+ //
+ // The waiting required by earliest_first causes a latest_last violation
+ let problem = Problem {
+ plan: Plan {
+ jobs: vec![create_delivery_job_with_times("job1", (5., 0.), vec![(0, 100)], 5.)],
+ ..create_empty_plan()
+ },
+ fleet: Fleet {
+ vehicles: vec![create_vehicle_with_job_time_constraints(Some(10.), Some(12.))],
+ ..create_default_fleet()
+ },
+ ..create_empty_problem()
+ };
+ let matrix = create_matrix_from_problem(&problem);
+
+ let solution = solve_with_metaheuristic(problem, Some(vec![matrix]));
+
+ // Job should be unassigned because waiting until earliest_first (10)
+ // plus service duration (5) = departure at 15 > latest_last (12)
+ assert!(solution.unassigned.is_some(), "Job should be unassigned");
+ assert_eq!(solution.unassigned.as_ref().unwrap()[0].job_id, "job1");
+}
+
+#[test]
+fn earliest_first_only_applies_to_first_job() {
+ // Two jobs:
+ // - job1 at (15, 0): arrival 15, satisfies earliest_first=10
+ // - job2 at (5, 0): would arrive at 5 if it were first, but it's second
+ //
+ // After job1 (at 15, depart ~16), travel to job2 (at 5,0)
+ // Distance from (15,0) to (5,0) is 10, so arrive at job2 around 26
+ // earliest_first should NOT apply to job2 since it's the second job
+ let problem = Problem {
+ plan: Plan {
+ jobs: vec![
+ create_delivery_job_with_times("job1", (15., 0.), vec![(0, 100)], 1.),
+ create_delivery_job_with_times("job2", (5., 0.), vec![(0, 100)], 1.),
+ ],
+ ..create_empty_plan()
+ },
+ fleet: Fleet {
+ vehicles: vec![create_vehicle_with_job_time_constraints(Some(10.), None)],
+ ..create_default_fleet()
+ },
+ ..create_empty_problem()
+ };
+ let matrix = create_matrix_from_problem(&problem);
+
+ let solution = solve_with_metaheuristic(problem, Some(vec![matrix]));
+
+ // Both jobs should be assigned
+ assert!(solution.unassigned.is_none(), "Both jobs should be assigned");
+ assert_eq!(solution.tours.len(), 1);
+ let assigned_jobs: Vec<_> =
+ solution.tours[0].stops.iter().flat_map(|stop| stop.activities().iter()).map(|a| a.job_id.as_str()).collect();
+ assert!(assigned_jobs.contains(&"job1"), "job1 should be assigned");
+ assert!(assigned_jobs.contains(&"job2"), "job2 should be assigned");
+}
+
+#[test]
+fn latest_last_only_applies_to_last_job() {
+ // Two jobs where only one can be last:
+ // - job1 at (10, 0): close to depot
+ // - job2 at (50, 0): far from depot, departure would be 51
+ //
+ // latest_last=30 means departure from last job must be <= 30
+ // If job2 is last: departure 51 > 30, violates latest_last
+ // If job1 is last: departure ~11 < 30, OK
+ //
+ // The solver should find a route where job1 is last (job2 -> job1)
+ // Or reject job2 if it can't find a valid ordering
+ let problem = Problem {
+ plan: Plan {
+ jobs: vec![
+ create_delivery_job_with_times("job1", (10., 0.), vec![(0, 100)], 1.),
+ create_delivery_job_with_times("job2", (25., 0.), vec![(0, 100)], 1.),
+ ],
+ ..create_empty_plan()
+ },
+ fleet: Fleet {
+ // latest_last=30 means we need to finish the last job by 30
+ vehicles: vec![create_vehicle_with_job_time_constraints(None, Some(30.))],
+ ..create_default_fleet()
+ },
+ ..create_empty_problem()
+ };
+ let matrix = create_matrix_from_problem(&problem);
+
+ let solution = solve_with_metaheuristic(problem, Some(vec![matrix]));
+
+ // Both jobs should be assigned with proper ordering
+ assert!(solution.unassigned.is_none(), "Both jobs should be assigned");
+ assert_eq!(solution.tours.len(), 1);
+
+ // Get the order of jobs in the tour
+ let job_order: Vec<_> = solution.tours[0]
+ .stops
+ .iter()
+ .flat_map(|stop| stop.activities().iter())
+ .filter(|a| a.activity_type == "delivery")
+ .map(|a| a.job_id.as_str())
+ .collect();
+
+ assert_eq!(job_order.len(), 2, "Should have 2 jobs in tour");
+}
+
+#[test]
+fn can_handle_tight_time_window() {
+ // Very tight window: earliest_first=10, latest_last=12
+ // Job at (10, 0): arrival 10, service 1, departure 11
+ // 10 >= earliest_first (10) and 11 <= latest_last (12)
+ // Just barely fits
+ let problem = Problem {
+ plan: Plan {
+ jobs: vec![create_delivery_job_with_times("job1", (10., 0.), vec![(0, 100)], 1.)],
+ ..create_empty_plan()
+ },
+ fleet: Fleet {
+ vehicles: vec![create_vehicle_with_job_time_constraints(Some(10.), Some(12.))],
+ ..create_default_fleet()
+ },
+ ..create_empty_problem()
+ };
+ let matrix = create_matrix_from_problem(&problem);
+
+ let solution = solve_with_metaheuristic(problem, Some(vec![matrix]));
+
+ assert!(solution.unassigned.is_none(), "Job should fit in tight window");
+ assert_eq!(solution.tours.len(), 1);
+}
+
+#[test]
+fn can_reject_job_outside_tight_time_window() {
+ // Very tight window: earliest_first=10, latest_last=11
+ // Job at (10, 0): arrival 10, service 2, departure 12
+ // 10 >= earliest_first (10) but 12 > latest_last (11)
+ // Doesn't fit
+ let problem = Problem {
+ plan: Plan {
+ jobs: vec![create_delivery_job_with_times("job1", (10., 0.), vec![(0, 100)], 2.)],
+ ..create_empty_plan()
+ },
+ fleet: Fleet {
+ vehicles: vec![create_vehicle_with_job_time_constraints(Some(10.), Some(11.))],
+ ..create_default_fleet()
+ },
+ ..create_empty_problem()
+ };
+ let matrix = create_matrix_from_problem(&problem);
+
+ let solution = solve_with_metaheuristic(problem, Some(vec![matrix]));
+
+ assert!(solution.unassigned.is_some(), "Job should not fit in tight window");
+ assert_eq!(solution.unassigned.as_ref().unwrap()[0].job_id, "job1");
+}
+
+#[test]
+fn can_apply_latest_last_on_open_route() {
+ // Open route (no return to depot) with latest_last constraint
+ // Job at (10, 0): arrival 10, service 1, departure 11
+ // latest_last=15: departure 11 < 15, OK
+ let problem = Problem {
+ plan: Plan {
+ jobs: vec![create_delivery_job_with_times("job1", (10., 0.), vec![(0, 100)], 1.)],
+ ..create_empty_plan()
+ },
+ fleet: Fleet {
+ vehicles: vec![create_open_route_vehicle_with_job_time_constraints(None, Some(15.))],
+ ..create_default_fleet()
+ },
+ ..create_empty_problem()
+ };
+ let matrix = create_matrix_from_problem(&problem);
+
+ let solution = solve_with_metaheuristic(problem, Some(vec![matrix]));
+
+ assert!(solution.unassigned.is_none(), "Job should be assigned on open route");
+ assert_eq!(solution.tours.len(), 1);
+}
+
+#[test]
+fn can_reject_job_on_open_route_when_latest_last_violated() {
+ // Open route with latest_last constraint
+ // Job at (20, 0): arrival 20, service 1, departure 21
+ // latest_last=15: departure 21 > 15, rejected
+ let problem = Problem {
+ plan: Plan {
+ jobs: vec![create_delivery_job_with_times("job1", (20., 0.), vec![(0, 100)], 1.)],
+ ..create_empty_plan()
+ },
+ fleet: Fleet {
+ vehicles: vec![create_open_route_vehicle_with_job_time_constraints(None, Some(15.))],
+ ..create_default_fleet()
+ },
+ ..create_empty_problem()
+ };
+ let matrix = create_matrix_from_problem(&problem);
+
+ let solution = solve_with_metaheuristic(problem, Some(vec![matrix]));
+
+ assert!(solution.unassigned.is_some(), "Job should be rejected on open route");
+ assert_eq!(solution.unassigned.as_ref().unwrap()[0].job_id, "job1");
+}
diff --git a/vrp-pragmatic/tests/features/limits/max_distance.rs b/vrp-pragmatic/tests/features/limits/max_distance.rs
index 261a8cd91..38896cce4 100644
--- a/vrp-pragmatic/tests/features/limits/max_distance.rs
+++ b/vrp-pragmatic/tests/features/limits/max_distance.rs
@@ -9,7 +9,12 @@ fn can_limit_by_max_distance() {
plan: Plan { jobs: vec![create_delivery_job("job1", (100., 0.))], ..create_empty_plan() },
fleet: Fleet {
vehicles: vec![VehicleType {
- limits: Some(VehicleLimits { max_distance: Some(99.), max_duration: None, tour_size: None }),
+ limits: Some(VehicleLimits {
+ max_distance: Some(99.),
+ max_duration: None,
+ tour_size: None,
+ min_tour_size: None,
+ }),
..create_default_vehicle_type()
}],
..create_default_fleet()
@@ -52,7 +57,12 @@ fn can_handle_empty_route() {
end: Some(ShiftEnd { earliest: None, latest: format_time(100.), location: (10., 0.).to_loc() }),
..create_default_open_vehicle_shift()
}],
- limits: Some(VehicleLimits { max_distance: Some(9.), max_duration: None, tour_size: None }),
+ limits: Some(VehicleLimits {
+ max_distance: Some(9.),
+ max_duration: None,
+ tour_size: None,
+ min_tour_size: None,
+ }),
..create_default_vehicle_type()
}],
..create_default_fleet()
diff --git a/vrp-pragmatic/tests/features/limits/max_duration.rs b/vrp-pragmatic/tests/features/limits/max_duration.rs
index 9ae73fe39..581544e2e 100644
--- a/vrp-pragmatic/tests/features/limits/max_duration.rs
+++ b/vrp-pragmatic/tests/features/limits/max_duration.rs
@@ -5,7 +5,12 @@ use vrp_core::prelude::Float;
fn create_vehicle_type_with_max_duration_limit(max_duration: Float) -> VehicleType {
VehicleType {
- limits: Some(VehicleLimits { max_distance: None, max_duration: Some(max_duration), tour_size: None }),
+ limits: Some(VehicleLimits {
+ max_distance: None,
+ max_duration: Some(max_duration),
+ tour_size: None,
+ min_tour_size: None,
+ }),
..create_default_vehicle_type()
}
}
diff --git a/vrp-pragmatic/tests/features/limits/min_tour_size.rs b/vrp-pragmatic/tests/features/limits/min_tour_size.rs
new file mode 100644
index 000000000..d8ebe37b2
--- /dev/null
+++ b/vrp-pragmatic/tests/features/limits/min_tour_size.rs
@@ -0,0 +1,303 @@
+use crate::format::problem::*;
+use crate::format::solution::Solution;
+use crate::helpers::*;
+
+fn create_objectives_with_min_tour_size() -> Option> {
+ Some(vec![
+ Objective::MinimizeUnassigned { breaks: None },
+ Objective::MinimizeTourSizeViolation,
+ Objective::MinimizeTours,
+ Objective::MinimizeCost,
+ ])
+}
+
+fn create_maximize_tours_objectives() -> Option> {
+ // Maximize tours to force creating more routes (opposite of minimize)
+ Some(vec![Objective::MinimizeUnassigned { breaks: None }, Objective::MaximizeTours, Objective::MinimizeCost])
+}
+
+fn create_maximize_tours_with_min_tour_size_objectives() -> Option> {
+ // Maximize tours BUT also minimize tour size violations
+ // The MinimizeTourSizeViolation should counteract MaximizeTours when routes become too small
+ Some(vec![
+ Objective::MinimizeUnassigned { breaks: None },
+ Objective::MinimizeTourSizeViolation,
+ Objective::MaximizeTours,
+ Objective::MinimizeCost,
+ ])
+}
+
+fn count_job_activities(solution: &Solution) -> Vec {
+ solution
+ .tours
+ .iter()
+ .map(|tour| {
+ tour.stops
+ .iter()
+ .flat_map(|stop| stop.activities())
+ .filter(|a| a.activity_type != "departure" && a.activity_type != "arrival")
+ .count()
+ })
+ .collect()
+}
+
+fn has_underfilled_routes(activity_counts: &[usize], min_size: usize) -> bool {
+ activity_counts.iter().any(|&count| count > 0 && count < min_size)
+}
+
+/// This test verifies that the MinimizeTourSizeViolation objective actually changes behavior.
+/// We use MaximizeTours to force creating multiple routes, then show that MinimizeTourSizeViolation
+/// prevents routes from having fewer than min_tour_size activities.
+#[test]
+fn can_verify_objective_changes_behavior() {
+ // Problem: 4 jobs, 4 vehicles with high capacity, min_tour_size=2
+ // Default behavior (no objectives defined): minimizes unassigned, tours, then cost
+ // With MaximizeTours + MinimizeTourSizeViolation: tries to create more tours but penalizes underfilled ones
+ let create_problem = |objectives: Option>, with_min_tour_size: bool| Problem {
+ plan: Plan {
+ jobs: vec![
+ create_delivery_job_with_demand("job1", (1., 0.), vec![1]),
+ create_delivery_job_with_demand("job2", (2., 0.), vec![1]),
+ create_delivery_job_with_demand("job3", (3., 0.), vec![1]),
+ create_delivery_job_with_demand("job4", (4., 0.), vec![1]),
+ ],
+ ..create_empty_plan()
+ },
+ fleet: Fleet {
+ vehicles: vec![VehicleType {
+ vehicle_ids: vec!["v1".to_string(), "v2".to_string(), "v3".to_string(), "v4".to_string()],
+ shifts: vec![create_default_open_vehicle_shift()],
+ capacity: vec![10], // High capacity - not a limiting factor
+ limits: if with_min_tour_size {
+ Some(VehicleLimits {
+ max_distance: None,
+ max_duration: None,
+ tour_size: None,
+ min_tour_size: Some(2),
+ })
+ } else {
+ None
+ },
+ ..create_default_vehicle_type()
+ }],
+ ..create_default_fleet()
+ },
+ objectives,
+ ..create_empty_problem()
+ };
+
+ let matrix_without = create_matrix_from_problem(&create_problem(None, false));
+ let matrix_with = create_matrix_from_problem(&create_problem(None, true));
+
+ // Solve with MaximizeTours but WITHOUT min_tour_size limit on vehicles
+ // This should create as many routes as possible (4 routes with 1 job each)
+ let solution_without = solve_with_metaheuristic_and_iterations(
+ create_problem(create_maximize_tours_objectives(), false),
+ Some(vec![matrix_without]),
+ 200,
+ );
+
+ // Solve with MaximizeTours AND MinimizeTourSizeViolation (with min_tour_size on vehicles)
+ // This should balance more tours vs the penalty for underfilled routes
+ let solution_with = solve_with_metaheuristic_and_iterations(
+ create_problem(create_maximize_tours_with_min_tour_size_objectives(), true),
+ Some(vec![matrix_with]),
+ 200,
+ );
+
+ let counts_without = count_job_activities(&solution_without);
+ let counts_with = count_job_activities(&solution_with);
+
+ let underfilled_without = has_underfilled_routes(&counts_without, 2);
+ let underfilled_with = has_underfilled_routes(&counts_with, 2);
+
+ println!(
+ "MaximizeTours only: {} tours with activities {:?}, underfilled: {}",
+ solution_without.tours.len(),
+ counts_without,
+ underfilled_without
+ );
+ println!(
+ "MaximizeTours + MinTourSizeViolation: {} tours with activities {:?}, underfilled: {}",
+ solution_with.tours.len(),
+ counts_with,
+ underfilled_with
+ );
+
+ // The key assertion: Without the objective, MaximizeTours creates underfilled routes
+ assert!(
+ underfilled_without,
+ "Without objective, MaximizeTours should create underfilled routes. Got: {:?}",
+ counts_without
+ );
+
+ // With the objective, routes should NOT be underfilled
+ assert!(!underfilled_with, "With objective, should have no underfilled routes. Got: {:?}", counts_with);
+
+ // All jobs should be assigned in the solution with objective
+ assert!(
+ solution_with.unassigned.is_none() || solution_with.unassigned.as_ref().unwrap().is_empty(),
+ "Solution with objective has unassigned jobs"
+ );
+}
+
+#[test]
+fn can_enforce_min_tour_size_by_consolidating_jobs() {
+ // Problem: 4 jobs, 2 vehicles, min_tour_size=2
+ // Expected: Solver should create routes with at least 2 jobs each,
+ // rather than spreading jobs across more routes with fewer jobs each.
+ let problem = Problem {
+ plan: Plan {
+ jobs: vec![
+ create_delivery_job("job1", (1., 0.)),
+ create_delivery_job("job2", (2., 0.)),
+ create_delivery_job("job3", (3., 0.)),
+ create_delivery_job("job4", (4., 0.)),
+ ],
+ ..create_empty_plan()
+ },
+ fleet: Fleet {
+ vehicles: vec![VehicleType {
+ vehicle_ids: vec!["v1".to_string(), "v2".to_string()],
+ shifts: vec![create_default_open_vehicle_shift()],
+ limits: Some(VehicleLimits {
+ max_distance: None,
+ max_duration: None,
+ tour_size: None,
+ min_tour_size: Some(2),
+ }),
+ ..create_default_vehicle_type()
+ }],
+ ..create_default_fleet()
+ },
+ objectives: create_objectives_with_min_tour_size(),
+ ..create_empty_problem()
+ };
+ let matrix = create_matrix_from_problem(&problem);
+
+ let solution = solve_with_metaheuristic_and_iterations(problem, Some(vec![matrix]), 100);
+
+ // Verify that all routes have at least 2 activities (excluding departure/arrival)
+ for tour in &solution.tours {
+ let job_activities: usize = tour
+ .stops
+ .iter()
+ .flat_map(|stop| stop.activities())
+ .filter(|a| a.activity_type != "departure" && a.activity_type != "arrival")
+ .count();
+
+ assert!(
+ job_activities >= 2 || job_activities == 0,
+ "Tour for vehicle {} has {} job activities, expected at least 2 (or 0 if empty)",
+ tour.vehicle_id,
+ job_activities
+ );
+ }
+
+ // Verify all jobs are assigned
+ assert!(
+ solution.unassigned.is_none() || solution.unassigned.as_ref().unwrap().is_empty(),
+ "Expected all jobs to be assigned, but found unassigned: {:?}",
+ solution.unassigned
+ );
+}
+
+#[test]
+fn can_reject_solution_violating_min_tour_size() {
+ // Problem: 3 jobs, 2 vehicles, min_tour_size=2
+ // This creates a scenario where it's impossible to satisfy the constraint
+ // (can't have 2 routes with 2+ jobs each when only 3 jobs exist)
+ // The solver should put all jobs in one route or leave some unassigned
+ let problem = Problem {
+ plan: Plan {
+ jobs: vec![
+ create_delivery_job("job1", (1., 0.)),
+ create_delivery_job("job2", (2., 0.)),
+ create_delivery_job("job3", (3., 0.)),
+ ],
+ ..create_empty_plan()
+ },
+ fleet: Fleet {
+ vehicles: vec![VehicleType {
+ vehicle_ids: vec!["v1".to_string(), "v2".to_string()],
+ shifts: vec![create_default_open_vehicle_shift()],
+ limits: Some(VehicleLimits {
+ max_distance: None,
+ max_duration: None,
+ tour_size: None,
+ min_tour_size: Some(2),
+ }),
+ ..create_default_vehicle_type()
+ }],
+ ..create_default_fleet()
+ },
+ objectives: create_objectives_with_min_tour_size(),
+ ..create_empty_problem()
+ };
+ let matrix = create_matrix_from_problem(&problem);
+
+ let solution = solve_with_metaheuristic_and_iterations(problem, Some(vec![matrix]), 100);
+
+ // All routes must have either 0 or >= 2 activities
+ for tour in &solution.tours {
+ let job_activities: usize = tour
+ .stops
+ .iter()
+ .flat_map(|stop| stop.activities())
+ .filter(|a| a.activity_type != "departure" && a.activity_type != "arrival")
+ .count();
+
+ assert!(
+ job_activities >= 2 || job_activities == 0,
+ "Tour for vehicle {} has {} job activities, expected at least 2 (or 0 if empty)",
+ tour.vehicle_id,
+ job_activities
+ );
+ }
+}
+
+#[test]
+fn can_handle_min_tour_size_with_single_vehicle() {
+ // Problem: 3 jobs, 1 vehicle, min_tour_size=2
+ // Expected: All jobs should be assigned to the single vehicle
+ let problem = Problem {
+ plan: Plan {
+ jobs: vec![
+ create_delivery_job("job1", (1., 0.)),
+ create_delivery_job("job2", (2., 0.)),
+ create_delivery_job("job3", (3., 0.)),
+ ],
+ ..create_empty_plan()
+ },
+ fleet: Fleet {
+ vehicles: vec![VehicleType {
+ shifts: vec![create_default_open_vehicle_shift()],
+ limits: Some(VehicleLimits {
+ max_distance: None,
+ max_duration: None,
+ tour_size: None,
+ min_tour_size: Some(2),
+ }),
+ ..create_default_vehicle_type()
+ }],
+ ..create_default_fleet()
+ },
+ objectives: create_objectives_with_min_tour_size(),
+ ..create_empty_problem()
+ };
+ let matrix = create_matrix_from_problem(&problem);
+
+ let solution = solve_with_metaheuristic_and_iterations(problem, Some(vec![matrix]), 100);
+
+ // Should have one tour with all 3 jobs
+ assert_eq!(solution.tours.len(), 1, "Expected exactly one tour");
+
+ let job_activities: usize = solution.tours[0]
+ .stops
+ .iter()
+ .flat_map(|stop| stop.activities())
+ .filter(|a| a.activity_type != "departure" && a.activity_type != "arrival")
+ .count();
+
+ assert_eq!(job_activities, 3, "Expected all 3 jobs in the tour");
+}
diff --git a/vrp-pragmatic/tests/features/limits/mod.rs b/vrp-pragmatic/tests/features/limits/mod.rs
index 2d5d0d88f..f1c41bdd6 100644
--- a/vrp-pragmatic/tests/features/limits/mod.rs
+++ b/vrp-pragmatic/tests/features/limits/mod.rs
@@ -1,3 +1,5 @@
+mod job_times;
mod max_distance;
mod max_duration;
+mod min_tour_size;
mod tour_size;
diff --git a/vrp-pragmatic/tests/features/limits/tour_size.rs b/vrp-pragmatic/tests/features/limits/tour_size.rs
index 3c4a9a6c6..2bcefef43 100644
--- a/vrp-pragmatic/tests/features/limits/tour_size.rs
+++ b/vrp-pragmatic/tests/features/limits/tour_size.rs
@@ -16,7 +16,12 @@ fn can_skip_job_from_multiple_because_of_tour_size() {
fleet: Fleet {
vehicles: vec![VehicleType {
shifts: vec![create_default_open_vehicle_shift()],
- limits: Some(VehicleLimits { max_distance: None, max_duration: None, tour_size: Some(2) }),
+ limits: Some(VehicleLimits {
+ max_distance: None,
+ max_duration: None,
+ tour_size: Some(2),
+ min_tour_size: None,
+ }),
..create_default_vehicle_type()
}],
..create_default_fleet()
diff --git a/vrp-pragmatic/tests/features/mod.rs b/vrp-pragmatic/tests/features/mod.rs
index f7d032198..132f4aa26 100644
--- a/vrp-pragmatic/tests/features/mod.rs
+++ b/vrp-pragmatic/tests/features/mod.rs
@@ -10,6 +10,7 @@ mod format;
mod group;
mod limits;
mod multjob;
+mod overdue;
mod pickdev;
mod priorities;
mod recharge;
diff --git a/vrp-pragmatic/tests/features/overdue/basic_overdue.rs b/vrp-pragmatic/tests/features/overdue/basic_overdue.rs
new file mode 100644
index 000000000..4c4e70355
--- /dev/null
+++ b/vrp-pragmatic/tests/features/overdue/basic_overdue.rs
@@ -0,0 +1,169 @@
+use crate::format::problem::*;
+use crate::format_time;
+use crate::helpers::*;
+
+/// Tests that jobs with earlier due dates are preferred when using minimize-overdue objective.
+#[test]
+fn can_prefer_jobs_with_earlier_due_dates() {
+ // Job 1 is due at timestamp 0 (epoch) - already overdue since shift starts at 0
+ // Job 2 is due at timestamp 100 - will be less overdue when scheduled on shift starting at 0
+ // With capacity of 1, solver should pick job2 (less overdue) over job1
+ let problem = Problem {
+ plan: Plan {
+ jobs: vec![
+ // job1 is due earlier, so if scheduled at time 0, it has no overdue
+ // But job2 is due at time 100, so if scheduled at time 0, job2 would have -100 overdue (actually on time)
+ create_delivery_job_with_due_date("job1", (1., 0.), &format_time(0.)),
+ create_delivery_job_with_due_date("job2", (2., 0.), &format_time(100.)),
+ ],
+ ..create_empty_plan()
+ },
+ fleet: Fleet {
+ vehicles: vec![VehicleType {
+ capacity: vec![1], // Can only serve one job
+ ..create_default_vehicle_type()
+ }],
+ ..create_default_fleet()
+ },
+ objectives: Some(vec![
+ Objective::MinimizeUnassigned { breaks: None },
+ Objective::MinimizeOverdue,
+ Objective::MinimizeCost,
+ ]),
+ ..create_empty_problem()
+ };
+ let matrix = create_matrix_from_problem(&problem);
+
+ let solution = solve_with_metaheuristic(problem, Some(vec![matrix]));
+
+ // Both jobs have due dates >= shift start, so neither is overdue
+ // The solver should assign one job and leave one unassigned due to capacity
+ assert_eq!(solution.tours.len(), 1);
+ assert!(solution.unassigned.is_some());
+}
+
+/// Tests that a job scheduled past its due date contributes to overdue cost.
+#[test]
+fn can_calculate_overdue_for_late_job() {
+ // Create a problem where the shift starts after the job's due date
+ // Due date: timestamp 0
+ // Shift starts: timestamp 86400 (1 day later)
+ // Expected overdue: 1 day
+ let problem = Problem {
+ plan: Plan {
+ jobs: vec![create_delivery_job_with_due_date("job1", (1., 0.), &format_time(0.))],
+ ..create_empty_plan()
+ },
+ fleet: Fleet {
+ vehicles: vec![VehicleType {
+ shifts: vec![VehicleShift {
+ start: ShiftStart {
+ earliest: format_time(86400.), // 1 day after epoch
+ latest: None,
+ location: (0., 0.).to_loc(),
+ },
+ end: Some(ShiftEnd {
+ earliest: None,
+ latest: format_time(172800.), // 2 days after epoch
+ location: (0., 0.).to_loc(),
+ }),
+ ..create_default_vehicle_shift()
+ }],
+ ..create_default_vehicle_type()
+ }],
+ ..create_default_fleet()
+ },
+ objectives: Some(vec![
+ Objective::MinimizeUnassigned { breaks: None },
+ Objective::MinimizeOverdue,
+ Objective::MinimizeCost,
+ ]),
+ ..create_empty_problem()
+ };
+ let matrix = create_matrix_from_problem(&problem);
+
+ let solution = solve_with_metaheuristic(problem, Some(vec![matrix]));
+
+ // Job should be assigned (we minimize unassigned first)
+ assert_eq!(solution.tours.len(), 1);
+ // Verify the job is in the tour
+ assert!(solution.tours[0].stops.iter().any(|stop| stop.activities().iter().any(|a| a.job_id == "job1")));
+}
+
+/// Tests that jobs without due dates don't contribute to overdue cost.
+#[test]
+fn can_handle_jobs_without_due_date() {
+ let problem = Problem {
+ plan: Plan {
+ jobs: vec![
+ create_delivery_job("job1", (1., 0.)), // No due date
+ create_delivery_job_with_due_date("job2", (2., 0.), &format_time(1000.)), // Has due date
+ ],
+ ..create_empty_plan()
+ },
+ fleet: Fleet { vehicles: vec![create_default_vehicle_type()], ..create_default_fleet() },
+ objectives: Some(vec![
+ Objective::MinimizeUnassigned { breaks: None },
+ Objective::MinimizeOverdue,
+ Objective::MinimizeCost,
+ ]),
+ ..create_empty_problem()
+ };
+ let matrix = create_matrix_from_problem(&problem);
+
+ let solution = solve_with_metaheuristic(problem, Some(vec![matrix]));
+
+ // Both jobs should be assigned
+ assert_eq!(solution.tours.len(), 1);
+ assert!(solution.unassigned.is_none());
+}
+
+/// Tests that with multiple shifts, jobs are assigned to minimize overdue.
+#[test]
+fn can_prefer_earlier_shift_to_minimize_overdue() {
+ // Job is due at timestamp 50
+ // Shift 1 starts at 0 (on time, 0 days overdue)
+ // Shift 2 starts at 86400 (1 day late, ~1 day overdue)
+ // Solver should prefer shift 1
+ let problem = Problem {
+ plan: Plan {
+ jobs: vec![create_delivery_job_with_due_date("job1", (1., 0.), &format_time(50.))],
+ ..create_empty_plan()
+ },
+ fleet: Fleet {
+ vehicles: vec![VehicleType {
+ shifts: vec![
+ VehicleShift {
+ start: ShiftStart { earliest: format_time(0.), latest: None, location: (0., 0.).to_loc() },
+ end: Some(ShiftEnd { earliest: None, latest: format_time(100.), location: (0., 0.).to_loc() }),
+ ..create_default_vehicle_shift()
+ },
+ VehicleShift {
+ start: ShiftStart { earliest: format_time(86400.), latest: None, location: (0., 0.).to_loc() },
+ end: Some(ShiftEnd {
+ earliest: None,
+ latest: format_time(172800.),
+ location: (0., 0.).to_loc(),
+ }),
+ ..create_default_vehicle_shift()
+ },
+ ],
+ ..create_default_vehicle_type()
+ }],
+ ..create_default_fleet()
+ },
+ objectives: Some(vec![
+ Objective::MinimizeUnassigned { breaks: None },
+ Objective::MinimizeOverdue,
+ Objective::MinimizeCost,
+ ]),
+ ..create_empty_problem()
+ };
+ let matrix = create_matrix_from_problem(&problem);
+
+ let solution = solve_with_metaheuristic(problem, Some(vec![matrix]));
+
+ // Job should be assigned to shift 0 (starts at 0) to minimize overdue
+ assert_eq!(solution.tours.len(), 1);
+ assert_eq!(solution.tours[0].shift_index, 0);
+}
diff --git a/vrp-pragmatic/tests/features/overdue/mod.rs b/vrp-pragmatic/tests/features/overdue/mod.rs
new file mode 100644
index 000000000..815510165
--- /dev/null
+++ b/vrp-pragmatic/tests/features/overdue/mod.rs
@@ -0,0 +1 @@
+mod basic_overdue;
diff --git a/vrp-pragmatic/tests/features/priorities/basic_order.rs b/vrp-pragmatic/tests/features/priorities/basic_order.rs
index a4cfcbe75..723a4fd3d 100644
--- a/vrp-pragmatic/tests/features/priorities/basic_order.rs
+++ b/vrp-pragmatic/tests/features/priorities/basic_order.rs
@@ -15,7 +15,7 @@ fn create_test_plan_with_three_jobs() -> Plan {
}
fn create_test_limit() -> Option {
- Some(VehicleLimits { max_distance: Some(15.), max_duration: None, tour_size: None })
+ Some(VehicleLimits { max_distance: Some(15.), max_duration: None, tour_size: None, min_tour_size: None })
}
fn create_order_objective(is_constrained: bool) -> Vec {
@@ -146,6 +146,7 @@ fn can_handle_order_between_special_activities() {
places: vec![JobPlace { times: None, location: location.to_loc(), duration: 100., tag: None }],
demand: Some(vec![1]),
order: Some(order),
+ due_date: None,
}]),
..create_job(id)
};
diff --git a/vrp-pragmatic/tests/features/reload/avoid_reload.rs b/vrp-pragmatic/tests/features/reload/avoid_reload.rs
index 9101c3e00..ab9da19a5 100644
--- a/vrp-pragmatic/tests/features/reload/avoid_reload.rs
+++ b/vrp-pragmatic/tests/features/reload/avoid_reload.rs
@@ -36,6 +36,7 @@ fn can_serve_multi_job_and_delivery_in_one_tour_avoiding_reload_impl(generations
..create_default_reload()
}]),
recharges: None,
+ job_times: None,
}],
capacity: vec![2],
..create_default_vehicle_type()
diff --git a/vrp-pragmatic/tests/features/reload/basic_reload.rs b/vrp-pragmatic/tests/features/reload/basic_reload.rs
index 29ff09053..129c1b675 100644
--- a/vrp-pragmatic/tests/features/reload/basic_reload.rs
+++ b/vrp-pragmatic/tests/features/reload/basic_reload.rs
@@ -44,6 +44,7 @@ fn can_use_vehicle_with_two_tours_and_two_jobs_impl(jobs: Vec, unassigned:
..create_default_reload()
}]),
recharges: None,
+ job_times: None,
}],
capacity: vec![1],
..create_default_vehicle_type()
diff --git a/vrp-pragmatic/tests/features/reload/diff_reload_places.rs b/vrp-pragmatic/tests/features/reload/diff_reload_places.rs
index 5d345c020..c76077dec 100644
--- a/vrp-pragmatic/tests/features/reload/diff_reload_places.rs
+++ b/vrp-pragmatic/tests/features/reload/diff_reload_places.rs
@@ -36,6 +36,7 @@ fn can_use_reloads_with_different_locations() {
},
]),
recharges: None,
+ job_times: None,
}],
capacity: vec![2],
..create_default_vehicle_type()
diff --git a/vrp-pragmatic/tests/features/reload/multi_job_reload.rs b/vrp-pragmatic/tests/features/reload/multi_job_reload.rs
index 8551f9ef8..b1c2fb37b 100644
--- a/vrp-pragmatic/tests/features/reload/multi_job_reload.rs
+++ b/vrp-pragmatic/tests/features/reload/multi_job_reload.rs
@@ -31,6 +31,7 @@ fn can_serve_multi_job_and_delivery_with_reload() {
..create_default_reload()
}]),
recharges: None,
+ job_times: None,
}],
capacity: vec![2],
..create_default_vehicle_type()
@@ -66,7 +67,7 @@ fn can_properly_handle_load_without_capacity_violation() {
},
fleet: Fleet {
vehicles: vec![VehicleType {
- costs: VehicleCosts { fixed: Some(20.0), distance: 0.002, time: 0.003 },
+ costs: VehicleCosts { fixed: Some(20.0), distance: 0.002, time: 0.003, span: None },
shifts: vec![VehicleShift {
reloads: Some(vec![
VehicleReload {
diff --git a/vrp-pragmatic/tests/features/reload/picks_devs_reload.rs b/vrp-pragmatic/tests/features/reload/picks_devs_reload.rs
index 2d85f824f..497676999 100644
--- a/vrp-pragmatic/tests/features/reload/picks_devs_reload.rs
+++ b/vrp-pragmatic/tests/features/reload/picks_devs_reload.rs
@@ -27,6 +27,7 @@ fn can_use_vehicle_with_pickups_and_deliveries() {
..create_default_reload()
}]),
recharges: None,
+ job_times: None,
}],
capacity: vec![1],
..create_default_vehicle_type()
diff --git a/vrp-pragmatic/tests/features/tour_shape/basic_vehicle_distance.rs b/vrp-pragmatic/tests/features/tour_shape/basic_vehicle_distance.rs
new file mode 100644
index 000000000..23bd9d8ed
--- /dev/null
+++ b/vrp-pragmatic/tests/features/tour_shape/basic_vehicle_distance.rs
@@ -0,0 +1,164 @@
+use crate::format::problem::Objective::*;
+use crate::format::problem::*;
+use crate::helpers::*;
+
+#[test]
+fn can_assign_jobs_to_nearest_vehicle() {
+ // Two vehicles: v1 at (0,0), v2 at (20,0).
+ // Two clusters of jobs: near v1 at (1,0),(2,0),(3,0) and near v2 at (18,0),(19,0),(20,0).
+ // With MinimizeVehicleDistance, jobs should be assigned to their nearest vehicle.
+ let problem = Problem {
+ plan: Plan {
+ jobs: vec![
+ create_delivery_job("job1", (1., 0.)),
+ create_delivery_job("job2", (2., 0.)),
+ create_delivery_job("job3", (3., 0.)),
+ create_delivery_job("job4", (18., 0.)),
+ create_delivery_job("job5", (19., 0.)),
+ create_delivery_job("job6", (20., 0.)),
+ ],
+ ..create_empty_plan()
+ },
+ fleet: Fleet {
+ vehicles: vec![
+ VehicleType {
+ vehicle_ids: vec!["v1_1".to_string()],
+ shifts: vec![create_default_vehicle_shift_with_locations((0., 0.), (0., 0.))],
+ ..create_vehicle_with_capacity("v1", vec![10])
+ },
+ VehicleType {
+ vehicle_ids: vec!["v2_1".to_string()],
+ shifts: vec![create_default_vehicle_shift_with_locations((20., 0.), (20., 0.))],
+ ..create_vehicle_with_capacity("v2", vec![10])
+ },
+ ],
+ ..create_default_fleet()
+ },
+ objectives: Some(vec![MinimizeUnassigned { breaks: None }, MinimizeVehicleDistance, MinimizeCost]),
+ };
+ let matrix = create_matrix_from_problem(&problem);
+
+ let solution = solve_with_metaheuristic_and_iterations(problem, Some(vec![matrix]), 500);
+
+ assert!(solution.unassigned.is_none());
+ assert_eq!(solution.tours.len(), 2);
+
+ for tour in &solution.tours {
+ let job_ids: Vec<&str> = tour
+ .stops
+ .iter()
+ .flat_map(|stop| stop.activities().iter())
+ .filter(|a| a.activity_type == "delivery")
+ .map(|a| a.job_id.as_str())
+ .collect();
+
+ if tour.vehicle_id == "v1_1" {
+ for id in &job_ids {
+ assert!(["job1", "job2", "job3"].contains(id), "v1 should serve nearby jobs, but got {id}");
+ }
+ } else if tour.vehicle_id == "v2_1" {
+ for id in &job_ids {
+ assert!(["job4", "job5", "job6"].contains(id), "v2 should serve nearby jobs, but got {id}");
+ }
+ }
+ }
+}
+
+/// Computes the total "excess distance" for a solution: for each job, how much farther
+/// is the assigned vehicle compared to the nearest vehicle.
+fn compute_excess_distance(solution: &crate::format::solution::Solution, vehicle_starts: &[(&str, (f64, f64))]) -> f64 {
+ let dist = |a: (f64, f64), b: (f64, f64)| ((a.0 - b.0).powi(2) + (a.1 - b.1).powi(2)).sqrt();
+
+ let mut total_excess = 0.0;
+ let mut job_count = 0;
+
+ for tour in &solution.tours {
+ let vehicle_start =
+ vehicle_starts.iter().find(|(vid, _)| *vid == tour.vehicle_id).map(|(_, loc)| *loc).unwrap();
+
+ for stop in &tour.stops {
+ let Some(stop_loc) = stop.location().map(|l| l.to_lat_lng()) else { continue };
+ for activity in stop.activities() {
+ if activity.activity_type == "departure" || activity.activity_type == "arrival" {
+ continue;
+ }
+
+ let job_loc = activity.location.as_ref().map(|l| l.to_lat_lng()).unwrap_or(stop_loc);
+
+ let dist_assigned = dist(job_loc, vehicle_start);
+ let dist_nearest = vehicle_starts.iter().map(|(_, vloc)| dist(job_loc, *vloc)).fold(f64::MAX, f64::min);
+ total_excess += (dist_assigned - dist_nearest).max(0.0);
+ job_count += 1;
+ }
+ }
+ }
+
+ if job_count > 0 { total_excess / job_count as f64 } else { 0.0 }
+}
+
+#[test]
+fn can_reduce_vehicle_distance_with_many_jobs() {
+ // 5 vehicles spread along X-axis at (0,0), (20,0), (40,0), (60,0), (80,0)
+ // 50 jobs in clusters of 10 around each vehicle position
+ // Compare: with vs without MinimizeVehicleDistance
+ let vehicle_positions: Vec<(f64, f64)> = vec![(0., 0.), (20., 0.), (40., 0.), (60., 0.), (80., 0.)];
+
+ let mut jobs = Vec::new();
+ for (cluster_idx, &(vx, vy)) in vehicle_positions.iter().enumerate() {
+ for j in 0..10 {
+ let offset_x = (j as f64 - 4.5) * 1.5; // spread jobs +-6.75 around vehicle
+ let offset_y = (j as f64 % 3.0 - 1.0) * 2.0; // slight Y variation
+ let job_id = format!("job_{}_{}", cluster_idx, j);
+ jobs.push(create_delivery_job(&job_id, (vx + offset_x, vy + offset_y)));
+ }
+ }
+
+ let vehicles: Vec = vehicle_positions
+ .iter()
+ .enumerate()
+ .map(|(i, &(x, y))| VehicleType {
+ vehicle_ids: vec![format!("v{}_1", i)],
+ shifts: vec![create_default_vehicle_shift_with_locations((x, y), (x, y))],
+ ..create_vehicle_with_capacity(&format!("v{i}"), vec![20])
+ })
+ .collect();
+
+ let vehicle_starts: Vec<(&str, (f64, f64))> =
+ vec![("v0_1", (0., 0.)), ("v1_1", (20., 0.)), ("v2_1", (40., 0.)), ("v3_1", (60., 0.)), ("v4_1", (80., 0.))];
+
+ // --- Solve WITHOUT MinimizeVehicleDistance ---
+ let problem_without = Problem {
+ plan: Plan { jobs: jobs.clone(), ..create_empty_plan() },
+ fleet: Fleet { vehicles: vehicles.clone(), ..create_default_fleet() },
+ objectives: Some(vec![MinimizeUnassigned { breaks: None }, MinimizeTours, MinimizeCost]),
+ };
+ let matrix_without = create_matrix_from_problem(&problem_without);
+ let solution_without = solve_with_metaheuristic_and_iterations(problem_without, Some(vec![matrix_without]), 500);
+
+ // --- Solve WITH MinimizeVehicleDistance ---
+ let problem_with = Problem {
+ plan: Plan { jobs, ..create_empty_plan() },
+ fleet: Fleet { vehicles, ..create_default_fleet() },
+ objectives: Some(vec![MinimizeUnassigned { breaks: None }, MinimizeVehicleDistance, MinimizeCost]),
+ };
+ let matrix_with = create_matrix_from_problem(&problem_with);
+ let solution_with = solve_with_metaheuristic_and_iterations(problem_with, Some(vec![matrix_with]), 500);
+
+ // Both should assign all jobs
+ assert!(solution_without.unassigned.is_none(), "without: has unassigned jobs");
+ assert!(solution_with.unassigned.is_none(), "with: has unassigned jobs");
+
+ let excess_without = compute_excess_distance(&solution_without, &vehicle_starts);
+ let excess_with = compute_excess_distance(&solution_with, &vehicle_starts);
+
+ eprintln!("=== MinimizeVehicleDistance effectiveness (50 jobs, 5 vehicles) ===");
+ eprintln!(" Avg excess distance WITHOUT objective: {excess_without:.2}");
+ eprintln!(" Avg excess distance WITH objective: {excess_with:.2}");
+ eprintln!(" Improvement: {:.1}%", (1.0 - excess_with / excess_without.max(0.001)) * 100.0);
+
+ // The objective should meaningfully reduce excess distance
+ assert!(
+ excess_with <= excess_without,
+ "MinimizeVehicleDistance should not increase excess distance: with={excess_with:.2}, without={excess_without:.2}"
+ );
+}
diff --git a/vrp-pragmatic/tests/features/tour_shape/mod.rs b/vrp-pragmatic/tests/features/tour_shape/mod.rs
index 3c0b8f89a..751604c9d 100644
--- a/vrp-pragmatic/tests/features/tour_shape/mod.rs
+++ b/vrp-pragmatic/tests/features/tour_shape/mod.rs
@@ -1 +1,2 @@
mod basic_tour_compactness;
+mod basic_vehicle_distance;
diff --git a/vrp-pragmatic/tests/generator/defaults.rs b/vrp-pragmatic/tests/generator/defaults.rs
index e178d21c6..0782e13b8 100644
--- a/vrp-pragmatic/tests/generator/defaults.rs
+++ b/vrp-pragmatic/tests/generator/defaults.rs
@@ -85,8 +85,8 @@ pub fn default_job_prototype() -> impl Strategy {
pub fn default_costs_prototype() -> impl Strategy {
from_costs(vec![
- VehicleCosts { fixed: Some(20.), distance: 0.0020, time: 0.003 },
- VehicleCosts { fixed: Some(30.), distance: 0.0015, time: 0.005 },
+ VehicleCosts { fixed: Some(20.), distance: 0.0020, time: 0.003, span: None },
+ VehicleCosts { fixed: Some(30.), distance: 0.0015, time: 0.005, span: None },
])
}
diff --git a/vrp-pragmatic/tests/generator/jobs.rs b/vrp-pragmatic/tests/generator/jobs.rs
index 81d534544..10517e4a7 100644
--- a/vrp-pragmatic/tests/generator/jobs.rs
+++ b/vrp-pragmatic/tests/generator/jobs.rs
@@ -76,7 +76,7 @@ prop_compose! {
tag: Some("p1".to_owned()),
..pickup
}
- ], demand: demand.clone(), order }
+ ], demand: demand.clone(), order, due_date: None }
]),
deliveries: Some(vec![
JobTask { places: vec![
@@ -84,14 +84,14 @@ prop_compose! {
tag: Some("d1".to_owned()),
..delivery
}
- ], demand, order: None }
+ ], demand, order: None, due_date: None }
]),
replacements: None,
services: None,
skills,
value,
group,
- compatibility
+ compatibility,
}
}
}
@@ -153,7 +153,7 @@ prop_compose! {
demand in demand_proto,
order in order_proto,
) -> JobTask {
- JobTask { places: vec![place], demand, order }
+ JobTask { places: vec![place], demand, order, due_date: None }
}
}
diff --git a/vrp-pragmatic/tests/generator/vehicles.rs b/vrp-pragmatic/tests/generator/vehicles.rs
index c36a50465..7e3e35bdb 100644
--- a/vrp-pragmatic/tests/generator/vehicles.rs
+++ b/vrp-pragmatic/tests/generator/vehicles.rs
@@ -34,6 +34,7 @@ prop_compose! {
capacity,
skills,
limits,
+ min_shifts: None,
}
}
}
@@ -110,6 +111,7 @@ prop_compose! {
breaks,
reloads,
recharges,
+ job_times: None,
}
}
}
diff --git a/vrp-pragmatic/tests/helpers/problem.rs b/vrp-pragmatic/tests/helpers/problem.rs
index 0d5c57782..715856c43 100644
--- a/vrp-pragmatic/tests/helpers/problem.rs
+++ b/vrp-pragmatic/tests/helpers/problem.rs
@@ -11,7 +11,7 @@ pub fn create_job_place(location: (f64, f64), tag: Option) -> JobPlace {
}
pub fn create_task(location: (f64, f64), tag: Option) -> JobTask {
- JobTask { places: vec![create_job_place(location, tag)], demand: Some(vec![1]), order: None }
+ JobTask { places: vec![create_job_place(location, tag)], demand: Some(vec![1]), order: None, due_date: None }
}
pub fn create_job(id: &str) -> Job {
@@ -38,6 +38,7 @@ pub fn create_delivery_job_with_order(id: &str, location: (f64, f64), order: i32
places: vec![create_job_place(location, None)],
demand: Some(vec![1]),
order: Some(order),
+ due_date: None,
}]),
..create_job(id)
}
@@ -49,6 +50,7 @@ pub fn create_delivery_job_with_group(id: &str, location: (f64, f64), group: &st
places: vec![create_job_place(location, None)],
demand: Some(vec![1]),
order: None,
+ due_date: None,
}]),
group: Some(group.to_string()),
..create_job(id)
@@ -61,6 +63,7 @@ pub fn create_delivery_job_with_compatibility(id: &str, location: (f64, f64), co
places: vec![create_job_place(location, None)],
demand: Some(vec![1]),
order: None,
+ due_date: None,
}]),
compatibility: Some(compatibility.to_string()),
..create_job(id)
@@ -81,6 +84,7 @@ pub fn create_delivery_job_with_duration(id: &str, location: (f64, f64), duratio
places: vec![JobPlace { duration, ..create_job_place(location, None) }],
demand: Some(vec![1]),
order: None,
+ due_date: None,
}]),
..create_job(id)
}
@@ -97,6 +101,7 @@ pub fn create_delivery_job_with_times(
places: vec![JobPlace { duration, times: convert_times(×), ..create_job_place(location, None) }],
demand: Some(vec![1]),
order: None,
+ due_date: None,
}]),
..create_job(id)
}
@@ -106,6 +111,18 @@ pub fn create_delivery_job_with_value(id: &str, location: (f64, f64), value: Flo
Job { deliveries: Some(vec![create_task(location, None)]), value: Some(value), ..create_job(id) }
}
+pub fn create_delivery_job_with_due_date(id: &str, location: (f64, f64), due_date: &str) -> Job {
+ Job {
+ deliveries: Some(vec![JobTask {
+ places: vec![create_job_place(location, None)],
+ demand: Some(vec![1]),
+ order: None,
+ due_date: Some(due_date.to_string()),
+ }]),
+ ..create_job(id)
+ }
+}
+
pub fn create_pickup_job(id: &str, location: (f64, f64)) -> Job {
Job { pickups: Some(vec![create_task(location, None)]), ..create_job(id) }
}
@@ -145,6 +162,7 @@ pub fn create_pickup_delivery_job_with_params(
}],
demand: Some(demand.clone()),
order: None,
+ due_date: None,
}]),
deliveries: Some(vec![JobTask {
places: vec![JobPlace {
@@ -154,6 +172,7 @@ pub fn create_pickup_delivery_job_with_params(
}],
demand: Some(demand),
order: None,
+ due_date: None,
}]),
..create_job(id)
@@ -166,6 +185,7 @@ pub fn create_delivery_job_with_index(id: &str, index: usize) -> Job {
places: vec![JobPlace { times: None, location: Location::Reference { index }, duration: 1., tag: None }],
demand: Some(vec![1]),
order: None,
+ due_date: None,
}]),
..create_job(id)
}
@@ -187,6 +207,7 @@ pub fn create_multi_job(
}],
demand: Some(demand),
order: None,
+ due_date: None,
})
.collect::>();
@@ -211,6 +232,7 @@ pub fn create_default_open_vehicle_shift() -> VehicleShift {
breaks: None,
reloads: None,
recharges: None,
+ job_times: None,
}
}
@@ -221,11 +243,12 @@ pub fn create_default_vehicle_shift_with_locations(start: (f64, f64), end: (f64,
breaks: None,
reloads: None,
recharges: None,
+ job_times: None,
}
}
pub fn create_default_vehicle_costs() -> VehicleCosts {
- VehicleCosts { fixed: Some(10.), distance: 1., time: 1. }
+ VehicleCosts { fixed: Some(10.), distance: 1., time: 1., span: None }
}
pub fn create_default_vehicle_profile() -> VehicleProfile {
@@ -254,6 +277,7 @@ pub fn create_vehicle_with_capacity(id: &str, capacity: Vec) -> VehicleType
capacity,
skills: None,
limits: None,
+ min_shifts: None,
}
}
diff --git a/vrp-pragmatic/tests/regression/break_test.rs b/vrp-pragmatic/tests/regression/break_test.rs
index c31024914..f5502fa55 100644
--- a/vrp-pragmatic/tests/regression/break_test.rs
+++ b/vrp-pragmatic/tests/regression/break_test.rs
@@ -25,6 +25,7 @@ fn can_handle_properly_invalid_break_removal() {
}],
demand: Some(vec![1]),
order: None,
+ due_date: None,
}]),
..create_job("job1")
},
@@ -41,6 +42,7 @@ fn can_handle_properly_invalid_break_removal() {
}],
demand: Some(vec![1]),
order: None,
+ due_date: None,
}]),
..create_job("job2")
},
@@ -57,6 +59,7 @@ fn can_handle_properly_invalid_break_removal() {
}],
demand: Some(vec![1]),
order: None,
+ due_date: None,
}]),
..create_job("job3")
},
@@ -73,6 +76,7 @@ fn can_handle_properly_invalid_break_removal() {
}],
demand: Some(vec![2]),
order: None,
+ due_date: None,
}]),
..create_job("job4")
},
@@ -89,6 +93,7 @@ fn can_handle_properly_invalid_break_removal() {
}],
demand: Some(vec![3]),
order: None,
+ due_date: None,
}]),
..create_job("job5")
},
@@ -105,6 +110,7 @@ fn can_handle_properly_invalid_break_removal() {
}],
demand: Some(vec![1]),
order: None,
+ due_date: None,
}]),
..create_job("job6")
},
@@ -116,7 +122,7 @@ fn can_handle_properly_invalid_break_removal() {
type_id: "vehicle1".to_string(),
vehicle_ids: vec!["vehicle1_1".to_string()],
profile: VehicleProfile { matrix: "car".to_string(), scale: None },
- costs: VehicleCosts { fixed: Some(20.), distance: 0.002, time: 0.003 },
+ costs: VehicleCosts { fixed: Some(20.), distance: 0.002, time: 0.003, span: None },
shifts: vec![VehicleShift {
start: ShiftStart {
earliest: "2020-07-04T09:00:00Z".to_string(),
@@ -138,10 +144,12 @@ fn can_handle_properly_invalid_break_removal() {
}]),
reloads: None,
recharges: None,
+ job_times: None,
}],
capacity: vec![5],
skills: None,
limits: None,
+ min_shifts: None,
}],
..create_default_fleet()
},
diff --git a/vrp-pragmatic/tests/regression/pd_same_stops.rs b/vrp-pragmatic/tests/regression/pd_same_stops.rs
index bef3768ac..690105387 100644
--- a/vrp-pragmatic/tests/regression/pd_same_stops.rs
+++ b/vrp-pragmatic/tests/regression/pd_same_stops.rs
@@ -16,6 +16,7 @@ fn can_handle_two_pd_jobs_with_same_locations_and_unusual_routing() {
}],
demand: Some(vec![1]),
order: None,
+ due_date: None,
};
let problem = Problem {
@@ -36,7 +37,7 @@ fn can_handle_two_pd_jobs_with_same_locations_and_unusual_routing() {
},
fleet: Fleet {
vehicles: vec![VehicleType {
- costs: VehicleCosts { fixed: None, distance: 0.0, time: 1.0 },
+ costs: VehicleCosts { fixed: None, distance: 0.0, time: 1.0, span: None },
shifts: vec![VehicleShift {
start: ShiftStart {
earliest: format_time(0.),
diff --git a/vrp-pragmatic/tests/unit/checker/assignment_test.rs b/vrp-pragmatic/tests/unit/checker/assignment_test.rs
index ff33aacbe..04cfce879 100644
--- a/vrp-pragmatic/tests/unit/checker/assignment_test.rs
+++ b/vrp-pragmatic/tests/unit/checker/assignment_test.rs
@@ -134,6 +134,7 @@ fn check_jobs_impl(
}],
demand: if tgt != "service" { Some(vec![1]) } else { None },
order: None,
+ due_date: None,
})
.collect()
};
diff --git a/vrp-pragmatic/tests/unit/checker/breaks_test.rs b/vrp-pragmatic/tests/unit/checker/breaks_test.rs
index e16f440b6..18d1bc0df 100644
--- a/vrp-pragmatic/tests/unit/checker/breaks_test.rs
+++ b/vrp-pragmatic/tests/unit/checker/breaks_test.rs
@@ -86,6 +86,7 @@ fn can_check_breaks_impl(
}]),
reloads: None,
recharges: None,
+ job_times: None,
}],
capacity: vec![5],
..create_default_vehicle_type()
diff --git a/vrp-pragmatic/tests/unit/checker/capacity_test.rs b/vrp-pragmatic/tests/unit/checker/capacity_test.rs
index 34554bf67..605d40c9c 100644
--- a/vrp-pragmatic/tests/unit/checker/capacity_test.rs
+++ b/vrp-pragmatic/tests/unit/checker/capacity_test.rs
@@ -45,6 +45,7 @@ fn can_check_load_impl(stop_loads: Vec, expected_result: Result<(), Vec,
) {
- let problem = create_test_problem(Some(VehicleLimits { max_distance, max_duration, tour_size: None }));
+ let problem =
+ create_test_problem(Some(VehicleLimits { max_distance, max_duration, tour_size: None, min_tour_size: None }));
let solution =
create_test_solution(Statistic { distance: actual, duration: actual, ..Statistic::default() }, vec![]);
let ctx = CheckerContext::new(create_example_problem(), problem, None, solution).unwrap();
@@ -71,8 +72,12 @@ pub fn can_check_shift_and_distance_limit_impl(
#[test]
pub fn can_check_tour_size_limit() {
- let problem =
- create_test_problem(Some(VehicleLimits { max_distance: None, max_duration: None, tour_size: Some(2) }));
+ let problem = create_test_problem(Some(VehicleLimits {
+ max_distance: None,
+ max_duration: None,
+ tour_size: Some(2),
+ min_tour_size: None,
+ }));
let solution = create_test_solution(
Statistic::default(),
vec![
@@ -114,6 +119,88 @@ pub fn can_check_tour_size_limit() {
);
}
+#[test]
+pub fn can_check_min_tour_size_limit() {
+ let problem = create_test_problem(Some(VehicleLimits {
+ max_distance: None,
+ max_duration: None,
+ tour_size: None,
+ min_tour_size: Some(3),
+ }));
+ let solution = create_test_solution(
+ Statistic::default(),
+ vec![
+ StopBuilder::default().coordinate((0., 0.)).schedule_stamp(0., 0.).load(vec![2]).build_departure(),
+ StopBuilder::default()
+ .coordinate((1., 0.))
+ .schedule_stamp(1., 1.)
+ .load(vec![1])
+ .distance(1)
+ .build_single("job1", "delivery"),
+ StopBuilder::default()
+ .coordinate((2., 0.))
+ .schedule_stamp(2., 2.)
+ .load(vec![0])
+ .distance(2)
+ .build_single("job2", "delivery"),
+ StopBuilder::default()
+ .coordinate((0., 0.))
+ .schedule_stamp(4., 4.)
+ .load(vec![0])
+ .distance(4)
+ .build_arrival(),
+ ],
+ );
+ let ctx = CheckerContext::new(create_example_problem(), problem, None, solution).unwrap();
+
+ let result = check_shift_limits(&ctx);
+
+ assert_eq!(
+ result,
+ Err("min tour size limit violation, expected: not less than 3, got: 2, vehicle id 'some_real_vehicle', shift index: 0"
+ .into())
+ );
+}
+
+#[test]
+pub fn can_pass_min_tour_size_limit_when_satisfied() {
+ let problem = create_test_problem(Some(VehicleLimits {
+ max_distance: None,
+ max_duration: None,
+ tour_size: None,
+ min_tour_size: Some(2),
+ }));
+ let solution = create_test_solution(
+ Statistic::default(),
+ vec![
+ StopBuilder::default().coordinate((0., 0.)).schedule_stamp(0., 0.).load(vec![2]).build_departure(),
+ StopBuilder::default()
+ .coordinate((1., 0.))
+ .schedule_stamp(1., 1.)
+ .load(vec![1])
+ .distance(1)
+ .build_single("job1", "delivery"),
+ StopBuilder::default()
+ .coordinate((2., 0.))
+ .schedule_stamp(2., 2.)
+ .load(vec![0])
+ .distance(2)
+ .build_single("job2", "delivery"),
+ StopBuilder::default()
+ .coordinate((0., 0.))
+ .schedule_stamp(4., 4.)
+ .load(vec![0])
+ .distance(4)
+ .build_arrival(),
+ ],
+ );
+ let ctx = CheckerContext::new(create_example_problem(), problem, None, solution).unwrap();
+
+ let result = check_shift_limits(&ctx);
+
+ assert_eq!(result, Ok(()));
+}
+
#[test]
fn can_check_shift_time() {
let problem = Problem {
diff --git a/vrp-pragmatic/tests/unit/checker/relations_test.rs b/vrp-pragmatic/tests/unit/checker/relations_test.rs
index f8e08168b..2f4c4b7ed 100644
--- a/vrp-pragmatic/tests/unit/checker/relations_test.rs
+++ b/vrp-pragmatic/tests/unit/checker/relations_test.rs
@@ -99,10 +99,12 @@ mod single {
..create_default_reload()
}]),
recharges: None,
+ job_times: None,
}],
capacity: vec![5],
skills: None,
limits: None,
+ min_shifts: None,
}],
..create_default_fleet()
},
diff --git a/vrp-pragmatic/tests/unit/format/coord_index_test.rs b/vrp-pragmatic/tests/unit/format/coord_index_test.rs
index da1ad6deb..25851b88f 100644
--- a/vrp-pragmatic/tests/unit/format/coord_index_test.rs
+++ b/vrp-pragmatic/tests/unit/format/coord_index_test.rs
@@ -20,6 +20,7 @@ fn can_use_index_with_coordinate_an_unknown_location_types() {
}],
demand: None,
order: None,
+ due_date: None,
}]),
..create_job("job3")
},
diff --git a/vrp-pragmatic/tests/unit/format/problem/model_test.rs b/vrp-pragmatic/tests/unit/format/problem/model_test.rs
index c130270fd..36ed5fcaa 100644
--- a/vrp-pragmatic/tests/unit/format/problem/model_test.rs
+++ b/vrp-pragmatic/tests/unit/format/problem/model_test.rs
@@ -1,5 +1,6 @@
use super::*;
use crate::helpers::{SIMPLE_MATRIX, SIMPLE_PROBLEM};
+use serde_json::from_str;
use std::io::BufReader;
fn assert_time_windows(actual: &Option>>, expected: (&str, &str)) {
@@ -64,3 +65,17 @@ fn can_deserialize_matrix() {
assert_eq!(matrix.distances.len(), 16);
assert_eq!(matrix.travel_times.len(), 16);
}
+
+#[test]
+fn can_deserialize_balance_shifts_objective_with_saturation() {
+ let objective: Objective = from_str(r#"{ "type": "balance-shifts", "saturation": 0.2, "weight": 3.5 }"#)
+ .expect("failed to deserialize objective");
+
+ match objective {
+ Objective::BalanceShifts { saturation, weight } => {
+ assert!((saturation.unwrap() - 0.2).abs() < 1e-9);
+ assert!((weight.unwrap() - 3.5).abs() < 1e-9);
+ }
+ _ => panic!("unexpected objective variant"),
+ }
+}
diff --git a/vrp-pragmatic/tests/unit/format/problem/reader_test.rs b/vrp-pragmatic/tests/unit/format/problem/reader_test.rs
index e4fbb50a5..2e6a677ed 100644
--- a/vrp-pragmatic/tests/unit/format/problem/reader_test.rs
+++ b/vrp-pragmatic/tests/unit/format/problem/reader_test.rs
@@ -80,6 +80,7 @@ fn can_read_complex_problem() {
}],
demand: Some(vec![0, 1]),
order: None,
+ due_date: None,
}]),
skills: Some(all_of_skills(vec!["unique".to_string()])),
..create_job("delivery_job")
@@ -97,6 +98,7 @@ fn can_read_complex_problem() {
}],
demand: Some(vec![2]),
order: None,
+ due_date: None,
}]),
deliveries: Some(vec![JobTask {
places: vec![JobPlace {
@@ -110,6 +112,7 @@ fn can_read_complex_problem() {
}],
demand: Some(vec![2]),
order: None,
+ due_date: None,
}]),
..create_job("pickup_delivery_job")
},
@@ -126,6 +129,7 @@ fn can_read_complex_problem() {
}],
demand: Some(vec![3]),
order: None,
+ due_date: None,
}]),
skills: Some(all_of_skills(vec!["unique2".to_string()])),
..create_job("pickup_job")
@@ -138,7 +142,7 @@ fn can_read_complex_problem() {
type_id: "my_vehicle".to_string(),
vehicle_ids: vec!["my_vehicle_1".to_string(), "my_vehicle_2".to_string()],
profile: create_default_vehicle_profile(),
- costs: VehicleCosts { fixed: Some(100.), distance: 1., time: 2. },
+ costs: VehicleCosts { fixed: Some(100.), distance: 1., time: 2., span: None },
shifts: vec![VehicleShift {
start: ShiftStart {
earliest: "1970-01-01T00:00:00Z".to_string(),
@@ -164,10 +168,17 @@ fn can_read_complex_problem() {
}]),
reloads: None,
recharges: None,
+ job_times: None,
}],
capacity: vec![10, 1],
skills: Some(vec!["unique1".to_string(), "unique2".to_string()]),
- limits: Some(VehicleLimits { max_distance: Some(123.1), max_duration: Some(100.), tour_size: Some(3) }),
+ limits: Some(VehicleLimits {
+ max_distance: Some(123.1),
+ max_duration: Some(100.),
+ tour_size: Some(3),
+ min_tour_size: None,
+ }),
+ min_shifts: None,
}],
..create_default_fleet()
},
diff --git a/vrp-pragmatic/tests/unit/format/solution/writer_test.rs b/vrp-pragmatic/tests/unit/format/solution/writer_test.rs
index 46c9d028e..6a77e3ca3 100644
--- a/vrp-pragmatic/tests/unit/format/solution/writer_test.rs
+++ b/vrp-pragmatic/tests/unit/format/solution/writer_test.rs
@@ -2,6 +2,7 @@ use crate::format::problem::*;
use crate::format::solution::solution_writer::create_tour;
use crate::format::solution::*;
use crate::helpers::*;
+use crate::parse_time;
use std::sync::Arc;
use vrp_core::construction::enablers::ReservedTimeSpan;
use vrp_core::models::common::{TimeSpan, TimeWindow};
@@ -208,3 +209,147 @@ fn can_merge_required_break_on_stop_arrival_time_properly() {
assert_eq!(tour.stops.len(), 3);
assert_eq!(get_ids_from_tour(&tour).into_iter().flatten().filter(|id| id == "break").count(), 1);
}
+
+#[test]
+fn can_keep_activity_time_when_break_starts_at_activity_end_on_point_stop() {
+ let (problem, mut coord_index) = create_test_problem_and_coord_index();
+ coord_index.add(&Location::Reference { index: 1 });
+ coord_index.add(&Location::Reference { index: 2 });
+
+ let create_delivery_with_duration = |id: &str, location: usize| {
+ let mut single = Arc::try_unwrap(create_single(id)).unwrap_or_else(|_| unreachable!());
+ single.places.first_mut().expect("place").location = Some(location);
+ single.places.first_mut().expect("place").duration = 1.;
+ Arc::new(single)
+ };
+
+ let activities = vec![
+ {
+ let mut activity = create_activity_with_job_at_location(create_delivery_with_duration("job1", 1), 1);
+ // One unit service [5..6], with waiting until departure at 8.
+ activity.schedule = DomainSchedule { arrival: 5., departure: 8. };
+ activity.place.duration = 1.;
+ activity
+ },
+ {
+ let mut activity = create_activity_with_job_at_location(create_delivery_with_duration("job2", 2), 2);
+ activity.schedule = DomainSchedule { arrival: 15., departure: 16. };
+ activity.place.duration = 1.;
+ activity
+ },
+ ];
+ let mut route = create_route_with_activities(&problem.fleet, "v1", activities);
+ route.tour.all_activities_mut().last().expect("last activity").schedule.arrival = 16.;
+ let reserved_times_index = vec![(
+ route.actor.clone(),
+ vec![ReservedTimeSpan {
+ // Break should be inserted as [6..8], touching job1 end boundary at 6.
+ time: TimeSpan::Window(TimeWindow::new(5., 6.)),
+ duration: 2.,
+ }],
+ )]
+ .into_iter()
+ .collect();
+
+ let tour = create_tour(&problem, &route, &coord_index, &reserved_times_index);
+
+ let break_count = get_ids_from_tour(&tour).into_iter().flatten().filter(|id| id == "break").count();
+ assert_eq!(break_count, 1, "expected exactly one break, got {break_count}, tour: {tour:?}");
+
+ let job1_stop = tour
+ .stops
+ .iter()
+ .find(|stop| stop.activities().iter().any(|activity| activity.job_id == "job1"))
+ .expect("expected to find stop with job1");
+ let job1_activity = job1_stop
+ .activities()
+ .iter()
+ .find(|activity| activity.job_id == "job1")
+ .expect("expected to find job1 activity");
+ let break_activity = job1_stop
+ .activities()
+ .iter()
+ .find(|activity| activity.activity_type == "break")
+ .expect("expected break at same stop as job1");
+
+ let job1_time = job1_activity.time.as_ref().expect("expected job1 time");
+ let break_time = break_activity.time.as_ref().expect("expected break time");
+ let job1_start = parse_time(&job1_time.start);
+ let job1_end = parse_time(&job1_time.end);
+ let break_start = parse_time(&break_time.start);
+ let break_end = parse_time(&break_time.end);
+
+ assert!((job1_start - 5.).abs() < 1e-9, "unexpected job1 start: {job1_start}, tour: {tour:?}");
+ assert!((job1_end - 6.).abs() < 1e-9, "unexpected job1 end: {job1_end}, tour: {tour:?}");
+ assert!((job1_end - job1_start - 1.).abs() < 1e-9, "job1 duration changed, tour: {tour:?}");
+ assert!((break_start - 6.).abs() < 1e-9, "unexpected break start: {break_start}, tour: {tour:?}");
+ assert!((break_end - 8.).abs() < 1e-9, "unexpected break end: {break_end}, tour: {tour:?}");
+ assert!(job1_end <= break_start + 1e-9, "job1 overlaps break at boundary, tour: {tour:?}");
+}
+
+#[test]
+fn can_align_break_to_activity_boundary_when_reserved_time_hits_mid_service() {
+ let (problem, mut coord_index) = create_test_problem_and_coord_index();
+ coord_index.add(&Location::Reference { index: 1 });
+
+ let create_delivery_with_duration = |id: &str, location: usize, duration: f64| {
+ let mut single = Arc::try_unwrap(create_single(id)).unwrap_or_else(|_| unreachable!());
+ let place = single.places.first_mut().expect("place");
+ place.location = Some(location);
+ place.duration = duration;
+ Arc::new(single)
+ };
+
+ let activities = vec![{
+ let mut activity = create_activity_with_job_at_location(create_delivery_with_duration("job1", 1, 3.), 1);
+ // Service duration is 3. Route departure includes extra break duration.
+ activity.schedule = DomainSchedule { arrival: 5., departure: 10. };
+ activity.place.duration = 3.;
+ activity
+ }];
+ let mut route = create_route_with_activities(&problem.fleet, "v1", activities);
+ route.tour.all_activities_mut().last().expect("last activity").schedule.arrival = 15.;
+ let reserved_times_index = vec![(
+ route.actor.clone(),
+ vec![ReservedTimeSpan {
+ // Break time [7..9] intersects service interval [5..8] and should be aligned to [8..10].
+ time: TimeSpan::Window(TimeWindow::new(7., 7.)),
+ duration: 2.,
+ }],
+ )]
+ .into_iter()
+ .collect();
+
+ let tour = create_tour(&problem, &route, &coord_index, &reserved_times_index);
+
+ let stop = tour
+ .stops
+ .iter()
+ .find(|stop| {
+ stop.activities().iter().any(|activity| activity.job_id == "job1")
+ && stop.activities().iter().any(|activity| activity.activity_type == "break")
+ })
+ .expect("expected to find job1 and break at same stop");
+
+ let job =
+ stop.activities().iter().find(|activity| activity.job_id == "job1").expect("expected to find job1 activity");
+ let brk = stop
+ .activities()
+ .iter()
+ .find(|activity| activity.activity_type == "break")
+ .expect("expected to find break activity");
+
+ let job_time = job.time.as_ref().expect("expected job time");
+ let break_time = brk.time.as_ref().expect("expected break time");
+ let job_start = parse_time(&job_time.start);
+ let job_end = parse_time(&job_time.end);
+ let break_start = parse_time(&break_time.start);
+ let break_end = parse_time(&break_time.end);
+
+ assert!((job_start - 5.).abs() < 1e-9, "unexpected job start: {job_start}, tour: {tour:?}");
+ assert!((job_end - 8.).abs() < 1e-9, "unexpected job end: {job_end}, tour: {tour:?}");
+ assert!((job_end - job_start - 3.).abs() < 1e-9, "job duration changed, tour: {tour:?}");
+ assert!((break_start - 8.).abs() < 1e-9, "unexpected break start: {break_start}, tour: {tour:?}");
+ assert!((break_end - 10.).abs() < 1e-9, "unexpected break end: {break_end}, tour: {tour:?}");
+ assert!(job_end <= break_start + 1e-9, "job overlaps break, tour: {tour:?}");
+}
diff --git a/vrp-pragmatic/tests/unit/validation/objectives_test.rs b/vrp-pragmatic/tests/unit/validation/objectives_test.rs
index 69dc52be9..b8ec708c2 100644
--- a/vrp-pragmatic/tests/unit/validation/objectives_test.rs
+++ b/vrp-pragmatic/tests/unit/validation/objectives_test.rs
@@ -130,7 +130,7 @@ fn can_detect_invalid_value_or_order_impl(value: Option, order: Option>, e
assert_eq!(result.err().map(|e| e.code), expected);
}
+
+parameterized_test! {can_detect_missing_min_tour_size_objective, (objectives, min_tour_size, expected), {
+ can_detect_missing_min_tour_size_objective_impl(objectives, min_tour_size, expected);
+}}
+
+can_detect_missing_min_tour_size_objective! {
+ case01_missing_objective: (Some(vec![
+ MinimizeUnassigned { breaks: None },
+ MinimizeCost,
+ ]), Some(2), Some("E1608".to_string())),
+ case02_has_objective: (Some(vec![
+ MinimizeUnassigned { breaks: None },
+ MinimizeTourSizeViolation,
+ MinimizeCost,
+ ]), Some(2), None),
+ case03_no_objectives_defined: (None, Some(2), None),
+ case04_no_min_tour_size: (Some(vec![
+ MinimizeUnassigned { breaks: None },
+ MinimizeCost,
+ ]), None, None),
+ case05_zero_min_tour_size: (Some(vec![
+ MinimizeUnassigned { breaks: None },
+ MinimizeCost,
+ ]), Some(0), None),
+}
+
+fn can_detect_missing_min_tour_size_objective_impl(
+ objectives: Option>,
+ min_tour_size: Option,
+ expected: Option,
+) {
+ let problem = Problem {
+ fleet: Fleet {
+ vehicles: vec![VehicleType {
+ limits: min_tour_size.map(|size| VehicleLimits {
+ max_distance: None,
+ max_duration: None,
+ tour_size: None,
+ min_tour_size: Some(size),
+ }),
+ ..create_default_vehicle_type()
+ }],
+ ..create_default_fleet()
+ },
+ objectives,
+ ..create_empty_problem()
+ };
+ let coord_index = CoordIndex::new(&problem);
+ let ctx = ValidationContext::new(&problem, None, &coord_index);
+ let objectives = get_objectives(&ctx).unwrap_or_default();
+
+ let result = check_e1608_vehicles_with_min_tour_size_but_no_objective(&ctx, objectives.as_slice());
+
+ assert_eq!(result.err().map(|e| e.code), expected);
+}
diff --git a/vrp-pragmatic/tests/unit/validation/vehicles_test.rs b/vrp-pragmatic/tests/unit/validation/vehicles_test.rs
index 8606759e6..b8b29d04c 100644
--- a/vrp-pragmatic/tests/unit/validation/vehicles_test.rs
+++ b/vrp-pragmatic/tests/unit/validation/vehicles_test.rs
@@ -45,7 +45,7 @@ fn can_detect_zero_costs_impl(costs: (Float, Float), expected: Option) {
let problem = Problem {
fleet: Fleet {
vehicles: vec![VehicleType {
- costs: VehicleCosts { fixed: None, distance, time },
+ costs: VehicleCosts { fixed: None, distance, time, span: None },
..create_default_vehicle_type()
}],
..create_default_fleet()
@@ -64,8 +64,8 @@ parameterized_test! {can_handle_rescheduling_with_required_break, (latest, expec
}}
can_handle_rescheduling_with_required_break! {
- case01: (None, Some("E1307".to_string())),
- case02: (Some(1.), Some("E1307".to_string())),
+ case01: (None, None),
+ case02: (Some(1.), None),
case03: (Some(0.), None),
}
@@ -92,13 +92,10 @@ fn can_handle_rescheduling_with_required_break_impl(latest: Option, expec
..create_empty_problem()
};
- let result = check_e1307_vehicle_offset_break_rescheduling(&ValidationContext::new(
- &problem,
- None,
- &CoordIndex::new(&problem),
- ));
+ let result = validate_vehicles(&ValidationContext::new(&problem, None, &CoordIndex::new(&problem)));
- assert_eq!(result.err().map(|err| err.code), expected);
+ let error_code = result.err().and_then(|err| err.errors.first().map(|err| err.code.clone()));
+ assert_eq!(error_code, expected);
}
parameterized_test! {can_handle_reload_resources, (resources, expected), {