OptaPlanner logo
  • Download
  • Learn
    • Documentation
    • Videos

    • Use cases
    • Compatibility
    • Testimonials and case studies
  • Get help
  • Blog
  • Source
  • Team
  • Services
  • Star
  • T
  • L
  • F
  • YT
Fork me on GitHub

Upgrade recipe 6.3

OptaPlanner’s public API classes are backwards compatible (per series), but users often also use impl classes (which are documented in the reference manual too). This upgrade recipe minimizes the pain to upgrade your code and to take advantage of the newest features in OptaPlanner 8.

Legend

Every upgrade note has an indication how likely your code will be affected by that change:

  • Major Likely to affect your code.
  • Minor Unlikely to affect your code (especially if you followed the examples), unless you have hacks.
  • Impl detail Will not affect your code, unless you have very deep hacks.
  • Recommended Not a backward incompatible change, but you probably want to do this.
  • Readme Read this to better understand why the subsequent major changes were made.
  • Automated Can be applied automatically using our migration tooling.

Upgrade from an older version

To upgrade from an older version, first apply the previous upgrade recipes. You will find the order of migration steps bellow:

Note for Red Hat Decision Manager customers

The RHDM version differs from the OptaPlanner version:

RHDM version OptaPlanner version
7.8 7.39
7.9 7.44
7.1 7.48
7.11 8.5 (and 7.52)
7.12 8.11 (and 7.59)
7.13 8.13 (and 7.67)

Automatic upgrade to the latest version

Update your code in seconds, with optaplanner-migration (an OpenRewrite recipe). Try it:

  1. Stash any local changes.
  2. Run this command in your project directory:
    mvn clean org.openrewrite.maven:rewrite-maven-plugin:LATEST:run -Drewrite.recipeArtifactCoordinates=org.optaplanner:optaplanner-migration:8.35.0.Final -Drewrite.activeRecipes=org.optaplanner.migration.ToLatest

    Note: The -Drewrite.recipeArtifactCoordinates might not work, use the more verbose pom.xml approach instead.

  3. Check the local changes and commit them.

It only does upgrade steps with an Automated badge.

From 6.2.0.Final to 6.3.0.Beta1

@InverseRelationShadowVariable for non-chained planning variables

@InverseRelationShadowVariable is now also supported for non-chained planning variables, in which case the inverse property must be Collection (Set or List). So it’s no longer needed to use a CustomShadowVariable to implement the bi-directional relationship behaviour.

Before in *.java:

public class CloudComputer {
    ...
    @CustomShadowVariable(variableListenerClass = MyCustomInverseVariableListener.class,
            sources = {@CustomShadowVariable.Source(entityClass = CloudProcess.class, variableName = "computer")})
    public List<CloudProcess> getProcessList() {
        return processList;
    }
}

After in *.java:

@PlanningEntity // Shadow variable only
public class CloudComputer {
    ...
    @InverseRelationShadowVariable(sourceVariableName = "computer")
    public List<CloudProcess> getProcessList() {
        return processList;
    }
}

Move.toString() changed

To adhere to Java best practices, which state that an Object.toString() should identify an instance and be short (instead of trying to verbalize its entire state), the Move.toString() methods have been modified to mention the old value too (as well as the entity and the new value). Their notations have also been made consistent:

  • ChangeMove: a {v1 -> v2}

  • SwapMove: a {v1} <-> b {v2}

  • PillarChangeMove: [a, b] {v1 -> v2}

  • PillarSwapMove: [a, b] {v1} <-> [c, d, e] {v2}

  • TailChainSwapMove: a3 {a2} <-tailChainSwap-> b1 {b0}

  • SubChainChangeMove: [a2..a5] {a1 -> b0}

    • Reversing: [a2..a5] {a1 -reversing-> b0}

  • SubChainSwapMove: [a2..a5] {a1} <-> [b1..b3] {b0}

    • Reversing: [a2..a5] {a1} <-reversing-> [b1..b3] {b0}

