-
Notifications
You must be signed in to change notification settings - Fork 109
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Declarative Shadow Variables #1421
base: main
Are you sure you want to change the base?
feat: Declarative Shadow Variables #1421
Conversation
e97e05d
to
f478a44
Compare
Before I do a proper review, I suggest we do some house-keeping first. (Otherwise comments would get lost when files are being renamed and moved around.)
|
core/src/main/java/ai/timefold/solver/core/impl/domain/entity/descriptor/EntityDescriptor.java
Outdated
Show resolved
Hide resolved
@@ -34,7 +49,9 @@ | |||
public final class VariableListenerSupport<Solution_> implements SupplyManager { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to be careful here. This feature should have no performance impact when disabled. I'll run some numbers to confirm that.
...in/java/ai/timefold/solver/core/impl/domain/variable/provided/AbstractVariableReference.java
Outdated
Show resolved
Hide resolved
...main/java/ai/timefold/solver/core/impl/domain/variable/provided/ChangedVariableNotifier.java
Outdated
Show resolved
Hide resolved
}); | ||
} | ||
|
||
public static <Solution_> ChangedVariableNotifier<Solution_> of(InnerScoreDirector<Solution_, ?> scoreDirector) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And this can be stored on a score director, meaning there would be a 1:1 mapping between these instances and score directors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The issue with storing it on the ScoreDirector is that the entire ScoreDirector would then need to be mocked to be mocked to implement tests. The class exists to abstract away the before...Change
and after...Changed
method calls (tests do not care, but they are required when solving).
...ava/ai/timefold/solver/core/impl/domain/variable/provided/DefaultGroupVariableReference.java
Outdated
Show resolved
Hide resolved
...a/ai/timefold/solver/core/impl/domain/variable/provided/DefaultShadowCalculationBuilder.java
Outdated
Show resolved
Hide resolved
...i/timefold/solver/core/impl/domain/variable/provided/InvalidityMarkerVariableDescriptor.java
Outdated
Show resolved
Hide resolved
.../main/java/ai/timefold/solver/core/preview/api/variable/provided/GroupVariableReference.java
Outdated
Show resolved
Hide resolved
@@ -0,0 +1,13 @@ | |||
package ai.timefold.solver.core.preview.api.variable.provided; | |||
|
|||
public interface ShadowVariableSession { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My understanding of this API is still limited, but do we actually need this public API anywhere in the solver? IMO this is internal in core, and only exposed in the tests; therefore I'd make this public in the tests, not in the solver itself.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It can be moved; it is here for convenience when testing the prototype. (core internally depends on the default implementation which has before/after methods).
import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; | ||
import ai.timefold.solver.core.impl.domain.variable.provided.MockShadowVariableSessionFactory; | ||
|
||
public interface ShadowVariableSessionFactory { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dtto.
Hypothetical scenario:
Do they all become part of the same graph? In other words - is the graph limited to one shadow provider, or is it built from all shadow providers combined, and then split into individual independent components? |
Although untested, it is built from all shadow providers combined, and then split into individual independent components. Defining the same shadow variable twice is an error though; (you can use it multiple time, but cannot declare it multiple times). |
Please add this to the list of things that need to be tested. |
|
||
@Target({ METHOD, FIELD }) | ||
@Retention(RUNTIME) | ||
public @interface InvalidityMarker { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will this mark any type of invalidity or only loops?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any invalidity caused by loops (so a variable in a cycle OR a variable that depends on a variable in a cycle). Any type of invalidity is too vague; some people will consider null to be a valid values, others would not.
import org.jspecify.annotations.Nullable; | ||
|
||
@NullMarked | ||
public interface ShadowVariableSession { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems similar to ConstraintVerifier, so the naming should be similar.
Does the ConstraintVerifier API have anything that is a Session?
return new MockShadowVariableSessionFactory<>(solutionDescriptor, shadowVariableProvider); | ||
} | ||
|
||
ShadowVariableSession forEntities(Object... entities); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Feels similar to ConstraintVerifier.given(), so maybe rename it to "given"?
import org.jspecify.annotations.NullMarked; | ||
|
||
@NullMarked | ||
public interface ShadowVariableSessionFactory { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe rename to something like ShadowVariableVerifier, as it serves a a simular purpose?
|
||
import org.jspecify.annotations.NonNull; | ||
|
||
public class TestdataFSRAssertionConstraintProvider implements ConstraintProvider { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is FSR being copy pasted into the solver test sources?
Or is it a subset?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A heavily simplified subset.
Provided shadow variables work by calculating the topological order of each shadow variable. The nodes in the graph are paths to each shadow variable, binded to a particular entity instance. - The path `e1:Entity.#id.a` is the source path for the shadow variable `a` on entity e1 - If `e2.previous = e1`, then the path `e2:Entity.#id.#previous.a` is an alias path for the shadow variable `a` on e1. - The path can have multiple parts; like `e1:Entity.#id.#previous.#previous.a`. In this case, `e1:Entity.#id.#previous` is the parent of `e1:Entity.#id.#previous.#previous`. The edges in the graph are the dependencies for each shadow variable. - There is a fixed edge from the parent to each of its children. (i.e. `e1:Entity.#id.#previous` -> `e1:Entity.#id.#previous.a`) - There is a fixed edge from the direct dependencies of a shadow variable to the shadow variable. (i.e. `e1:Entity.#id.#previous.readyTime` -> `e1:Entity.#id.#startTime`) - There is a dynamic edge from each shadow variable to all its aliases. (i.e. `e1:Entity.#id.startTime` -> `e2:Entity.#id.#previous.startTime`, if e1 is the previous of e2.) Tarjan's algorithm is used to calculate the topological order of each node. Once the topological order of each node is known, to update from a set of changes: 1. Pick a changed node with the minimum topological order that was not visited. 2. Update the changed node. 3. If the value of the node changed, marked all its children as changed.
- Only facts of entities can be group - If e is in a group on g, then there is an edge from `e:Entity.#id.shadow` to `g:Entity.#id.group.shadow`.
ESC is consistent with Assertion ESC for visit group if we recalculate everything from scratch, which implies the graph edges/topological order is correct, but not everything that is changed is marked as changed.
… in ESC VisitGroups are now working on FULL_ASSERT
067498d
to
77dae18
Compare
|
VariableListeners are notoriously difficult to write, especially when you have multiple variable depending on each others.
Declarative shadow variables fixes this by providing a declarative way to define the dependencies of your shadow variables and how to calculate them.
First, annotate your variables as
@ProvidedShadowVariable
:Next, create a
ShadowVariableProvider
to define your variables:and finally, write tests to ensure your shadow variables work correctly:
Behind the scenes, Timefold Solver calculates a valid topological order for each of your shadow variables.
This allows Timefold Solver to:
Currently a WIP: