BatteryRampPegSolver.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445
  1. using System.Diagnostics;
  2. using Robust.Shared.Utility;
  3. using System.Linq;
  4. using Robust.Shared.Threading;
  5. using static Content.Server.Power.Pow3r.PowerState;
  6. namespace Content.Server.Power.Pow3r
  7. {
  8. public sealed class BatteryRampPegSolver : IPowerSolver
  9. {
  10. private UpdateNetworkJob _networkJob;
  11. private bool _disableParallel;
  12. public BatteryRampPegSolver(bool disableParallel = false)
  13. {
  14. _disableParallel = disableParallel;
  15. _networkJob = new()
  16. {
  17. Solver = this,
  18. };
  19. }
  20. private sealed class HeightComparer : Comparer<Network>
  21. {
  22. public static HeightComparer Instance { get; } = new();
  23. public override int Compare(Network? x, Network? y)
  24. {
  25. if (x!.Height == y!.Height) return 0;
  26. if (x!.Height > y!.Height) return 1;
  27. return -1;
  28. }
  29. }
  30. public void Tick(float frameTime, PowerState state, IParallelManager parallel)
  31. {
  32. ClearLoadsAndSupplies(state);
  33. state.GroupedNets ??= GroupByNetworkDepth(state);
  34. DebugTools.Assert(state.GroupedNets.Select(x => x.Count).Sum() == state.Networks.Count);
  35. _networkJob.State = state;
  36. _networkJob.FrameTime = frameTime;
  37. ValidateNetworkGroups(state, state.GroupedNets);
  38. // Each network height layer can be run in parallel without issues.
  39. foreach (var group in state.GroupedNets)
  40. {
  41. // Note that many net-layers only have a handful of networks.
  42. // E.g., the number of nets from lowest to highest for box and saltern are:
  43. // Saltern: 1477, 11, 2, 2, 3.
  44. // Box: 3308, 20, 1, 5.
  45. //
  46. // I have NFI what the overhead for a Parallel.ForEach is, and how it compares to computing differently
  47. // sized nets. Basic benchmarking shows that this is better, but maybe the highest-tier nets should just
  48. // be run sequentially? But then again, maybe they are 2-3 very BIG networks at the top? So maybe:
  49. //
  50. // TODO make GroupByNetworkDepth evaluate the TOTAL size of each layer (i.e. loads + chargers +
  51. // suppliers + discharger) Then decide based on total layer size whether its worth parallelizing that
  52. // layer?
  53. _networkJob.Networks = group;
  54. if (_disableParallel)
  55. parallel.ProcessSerialNow(_networkJob, group.Count);
  56. else
  57. parallel.ProcessNow(_networkJob, group.Count);
  58. }
  59. ClearBatteries(state);
  60. PowerSolverShared.UpdateRampPositions(frameTime, state);
  61. }
  62. private void ClearLoadsAndSupplies(PowerState state)
  63. {
  64. foreach (var load in state.Loads.Values)
  65. {
  66. if (load.Paused)
  67. continue;
  68. load.ReceivingPower = 0;
  69. }
  70. foreach (var supply in state.Supplies.Values)
  71. {
  72. if (supply.Paused)
  73. continue;
  74. supply.CurrentSupply = 0;
  75. supply.SupplyRampTarget = 0;
  76. }
  77. }
  78. private void UpdateNetwork(Network network, PowerState state, float frameTime)
  79. {
  80. // TODO Look at SIMD.
  81. // a lot of this is performing very basic math on arrays of data objects like batteries
  82. // this really shouldn't be hard to do.
  83. // except for maybe the paused/enabled guff. If its mostly false, I guess they could just be 0 multipliers?
  84. // Add up demand from loads.
  85. var demand = 0f;
  86. foreach (var loadId in network.Loads)
  87. {
  88. var load = state.Loads[loadId];
  89. if (!load.Enabled || load.Paused)
  90. continue;
  91. DebugTools.Assert(load.DesiredPower >= 0);
  92. demand += load.DesiredPower;
  93. }
  94. // TODO: Consider having battery charge loads be processed "after" pass-through loads.
  95. // This would mean that charge rate would have no impact on throughput rate like it does currently.
  96. // Would require a second pass over the network, or something. Not sure.
  97. // Add demand from batteries
  98. foreach (var batteryId in network.BatteryLoads)
  99. {
  100. var battery = state.Batteries[batteryId];
  101. if (!battery.Enabled || !battery.CanCharge || battery.Paused)
  102. continue;
  103. var batterySpace = (battery.Capacity - battery.CurrentStorage) * (1 / battery.Efficiency);
  104. batterySpace = Math.Max(0, batterySpace);
  105. var scaledSpace = batterySpace / frameTime;
  106. var chargeRate = battery.MaxChargeRate + battery.LoadingNetworkDemand / battery.Efficiency;
  107. battery.DesiredPower = Math.Min(chargeRate, scaledSpace);
  108. DebugTools.Assert(battery.DesiredPower >= 0);
  109. demand += battery.DesiredPower;
  110. }
  111. DebugTools.Assert(demand >= 0);
  112. // Add up supply in network.
  113. var totalSupply = 0f;
  114. var totalMaxSupply = 0f;
  115. foreach (var supplyId in network.Supplies)
  116. {
  117. var supply = state.Supplies[supplyId];
  118. if (!supply.Enabled || supply.Paused)
  119. continue;
  120. var rampMax = supply.SupplyRampPosition + supply.SupplyRampTolerance;
  121. var effectiveSupply = Math.Min(rampMax, supply.MaxSupply);
  122. DebugTools.Assert(effectiveSupply >= 0);
  123. DebugTools.Assert(supply.MaxSupply >= 0);
  124. supply.AvailableSupply = effectiveSupply;
  125. totalSupply += effectiveSupply;
  126. totalMaxSupply += supply.MaxSupply;
  127. }
  128. var unmet = Math.Max(0, demand - totalSupply);
  129. DebugTools.Assert(totalSupply >= 0);
  130. DebugTools.Assert(totalMaxSupply >= 0);
  131. // Supplying batteries. Batteries need to go after local supplies so that local supplies are prioritized.
  132. // Also, it makes demand-pulling of batteries. Because all batteries will desire the unmet demand of their
  133. // loading network, there will be a "rush" of input current when a network powers on, before power
  134. // stabilizes in the network. This is fine.
  135. var totalBatterySupply = 0f;
  136. var totalMaxBatterySupply = 0f;
  137. if (unmet > 0)
  138. {
  139. // determine supply available from batteries
  140. foreach (var batteryId in network.BatterySupplies)
  141. {
  142. var battery = state.Batteries[batteryId];
  143. if (!battery.Enabled || !battery.CanDischarge || battery.Paused)
  144. continue;
  145. var scaledSpace = battery.CurrentStorage / frameTime;
  146. var supplyCap = Math.Min(battery.MaxSupply,
  147. battery.SupplyRampPosition + battery.SupplyRampTolerance);
  148. var supplyAndPassthrough = supplyCap + battery.CurrentReceiving * battery.Efficiency;
  149. battery.AvailableSupply = Math.Min(scaledSpace, supplyAndPassthrough);
  150. battery.LoadingNetworkDemand = unmet;
  151. battery.MaxEffectiveSupply = Math.Min(battery.CurrentStorage / frameTime, battery.MaxSupply + battery.CurrentReceiving * battery.Efficiency);
  152. totalBatterySupply += battery.AvailableSupply;
  153. totalMaxBatterySupply += battery.MaxEffectiveSupply;
  154. }
  155. }
  156. network.LastCombinedLoad = demand;
  157. network.LastCombinedSupply = totalSupply + totalBatterySupply;
  158. network.LastCombinedMaxSupply = totalMaxSupply + totalMaxBatterySupply;
  159. var met = Math.Min(demand, network.LastCombinedSupply);
  160. if (met == 0)
  161. return;
  162. var supplyRatio = met / demand;
  163. // if supply ratio == 1 (or is close to) we could skip some math for each load & battery.
  164. // Distribute supply to loads.
  165. foreach (var loadId in network.Loads)
  166. {
  167. var load = state.Loads[loadId];
  168. if (!load.Enabled || load.DesiredPower == 0 || load.Paused)
  169. continue;
  170. load.ReceivingPower = load.DesiredPower * supplyRatio;
  171. }
  172. // Distribute supply to batteries
  173. foreach (var batteryId in network.BatteryLoads)
  174. {
  175. var battery = state.Batteries[batteryId];
  176. if (!battery.Enabled || battery.DesiredPower == 0 || battery.Paused || !battery.CanCharge)
  177. continue;
  178. battery.LoadingMarked = true;
  179. battery.CurrentReceiving = battery.DesiredPower * supplyRatio;
  180. battery.CurrentStorage += frameTime * battery.CurrentReceiving * battery.Efficiency;
  181. DebugTools.Assert(battery.CurrentStorage <= battery.Capacity || MathHelper.CloseTo(battery.CurrentStorage, battery.Capacity, 1e-5));
  182. battery.CurrentStorage = MathF.Min(battery.CurrentStorage, battery.Capacity);
  183. }
  184. // Target output capacity for supplies
  185. var metSupply = Math.Min(demand, totalSupply);
  186. if (metSupply > 0)
  187. {
  188. var relativeSupplyOutput = metSupply / totalSupply;
  189. var targetRelativeSupplyOutput = Math.Min(demand, totalMaxSupply) / totalMaxSupply;
  190. // Apply load to supplies
  191. foreach (var supplyId in network.Supplies)
  192. {
  193. var supply = state.Supplies[supplyId];
  194. if (!supply.Enabled || supply.Paused)
  195. continue;
  196. supply.CurrentSupply = supply.AvailableSupply * relativeSupplyOutput;
  197. // Supply ramp assumes all supplies ramp at the same rate. If some generators spin up very slowly, in
  198. // principle the fast supplies should try over-shoot until they can settle back down. E.g., all supplies
  199. // need to reach 50% capacity, but it takes the nuclear reactor 1 hour to reach that, then our lil coal
  200. // furnaces should run at 100% for a while. But I guess this is good enough for now.
  201. supply.SupplyRampTarget = supply.MaxSupply * targetRelativeSupplyOutput;
  202. }
  203. }
  204. // Return if normal supplies met all demand or there are no supplying batteries
  205. if (unmet <= 0 || totalMaxBatterySupply <= 0)
  206. return;
  207. // Target output capacity for batteries
  208. var relativeBatteryOutput = Math.Min(unmet, totalBatterySupply) / totalBatterySupply;
  209. var relativeTargetBatteryOutput = Math.Min(unmet, totalMaxBatterySupply) / totalMaxBatterySupply;
  210. // Apply load to supplying batteries
  211. foreach (var batteryId in network.BatterySupplies)
  212. {
  213. var battery = state.Batteries[batteryId];
  214. if (!battery.Enabled || battery.Paused || !battery.CanDischarge)
  215. continue;
  216. battery.SupplyingMarked = true;
  217. battery.CurrentSupply = battery.AvailableSupply * relativeBatteryOutput;
  218. // Note that because available supply is always greater than or equal to the current ramp target, if you
  219. // have multiple batteries running at less than 100% output, then batteries with greater ramp tolerances
  220. // will contribute a larger relative fraction of output power. This is because while they will both ramp
  221. // to the same relative maximum output, the larger tolerance will mean that one will have a larger
  222. // available supply. IMO this is undesirable, but I can't think of an easy fix ATM.
  223. battery.CurrentStorage -= frameTime * battery.CurrentSupply;
  224. #if DEBUG
  225. // Manual "MathHelper.CloseToPercent" using the subtracted value to define the relative error.
  226. if (battery.CurrentStorage < 0)
  227. {
  228. float epsilon = Math.Max(frameTime * battery.CurrentSupply, 1) * 1e-4f;
  229. DebugTools.Assert(battery.CurrentStorage > -epsilon);
  230. }
  231. #endif
  232. battery.CurrentStorage = MathF.Max(0, battery.CurrentStorage);
  233. battery.SupplyRampTarget = battery.MaxEffectiveSupply * relativeTargetBatteryOutput - battery.CurrentReceiving * battery.Efficiency;
  234. DebugTools.Assert(battery.MaxEffectiveSupply * relativeTargetBatteryOutput <= battery.LoadingNetworkDemand
  235. || MathHelper.CloseToPercent(battery.MaxEffectiveSupply * relativeTargetBatteryOutput, battery.LoadingNetworkDemand, 0.001));
  236. }
  237. }
  238. private void ClearBatteries(PowerState state)
  239. {
  240. // Clear supplying/loading on any batteries that haven't been marked by usage.
  241. // Because we need this data while processing ramp-pegging, we can't clear it at the start.
  242. foreach (var battery in state.Batteries.Values)
  243. {
  244. if (battery.Paused)
  245. continue;
  246. if (!battery.SupplyingMarked)
  247. {
  248. battery.CurrentSupply = 0;
  249. battery.SupplyRampTarget = 0;
  250. battery.LoadingNetworkDemand = 0;
  251. }
  252. if (!battery.LoadingMarked)
  253. {
  254. battery.CurrentReceiving = 0;
  255. }
  256. battery.SupplyingMarked = false;
  257. battery.LoadingMarked = false;
  258. }
  259. }
  260. private List<List<Network>> GroupByNetworkDepth(PowerState state)
  261. {
  262. List<List<Network>> groupedNetworks = new();
  263. foreach (var network in state.Networks.Values)
  264. {
  265. network.Height = -1;
  266. }
  267. foreach (var network in state.Networks.Values)
  268. {
  269. if (network.Height == -1)
  270. RecursivelyEstimateNetworkDepth(state, network, groupedNetworks);
  271. }
  272. ValidateNetworkGroups(state, groupedNetworks);
  273. return groupedNetworks;
  274. }
  275. /// <summary>
  276. /// Validate that network grouping is up to date. I.e., that it is safe to solve each networking in a given
  277. /// group in parallel. This assumes that batteries are the only device that connects to multiple networks, and
  278. /// is thus the only obstacle to solving everything in parallel.
  279. /// </summary>
  280. [Conditional("DEBUG")]
  281. private void ValidateNetworkGroups(PowerState state, List<List<Network>> groupedNetworks)
  282. {
  283. HashSet<Network> nets = new();
  284. HashSet<NodeId> netIds = new();
  285. foreach (var layer in groupedNetworks)
  286. {
  287. nets.Clear();
  288. netIds.Clear();
  289. foreach (var net in layer)
  290. {
  291. foreach (var batteryId in net.BatteryLoads)
  292. {
  293. var battery = state.Batteries[batteryId];
  294. if (battery.LinkedNetworkDischarging == default)
  295. continue;
  296. var subNet = state.Networks[battery.LinkedNetworkDischarging];
  297. if (battery.LinkedNetworkDischarging == net.Id)
  298. {
  299. DebugTools.Assert(subNet == net);
  300. continue;
  301. }
  302. DebugTools.Assert(!nets.Contains(subNet));
  303. DebugTools.Assert(!netIds.Contains(subNet.Id));
  304. DebugTools.Assert(subNet.Height < net.Height);
  305. }
  306. foreach (var batteryId in net.BatterySupplies)
  307. {
  308. var battery = state.Batteries[batteryId];
  309. if (battery.LinkedNetworkCharging == default)
  310. continue;
  311. var parentNet = state.Networks[battery.LinkedNetworkCharging];
  312. if (battery.LinkedNetworkCharging == net.Id)
  313. {
  314. DebugTools.Assert(parentNet == net);
  315. continue;
  316. }
  317. DebugTools.Assert(!nets.Contains(parentNet));
  318. DebugTools.Assert(!netIds.Contains(parentNet.Id));
  319. DebugTools.Assert(parentNet.Height > net.Height);
  320. }
  321. DebugTools.Assert(nets.Add(net));
  322. DebugTools.Assert(netIds.Add(net.Id));
  323. }
  324. }
  325. }
  326. private static void RecursivelyEstimateNetworkDepth(PowerState state, Network network, List<List<Network>> groupedNetworks)
  327. {
  328. network.Height = -2;
  329. var height = -1;
  330. foreach (var batteryId in network.BatteryLoads)
  331. {
  332. var battery = state.Batteries[batteryId];
  333. if (battery.LinkedNetworkDischarging == default || battery.LinkedNetworkDischarging == network.Id)
  334. continue;
  335. var subNet = state.Networks[battery.LinkedNetworkDischarging];
  336. if (subNet.Height == -1)
  337. RecursivelyEstimateNetworkDepth(state, subNet, groupedNetworks);
  338. else if (subNet.Height == -2)
  339. {
  340. // this network is currently computing its own height (we encountered a loop).
  341. continue;
  342. }
  343. height = Math.Max(subNet.Height, height);
  344. }
  345. network.Height = 1 + height;
  346. if (network.Height >= groupedNetworks.Count)
  347. groupedNetworks.Add(new() { network });
  348. else
  349. groupedNetworks[network.Height].Add(network);
  350. }
  351. #region Jobs
  352. private record struct UpdateNetworkJob : IParallelRobustJob
  353. {
  354. public int BatchSize => 4;
  355. public BatteryRampPegSolver Solver;
  356. public PowerState State;
  357. public float FrameTime;
  358. public List<Network> Networks;
  359. public void Execute(int index)
  360. {
  361. Solver.UpdateNetwork(Networks[index], State, FrameTime);
  362. }
  363. }
  364. #endregion
  365. }
  366. }