This mainly affects the logging output. In the examples, the toString() method of planning entities and planning values has been modified accordingly to avoid mentioning the old value twice:

Before in *.java:

public class CloudProcess {
    ...

    public String toString() {
        return processName + "@" + computer.getName();
    }
}

After in *.java:

public class CloudProcess {
    ...

    public String toString() {
        return processName;
    }
}

Empty <constructionHeuristic>: default algorithm changed

Default parameter tweaked: an empty <constructionHeuristic> changes its default algorithm from FIRST_FIT to ALLOCATE_ENTITY_FROM_QUEUE. If no entity difficulty comparison and no planning value strength comparison is defined, the behavior is exactly the same. Otherwise, the behaviour is FIRST_FIT_DECREASING or WEAKEST_FIT(_DECREASING).

<solverBenchmarkBluePrintType>: ALL_CONSTRUCTION_HEURISTIC_TYPES renamed

The <solverBenchmarkBluePrintType> ALL_CONSTRUCTION_HEURISTIC_TYPES has been renamed to EVERY_CONSTRUCTION_HEURISTIC_TYPE. The old name ALL_CONSTRUCTION_HEURISTIC_TYPES is deprecated and will be removed in 7.0.

Before in *BenchmarkConfig.xml:

<solverBenchmarkBluePrintType>ALL_CONSTRUCTION_HEURISTIC_TYPES</solverBenchmarkBluePrintType>

After in *BenchmarkConfig.xml:

<solverBenchmarkBluePrintType>EVERY_CONSTRUCTION_HEURISTIC_TYPE</solverBenchmarkBluePrintType>

Real-time planning: addProblemFactChange() resets time spent terminations

In real-time planning, calling addProblemFactChange() now resets the time spent terminations too. It did already reset all other terminations (despite that the docs claimed otherwise). The docs have also been fixed to reflect reality, which is also the desired behaviour by users.

Real-time planning: addProblemFactChange() keeps the ScoreDirector

In real-time planning, addProblemFactChange() no longer causes the ScoreDirector to be replaced (so it no longer creates a new KieSession) upon solver restart. This might expose a hidden bug in your ProblemFactChange implementation. Enable environmentMode FULL_ASSERT and do a few addProblemFactChange() calls to validate that there are no such bugs.

From 6.3.0.Beta1 to 6.3.0.Beta2

Custom ValueRange: new method isEmpty()

If you implemented a custom ValueRange, also implement the method isEmpty(). Normally, you should not have any need for a custom ValueRange, because ValueRangeFactory supports all sensible ranges.

Before in *.java:

public class MyDoubleValueRange extends AbstractUncountableValueRange<Double> {
    ...
}

After in *.java:

public class MyDoubleValueRange extends AbstractUncountableValueRange<Double> {
    ...
    @Override
    public boolean isEmpty() {
        return from == to;
    }
}

Multiple planning variables: use new folded configuration

If you use multiple planning variables, consider switching to the folded configuration.

Before in *SolverConfig.xml and *BenchmarkConfig.xml:

  <changeMoveSelector>
    <valueSelector>
      <variableName>period</variableName>
    </valueSelector>
  </changeMoveSelector>
  <changeMoveSelector>
    <valueSelector>
      <variableName>room</variableName>
    </valueSelector>
  </changeMoveSelector>

After in *SolverConfig.xml and *BenchmarkConfig.xml:

  <changeMoveSelector/>

Multiple entity classes: use new folded configuration

If you use multiple entity classes, consider switching to the folded configuration.

Before in *SolverConfig.xml and *BenchmarkConfig.xml:

  <changeMoveSelector>
    <entitySelector>
      <entityClass>...CoachEntity</entityClass>
    </entitySelector>
  </changeMoveSelector>
  <changeMoveSelector>
    <entitySelector>
      <entityClass>...ShuttleEntity</entityClass>
    </entitySelector>
  </changeMoveSelector>
  <swapMoveSelector>
    <entitySelector>
      <entityClass>...CoachEntity</entityClass>
    </entitySelector>
  </swapMoveSelector>
  <swapMoveSelector>
    <entitySelector>
      <entityClass>...ShuttleEntity</entityClass>
    </entitySelector>
  </swapMoveSelector>

