Repository navigation
Conversation
…in node in the declarative graph A planning list variable model with declarative variables on both the list entity and its elements is classified ARBITRARY: for example a vehicle whose end time is sourced from its visits, or a multi-trip model where a trip starts where the previous one ends. The arbitrary graph keeps a node per element and variable, adds and removes edges on every list change and recomputes its topological order, even though an element only ever reads its chain, itself and its list entity. This generalizes SINGLE_DIRECTIONAL_PARENT: the walk of the elements along their list is taken out of the graph when the model allows it, and the rest of the graph stays as it is. Each list entity gets a single chain node for its elements, ordered after the list entity's variables its elements read (pre-chain) and before the ones sourced from its elements (post-chain). Detection happens in two steps: a shape check in GraphStructure (new structure LIST_CHAIN), then a build-time loop check that falls back to ARBITRARY when the chain node's overapproximated edges would close a loop the arbitrary graph would not. Graph construction skips the list element locators entirely, so no per-element edge is ever added; the chain node carries fixed edges from its declarative pre-chain variables and to its post-chain variables. Ports the multi-entity chain domains and tests: fact-collection and plain-fact vehicle chains, a next-directional model, rebasing elements, non-contiguous dirty elements, fixed loop fail-fast, and fallback models. A seeded randomized differential test and a call-count test (every affected supplier recomputed exactly once per update) run against an explicit ARBITRARY recomputation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s changed elements, consolidate its test coverage Leaves the shared declarative graph code as it was wherever the chain node does not need it, and documents what the chain node's comments rely on. Strengthens the event coverage (the chain node count test now sends the same list change events a real move sends), removes list chain tests that other tests already cover (fallback domains, the next-directional solve the differential test already covers, duplicated test cases), and names the remaining test domains after their package and what they test.
…re detection, track its changes in the graph Folds the chain detection into the existing GraphStructure detection instead of a separate pass, and shares one updater list among the chain nodes instead of each owning its own. Finds the unassigned elements an update recomputed in constant time, and tracks the chain's changes in the graph itself rather than in the updater.
A pre-chain variable can be a genuine (planning) variable of the list entity rather than a declarative one, for example a vehicle's departure time: its change alone marks the chain node directly, with no node of its own, so the elements that read it through their inverse no longer need a separate domain and test class. Builds the arbitrary graph the fixed point assertion compares against once per solution class rather than once per test, since the solution descriptor is the expensive part of the comparison. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The node walks a list entity's chain, which the pre-chain and post-chain variables already refer to; one term for both reads better than two. Adds the chain node's edges to the builder before testing them for a loop, returns the list chain structure from its detection instead of a boolean, and clears a looped chain's elements in list order. Covers the genuine pre-chain variable with the canonical list chain domain. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…lement Hoists the owner-to-chain-node map out at construction, marks dirty chain nodes once per owner after classification, and records a changed range only through the after list-variable event, halving the classification work. The chain node walks its elements by list index instead of through the list variable state, and recomputes each unassigned element once per update. Detects a pre-chain change by value instead of by notifier, and leaves the loop decision (fail-fast vs. fall back to ARBITRARY) to the build rather than the updater. A dependency loop that an update gives up on keeps its dirty chains, and walking the whole chain of a list entity that leaves such a loop reports every element the loop runs through, matching the arbitrary graph. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… chain graph Setting a new working solution builds its graph while the previous one still receives the events, among them the consistency of the new list entities, reported through their first declarative variable, which may be a pre-chain one. Every solve sets two working solutions, so it failed on such a model. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Declares that a list chain state lookup may find none instead of assuming it always does. Keeps the single directional parent's early termination check as upstream: the chain node gets its own check, which ignores the inverse sources, since a change of the pre-chain variable they read must walk the whole chain. Derives the chain element class from the directional parent's metamodel instead of carrying it as a separate, often-null component. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Keep the chain node in its list chain state instead of a second map, drop the list entity class check the chain state lookup already makes, update the list chain graph without a finally, as the graphs it wraps, and walk a new list chain whole through its stale flag instead of a sentinel. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
@fodzal Thanks for the PR! This is quite a sizable one, here's how I propose we deal with it:
Unfortunately, you picked a part of the codebase which is undergoing changes relatively often; for example, I already have a PR out which is part perf improvement, part bugfix. Due to its bugfix nature, it will be merged soon, before your PR and therefore causing you merge conflicts. The only way how I see us avoiding these issues going forward is to discuss what our plans are and what your plans are, and figuring out a way to not interfere with each other's work too much. I am open to that conversation. |
|
Thanks @triceo, that works for me.
|
|
I have merged the second-to-last change to shadows that I had planned. I have also submitted the final PR where I intend to change that area: So, after that, this PR can be safely rebased and finished. |
New version of this PR
This PR replaces the list element cascade (#2548). The detected
models are the same; the update is new.
graph again, until nothing changed; a notifier wrapper flagged the chains to re-walk, and a post-chain
variable could be computed twice. Here a chain is a node of the graph, so a single topological pass
computes every affected supplier once.
The production code is a bit bigger than for the former PR; I think it's better like this but let me know if you prefer the another one!
Why
Follow-up to #2357 and #2506. A planning list variable model with declarative variables on both the list
entity and its elements is classified
ARBITRARY: for example a vehicle whose end time is sourced from itsvisits (
"visits[].endServiceTime"), or a multi-trip model where a trip starts where the previous one ends.The arbitrary graph then keeps a node per element and variable, adds and removes edges on every list
change and recomputes its topological order. The elements outnumber the other entities by far, yet their
dependencies need no graph: an element only reads its chain, itself and its list entity.
What
A generalization of
SINGLE_DIRECTIONAL_PARENT: the walk of the elements along their list is taken out ofthe graph when the model allows it, and the rest of the graph stays as it is. Each list entity gets a single
chain node for its elements, ordered after the list entity's variables its elements read (pre-chain,
vehicle.startTime) and before the ones sourced from its elements (post-chain,"visits[].endServiceTime"):The graph shrinks from
O(elements)toO(list entities)nodes and loses all per-element edges. In themeasured models no dynamic edge is left, so it is a
FixedVariableReferenceGraph.How
Detection, in two steps.
GraphStructure(new structureLIST_CHAIN): the elements have a singleprevious/nextparent, and their sources stay in their chain, on themselves, or on a declarative orgenuine variable of their list entity reached through their inverse; the other classes reach the elements only
through
"visits[].x"; the list entity has declarative variables; no alignment key. Anything else keepstoday's structure.
against the fixed edges, and falls back to
ARBITRARYwhen they close a loop. A genuine fixed loop stillfails fast, in the arbitrary graph's build, with its usual message.
Without a score director or without any list entity, the structure is built as
ARBITRARYtoo.Graph construction.
machinery. The list element locators are skipped, so no per-element edge is ever added.
post-chain variables. A genuine pre-chain variable (
vehicle.departureTime, a planning variable) has nonode: its change marks the chain node directly.
VariableUpdateris extracted fromVariableUpdaterInfoso that a node can carry the chain updater.
Graph traversal.
updateChanged()pass in topological order, as today. Beforehand, a thin wrapperrecords the elements whose sources changed, classifies them by list entity (as list indexes), and marks
those chain nodes changed.
SINGLE_DIRECTIONAL_PARENTdoes: from eachchanged element, in chain order, until an element comes out unchanged. It walks the whole chain when a
pre-chain value changed (any element may read it) or when the list changed without any element recording
itself (the forced update of
FULL_ASSERT). Its return value propagates to the post-chain variablesthrough the graph's edges.
them.
getVariableLoops()reports the elements, as the arbitrary graph does.Note:
SINGLE_DIRECTIONAL_PARENTSINGLE_DIRECTIONAL_PARENTcould be replaced by this graph: routing its models to chain nodes measured11 % to 29 % faster. Most of the gain comes from walking
intlist indexes, following #2660, rather thanfollowing and sorting elements through the list variable state, which
SINGLE_DIRECTIONAL_PARENTcouldadopt directly. This is optional, so
SINGLE_DIRECTIONAL_PARENTis left unchanged here.Possible follow-ups
Other shapes can be covered with little code, for example elements whose sources are variables of another
list entity than their own. Or groups (alignment key) which stays in the same list entity.
Design choices
VariableReferenceGraphBuilder.addVariableReferenceEntitycreates one node per group key and entity,and the other updaters' keys are the
intgroup ids thatgetGroupVariableUpdaterInfoMapallocatescounting up from zero. A reserved id such as
-1works, but only as long as that allocation neverproduces it: an invariant held by another method, which a change there would break silently, by merging
a chain node into another updater's node. A nullable id, meaning "no group", was also tried; it adds
null branches to the shared builder. The metamodel cannot collide with an
Integer, whatever the idsare, and it is already the chain node's
id(), the key it is looked up by. The cost is the builder'smap, now keyed by
Objectrather thanInteger.Testing
departure time changes) and a
FULL_ASSERTsolve with every list move, both against an explicitlyarbitrary recomputation.
elements between its changes alone; unassigned elements are computed once.
the whole route.
pre-chain variable.
equal to the arbitrary graph's; an update given up on a loop, then another move without undo.
GraphStructureTest: every accepted shape and every rejection; the build-time fallback and fail-fast.ListElementShadowVariableTest(feat: support sourcing declarative shadow variables from planning list variable elements #2506) runs on the chain graph unchanged.Note:
DeclarativeShadowVariableAssertionsThis PR adds a test helper, used by the chain tests only: a fixed point assertion, a random list move
generator, and a
FULL_ASSERTsolve with every list move. The assertion recomputes the declarativevariables with an explicitly
ARBITRARYgraph and compares them to the incremental ones.FULL_ASSERTrecomputes with the chain graph itself, so its reference depends on the code under test;the
ARBITRARYgraph does not.Benchmark
Local harness, not part of this PR: a fresh JVM per arm and scenario, a discarded warm-up repetition, the
same seed and move count limit on every arm; a checksum over every shadow variable is identical across
arms. Arms:
main(14249b71d7), the cascade (this PR's previous head,3a85ed155d), and this PR.mainmoves/smainpairedTripspairedTripsSmalltripChainsmanyOwnerslongRoutesWindowsclassicAggregateSINGLE_DIRECTIONAL_PARENT, non-list