Replies: 3 comments
-
|
Hello @fodzal! Interesting idea - we will review and get back to you. Please understand that this may take a while, as we'll need to look at this very carefully. |
Beta Was this translation helpful? Give feedback.
-
|
So if I am understanding you correctly:
From the sounds of things, this optimization is an extension of
With this new graph structure, it would behave similar, except:
The proposed optimization is valid, but I am pretty sure we don't support accessing a shadow variable from a What did you put as For your open questions:
A contribution would be welcome, but based on your description, it seems to be using a feature we do not support yet (accessing a shadow variable via a planning list variable), and hence such a contribution would need to wait for that feature to be supported. |
Beta Was this translation helpful? Give feedback.
-
|
Thanks, this is really helpful — and your reading of the structure is exactly right. Let me answer your My shadow-variable graphTwo entity classes.
@InverseRelationShadowVariable(sourceVariableName = "visits")
Vehicle vehicle;
@PreviousElementShadowVariable(sourceVariableName = "visits")
Visit previousVisit;
@ShadowSources({"vehicle", "vehicle.previousEndPosition", "previousVisit", "previousVisit.endServiceTime"})
LocalDateTime arrivalTime;
@ShadowSources({"vehicle", "arrivalTime", "previousVisit"})
LocalDateTime startServiceTime;
@ShadowSources({"vehicle", "startServiceTime", "previousVisit"})
LocalDateTime endServiceTime;
@ShadowSources({"visits"})
Visit lastVisit;
@ShadowSources({"lastVisit", "visits"}) // body reads lastVisit.getEndServiceTime()
LocalDateTime endTime;
@ShadowSources({"previousVehicles[].endPosition", "lastVisit", "endTime", "visits"})
Collection<LocationDeparture> endPosition;
@ShadowSources({"previousVehicles[].endPosition"})
Collection<LocationDeparture> previousEndPosition;
The Vehicle ← Visit shadow edge does not actually exist (on purpose)This is the key point. The dependency "a Vehicle's I left it undeclared deliberately, because the dependency cannot be expressed at all — both natural spellings fail-fast in
So today I simply cannot express "this owner depends on its list elements' shadows". Instead, my fork hard-codes that propagation. That's exactly why it works for me but isn't a clean contribution as-is: on stock Timefold, the missing back-edge means the owner's aggregate shadow is never ordered after, nor retriggered by, its values, so it is computed incorrectly. My fork sidesteps that with the hard-coded ordering; stock currently can't express it. So yes — "shadow via a list variable" is the missing primitive, and it has to come firstYou are right, the missing piece is precisely that back-edge: once the cascade over a list owner's values is finished, trigger the owner's shadows (and from there the next owner). Declaratively that's I didn't explore on what to do in the Timefold repo. Do you think it's easy to setup? Then the optimization PR (
|
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Proposal: a dedicated
GraphStructurefor "chained entity + fixed-entity DAG" (multi-entity declarative shadow variables)Summary
I'd like to propose recognizing a new graph structure for declarative shadow variables: models with two entity classes, where one class forms chains via a single directional parent (
PREVIOUS/NEXT) and the other forms a fixed, acyclic DAG that consumes the chain's results and pushes them forward.Today this case is classified as
ARBITRARYand pays a full Tarjan SCC recompute on every move, even though the structure is fully known up front and contains no real cycles. I already have a working fork that handles it as an O(k) chain-walk + a pre-computed DAG order, and I'd like to know whether a cleaned-up version would be welcome upstream.My use case
In my VRP, I have successive
VehicleShifts. Propagation flows along the visits of one shift, and the end state of that shift (end time, end location, last visit, …) must be pushed to the next shift, which itself does not move — the shift-to-shift ordering is a problem fact.This pattern models several real needs:
I model it with declarative shadow variables (
@ShadowVariablesupplier methods) on two entity classes:Visit):arrivalTime/departureTime, computed from the previous element (aPREVIOUSsource). Visits are a@PlanningListVariableon each shift.VehicleShift):lastVisit/endTime/endLocation, plus a "previous shift"GROUPsource that carriesshiftA.end → shiftB.start.So the instance-level dependency graph is: chains within each shift + an acyclic DAG across shifts (
A → B → …). No cycles, fully determined by the model and the problem facts.The problem
As soon as there are 2+ entity classes with declarative shadow variables,
GraphStructure.determine()classifies the model asARBITRARY:That routes everything through
DefaultVariableReferenceGraph, whoseinnerUpdateChanged()runs, on every move:The propagation step (
AffectedEntitiesUpdater) is already incremental and short-circuits on no-change. The cost is thecommitChanges()call: a full Tarjan SCC recompute over the whole instance graph, O(V + E) per move.For my models (large number of visits) this dominates runtime — it's the difference between unusable and fine. And it's pure overhead here: there are no cycles to detect, and the structure never changes shape, only the dynamic
PREVIOUS/NEXTedges do.This is the same kind of situation the existing
SINGLE_DIRECTIONAL_PARENTstructure already optimizes — but that one only applies to a single entity class.What I've built (working fork)
A new structure
MULTI_ENTITY_SINGLE_DIRECTIONAL_PARENTand aMultiEntityChainedVariableReferenceGraphthat handles exactly this shape, with no generic instance graph and no Tarjan.Classification (once, at build time) — in
GraphStructure.determine(), the multi-entity case is accepted when:INDIRECTsources;PREVIOUS/NEXT) on one entity class (the "chained" entity);PREVIOUS/NEXT/NO_PARENT(+INVERSEallowed only if it targets the fixed entity, e.g.visit.vehicleShift);VARIABLE/GROUP/NO_PARENT.Anything outside these preconditions falls back to
ARBITRARYsafely.Fixed-entity DAG order — computed once via Kahn's algorithm from the
GROUPsources (A → Bedges), stored as a fixed topological order.Per move:
PriorityQueue(handles DAG merges correctly);GROUP-only, e.g.previousEndPosition) → chain-walk the visits in list order (O(k), with change-detection short-circuit) → post-chain updaters (lastVisit/endTime/endLocation);Result: no Tarjan, no per-node instance graph, no SCC/looped tracking. Complexity per move ≈ O(k + V_affected · log V), where
k= affected chain length andV_affected= number of shifts whose shadows changed.The fork also handles two correctness details that come with walking the chain directly: resetting shadow values on unassign (undo), and not terminating the walk early when there are non-contiguous dirty visits on the same shift after a swap.
Open questions before I clean this up for a PR
ARBITRARYfallback acceptable as a first step?I'm happy to open a draft PR, share the fork, and provide benchmarks on a representative model. Would a contribution along these lines be welcome?
Beta Was this translation helpful? Give feedback.
All reactions