After in *SolverConfig.xml and *BenchmarkConfig.xml:

  <changeMoveSelector/>
  <swapMoveSelector/>

Planning solution with a superclass with planner annotations: behaviour change

If your planning solution has a superclass with planner annotations, those will now be ignored (just like solution subclass annotations are ignored and just like entity superclass or subclass annotations are ignored unless they are a declared planning entity class too). Declare the @PlanningSolution on the superclass instead, the solver will handle subclass instances gracefully (presuming there are no planner annotations in the subclass).

Before in *.java:

public abstract class ParentSolution {
    @ValueRangeProvider(...)
    public List<Computer> getComputers() {...}
}
@PlanningSolution
public class ChildSolution extends ParentSolution {...}

Before in *SolverConfig.xml and *BenchmarkConfig.xml:

<solutionClass>...ChildSolution</solutionClass>

After in *.java:

@PlanningSolution
public abstract class ParentSolution {
    @ValueRangeProvider(...)
    public List<Computer> getComputers() {...}
}
public class ChildSolution extends ParentSolution {...}

After in *SolverConfig.xml and *BenchmarkConfig.xml:

<solutionClass>...ParentSolution</solutionClass>

From 6.3.0.Beta2 to 6.3.0.CR1

Custom VariableListener that changes 2 shadow variables: use variableListenerRef

If a custom VariableListener changes 2 shadow variables, use the new variableListenerRef property accordingly to indicate that the VariableListener class of another shadow variable also updates this shadow variable:

Before in *.java:

@PlanningVariable(...)
public Standstill getPreviousStandstill() {
    return previousStandstill;
}
@CustomShadowVariable(variableListenerClass = TransportTimeAndCapacityUpdatingVariableListener.class,
        sources = {@CustomShadowVariable.Source(variableName = "previousStandstill")})
public Integer getTransportTime() {
    return transportTime;
}
@CustomShadowVariable(variableListenerClass = DummyListener.class, sources = ...)
public Integer getCapacity() {
    return capacity;
}

After in *.java:

@PlanningVariable(...)
public Standstill getPreviousStandstill() {
    return previousStandstill;
}
@CustomShadowVariable(variableListenerClass = TransportTimeAndCapacityUpdatingVariableListener.class,
        sources = {@CustomShadowVariable.Source(variableName = "previousStandstill")})
public Integer getTransportTime() {
    return transportTime;
}
@CustomShadowVariable(variableListenerRef = @PlanningVariableReference(variableName = "transportTime"))
public Integer getCapacity() {
    return capacity;
}

From 6.3.0.CR1 to 6.3.0.CR2

VariableListeners no longer trigger chaotically

VariableListeners no longer trigger chaotically. This applies to both out of the box shadow variables and custom shadow variables. Planner now guarantees that the first VariableListener’s after*() method triggers AFTER the last genuine variable has been modified. This means a VariableListener is no longer exposed to intermediate state. The before*() methods still trigger immediately (otherwise they would not be able to capture the source variable’s original state). Furthermore, Planner guarantees this triggering in stages also for VariableListener for a shadow variable that depend on earlier shadow variables.

Custom Move: doMove() must call triggerVariableListeners()

If you have a custom Move, its doMove() method must now call scoreDirector.triggerVariableListeners() at the end. In practice, you should have extended AbstractMove - which does the triggerVariableListeners() call for you - but you’ll need to rename your doMove() method to doMoveOnGenuineVariables().

Before in *.java:

public class CloudComputerChangeMove extends AbstractMove {
    ...
    public void doMove(ScoreDirector scoreDirector) {
        CloudBalancingMoveHelper.moveCloudComputer(scoreDirector, cloudProcess, toCloudComputer);
    }
}

