Skip to content

Commit 132a232

Browse files
authored
feat: Improve @CascadingUpdateShadowVariable logic (#1020)
Enhances the logic of the shadow variable @CascadingUpdateShadowVariable. The new approach eliminates the need to define sources and automatically runs the listener at the end of the events lifecycle.
1 parent a726146 commit 132a232

File tree

60 files changed

+479
-3495
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+479
-3495
lines changed

.github/workflows/downstream_python_quickstarts.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ jobs:
4343

4444
# Need to check for stale repo, since Github is not aware of the build chain and therefore doesn't automate it.
4545
- name: Checkout timefold-quickstarts (PR) # Checkout the PR branch first, if it exists
46-
id: checkout-quickstarts
46+
id: checkout-quickstarts-pr
4747
uses: actions/checkout@v4
4848
continue-on-error: true
4949
with:
@@ -52,7 +52,7 @@ jobs:
5252
path: ./timefold-quickstarts
5353
fetch-depth: 0 # Otherwise merge will fail on account of not having history.
5454
- name: Checkout timefold-quickstarts (development) # Checkout the development branch if the PR branch does not exist
55-
if: steps.checkout-solver-quickstarts.outcome != 'success'
55+
if: steps.checkout-quickstarts-pr.outcome != 'success'
5656
uses: actions/checkout@v4
5757
with:
5858
repository: TimefoldAI/timefold-quickstarts

core/src/main/java/ai/timefold/solver/core/api/domain/variable/CascadingUpdateShadowVariable.java

Lines changed: 16 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -3,54 +3,37 @@
33
import static java.lang.annotation.ElementType.FIELD;
44
import static java.lang.annotation.RetentionPolicy.RUNTIME;
55

6-
import java.lang.annotation.Repeatable;
76
import java.lang.annotation.Retention;
87
import java.lang.annotation.Target;
98

10-
import ai.timefold.solver.core.api.domain.entity.PlanningEntity;
11-
129
/**
13-
* Specifies that field may be updated by the target method when one or more source variables change.
10+
* Specifies that a field may be updated by the target method when any of its variables change, genuine or shadow.
11+
* <p>
12+
* Automatically cascades change events to the subsequent elements of a {@link PlanningListVariable}.
13+
* <p>
14+
* A single listener is created
15+
* to execute user-defined logic from the {@code targetMethod} after all variable changes have been applied.
16+
* This means it will be the last step executed during the event lifecycle.
17+
* <p>
18+
* It can be applied in multiple fields to update various shadow variables.
19+
* The user's logic is responsible for defining the order in which each variable is updated.
20+
* <p>
21+
* Distinct {@code targetMethod} can be defined, but there is no guarantee about the order in which they are executed.
22+
* Therefore, caution is required when using multiple {@code targetMethod} per model.
1423
* <p>
15-
* Automatically cascades change events to {@link NextElementShadowVariable} of a {@link PlanningListVariable}.
24+
* Except for {@link PiggybackShadowVariable},
25+
* the use of {@link CascadingUpdateShadowVariable} as a source for other variables,
26+
* such as {@link ShadowVariable}, is not allowed.
1627
* <p>
1728
* Important: it must only change the shadow variable(s) for which it's configured.
18-
* It is only possible to define either {@code sourceVariableName} or {@code sourceVariableNames}.
19-
* It can be applied to multiple fields to modify different shadow variables.
2029
* It should never change a genuine variable or a problem fact.
2130
* It can change its shadow variable(s) on multiple entity instances
2231
* (for example: an arrivalTime change affects all trailing entities too).
2332
*/
2433
@Target({ FIELD })
2534
@Retention(RUNTIME)
26-
@Repeatable(CascadingUpdateShadowVariable.List.class)
2735
public @interface CascadingUpdateShadowVariable {
2836

29-
/**
30-
* The source variable name.
31-
*
32-
* @return never null, a genuine or shadow variable name
33-
*/
34-
String sourceVariableName() default "";
35-
36-
/**
37-
* The source variable name.
38-
*
39-
* @return never null, a genuine or shadow variable name
40-
*/
41-
String[] sourceVariableNames() default {};
42-
43-
/**
44-
* The {@link PlanningEntity} class of the source variable.
45-
* <p>
46-
* Specified if the source variable is on a different {@link Class} than the class that uses this referencing annotation.
47-
*
48-
* @return {@link CascadingUpdateShadowVariable.NullEntityClass} when the attribute is omitted
49-
* (workaround for annotation limitation).
50-
* Defaults to the same {@link Class} as the one that uses this annotation.
51-
*/
52-
Class<?> sourceEntityClass() default CascadingUpdateShadowVariable.NullEntityClass.class;
53-
5437
/**
5538
* The target method element.
5639
* <p>
@@ -62,18 +45,4 @@
6245
* @return method name of the source host element which will update the shadow variable
6346
*/
6447
String targetMethodName();
65-
66-
/**
67-
* Defines several {@link ShadowVariable} annotations on the same element.
68-
*/
69-
@Target({ FIELD })
70-
@Retention(RUNTIME)
71-
@interface List {
72-
73-
CascadingUpdateShadowVariable[] value();
74-
}
75-
76-
/** Workaround for annotation limitation in {@link #sourceEntityClass()}. */
77-
interface NullEntityClass {
78-
}
7948
}

core/src/main/java/ai/timefold/solver/core/impl/domain/entity/descriptor/EntityDescriptor.java

Lines changed: 12 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,7 @@ public class EntityDescriptor<Solution_> {
8989
ShadowVariable.List.class,
9090
PiggybackShadowVariable.class,
9191
CustomShadowVariable.class,
92-
CascadingUpdateShadowVariable.class,
93-
CascadingUpdateShadowVariable.List.class };
92+
CascadingUpdateShadowVariable.class };
9493

