eradicator wrote: Tue Jul 02, 2019 6:13 am
The EEI exists explicitly to emulate production/consumption machines via script so it'd kinda be weird if it didn't work. And as far as i can tell it works as expected when given a different usage_priority. I'm more suprised that you said accumulators don't respect usage_priority, just tried and at runtime they report their priority as "managed-accumulator" while the EEI reports "tertiary", and i found
a post by @Rsed that mentions the hardcoding of the accu priority. "You learn something new every day." Narf.
Care to share details about your algorythm? Ever since runtime-changability of flow limits of the EEI was removed i can't see a way to do it without having ridiculously large buffer sizes. How do you not have to iterate over everything regularly? Are tesseracts not supposed to have an even draw across all power input/output points even if they're on different networks? What is the performance impact if you i.e. pop 100~200 pairs on an empty map? What happens on input starvation?
I too have really struggled with how to go about performing transfers. All of my early algorithms used large buffers (which at some point almost made tesseracts more useful at being accumulators than actual accumulators). I've ended up with three different algorithms for doing transfers. Each of them listens to entity/equipment creation events and stores the entity in global (I'll get to the actual data structures later).
script.on_event(defines.events.on_built_entity, on_creation )
script.on_event(defines.events.on_robot_built_entity, on_creation )
script.on_event(defines.events.on_player_placed_equipment, on_creation )
During on_tick, I check entity.valid for any tesseracts that I'm going to evaluate that tick, and drop invalid entities from global. This means that I will never have to loop through non-tesseract entities. (Kinda embarrassing how long this tick took me to develop; seems super obvious now)
The first algorithm I developed merely gave everything a buffer that would last for X ticks, threw it into a list and split the entity evaluation up over X ticks with a global buffer equal in size to the sum of all input buffers. So every tick, you'd evaluate tesseract (tick mod X), ((tick mod X) plus X), etc. This was easily the worst of my algorithms because it gave choppy energy graphs on the side which was starved (eg, if input was higher than output, the input side had a tick-by-tick jitter). It had great performance because I didn't have to evaluate much each tick, but that was the only selling point. It doesn't even guarantee even draw/flow since each tesseract just takes whatever is available to it whenever it's evaluated. You can see this algorithm in the first couple versions of Wireless Power, but it is quickly replaced with the second one.
My second algorithm used a map which had tick numbers as keys, and lists of tesseracts as values. I still had a buffer that lasted for X ticks, as before. New tesseracts were evaluated on the tick they were created, and each tick I would evaluate the list for that tick and then delete the list. Whenever a tesseract was evaluated, I check to see if I can satisfy the tesseract (eg, drain an input buffer or fill an output buffer) for at least two ticks. If I can't, I disable the tesseract for a few seconds by placing it in the map at index (tick + X * 2). If I can satisfy it at least partially, I do so and put it into the map at index (tick + (ticks until full/empty)). This kept the performance characteristics of the first algorithm, but eliminated almost all of the jitter on the electric network. Accumulator power still climbs and falls, and the electric graph still shows stepping functionality whenever a buffer is disabled or reenabled. It doesn't really solve any of the issues in the first algorithm, but they are all dramatically lessened. You can see this algorithm at work in version 0.0.4 and 0.0.5 of Wireless Power.
My final algorithm sacrifices some performance by iterating over the entire list of tesseracts twice each tick, giving it a O(N) performance (as opposed to the O(N/X) expected performance of the previous algorithms). Each tesseract has a buffer equal to flow_limit/60 (eg, one tick worth of buffer) which is the key to completely smoothing the electric graph. Each tick, I sum up all of the supply (eg, energy) from input tesseracts and request (eg, buffer_capacity - energy) from output tesseracts. The I calculate the ratio of supply to request (accounting for the fact that global drain costs are prioritized over actually filling buffers). I can then iterate over all tesseracts a second time and set their energy to the appropriate level. Since each tesseract's buffer is only equal to one tick worth of energy, manipulating how much energy I take or give them effectively manipulates the flow_limit as well. You can see this algorithm at work in version 0.0.5 and beyond (in 0.0.5 it's what effectively happens when you disable 'UPS friendly mode').
In the below code, global.list is a list of tables which contain information about a single tesseract.
Code: Select all
local drain_total = highest_active.global_drain_per_tick
local req_total = 0
local sup_total = 0
-- Figure out what is being requested.
for i=1, #global.list do
local entity = global.list[i].entity
local config = get_tesseract_config(entity.name)
if is_input(config, entity.name) then
sup_total = sup_total + (entity.energy * config.individual_efficiency)
else
local req = ((entity[config.buffer_key] - entity.energy) / config.individual_efficiency)
if req > 0 then
req_total = req_total + req
drain_total = drain_total + config.individual_drain_per_tick
end
end
end
-- Calculate transfer ratios.
local req_ratio = (sup_total - drain_total) / req_total
if req_ratio ~= req_ratio then -- Test for NaN
req_ratio = 0
end
req_ratio = math.max(0, math.min(req_ratio, 1))
local sup_ratio = (req_total + drain_total) / sup_total
if sup_ratio ~= sup_ratio then -- Test for NaN
sup_ratio = 0
end
sup_ratio = math.max(0, math.min(sup_ratio, 1))
if game.tick % (5 * 60) == 0 then
debug("Total request: " .. math.floor(req_total * 60 / 1000) .. "kW, Ratio: " .. math.floor(req_ratio * 100) .. "%, Total supply: " .. math.floor(sup_total * 60 / 1000) .. "kW, Ratio: " .. math.floor(sup_ratio * 100) .. "%, Drain: " .. math.floor(drain_total * 60 / 1000) .. "kW")
end
-- Perform transfers.
for i=1, #global.list do
local entity = global.list[i].entity
local config = get_tesseract_config(entity.name)
if is_input(config, entity.name) then
set_energy_level(global.list[i], entity.energy * (1 - sup_ratio))
else
set_energy_level(global.list[i], entity.energy + ((entity[config.buffer_key] - entity.energy) * req_ratio))
end
end
Also, protip, never use table.remove. It's horribly inefficient and has plagued my performance. See
this Stack Overflow post for a much better algorithm (scroll down to the answer that starts off with "Efficiency!").