After in *.java:

public class CloudComputerChangeMove extends AbstractMove {
    ...
    protected void doMoveOnGenuineVariables(ScoreDirector scoreDirector) {
        CloudBalancingMoveHelper.moveCloudComputer(scoreDirector, cloudProcess, toCloudComputer);
    }
}

Real-time planning ProblemFactChange: doChange() must call triggerVariableListeners()

If you have a ProblemFactChange, its doChange() method must now call scoreDirector.triggerVariableListeners() after every set of changes (before calling calculateScore() or relying on shadow variables).

Before in *.java:

public void deleteComputer(final CloudComputer computer) {
    doProblemFactChange(new ProblemFactChange() {
        public void doChange(ScoreDirector scoreDirector) {
            CloudBalance cloudBalance = (CloudBalance) scoreDirector.getWorkingSolution();
            // First remove the problem fact from all planning entities that use it
            for (CloudProcess process : cloudBalance.getProcessList()) {
                if (Objects.equals(process.getComputer(), computer)) {
                    scoreDirector.beforeVariableChanged(process, "computer");
                    process.setComputer(null);
                    scoreDirector.afterVariableChanged(process, "computer");
                }
            }
            ...
        }
    });
}

After in *.java:

public void deleteComputer(final CloudComputer computer) {
    doProblemFactChange(new ProblemFactChange() {
        public void doChange(ScoreDirector scoreDirector) {
            CloudBalance cloudBalance = (CloudBalance) scoreDirector.getWorkingSolution();
            // First remove the problem fact from all planning entities that use it
            for (CloudProcess process : cloudBalance.getProcessList()) {
                if (Objects.equals(process.getComputer(), computer)) {
                    scoreDirector.beforeVariableChanged(process, "computer");
                    process.setComputer(null);
                    scoreDirector.afterVariableChanged(process, "computer");
                }
            }
            scoreDirector.triggerVariableListeners();
            ...
        }
    });
}

Custom phase CustomPhaseCommand: changeWorkingSolution() must call triggerVariableListeners()

If you have a CustomPhaseCommand, its changeWorkingSolution() method must now call scoreDirector.triggerVariableListeners() after every set of changes (before calling calculateScore() or relying on shadow variables).

Before in *.java:

public class MyCustomPhase implements CustomPhaseCommand {
    public void changeWorkingSolution(ScoreDirector scoreDirector) {
        scoreDirector.beforeVariableChanged(processAssignment, "machine");
        processAssignment.setMachine(machine);
        scoreDirector.afterVariableChanged(processAssignment, "machine");
        Score score = scoreDirector.calculateScore();
    }
}

After in *.java:

public class MyCustomPhase implements CustomPhaseCommand {
    public void changeWorkingSolution(ScoreDirector scoreDirector) {
        scoreDirector.beforeVariableChanged(processAssignment, "machine");
        processAssignment.setMachine(machine);
        scoreDirector.afterVariableChanged(processAssignment, "machine");
        scoreDirector.triggerVariableListeners();
        Score score = scoreDirector.calculateScore();
    }
}

Custom Move: read shadow variables first

A custom Move must now read any shadow variables it needs before its first beforeVariableChanged() call. It no longer needs to assign genuine variables to intermediate values to avoid errors in the VariableListeners that update shadow variables.

All built-in chained moves simplified

All built-in moves that affect chained variables have been greatly simplified due to the new VariableListener guarantee.

Chained moves: constructors changed

The constructor of ChainedChangeMove, ChainedSwapMove, SubChainChangeMove and SubChainSwapMove now require the SingletonInverseVariableSupply parameter.

Before in *.java:

return new ChainedChangeMove(entity, variableDescriptor, toValue);

After in *.java:

SingletonInverseVariableSupply inverseVariableSupply = ((InnerScoreDirector) scoreDirector).getSupplyManager()
        .demand(new SingletonInverseVariableDemand(variableDescriptor));
