Compare commits

...

2 Commits

View File

@ -31,11 +31,31 @@ class EnergyCableGraph : GraphNodeList<EnergyCableBlockEntity.Node, EnergyCableG
} }
} }
// TODO: LRU cache? private class CacheEntry {
private val pathCache = HashMap<Pair<EnergyCableBlockEntity.Node, EnergyCableBlockEntity.Node>, ArrayList<SegmentPath?>>() val paths = ArrayList<SegmentPath>(1)
var saturated = false
}
private class SearchNode(val node: EnergyCableBlockEntity.Node, target: EnergyCableBlockEntity.Node, var parent: SearchNode? = null) : Comparable<SearchNode> { // TODO: LRU cache?
val heuristics: Double = node.position.distSqr(target.position) * 0.0001 - ln(node.segment.availableThroughput.coerceAtMost(Decimal.LONG_MAX_VALUE).toDouble()) 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 { override fun compareTo(other: SearchNode): Int {
return heuristics.compareTo(other.heuristics) return heuristics.compareTo(other.heuristics)
@ -51,6 +71,18 @@ class EnergyCableGraph : GraphNodeList<EnergyCableBlockEntity.Node, EnergyCableG
val availableThroughput: Decimal val availableThroughput: Decimal
get() = segments.minOfOrNull { it.availableThroughput } ?: Decimal.ZERO get() = segments.minOfOrNull { it.availableThroughput } ?: Decimal.ZERO
operator fun contains(node: EnergyCableBlockEntity.Node): Boolean {
return segments.any { node in it }
}
override fun equals(other: Any?): Boolean {
return this === other || other is SegmentPath && segments == other.segments
}
override fun hashCode(): Int {
return segments.hashCode()
}
fun transfer(amount: Decimal, simulate: Boolean, instantSnapshot: MutableMap<Segment, Decimal>): Decimal { fun transfer(amount: Decimal, simulate: Boolean, instantSnapshot: MutableMap<Segment, Decimal>): Decimal {
if (!amount.isPositive || shortCircuit) { if (!amount.isPositive || shortCircuit) {
return Decimal.ZERO return Decimal.ZERO
@ -66,19 +98,22 @@ class EnergyCableGraph : GraphNodeList<EnergyCableBlockEntity.Node, EnergyCableG
} }
} }
var min = amount val min = if (simulate) segments.minOf { it.throughput(instantSnapshot) } else segments.minOf { it.availableThroughput }
for (segment in segments) { if (min.isPositive) {
min = minOf(segment.transfer(amount, instantSnapshot), min) if (simulate)
if (!min.isPositive) return Decimal.ZERO segments.forEach { it.transfer(min, instantSnapshot) }
else
segments.forEach { it.transfer(min) }
} }
if (!simulate)
segments.forEach { it.transfer(min) }
return min return min
} }
fun refund(amount: Decimal, simulate: Boolean, instantSnapshot: MutableMap<Segment, Decimal>) {
segments.forEach { it.refund(amount, simulate, instantSnapshot) }
}
fun remove() { fun remove() {
segments.toTypedArray().forEach { it.remove(this) } segments.toTypedArray().forEach { it.remove(this) }
} }
@ -93,18 +128,25 @@ class EnergyCableGraph : GraphNodeList<EnergyCableBlockEntity.Node, EnergyCableG
private val nodes = HashSet<EnergyCableBlockEntity.Node>() private val nodes = HashSet<EnergyCableBlockEntity.Node>()
private val paths = HashSet<SegmentPath>() private val paths = HashSet<SegmentPath>()
operator fun contains(node: EnergyCableBlockEntity.Node): Boolean {
return node in nodes
}
var throughput = Decimal.ZERO var throughput = Decimal.ZERO
private set private set
private fun checkThroughput() {
if (!throughputKnown) {
throughput = nodes.maxOf { it.energyThroughput }
throughputKnown = true
}
}
var transferredLastTick = Decimal.ZERO var transferredLastTick = Decimal.ZERO
private set private set
val availableThroughput: Decimal get() { val availableThroughput: Decimal get() {
if (!throughputKnown) { checkThroughput()
throughput = nodes.maxOf { it.energyThroughput }
throughputKnown = true
}
return throughput - transferredLastTick return throughput - transferredLastTick
} }
@ -141,6 +183,17 @@ class EnergyCableGraph : GraphNodeList<EnergyCableBlockEntity.Node, EnergyCableG
} }
} }
fun throughput(instantSnapshot: Map<Segment, Decimal>): Decimal {
if (lastTick != UNIVERSE_TICKS) {
transferredLastTick = Decimal.ZERO
lastTick = UNIVERSE_TICKS
}
checkThroughput()
val currentTransferred = instantSnapshot[this] ?: transferredLastTick
return throughput - currentTransferred
}
fun transfer(amount: Decimal, instantSnapshot: MutableMap<Segment, Decimal>): Decimal { fun transfer(amount: Decimal, instantSnapshot: MutableMap<Segment, Decimal>): Decimal {
if (lastTick != UNIVERSE_TICKS) { if (lastTick != UNIVERSE_TICKS) {
transferredLastTick = Decimal.ZERO transferredLastTick = Decimal.ZERO
@ -180,6 +233,16 @@ class EnergyCableGraph : GraphNodeList<EnergyCableBlockEntity.Node, EnergyCableG
return diff 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) { 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" } check(nodes.remove(node)) { "Tried to remove node $node from segment $this, but that node does not belong to this segment" }
@ -198,10 +261,7 @@ class EnergyCableGraph : GraphNodeList<EnergyCableBlockEntity.Node, EnergyCableG
check(paths.add(path)) { "Path $path should already contain $this" } check(paths.add(path)) { "Path $path should already contain $this" }
check(path.segments.add(this)) { "Path set and Segment disagree whenever $this is absent from $path" } check(path.segments.add(this)) { "Path set and Segment disagree whenever $this is absent from $path" }
if (!throughputKnown) { checkThroughput()
throughput = nodes.maxOf { it.energyThroughput }
throughputKnown = true
}
} }
fun remove(path: SegmentPath) { fun remove(path: SegmentPath) {
@ -241,23 +301,18 @@ class EnergyCableGraph : GraphNodeList<EnergyCableBlockEntity.Node, EnergyCableG
nodes.forEach { it.onInvalidate() } nodes.forEach { it.onInvalidate() }
} }
// TODO: Multiple paths, so energy can be delivered to receiver through different paths if previously found path is congested // isn't exactly A*, but greedy algorithm, which searched for locally optimal solutions because they lead to globally optimal ones
private fun getPath(a: EnergyCableBlockEntity.Node, b: EnergyCableBlockEntity.Node, energyToTransfer: Decimal): SegmentPath? { private fun findPath(a: EnergyCableBlockEntity.Node, b: EnergyCableBlockEntity.Node, existing: Collection<SegmentPath>, threshold: Decimal): SegmentPath? {
if (!a.canTraverse || !b.canTraverse) val seenTop = existing.any { a in it }
if (seenTop && a.energyThroughput <= threshold) {
return null 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 openNodes = PriorityQueue<SearchNode>()
val seenNodes = HashSet<EnergyCableBlockEntity.Node>() val seenNodes = HashSet<EnergyCableBlockEntity.Node>()
openNodes.add(SearchNode(a, b)) openNodes.add(SearchNode(a, b, null, seenTop))
while (openNodes.isNotEmpty()) { while (openNodes.isNotEmpty()) {
val first = openNodes.remove() val first = openNodes.remove()
@ -280,21 +335,45 @@ class EnergyCableGraph : GraphNodeList<EnergyCableBlockEntity.Node, EnergyCableG
val path = SegmentPath(a.blockEntity.blockPos, b.blockEntity.blockPos) val path = SegmentPath(a.blockEntity.blockPos, b.blockEntity.blockPos)
touchedSegments.forEach { it.add(path) } touchedSegments.forEach { it.add(path) }
list.add(path)
return path return path
} else { } else {
for (neighbour in first.node.neighboursView.values) { for (neighbour in first.node.neighboursView.values) {
if (!seenNodes.add(neighbour) || !neighbour.canTraverse) continue 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 return null
} }
private fun getPath(a: EnergyCableBlockEntity.Node, b: EnergyCableBlockEntity.Node, energyToTransfer: Decimal): List<SegmentPath> {
if (!a.canTraverse || !b.canTraverse)
return listOf()
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 || find in list.paths) {
list.saturated = true
} else {
list.paths.add(find)
maxThroughput = maxOf(maxThroughput, find.availableThroughput)
}
}
}
list.paths.sortByDescending { it.availableThroughput }
return list.paths
}
override fun onNodeRemoved(node: EnergyCableBlockEntity.Node) { override fun onNodeRemoved(node: EnergyCableBlockEntity.Node) {
if (livelyNodes.remove(node)) { if (livelyNodes.remove(node)) {
check(livelyNodesList.remove(node)) check(livelyNodesList.remove(node))
@ -321,31 +400,79 @@ class EnergyCableGraph : GraphNodeList<EnergyCableBlockEntity.Node, EnergyCableG
var hit = false var hit = false
for (side in node.sides.values) { for (side in node.sides.values) {
if (side.isEnabled) { if (!side.isEnabled)
if (fromNode === node && side.side === fromSide) { continue
hit = true else if (fromNode === node && side.side === fromSide) {
hit = true
continue
}
val it = side.neighbour.get() ?: continue
if (it is EnergyCableBlockEntity.CableSide) continue
val paths = getPath(fromNode, node, residue)
hit = true
if (paths.size == 1) {
// Single path, fast scenario
val path = paths[0]
val pathTransferred = path.transfer(residue, simulate, snapshot)
val thisReceived = it.receiveEnergy(pathTransferred, simulate)
// If cable transferred more than machine accepted, then "refund" energy
// so cables record actual value transferred through them
if (thisReceived != pathTransferred) {
path.refund(pathTransferred - thisReceived, simulate, snapshot)
}
received += thisReceived
residue -= thisReceived
if (!residue.isPositive) return received
} else if (paths.size >= 2) {
// Multiple paths, a bit more complicated
// Determine how much machine is likely to accept
val potentiallyAccepted = it.receiveEnergy(residue, true)
// Won't accept anything
if (potentiallyAccepted <= Decimal.ZERO)
continue
// Now determine combined available throughput
// Make a copy of snapshot, so we can freely write into it
val copy = snapshot.clone()
var calcResidue = potentiallyAccepted
// TODO: Currently, all transfers cause Braess's paradox, because of greedy selection of "fastest" cable
// Need to implement heuristics to better distribute load across different paths/segments
for (path in paths) {
val passed = path.transfer(calcResidue, true, copy)
calcResidue -= passed
if (calcResidue <= Decimal.ZERO) break
}
if (calcResidue == potentiallyAccepted) {
// мда
continue continue
} }
side.neighbour.get()?.let { var thisReceived = it.receiveEnergy(potentiallyAccepted - calcResidue, simulate)
if (it !is EnergyCableBlockEntity.CableSide) { received += thisReceived
val path = getPath(fromNode, node, residue) residue -= thisReceived
hit = true
if (path != null) { for (path in paths) {
val thisReceived = it.receiveEnergy(path.transfer(residue, simulate, snapshot), simulate) val passed = path.transfer(thisReceived, simulate, snapshot)
received += thisReceived thisReceived -= passed
residue -= thisReceived if (thisReceived <= Decimal.ZERO) break
if (!residue.isPositive) return received
}
}
} }
if (!residue.isPositive) return received
check(thisReceived <= Decimal.ZERO) { "Путом, алло, Путом, какого чёрта Путом? Путом почему ты заблокировал логику, а Путом?" }
} }
} }
if (!hit) { if (!hit) {
itr.remove() itr.remove()
check(livelyNodes.remove(node)) check(livelyNodes.remove(node)) { "Lively nodes Set does not contain $node" }
} }
} }