Multi path in cable system
This commit is contained in:
parent
436456dcc5
commit
61eab5fb37
@ -31,11 +31,31 @@ class EnergyCableGraph : GraphNodeList<EnergyCableBlockEntity.Node, EnergyCableG
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: LRU cache?
|
||||
private val pathCache = HashMap<Pair<EnergyCableBlockEntity.Node, EnergyCableBlockEntity.Node>, ArrayList<SegmentPath?>>()
|
||||
private class CacheEntry {
|
||||
val paths = ArrayList<SegmentPath>(1)
|
||||
var saturated = false
|
||||
}
|
||||
|
||||
private class SearchNode(val node: EnergyCableBlockEntity.Node, target: EnergyCableBlockEntity.Node, var parent: SearchNode? = null) : Comparable<SearchNode> {
|
||||
val heuristics: Double = node.position.distSqr(target.position) * 0.0001 - ln(node.segment.availableThroughput.coerceAtMost(Decimal.LONG_MAX_VALUE).toDouble())
|
||||
// TODO: LRU cache?
|
||||
private val pathCache = HashMap<Pair<EnergyCableBlockEntity.Node, EnergyCableBlockEntity.Node>, CacheEntry>()
|
||||
|
||||
private class SearchNode(
|
||||
val node: EnergyCableBlockEntity.Node,
|
||||
target: EnergyCableBlockEntity.Node,
|
||||
var parent: SearchNode? = null,
|
||||
previouslySeen: Boolean
|
||||
) : Comparable<SearchNode> {
|
||||
val heuristics: Double
|
||||
|
||||
init {
|
||||
var heuristics = node.position.distSqr(target.position) * 0.0001
|
||||
|
||||
if (!previouslySeen) {
|
||||
heuristics -= ln(node.segment.availableThroughput.coerceAtMost(Decimal.LONG_MAX_VALUE).toDouble())
|
||||
}
|
||||
|
||||
this.heuristics = heuristics
|
||||
}
|
||||
|
||||
override fun compareTo(other: SearchNode): Int {
|
||||
return heuristics.compareTo(other.heuristics)
|
||||
@ -51,6 +71,10 @@ class EnergyCableGraph : GraphNodeList<EnergyCableBlockEntity.Node, EnergyCableG
|
||||
val availableThroughput: Decimal
|
||||
get() = segments.minOfOrNull { it.availableThroughput } ?: Decimal.ZERO
|
||||
|
||||
operator fun contains(node: EnergyCableBlockEntity.Node): Boolean {
|
||||
return segments.any { node in it }
|
||||
}
|
||||
|
||||
fun transfer(amount: Decimal, simulate: Boolean, instantSnapshot: MutableMap<Segment, Decimal>): Decimal {
|
||||
if (!amount.isPositive || shortCircuit) {
|
||||
return Decimal.ZERO
|
||||
@ -79,6 +103,10 @@ class EnergyCableGraph : GraphNodeList<EnergyCableBlockEntity.Node, EnergyCableG
|
||||
return min
|
||||
}
|
||||
|
||||
fun refund(amount: Decimal, simulate: Boolean, instantSnapshot: MutableMap<Segment, Decimal>) {
|
||||
segments.forEach { it.refund(amount, simulate, instantSnapshot) }
|
||||
}
|
||||
|
||||
fun remove() {
|
||||
segments.toTypedArray().forEach { it.remove(this) }
|
||||
}
|
||||
@ -93,6 +121,10 @@ class EnergyCableGraph : GraphNodeList<EnergyCableBlockEntity.Node, EnergyCableG
|
||||
private val nodes = HashSet<EnergyCableBlockEntity.Node>()
|
||||
private val paths = HashSet<SegmentPath>()
|
||||
|
||||
operator fun contains(node: EnergyCableBlockEntity.Node): Boolean {
|
||||
return node in nodes
|
||||
}
|
||||
|
||||
var throughput = Decimal.ZERO
|
||||
private set
|
||||
|
||||
@ -180,6 +212,16 @@ class EnergyCableGraph : GraphNodeList<EnergyCableBlockEntity.Node, EnergyCableG
|
||||
return diff
|
||||
}
|
||||
|
||||
fun refund(amount: Decimal, simulate: Boolean, instantSnapshot: MutableMap<Segment, Decimal>) {
|
||||
if (simulate) {
|
||||
if (this in instantSnapshot) {
|
||||
instantSnapshot[this] = maxOf(instantSnapshot[this]!! - amount, Decimal.ZERO)
|
||||
}
|
||||
} else {
|
||||
transferredLastTick = maxOf(transferredLastTick, Decimal.ZERO)
|
||||
}
|
||||
}
|
||||
|
||||
fun remove(node: EnergyCableBlockEntity.Node) {
|
||||
check(nodes.remove(node)) { "Tried to remove node $node from segment $this, but that node does not belong to this segment" }
|
||||
|
||||
@ -241,23 +283,18 @@ class EnergyCableGraph : GraphNodeList<EnergyCableBlockEntity.Node, EnergyCableG
|
||||
nodes.forEach { it.onInvalidate() }
|
||||
}
|
||||
|
||||
// TODO: Multiple paths, so energy can be delivered to receiver through different paths if previously found path is congested
|
||||
private fun getPath(a: EnergyCableBlockEntity.Node, b: EnergyCableBlockEntity.Node, energyToTransfer: Decimal): SegmentPath? {
|
||||
if (!a.canTraverse || !b.canTraverse)
|
||||
// isn't exactly A*, but greedy algorithm, which searched for locally optimal solutions because they lead to globally optimal ones
|
||||
private fun findPath(a: EnergyCableBlockEntity.Node, b: EnergyCableBlockEntity.Node, existing: Collection<SegmentPath>, threshold: Decimal): SegmentPath? {
|
||||
val seenTop = existing.any { a in it }
|
||||
|
||||
if (seenTop && a.energyThroughput <= threshold) {
|
||||
return null
|
||||
}
|
||||
|
||||
val list = pathCache.computeIfAbsent(a to b) { ArrayList(1) }
|
||||
|
||||
if (list.isNotEmpty())
|
||||
return list.first
|
||||
|
||||
// no free paths available, try to find extra one
|
||||
// while this use A* algorithm, this is done purely for biasing search towards end point (to speed up search),
|
||||
// on small cable networks simple flooding will do just fine, if we consider overloaded cables as closed flood gates
|
||||
val openNodes = PriorityQueue<SearchNode>()
|
||||
val seenNodes = HashSet<EnergyCableBlockEntity.Node>()
|
||||
|
||||
openNodes.add(SearchNode(a, b))
|
||||
openNodes.add(SearchNode(a, b, null, seenTop))
|
||||
|
||||
while (openNodes.isNotEmpty()) {
|
||||
val first = openNodes.remove()
|
||||
@ -280,21 +317,46 @@ class EnergyCableGraph : GraphNodeList<EnergyCableBlockEntity.Node, EnergyCableG
|
||||
val path = SegmentPath(a.blockEntity.blockPos, b.blockEntity.blockPos)
|
||||
touchedSegments.forEach { it.add(path) }
|
||||
|
||||
list.add(path)
|
||||
return path
|
||||
} else {
|
||||
for (neighbour in first.node.neighboursView.values) {
|
||||
if (!seenNodes.add(neighbour) || !neighbour.canTraverse) continue
|
||||
openNodes.add(SearchNode(neighbour, b, first))
|
||||
val seen = existing.any { neighbour in it }
|
||||
if (seen && neighbour.energyThroughput <= threshold) continue
|
||||
openNodes.add(SearchNode(neighbour, b, first, seen))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// solution does not exist
|
||||
list.add(null)
|
||||
return null
|
||||
}
|
||||
|
||||
private fun getPath(a: EnergyCableBlockEntity.Node, b: EnergyCableBlockEntity.Node, energyToTransfer: Decimal): SegmentPath? {
|
||||
if (!a.canTraverse || !b.canTraverse)
|
||||
return null
|
||||
|
||||
val list = pathCache.computeIfAbsent(a to b) { CacheEntry() }
|
||||
|
||||
if (!list.saturated) {
|
||||
var maxThroughput = list.paths.maxOfOrNull { it.availableThroughput } ?: Decimal.ZERO
|
||||
|
||||
while (maxThroughput < energyToTransfer && !list.saturated) {
|
||||
val find = findPath(a, b, list.paths, maxThroughput)
|
||||
|
||||
if (find == null) {
|
||||
list.saturated
|
||||
} else {
|
||||
list.paths.add(find)
|
||||
maxThroughput = maxOf(maxThroughput, find.availableThroughput)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Immediate load selection is bad because it can easily lead to oscillations and non optimal throughput
|
||||
// without user having any clue why their shit work suboptimally
|
||||
return list.paths.maxByOrNull { it.availableThroughput }
|
||||
}
|
||||
|
||||
override fun onNodeRemoved(node: EnergyCableBlockEntity.Node) {
|
||||
if (livelyNodes.remove(node)) {
|
||||
check(livelyNodesList.remove(node))
|
||||
|
Loading…
Reference in New Issue
Block a user