return new ChainedChangeMove(entity, variableDescriptor, inverseVariableSupply, toValue);

Furthermore, a ChainedSwapMove’s constructor now requires a List instead of Collection of VariableDescriptors.

InnerScoreDirector: getTrailingEntity() removed

The method InnerScoreDirector.getTrailingEntity() has been removed. Use SingletonInverseVariableSupply instead.

One score rule that affects 2 score levels

One score rule can now change 2 score levels in its RHS.

Before in *.drl:

rule "Costly and unfair: part 1"
when
    // Complex pattern
then
    scoreHolder.addMediumConstraintMatch(kcontext, -1); // Financial cost
end
rule "Costly and unfair: part 2"
when
    // Complex pattern (duplication)
then
    scoreHolder.addSoftConstraintMatch(kcontext, -1); // Employee happiness cost
end

After in *.drl:

rule "Costly and unfair"
when
    // Complex pattern
then
    scoreHolder.addMediumConstraintMatch(kcontext, -1); // Financial cost
    scoreHolder.addSoftConstraintMatch(kcontext, -1); // Employee happiness cost
end

From 6.3.0.CR2 to 6.3.0.CR3

Build a Solver from API: use SolverFactory.createEmpty()

If you build a Solver entirely from API (not recommended - it’s better to load it partially from XML), use SolverFactory.createEmpty() and solverFactory.getSolverConfig() accordingly.

Before in *.java:

    SolverConfig solverConfig = new SolverConfig();
    ...
    return solverConfig.buildSolver();

After in *.java:

    SolverFactory solverFactory = SolverFactory.createEmpty();
    SolverConfig solverConfig = solverFactory.getSolverConfig();
    ...
    return solverFactory.buildSolver();
Latest release
  • 8.35.0.Final released
    Fri 3 March 2023
Upcoming events
    Add event / Archive
Latest blog posts
  • OptaPlanner 9 is coming
    Tue 21 February 2023
    Lukáš Petrovický
  • Farewell - a new lead
    Tue 15 November 2022
    Geoffrey De Smet
  • Run OptaPlanner workloads on OpenShift, part II
    Wed 9 November 2022
    Radovan Synek
  • Bavet - A faster score engine for OptaPlanner
    Tue 6 September 2022
    Geoffrey De Smet
  • Run OptaPlanner workloads on OpenShift, part I.
    Thu 9 June 2022
    Radovan Synek
  • OptaPlanner deprecates score DRL
    Thu 26 May 2022
    Lukáš Petrovický
  • Real-time planning meets SolverManager
    Mon 7 March 2022
    Radovan Synek
  • Blog archive
Latest videos
  • The Vehicle Routing Problem
    Fri 23 September 2022
    Geoffrey De Smet
  • Introduction to OptaPlanner AI constraint solver
    Thu 25 August 2022
    Anna Dupliak
  • On schedule: Artificial Intelligence plans that meet expectations
    Sat 23 July 2022
    Geoffrey De Smet
  • Host your OptaPlanner app on OpenShift (Kubernetes)
    Mon 7 February 2022
    Geoffrey De Smet
  • OptaPlanner - A fast, easy-to-use, open source AI constraint solver for software developers
    Mon 31 January 2022
  • Order picking planning with OptaPlanner
    Fri 31 December 2021
    Anna Dupliak
  • AI lesson scheduling on Quarkus with OptaPlanner
    Thu 18 November 2021
    Geoffrey De Smet
  • Video archive

OptaPlanner is open. All dependencies of this project are available under the Apache Software License 2.0 or a compatible license. OptaPlanner is trademarked.

This website was built with JBake and is open source.

Community

  • Blog
  • Get Help
  • Team
  • Governance
  • Academic research

Code

  • Build from source
  • Issue tracker
  • Release notes
  • Upgrade recipes
  • Logo and branding
CC by 3.0 | Privacy Policy
Sponsored by Red Hat