TLDR: Enabling multithreading in Optaplanner is supposed to be a one-liner, but it throws an exception
I'm trying to optimize a damage calculation using configurable loadouts in a videogame. For context, a player may configure each item they own with a "reforge," which adds stats for strength or crit damage. The final damage calculation must be maximized as a combination of strength and crit damage. For this reason, I am using Optaplanner to allocate reforges to items.
However, enabling multithreading through <moveThreadCount>AUTO</moveThreadCount>
in the XML solver config throws an exception (that does not occur in single-threaded execution):
Caused by: java.lang.IllegalStateException: The externalObject (ReforgeProblemFact(id=897f4bab-80e0-4eb9-a1d7-974f7cddfd9e, name=Fierce, rarity=COMMON, strength=4, critDamage=0)) with planningId ((class net.javaman.optaplanner_reproducible.ReforgeProblemFact,897f4bab-80e0-4eb9-a1d7-974f7cddfd9e)) has no known workingObject (null).
Maybe the workingObject was never added because the planning solution doesn't have a @ProblemFactCollectionProperty annotation on a member with instances of the externalObject's class (class net.javaman.optaplanner_reproducible.ReforgeProblemFact).
This SO question is similar, but its answer doesn't fix the exception in this example.
I removed packages and imports in the code below. Full GitHub repository link
Project structure:
src/main/
kotlin/
net/javaman/optaplanner_reproducible/
Rarity.kt
ReforgeProblemFact.kt
ItemPlanningEntity.kt
ReforgePlanningSolution.kt
MaximizeDamageConstraintProvider.kt
Main.kt
resources/
reforgeSolverConfig.xml
Rarity.kt:
enum class Rarity {
COMMON,
RARE,
LEGENDARY
}
ReforgeProblemFact.kt:
data class ReforgeProblemFact(
@PlanningId
val id: UUID,
val name: String,
val rarity: Rarity,
val strength: Int,
val critDamage: Int
)
ItemPlanningEntity.kt:
@PlanningEntity
data class ItemPlanningEntity @JvmOverloads constructor(
@PlanningId
val id: UUID? = null,
val rarity: Rarity? = null,
@PlanningVariable(valueRangeProviderRefs = ["reforgeRange"])
var reforge: ReforgeProblemFact? = null,
@ValueRangeProvider(id = "reforgeRange")
@ProblemFactCollectionProperty
val availableReforges: List<ReforgeProblemFact>? = null
)
ReforgePlanningSolution.kt:
@PlanningSolution
class ReforgePlanningSolution @JvmOverloads constructor(
@PlanningEntityCollectionProperty
val availableItems: List<ItemPlanningEntity>? = null,
@PlanningScore
val score: HardSoftScore? = null,
)
MaximizeDamageConstraintProvider.kt:
class MaximizeDamageConstraintProvider : ConstraintProvider {
override fun defineConstraints(factory: ConstraintFactory): Array<Constraint> = arrayOf(maximizeDamage(factory))
// This approach does not take full advantage of incremental solving,
// but it is necessary to compute strength and critDamage together in the same equation
private fun maximizeDamage(factory: ConstraintFactory) = factory.from(ItemPlanningEntity::class.java)
.map(ItemPlanningEntity::reforge) // Get each item's reforge
.groupBy({ 0 }, toList { reforge: ReforgeProblemFact? -> reforge }) // Compile into one List<ReforgeProblemFact>
.reward("damage", HardSoftScore.ONE_SOFT) { _, reforges: List<ReforgeProblemFact?> ->
val strengthSum = reforges.stream().collect(Collectors.summingInt { reforge -> reforge?.strength ?: 0 })
val critDamageSum = reforges.stream().collect(Collectors.summingInt { reforge -> reforge?.critDamage ?: 0 })
(100 strengthSum) * (100 critDamageSum)
}
}
Main.kt:
class Main {
companion object {
private val allReforges = listOf(
ReforgeProblemFact(UUID.randomUUID(), "Clean", Rarity.COMMON, 0, 3),
ReforgeProblemFact(UUID.randomUUID(), "Fierce", Rarity.COMMON, 4, 0),
ReforgeProblemFact(UUID.randomUUID(), "Shiny", Rarity.COMMON, 2, 1),
ReforgeProblemFact(UUID.randomUUID(), "Clean", Rarity.RARE, 1, 3),
ReforgeProblemFact(UUID.randomUUID(), "Fierce", Rarity.RARE, 5, 0),
ReforgeProblemFact(UUID.randomUUID(), "Shiny", Rarity.RARE, 3, 2),
ReforgeProblemFact(UUID.randomUUID(), "Clean", Rarity.LEGENDARY, 1, 4),
ReforgeProblemFact(UUID.randomUUID(), "Fierce", Rarity.LEGENDARY, 6, 0),
ReforgeProblemFact(UUID.randomUUID(), "Shiny", Rarity.LEGENDARY, 4, 2),
)
private val solverManager: SolverManager<ReforgePlanningSolution, UUID> = SolverManager.create(
SolverConfig.createFromXmlResource("reforgeSolverConfig.xml")
)
@JvmStatic
fun main(args: Array<String>) {
val availableItems = generateAvailableItems(
mapOf(
Rarity.COMMON to 4,
Rarity.RARE to 3,
Rarity.LEGENDARY to 1
)
)
val solverJob = solverManager.solve(UUID.randomUUID(), ReforgePlanningSolution(availableItems))
val solution = solverJob.finalBestSolution
solution.availableItems!!
.map { it.reforge!! }
.forEach { println(it.rarity.name " " it.name) }
}
private fun generateAvailableItems(itemCounts: Map<Rarity, Int>): MutableList<ItemPlanningEntity> {
val availableItems = mutableListOf<ItemPlanningEntity>()
for (itemCount in itemCounts) {
for (count in 0 until itemCount.value) {
val rarity = itemCount.key
availableItems.add(
ItemPlanningEntity(
UUID.randomUUID(),
rarity,
null,
allReforges.filter { it.rarity == rarity }
)
)
}
}
return availableItems
}
}
}
CodePudding user response:
The externalObject (ReforgeProblemFact(id=897f...)) with planningId ((class ReforgeProblemFact,897f...)) has no known workingObject (null).
That planningId ((class ReforgeProblemFact
makes no sense, as the planningId class is UUID in your model.
Looking at the code of PlanningIdLookUpStrategy
line 71, the error message is correct. Put a breakpoint on that line and look at what kind of class the planningId variable is. It should be a UUID.
CodePudding user response:
I revisited the similar SO question. After trying a few different versions of the answer, it finally worked. Here's a more detailed explanation than the other post:
Each PlanningEntity
's ProblemFactCollectionProperty
must be part of the main ProblemFactCollectionProperty
in the PlanningSolution
. This means that both the entity and solution should have their problem facts defined. Here's what fixed it for me:
Keep ItemPlanningEntity.kt the same.
Include a global ProblemFactCollectionProperty
in ReforgePlanningSolution.kt:
@PlanningSolution
class ReforgePlanningSolution @JvmOverloads constructor(
@PlanningEntityCollectionProperty
val availableItems: List<ItemPlanningEntity>? = null,
@ProblemFactCollectionProperty
val allReforges: List<ReforgeProblemFact>? = null,
@PlanningScore
val score: HardSoftScore? = null
)
Define the global collection when instantiating the solution in Main.kt:
val solverJob = solverManager.solve(UUID.randomUUID(), ReforgePlanningSolution(availableItems, allReforges))