9594
private static final Logger LOGGER = LoggerFactory.getLogger(EntityDescriptor.class);
9695

@@ -217,7 +216,6 @@ public void processAnnotations(DescriptorPolicy descriptorPolicy) {
217216
+ ") should have at least 1 getter method or 1 field with a "
218217
+ PlanningVariable.class.getSimpleName() + " annotation or a shadow variable annotation.");
219218
}
220-
processPiggyBackForCascadingUpdateShadowVariables();
221219
processVariableAnnotations(descriptorPolicy);
222220
}
223221

@@ -292,8 +290,7 @@ private void processPlanningVariableAnnotation(MutableInt variableDescriptorCoun
292290
|| variableAnnotationClass.equals(ShadowVariable.class)
293291
|| variableAnnotationClass.equals(ShadowVariable.List.class)
294292
|| variableAnnotationClass.equals(PiggybackShadowVariable.class)
295-
|| variableAnnotationClass.equals(CascadingUpdateShadowVariable.class)
296-
|| variableAnnotationClass.equals(CascadingUpdateShadowVariable.List.class)) {
293+
|| variableAnnotationClass.equals(CascadingUpdateShadowVariable.class)) {
297294
memberAccessorType = FIELD_OR_GETTER_METHOD;
298295
} else {
299296
memberAccessorType = FIELD_OR_GETTER_METHOD_WITH_SETTER;
@@ -305,38 +302,6 @@ private void processPlanningVariableAnnotation(MutableInt variableDescriptorCoun
305302
}
306303
}
307304

