feat: implement multi transport types for the same dataset - #2579
diogodanielsoaresferreira wants to merge 13 commits into
Conversation
|
Add a TransportType value type and per-mode travel-time/distance matrices on Location, so the same routing problem can use more than one transport profile (e.g. car + bike). The default CAR mode keeps the existing scalar/ timeframe fields and index-cache fast path; extra modes are stored in opt-in maps that stay null for single-mode problems. The TravelTimeMatrixEnricher fetches one matrix set per transport type (each resolving to its own OSRM instance), and map-service.transport-type now accepts a comma-separated list of modes (first entry is primary). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ea51de8 to
862e09d
Compare
This comment has been minimized.
This comment has been minimized.
…type only in solver
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
Warning Threat Detection Engine Failure — The analysis engine could not complete. This is a tooling failure, not a security finding. What happenedThe threat detection engine failed to produce results. Review the workflow run logs for details. gh-aw-workflow-id: solver-docs-drift-review The documentation drift reported earlier is gone; this pull request now covers the transport-type API and multi-transport enrichment behavior.
|
| @Schema(description = "The type of transport used (car, bicycle, ... ).") | ||
| public enum TransportType { | ||
|
|
||
| CAR("car"), |
There was a problem hiding this comment.
Do we need the lower-case value? If we drop it, we don't need to override the enum methods nor creating the constructor.
|
|
||
| @JsonCreator | ||
| public static TransportType of(String value) { | ||
| Objects.requireNonNull(value, "TransportType value must not be null."); |
There was a problem hiding this comment.
Compared to the constructor, we don't check if the value is empty.
But see my proposal about simplifying the enum, which would leave only this method and eliminate the constructor.
| String maxDistanceFromRoadOption = maxDistanceFromRoad.map(MapServiceOptions::getMaxDistanceFromRoadOption).orElse(""); | ||
| String transportTypeOption = transportType.map(MapServiceOptions::getTransportTypeOption).orElse(""); | ||
| String transportTypeOption = transportType == null || transportType.isAutoSelect() | ||
| ? "" |
There was a problem hiding this comment.
So, for the map service, the default is auto-select?
|
|
||
| public String getOptions(TransportType transportType) { | ||
| return getOptions((String) null, transportType); | ||
| } |
There was a problem hiding this comment.
Instead of adding overloaded methods and passing nulls to override what's coming from the configuration parameters, we may decouple the config from these getOptions() parameters by introducing a builder that registers the overrides and only then builds the options String.
| List<Location> getLocations(); | ||
|
|
||
| default List<TransportType> getTransportTypes() { | ||
| return List.of(); |
There was a problem hiding this comment.
If this leads to auto-select, let's return it here already; it will become more obvious.
| A deployment may additionally be restricted to a subset of transport types with | ||
| `timefold.platform.map-service.allowed-transport-types`, a comma-separated list; when it is not set, every transport | ||
| type is allowed. |
There was a problem hiding this comment.
Who restricts that?
I assume this comes as a platform config, in which case the model developer cannot much influence that if they don't run the model locally.
| If your model stores matrices itself rather than relying on the enricher, the setters take the transport type too: | ||
|
|
||
| [source,java,options="nowrap"] | ||
| ---- | ||
| location.setTravelTimeMatrix(TransportType.BICYCLE, travelTimeMatrix); | ||
| location.setDistanceMatrix(TransportType.BICYCLE, distanceMatrix); |
There was a problem hiding this comment.
Let's not expose this as an API. We should take care of the matrices, not the user.
| Each of those transport types is validated against the allowed transport types and then fetched in its own map service | ||
| request, so a dataset using two transport types results in two requests and two sets of matrices per `Location`. | ||
| A dataset that uses a transport type the deployment is not allowed to route with is rejected before any request is made. | ||
| When the solution returns an empty list, the default transport type is used. | ||
|
|
||
| One of the transport types is the *primary* one: the configured one when it is fixed, otherwise `car` if the dataset | ||
| uses it, and the first one in declaration order otherwise. The primary transport type is special in two ways: | ||
|
|
||
| * The model-level map metadata — the locations that are not in the map, and the resolved map region — comes from its | ||
| request only. | ||
| * Its matrices also back the lookups that do not name a transport type, so those keep answering on a deployment | ||
| configured for a single non-default transport type. |
There was a problem hiding this comment.
Why should the user care? This text explains how we implement the transport types, but that's the complexity we are trying to remove from the user.
| private DistanceMatrix travelTimeMatrixForMode(TransportType transportType, OffsetDateTime departureTime) { | ||
| if (isDefaultMode(transportType)) { | ||
| return hasTimeframeMatrices(travelTimesByTimeframe) | ||
| ? resolveTimeframeMatrix(travelTimesByTimeframe, departureTime, "travel time") | ||
| : travelTimeMatrix; | ||
| } | ||
| DistanceMatrix[] byTimeframe = travelTimesByTimeframeByMode == null | ||
| ? null | ||
| : travelTimesByTimeframeByMode[transportType.ordinal()]; | ||
| if (byTimeframe != null && timeframeIndexResolver != null) { | ||
| return resolveTimeframeMatrix(byTimeframe, departureTime, "travel time"); | ||
| } | ||
| return travelTimeMatrixByMode == null ? null : travelTimeMatrixByMode[transportType.ordinal()]; | ||
| } | ||
|
|
||
| private DistanceMatrix distanceMatrixForMode(TransportType transportType) { | ||
| if (isDefaultMode(transportType)) { | ||
| return distanceMatrix; | ||
| } | ||
| return distanceMatrixByMode == null ? null : distanceMatrixByMode[transportType.ordinal()]; | ||
| } | ||
|
|
||
| private DistanceMatrix distanceMatrixForMode(TransportType transportType, OffsetDateTime departureTime) { | ||
| if (isDefaultMode(transportType)) { | ||
| return hasTimeframeMatrices(distancesByTimeframe) | ||
| ? resolveTimeframeMatrix(distancesByTimeframe, departureTime, "distance") | ||
| : distanceMatrix; | ||
| } | ||
| DistanceMatrix[] byTimeframe = distancesByTimeframeByMode == null | ||
| ? null | ||
| : distancesByTimeframeByMode[transportType.ordinal()]; | ||
| if (byTimeframe != null && timeframeIndexResolver != null) { | ||
| return resolveTimeframeMatrix(byTimeframe, departureTime, "distance"); | ||
| } | ||
| return distanceMatrixByMode == null ? null : distanceMatrixByMode[transportType.ordinal()]; | ||
| } |
There was a problem hiding this comment.
According to SonarCloud, these methods that resolve the distance matrix are not covered by tests. We likely miss more tests that would indirectly trigger them.
| return travelTimeMatrixByMode == null ? null : travelTimeMatrixByMode[transportType.ordinal()]; | ||
| } | ||
|
|
||
| private DistanceMatrix travelTimeMatrixForMode(TransportType transportType, OffsetDateTime departureTime) { |
There was a problem hiding this comment.
Since we are adding more logic to resolve the matrix on the hot path, do we have any benchmarks showing the impact on move evaluation speed for multiple vs. a single transport type?


Allow to solve routing problems with more than one transportType.
Created additional properties in Location so that we keep the hot path without the Map access.
Related with https://github.com/TimefoldAI/timefold-platform/issues/2690