Parallelize BatteryRampPegSolver (#12351)
This commit is contained in:
@@ -1,4 +1,8 @@
|
||||
using Robust.Shared.Utility;
|
||||
using Pidgin;
|
||||
using Robust.Shared.Utility;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading.Tasks;
|
||||
using static Content.Server.Power.Pow3r.PowerState;
|
||||
|
||||
namespace Content.Server.Power.Pow3r
|
||||
@@ -17,11 +21,39 @@ namespace Content.Server.Power.Pow3r
|
||||
}
|
||||
}
|
||||
|
||||
private readonly PriorityQueue<int, Network> _sortBuffer = new(new HeightComparer());
|
||||
|
||||
public void Tick(float frameTime, PowerState state)
|
||||
public void Tick(float frameTime, PowerState state, int parallel)
|
||||
{
|
||||
ClearLoadsAndSupplies(state);
|
||||
|
||||
state.GroupedNets ??= GroupByNetworkDepth(state);
|
||||
DebugTools.Assert(state.GroupedNets.Select(x => x.Count).Sum() == state.Networks.Count);
|
||||
|
||||
// Each network height layer can be run in parallel without issues.
|
||||
var opts = new ParallelOptions { MaxDegreeOfParallelism = parallel };
|
||||
foreach (var group in state.GroupedNets)
|
||||
{
|
||||
// Note that many net-layers only have a handful of networks.
|
||||
// E.g., the number of nets from lowest to heights for box and saltern are:
|
||||
// Saltern: 1477, 11, 2, 2, 3.
|
||||
// Box: 3308, 20, 1, 5.
|
||||
//
|
||||
// I have NFI what the overhead for a Parallel.ForEach is, and how it compares to computing differently
|
||||
// sized nets. Basic benchmarking shows that this is better, but maybe the highest-tier nets should just
|
||||
// be run sequentially? But then again, maybe they are 2-3 very BIG networks at the top? So maybe:
|
||||
//
|
||||
// TODO make GroupByNetworkDepth evaluate the TOTAL size of each layer (i.e. loads + chargers +
|
||||
// suppliers + discharger) Then decide based on total layer size whether its worth parallelizing that
|
||||
// layer?
|
||||
Parallel.ForEach(group, opts, net => UpdateNetwork(net, state, frameTime));
|
||||
}
|
||||
|
||||
ClearBatteries(state);
|
||||
|
||||
PowerSolverShared.UpdateRampPositions(frameTime, state);
|
||||
}
|
||||
|
||||
private void ClearLoadsAndSupplies(PowerState state)
|
||||
{
|
||||
// Clear loads and supplies.
|
||||
foreach (var load in state.Loads.Values)
|
||||
{
|
||||
if (load.Paused)
|
||||
@@ -38,106 +70,87 @@ namespace Content.Server.Power.Pow3r
|
||||
supply.CurrentSupply = 0;
|
||||
supply.SupplyRampTarget = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Run a pass to estimate network tree graph height.
|
||||
// This is so that we can run networks before their children,
|
||||
// to avoid draining batteries for a tick if their passing-supply gets cut off.
|
||||
// It's not a big loss if this doesn't work (it won't, in some scenarios), but it's a nice-to-have.
|
||||
foreach (var network in state.Networks.Values)
|
||||
{
|
||||
network.HeightTouched = false;
|
||||
network.Height = -1;
|
||||
}
|
||||
private void UpdateNetwork(Network network, PowerState state, float frameTime)
|
||||
{
|
||||
// TODO Look at SIMD.
|
||||
// a lot of this is performing very basic math on arrays of data objects like batteries
|
||||
// this really shouldn't be hard to do.
|
||||
// except for maybe the paused/enabled guff. If its mostly false, I guess they could just be 0 multipliers?
|
||||
|
||||
foreach (var network in state.Networks.Values)
|
||||
// Add up demand from loads.
|
||||
var demand = 0f;
|
||||
foreach (var loadId in network.Loads)
|
||||
{
|
||||
if (network.BatteriesDischarging.Count != 0)
|
||||
var load = state.Loads[loadId];
|
||||
|
||||
if (!load.Enabled || load.Paused)
|
||||
continue;
|
||||
|
||||
EstimateNetworkDepth(state, network);
|
||||
DebugTools.Assert(load.DesiredPower >= 0);
|
||||
demand += load.DesiredPower;
|
||||
}
|
||||
|
||||
foreach (var network in state.Networks.Values)
|
||||
// TODO: Consider having battery charge loads be processed "after" pass-through loads.
|
||||
// This would mean that charge rate would have no impact on throughput rate like it does currently.
|
||||
// Would require a second pass over the network, or something. Not sure.
|
||||
|
||||
// Add demand from batteries
|
||||
foreach (var batteryId in network.BatteryLoads)
|
||||
{
|
||||
_sortBuffer.Enqueue(network.Height, network);
|
||||
var battery = state.Batteries[batteryId];
|
||||
if (!battery.Enabled || !battery.CanCharge || battery.Paused)
|
||||
continue;
|
||||
|
||||
var batterySpace = (battery.Capacity - battery.CurrentStorage) * (1 / battery.Efficiency);
|
||||
batterySpace = Math.Max(0, batterySpace);
|
||||
var scaledSpace = batterySpace / frameTime;
|
||||
|
||||
var chargeRate = battery.MaxChargeRate + battery.LoadingNetworkDemand / battery.Efficiency;
|
||||
|
||||
battery.DesiredPower = Math.Min(chargeRate, scaledSpace);
|
||||
DebugTools.Assert(battery.DesiredPower >= 0);
|
||||
demand += battery.DesiredPower;
|
||||
}
|
||||
|
||||
// Go over every network.
|
||||
while (_sortBuffer.TryDequeue(out _, out var network))
|
||||
DebugTools.Assert(demand >= 0);
|
||||
|
||||
// Add up supply in network.
|
||||
var totalSupply = 0f;
|
||||
var totalMaxSupply = 0f;
|
||||
foreach (var supplyId in network.Supplies)
|
||||
{
|
||||
// Add up demand in network.
|
||||
var demand = 0f;
|
||||
foreach (var loadId in network.Loads)
|
||||
{
|
||||
var load = state.Loads[loadId];
|
||||
var supply = state.Supplies[supplyId];
|
||||
if (!supply.Enabled || supply.Paused)
|
||||
continue;
|
||||
|
||||
if (!load.Enabled || load.Paused)
|
||||
continue;
|
||||
var rampMax = supply.SupplyRampPosition + supply.SupplyRampTolerance;
|
||||
var effectiveSupply = Math.Min(rampMax, supply.MaxSupply);
|
||||
|
||||
DebugTools.Assert(load.DesiredPower >= 0);
|
||||
demand += load.DesiredPower;
|
||||
}
|
||||
DebugTools.Assert(effectiveSupply >= 0);
|
||||
DebugTools.Assert(supply.MaxSupply >= 0);
|
||||
|
||||
// TODO: Consider having battery charge loads be processed "after" pass-through loads.
|
||||
// This would mean that charge rate would have no impact on throughput rate like it does currently.
|
||||
// Would require a second pass over the network, or something. Not sure.
|
||||
supply.AvailableSupply = effectiveSupply;
|
||||
totalSupply += effectiveSupply;
|
||||
totalMaxSupply += supply.MaxSupply;
|
||||
}
|
||||
|
||||
// Loading batteries.
|
||||
foreach (var batteryId in network.BatteriesCharging)
|
||||
{
|
||||
var battery = state.Batteries[batteryId];
|
||||
if (!battery.Enabled || !battery.CanCharge || battery.Paused)
|
||||
continue;
|
||||
var unmet = Math.Max(0, demand - totalSupply);
|
||||
DebugTools.Assert(totalSupply >= 0);
|
||||
DebugTools.Assert(totalMaxSupply >= 0);
|
||||
|
||||
var batterySpace = (battery.Capacity - battery.CurrentStorage) * (1 / battery.Efficiency);
|
||||
batterySpace = Math.Max(0, batterySpace);
|
||||
var scaledSpace = batterySpace / frameTime;
|
||||
// Supplying batteries. Batteries need to go after local supplies so that local supplies are prioritized.
|
||||
// Also, it makes demand-pulling of batteries. Because all batteries will desire the unmet demand of their
|
||||
// loading network, there will be a "rush" of input current when a network powers on, before power
|
||||
// stabilizes in the network. This is fine.
|
||||
|
||||
var chargeRate = battery.MaxChargeRate + battery.LoadingNetworkDemand / battery.Efficiency;
|
||||
|
||||
var batDemand = Math.Min(chargeRate, scaledSpace);
|
||||
|
||||
DebugTools.Assert(batDemand >= 0);
|
||||
|
||||
battery.DesiredPower = batDemand;
|
||||
demand += batDemand;
|
||||
}
|
||||
|
||||
DebugTools.Assert(demand >= 0);
|
||||
|
||||
// Add up supply in network.
|
||||
var availableSupplySum = 0f;
|
||||
var maxSupplySum = 0f;
|
||||
foreach (var supplyId in network.Supplies)
|
||||
{
|
||||
var supply = state.Supplies[supplyId];
|
||||
if (!supply.Enabled || supply.Paused)
|
||||
continue;
|
||||
|
||||
var rampMax = supply.SupplyRampPosition + supply.SupplyRampTolerance;
|
||||
var effectiveSupply = Math.Min(rampMax, supply.MaxSupply);
|
||||
|
||||
DebugTools.Assert(effectiveSupply >= 0);
|
||||
DebugTools.Assert(supply.MaxSupply >= 0);
|
||||
|
||||
supply.EffectiveMaxSupply = effectiveSupply;
|
||||
availableSupplySum += effectiveSupply;
|
||||
maxSupplySum += supply.MaxSupply;
|
||||
}
|
||||
|
||||
var unmet = Math.Max(0, demand - availableSupplySum);
|
||||
|
||||
DebugTools.Assert(availableSupplySum >= 0);
|
||||
DebugTools.Assert(maxSupplySum >= 0);
|
||||
|
||||
// Supplying batteries.
|
||||
// Batteries need to go after local supplies so that local supplies are prioritized.
|
||||
// Also, it makes demand-pulling of batteries
|
||||
// Because all batteries will will desire the unmet demand of their loading network,
|
||||
// there will be a "rush" of input current when a network powers on,
|
||||
// before power stabilizes in the network.
|
||||
// This is fine.
|
||||
foreach (var batteryId in network.BatteriesDischarging)
|
||||
var totalBatterySupply = 0f;
|
||||
var totalMaxBatterySupply = 0f;
|
||||
if (unmet > 0)
|
||||
{
|
||||
// determine supply available from batteries
|
||||
foreach (var batteryId in network.BatterySupplies)
|
||||
{
|
||||
var battery = state.Batteries[batteryId];
|
||||
if (!battery.Enabled || !battery.CanDischarge || battery.Paused)
|
||||
@@ -147,103 +160,107 @@ namespace Content.Server.Power.Pow3r
|
||||
var supplyCap = Math.Min(battery.MaxSupply,
|
||||
battery.SupplyRampPosition + battery.SupplyRampTolerance);
|
||||
var supplyAndPassthrough = supplyCap + battery.CurrentReceiving * battery.Efficiency;
|
||||
var tempSupply = Math.Min(scaledSpace, supplyAndPassthrough);
|
||||
// Clamp final supply to the unmet demand, so that batteries refrain from taking power away from supplies.
|
||||
var clampedSupply = Math.Min(unmet, tempSupply);
|
||||
|
||||
DebugTools.Assert(clampedSupply >= 0);
|
||||
|
||||
battery.TempMaxSupply = clampedSupply;
|
||||
availableSupplySum += clampedSupply;
|
||||
// TODO: Calculate this properly.
|
||||
maxSupplySum += clampedSupply;
|
||||
|
||||
battery.AvailableSupply = Math.Min(scaledSpace, supplyAndPassthrough);
|
||||
battery.LoadingNetworkDemand = unmet;
|
||||
battery.LoadingDemandMarked = true;
|
||||
}
|
||||
|
||||
network.LastAvailableSupplySum = availableSupplySum;
|
||||
network.LastMaxSupplySum = maxSupplySum;
|
||||
|
||||
var met = Math.Min(demand, availableSupplySum);
|
||||
|
||||
if (met != 0)
|
||||
{
|
||||
// Distribute supply to loads.
|
||||
foreach (var loadId in network.Loads)
|
||||
{
|
||||
var load = state.Loads[loadId];
|
||||
if (!load.Enabled || load.DesiredPower == 0 || load.Paused)
|
||||
continue;
|
||||
|
||||
var ratio = load.DesiredPower / demand;
|
||||
load.ReceivingPower = ratio * met;
|
||||
}
|
||||
|
||||
// Loading batteries
|
||||
foreach (var batteryId in network.BatteriesCharging)
|
||||
{
|
||||
var battery = state.Batteries[batteryId];
|
||||
|
||||
if (!battery.Enabled || battery.DesiredPower == 0 || battery.Paused)
|
||||
continue;
|
||||
|
||||
var ratio = battery.DesiredPower / demand;
|
||||
battery.CurrentReceiving = ratio * met;
|
||||
var receivedPower = frameTime * battery.CurrentReceiving;
|
||||
receivedPower *= battery.Efficiency;
|
||||
battery.CurrentStorage = Math.Min(
|
||||
battery.Capacity,
|
||||
battery.CurrentStorage + receivedPower);
|
||||
battery.LoadingMarked = true;
|
||||
}
|
||||
|
||||
// Load to supplies
|
||||
foreach (var supplyId in network.Supplies)
|
||||
{
|
||||
var supply = state.Supplies[supplyId];
|
||||
if (!supply.Enabled || supply.EffectiveMaxSupply == 0 || supply.Paused)
|
||||
continue;
|
||||
|
||||
var ratio = supply.EffectiveMaxSupply / availableSupplySum;
|
||||
supply.CurrentSupply = ratio * met;
|
||||
|
||||
if (supply.MaxSupply != 0)
|
||||
{
|
||||
var maxSupplyRatio = supply.MaxSupply / maxSupplySum;
|
||||
|
||||
supply.SupplyRampTarget = maxSupplyRatio * demand;
|
||||
}
|
||||
else
|
||||
{
|
||||
supply.SupplyRampTarget = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Supplying batteries
|
||||
foreach (var batteryId in network.BatteriesDischarging)
|
||||
{
|
||||
var battery = state.Batteries[batteryId];
|
||||
if (!battery.Enabled || battery.TempMaxSupply == 0 || battery.Paused)
|
||||
continue;
|
||||
|
||||
var ratio = battery.TempMaxSupply / availableSupplySum;
|
||||
battery.CurrentSupply = ratio * met;
|
||||
|
||||
battery.CurrentStorage = Math.Max(
|
||||
0,
|
||||
battery.CurrentStorage - frameTime * battery.CurrentSupply);
|
||||
|
||||
battery.SupplyRampTarget = battery.CurrentSupply - battery.CurrentReceiving * battery.Efficiency;
|
||||
|
||||
/*var maxSupplyRatio = supply.MaxSupply / maxSupplySum;
|
||||
|
||||
supply.SupplyRampTarget = maxSupplyRatio * demand;*/
|
||||
battery.SupplyingMarked = true;
|
||||
}
|
||||
battery.MaxEffectiveSupply = Math.Min(battery.CurrentStorage / frameTime, battery.MaxSupply + battery.CurrentReceiving * battery.Efficiency);
|
||||
totalBatterySupply += battery.AvailableSupply;
|
||||
totalMaxBatterySupply += battery.MaxEffectiveSupply;
|
||||
}
|
||||
}
|
||||
|
||||
network.LastCombinedSupply = totalSupply + totalBatterySupply;
|
||||
network.LastCombinedMaxSupply = totalMaxSupply + totalMaxBatterySupply;
|
||||
|
||||
var met = Math.Min(demand, network.LastCombinedSupply);
|
||||
if (met == 0)
|
||||
return;
|
||||
|
||||
var supplyRatio = met / demand;
|
||||
|
||||
// Distribute supply to loads.
|
||||
foreach (var loadId in network.Loads)
|
||||
{
|
||||
var load = state.Loads[loadId];
|
||||
if (!load.Enabled || load.DesiredPower == 0 || load.Paused)
|
||||
continue;
|
||||
|
||||
load.ReceivingPower = load.DesiredPower * supplyRatio;
|
||||
}
|
||||
|
||||
// Distribute supply to batteries
|
||||
foreach (var batteryId in network.BatteryLoads)
|
||||
{
|
||||
var battery = state.Batteries[batteryId];
|
||||
if (!battery.Enabled || battery.DesiredPower == 0 || battery.Paused)
|
||||
continue;
|
||||
|
||||
battery.LoadingMarked = true;
|
||||
battery.CurrentReceiving = battery.DesiredPower * supplyRatio;
|
||||
battery.CurrentStorage += frameTime * battery.CurrentReceiving * battery.Efficiency;
|
||||
|
||||
DebugTools.Assert(battery.CurrentStorage <= battery.Capacity || MathHelper.CloseTo(battery.CurrentStorage, battery.Capacity));
|
||||
}
|
||||
|
||||
// Target output capacity for supplies
|
||||
var metSupply = Math.Min(demand, totalSupply);
|
||||
if (metSupply > 0)
|
||||
{
|
||||
var relativeSupplyOutput = metSupply / totalSupply;
|
||||
var targetRelativeSupplyOutput = Math.Min(demand, totalMaxSupply) / totalMaxSupply;
|
||||
|
||||
// Apply load to supplies
|
||||
foreach (var supplyId in network.Supplies)
|
||||
{
|
||||
var supply = state.Supplies[supplyId];
|
||||
if (!supply.Enabled || supply.Paused)
|
||||
continue;
|
||||
|
||||
supply.CurrentSupply = supply.AvailableSupply * relativeSupplyOutput;
|
||||
|
||||
// Supply ramp assumes all supplies ramp at the same rate. If some generators spin up very slowly, in
|
||||
// principle the fast supplies should try over-shoot until they can settle back down. E.g., all supplies
|
||||
// need to reach 50% capacity, but it takes the nuclear reactor 1 hour to reach that, then our lil coal
|
||||
// furnaces should run at 100% for a while. But I guess this is good enough for now.
|
||||
supply.SupplyRampTarget = supply.MaxSupply * targetRelativeSupplyOutput;
|
||||
}
|
||||
}
|
||||
|
||||
if (unmet <= 0 || totalBatterySupply <= 0)
|
||||
return;
|
||||
|
||||
// Target output capacity for batteries
|
||||
var relativeBatteryOutput = Math.Min(unmet, totalBatterySupply) / totalBatterySupply;
|
||||
var relativeTargetBatteryOutput = Math.Min(unmet, totalMaxBatterySupply) / totalMaxBatterySupply;
|
||||
|
||||
// Apply load to supplying batteries
|
||||
foreach (var batteryId in network.BatterySupplies)
|
||||
{
|
||||
var battery = state.Batteries[batteryId];
|
||||
if (!battery.Enabled || battery.Paused)
|
||||
continue;
|
||||
|
||||
battery.SupplyingMarked = true;
|
||||
battery.CurrentSupply = battery.AvailableSupply * relativeBatteryOutput;
|
||||
// Note that because available supply is always greater than or equal to the current ramp target, if you
|
||||
// have multiple batteries running at less than 100% output, then batteries with greater ramp tolerances
|
||||
// will contribute a larger relative fraction of output power. This is because while they will both ramp
|
||||
// to the same relative maximum output, the larger tolerance will mean that one will have a larger
|
||||
// available supply. IMO this is undesirable, but I can't think of an easy fix ATM.
|
||||
|
||||
battery.CurrentStorage -= frameTime * battery.CurrentSupply;
|
||||
DebugTools.Assert(battery.CurrentStorage >= 0 || MathHelper.CloseTo(battery.CurrentStorage, 0));
|
||||
|
||||
battery.SupplyRampTarget = battery.MaxEffectiveSupply * relativeTargetBatteryOutput - battery.CurrentReceiving * battery.Efficiency;
|
||||
|
||||
DebugTools.Assert(battery.SupplyRampTarget + battery.CurrentReceiving * battery.Efficiency <= battery.LoadingNetworkDemand
|
||||
|| MathHelper.CloseTo(battery.SupplyRampTarget + battery.CurrentReceiving * battery.Efficiency, battery.LoadingNetworkDemand, 0.01));
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearBatteries(PowerState state)
|
||||
{
|
||||
// Clear supplying/loading on any batteries that haven't been marked by usage.
|
||||
// Because we need this data while processing ramp-pegging, we can't clear it at the start.
|
||||
foreach (var battery in state.Batteries.Values)
|
||||
@@ -252,48 +269,69 @@ namespace Content.Server.Power.Pow3r
|
||||
continue;
|
||||
|
||||
if (!battery.SupplyingMarked)
|
||||
{
|
||||
battery.CurrentSupply = 0;
|
||||
battery.SupplyRampTarget = 0;
|
||||
battery.LoadingNetworkDemand = 0;
|
||||
}
|
||||
|
||||
if (!battery.LoadingMarked)
|
||||
{
|
||||
battery.CurrentReceiving = 0;
|
||||
|
||||
if (!battery.LoadingDemandMarked)
|
||||
battery.LoadingNetworkDemand = 0;
|
||||
}
|
||||
|
||||
battery.SupplyingMarked = false;
|
||||
battery.LoadingMarked = false;
|
||||
battery.LoadingDemandMarked = false;
|
||||
}
|
||||
|
||||
PowerSolverShared.UpdateRampPositions(frameTime, state);
|
||||
}
|
||||
|
||||
private static void EstimateNetworkDepth(PowerState state, Network network)
|
||||
private List<List<Network>> GroupByNetworkDepth(PowerState state)
|
||||
{
|
||||
network.HeightTouched = true;
|
||||
|
||||
if (network.BatteriesCharging.Count == 0)
|
||||
List<List<Network>> groupedNetworks = new();
|
||||
foreach (var network in state.Networks.Values)
|
||||
{
|
||||
network.Height = 1;
|
||||
return;
|
||||
network.Height = -1;
|
||||
}
|
||||
|
||||
var max = 0;
|
||||
foreach (var batteryId in network.BatteriesCharging)
|
||||
foreach (var network in state.Networks.Values)
|
||||
{
|
||||
if (network.Height == -1)
|
||||
RecursivelyEstimateNetworkDepth(state, network, groupedNetworks);
|
||||
}
|
||||
|
||||
return groupedNetworks;
|
||||
}
|
||||
|
||||
private static void RecursivelyEstimateNetworkDepth(PowerState state, Network network, List<List<Network>> groupedNetworks)
|
||||
{
|
||||
network.Height = -2;
|
||||
var height = -1;
|
||||
|
||||
foreach (var batteryId in network.BatteryLoads)
|
||||
{
|
||||
var battery = state.Batteries[batteryId];
|
||||
|
||||
if (battery.LinkedNetworkDischarging == default)
|
||||
if (battery.LinkedNetworkDischarging == default || battery.LinkedNetworkDischarging == network.Id)
|
||||
continue;
|
||||
|
||||
var subNet = state.Networks[battery.LinkedNetworkDischarging];
|
||||
if (!subNet.HeightTouched)
|
||||
EstimateNetworkDepth(state, subNet);
|
||||
if (subNet.Height == -1)
|
||||
RecursivelyEstimateNetworkDepth(state, subNet, groupedNetworks);
|
||||
else if (subNet.Height == -2)
|
||||
{
|
||||
// this network is currently computing its own height (we encountered a loop).
|
||||
continue;
|
||||
}
|
||||
|
||||
max = Math.Max(subNet.Height, max);
|
||||
height = Math.Max(subNet.Height, height);
|
||||
}
|
||||
|
||||
network.Height = 1 + max;
|
||||
network.Height = 1 + height;
|
||||
|
||||
if (network.Height >= groupedNetworks.Count)
|
||||
groupedNetworks.Add(new() { network });
|
||||
else
|
||||
groupedNetworks[network.Height].Add(network);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user