308-
private void processPiggyBackForCascadingUpdateShadowVariables() {
309-
if (!declaredCascadingUpdateShadowVariableDecriptorMap.isEmpty()) {
310-
var piggybackShadowVariableDescriptorList = declaredShadowVariableDescriptorMap
311-
.values()
312-
.stream()
313-
.filter(v -> PiggybackShadowVariableDescriptor.class.isAssignableFrom(v.getClass()))
314-
.map(v -> (PiggybackShadowVariableDescriptor<Solution_>) v)
315-
.toList();
316-
for (var descriptor : piggybackShadowVariableDescriptorList) {
317-
var cascadingUpdateShadowVariableDescriptor =
318-
findNotifiableCascadingUpdateDescriptor(descriptor.getShadowVariableName());
319-
if (cascadingUpdateShadowVariableDescriptor != null) {
320-
cascadingUpdateShadowVariableDescriptor.addTargetVariable(descriptor.getEntityDescriptor(),
321-
descriptor.getMemberAccessor());
322-
}
323-
}
324-
}
325-
}
326-
327-
private CascadingUpdateShadowVariableDescriptor<Solution_>
328-
findNotifiableCascadingUpdateDescriptor(String variableName) {
329-
var descriptor = declaredShadowVariableDescriptorMap.get(variableName);
330-
var isCascadingUpdateDescriptor =
331-
descriptor != null && CascadingUpdateShadowVariableDescriptor.class.isAssignableFrom(descriptor.getClass());
332-
if (isCascadingUpdateDescriptor && !descriptor.hasVariableListener()) {
333-
descriptor =
334-
declaredCascadingUpdateShadowVariableDecriptorMap
335-
.get(((CascadingUpdateShadowVariableDescriptor<Solution_>) descriptor).getTargetMethodName());
336-
}
337-
return isCascadingUpdateDescriptor ? (CascadingUpdateShadowVariableDescriptor<Solution_>) descriptor : null;
338-
}
339-
340305
private void registerVariableAccessor(int nextVariableDescriptorOrdinal,
341306
Class<? extends Annotation> variableAnnotationClass, MemberAccessor memberAccessor) {
342307
var memberName = memberAccessor.getName();
@@ -392,24 +357,17 @@ The entityClass (%s) has a @%s annotated member (%s) that has an unsupported typ
392357
|| variableAnnotationClass.equals(ShadowVariable.List.class)) {
393358
var variableDescriptor = new CustomShadowVariableDescriptor<>(nextVariableDescriptorOrdinal, this, memberAccessor);
394359
declaredShadowVariableDescriptorMap.put(memberName, variableDescriptor);
395-
} else if (variableAnnotationClass.equals(CascadingUpdateShadowVariable.class)
396-
|| variableAnnotationClass.equals(CascadingUpdateShadowVariable.List.class)) {
360+
} else if (variableAnnotationClass.equals(CascadingUpdateShadowVariable.class)) {
397361
var variableDescriptor =
398362
new CascadingUpdateShadowVariableDescriptor<>(nextVariableDescriptorOrdinal, this, memberAccessor);
399363
declaredShadowVariableDescriptorMap.put(memberName, variableDescriptor);
400364
if (declaredCascadingUpdateShadowVariableDecriptorMap.containsKey(variableDescriptor.getTargetMethodName())) {
401365
// If the target method is already set,
402366
// it means that multiple fields define the cascading shadow variable
403367
// and point to the same target method.
404-
// As a result, only one listener will be created for the related target method,
405-
// which will include all sources from all fields.
406-
// This specific shadow variable will not be notifiable,
407-
// and no listener will be created from CascadingUpdateVariableListenerDescriptor#buildVariableListeners.
408-
variableDescriptor.setNotifiable(false);
409368
declaredCascadingUpdateShadowVariableDecriptorMap.get(variableDescriptor.getTargetMethodName())
410369
.addTargetVariable(this, memberAccessor);
411370
} else {
412-
// The first shadow variable read is notifiable and will generate a listener.
413371
declaredCascadingUpdateShadowVariableDecriptorMap.put(variableDescriptor.getTargetMethodName(),
414372
variableDescriptor);
415373
}
@@ -608,6 +566,10 @@ public boolean hasEffectiveMovableEntitySelectionFilter() {
608566
return effectiveMovableEntitySelectionFilter != null;
609567
}
610568

569+
public boolean hasCascadingShadowVariables() {
570+
return !declaredShadowVariableDescriptorMap.isEmpty();
571+
}
572+
611573
public boolean supportsPinning() {
612574
return hasEffectiveMovableEntitySelectionFilter() || effectivePlanningPinToIndexReader != null;
613575
}
@@ -691,6 +653,11 @@ public Collection<ShadowVariableDescriptor<Solution_>> getDeclaredShadowVariable
691653
return declaredShadowVariableDescriptorMap.values();
692654
}
693655

656+
public Collection<CascadingUpdateShadowVariableDescriptor<Solution_>>
657+
getDeclaredCascadingUpdateShadowVariableDescriptors() {
658+
return declaredCascadingUpdateShadowVariableDecriptorMap.values();
659+
}
660+
694661
public Collection<VariableDescriptor<Solution_>> getDeclaredVariableDescriptors() {
695662
Collection<VariableDescriptor<Solution_>> variableDescriptors = new ArrayList<>(
696663
declaredGenuineVariableDescriptorMap.size() + declaredShadowVariableDescriptorMap.size());

core/src/main/java/ai/timefold/solver/core/impl/domain/variable/cascade/AbstractCascadingUpdateShadowVariableListener.java

Lines changed: 0 additions & 81 deletions
This file was deleted.

0 commit comments

Comments
 (0)