99import ai .timefold .solver .core .impl .heuristic .selector .move .AbstractMoveSelector ;
1010import ai .timefold .solver .core .impl .heuristic .selector .move .MoveSelector ;
1111import ai .timefold .solver .core .impl .phase .scope .AbstractPhaseScope ;
12+ import ai .timefold .solver .core .impl .solver .termination .PhaseTermination ;
1213
1314public final class FilteringMoveSelector <Solution_ > extends AbstractMoveSelector <Solution_ > {
1415
16+ private static final long BAIL_OUT_MULTIPLIER = 10L ;
17+
1518 public static <Solution_ > FilteringMoveSelector <Solution_ > of (MoveSelector <Solution_ > moveSelector ,
1619 SelectionFilter <Solution_ , Move <Solution_ >> filter ) {
1720 if (moveSelector instanceof FilteringMoveSelector <Solution_ > filteringMoveSelector ) {
@@ -24,6 +27,7 @@ public static <Solution_> FilteringMoveSelector<Solution_> of(MoveSelector<Solut
2427 private final MoveSelector <Solution_ > childMoveSelector ;
2528 private final SelectionFilter <Solution_ , Move <Solution_ >> filter ;
2629 private final boolean bailOutEnabled ;
30+ private AbstractPhaseScope <Solution_ > phaseScope ;
2731
2832 private ScoreDirector <Solution_ > scoreDirector = null ;
2933
@@ -42,13 +46,15 @@ private FilteringMoveSelector(MoveSelector<Solution_> childMoveSelector,
4246 @ Override
4347 public void phaseStarted (AbstractPhaseScope <Solution_ > phaseScope ) {
4448 super .phaseStarted (phaseScope );
45- scoreDirector = phaseScope .getScoreDirector ();
49+ this .scoreDirector = phaseScope .getScoreDirector ();
50+ this .phaseScope = phaseScope ;
4651 }
4752
4853 @ Override
4954 public void phaseEnded (AbstractPhaseScope <Solution_ > phaseScope ) {
5055 super .phaseEnded (phaseScope );
51- scoreDirector = null ;
56+ this .scoreDirector = null ;
57+ this .phaseScope = null ;
5258 }
5359
5460 @ Override
@@ -68,23 +74,53 @@ public long getSize() {
6874
6975 @ Override
7076 public Iterator <Move <Solution_ >> iterator () {
71- return new JustInTimeFilteringMoveIterator (childMoveSelector .iterator (), determineBailOutSize ());
77+ return new JustInTimeFilteringMoveIterator (childMoveSelector .iterator (), determineBailOutSize (), phaseScope );
78+ }
79+
80+ private long determineBailOutSize () {
81+ if (!bailOutEnabled ) {
82+ return -1L ;
83+ }
84+ try {
85+ return childMoveSelector .getSize () * BAIL_OUT_MULTIPLIER ;
86+ } catch (Exception ex ) {
87+ // Some move selectors throw an exception when getSize() is called.
88+ // In this case, we choose to disregard it and pick a large-enough bail-out size anyway.
89+ // The ${bailOutSize+1}th move could in theory show up where previous ${bailOutSize} moves did not,
90+ // but we consider this to be an acceptable risk,
91+ // outweighed by the benefit of the solver never running into an endless loop.
92+ // The exception itself is swallowed, as it doesn't bring any useful information.
93+ long bailOutSize = Short .MAX_VALUE * BAIL_OUT_MULTIPLIER ;
94+ logger .trace (
95+ " Never-ending move selector ({}) failed to provide size, choosing a bail-out size of ({}) attempts." ,
96+ childMoveSelector , bailOutSize );
97+ return bailOutSize ;
98+ }
7299 }
73100
74101 private class JustInTimeFilteringMoveIterator extends UpcomingSelectionIterator <Move <Solution_ >> {
75102
103+ private final long TERMINATION_BAIL_OUT_SIZE = 1000L ;
76104 private final Iterator <Move <Solution_ >> childMoveIterator ;
77105 private final long bailOutSize ;
106+ private final AbstractPhaseScope <Solution_ > phaseScope ;
107+ private final PhaseTermination <Solution_ > termination ;
78108
79- public JustInTimeFilteringMoveIterator (Iterator <Move <Solution_ >> childMoveIterator , long bailOutSize ) {
109+ public JustInTimeFilteringMoveIterator (Iterator <Move <Solution_ >> childMoveIterator , long bailOutSize ,
110+ AbstractPhaseScope <Solution_ > phaseScope ) {
80111 this .childMoveIterator = childMoveIterator ;
81112 this .bailOutSize = bailOutSize ;
113+ this .phaseScope = phaseScope ;
114+ this .termination = phaseScope != null ? phaseScope .getTermination () : null ;
82115 }
83116
84117 @ Override
85118 protected Move <Solution_ > createUpcomingSelection () {
86119 Move <Solution_ > next ;
87120 long attemptsBeforeBailOut = bailOutSize ;
121+ // To reduce the impact of checking for termination on each move,
122+ // we only check for termination after filtering out 1000 moves.
123+ long attemptsBeforeCheckTermination = TERMINATION_BAIL_OUT_SIZE ;
88124 do {
89125 if (!childMoveIterator .hasNext ()) {
90126 return noUpcomingSelection ();
@@ -95,8 +131,18 @@ protected Move<Solution_> createUpcomingSelection() {
95131 logger .trace ("Bailing out of neverEnding selector ({}) after ({}) attempts to avoid infinite loop." ,
96132 FilteringMoveSelector .this , bailOutSize );
97133 return noUpcomingSelection ();
134+ } else if (termination != null && attemptsBeforeCheckTermination <= 0L ) {
135+ // Reset the counter
136+ attemptsBeforeCheckTermination = TERMINATION_BAIL_OUT_SIZE ;
137+ if (termination .isPhaseTerminated (phaseScope )) {
138+ logger .trace (
139+ "Bailing out of neverEnding selector ({}) because the termination setting has been triggered." ,
140+ FilteringMoveSelector .this );
141+ return noUpcomingSelection ();
142+ }
98143 }
99144 attemptsBeforeBailOut --;
145+ attemptsBeforeCheckTermination --;
100146 }
101147 next = childMoveIterator .next ();
102148 } while (!accept (scoreDirector , next ));
@@ -105,34 +151,12 @@ protected Move<Solution_> createUpcomingSelection() {
105151
106152 }
107153
108- private long determineBailOutSize () {
109- if (!bailOutEnabled ) {
110- return -1L ;
111- }
112- try {
113- return childMoveSelector .getSize () * 10L ;
114- } catch (Exception ex ) {
115- // Some move selectors throw an exception when getSize() is called.
116- // In this case, we choose to disregard it and pick a large-enough bail-out size anyway.
117- // The ${bailOutSize+1}th move could in theory show up where previous ${bailOutSize} moves did not,
118- // but we consider this to be an acceptable risk,
119- // outweighed by the benefit of the solver never running into an endless loop.
120- // The exception itself is swallowed, as it doesn't bring any useful information.
121- long bailOutSize = Short .MAX_VALUE * 10L ;
122- logger .trace (
123- " Never-ending move selector ({}) failed to provide size, choosing a bail-out size of ({}) attempts." ,
124- childMoveSelector , bailOutSize );
125- return bailOutSize ;
126- }
127- }
128-
129154 private boolean accept (ScoreDirector <Solution_ > scoreDirector , Move <Solution_ > move ) {
130- if (filter != null ) {
131- if (!filter .accept (scoreDirector , move )) {
132- logger .trace (" Move ({}) filtered out by a selection filter ({})." , move , filter );
133- return false ;
134- }
155+ if (filter != null && !filter .accept (scoreDirector , move )) {
156+ logger .trace (" Move ({}) filtered out by a selection filter ({})." , move , filter );
157+ return false ;
135158 }
159+
136160 return true ;
137161 }
138162
0 commit comments