Modded Accumulators Which Can Charge/Be Charged By Other Accumulators?

Place to get help with not working mods / modding interface.
User avatar
Rainboy
Inserter
Inserter
Posts: 22
Joined: Sat Sep 17, 2016 11:46 pm
Contact:

Modded Accumulators Which Can Charge/Be Charged By Other Accumulators?

Post by Rainboy »

Hello! I'm working on a mod to provide balanced long range power transmission with electricity costs for use; eg, not just a power pole with infinite range. Under the hood, the best way I've discovered so far to implement it is to configure each entity as an accumulator and then manipulate the energy storage therein each tick to "transfer" the power around with the appropriate costs. I'm currently bumping into a problem, however, which could easily just be me not understanding how electric energy sources work.

The basic issue is that I would like my long range power solution to play nice with solar powered bases. This means that my input tesseracts need to be able to be charged by accumulators within the same network. Secondarily, I would like to allow my output tesseracts to support charging other accumulators on their networks, since they are rate limited and somewhat expensive. I was thinking that both of these problems would be solved setting the usage_priority to "primary-input" or "secondary-input" and "primary-output" or "secondary-output" respectively. However, even with these settings, I still don't see any transfer of power between any accumulators and tesseracts.

Is this a limitation of the modding interface that prevents any accumulators from charging other accumulators? If not, what the heck am I missing? If so, does anyone see any clean way around this issue?

User avatar
Optera
Smart Inserter
Smart Inserter
Posts: 2919
Joined: Sat Jun 11, 2016 6:41 am
Contact:

Re: Modded Accumulators Which Can Charge/Be Charged By Other Accumulators?

Post by Optera »

Accu need to be tertiary to be drained and charged. That feature is hardcoded to this priority and is not available for secondary or primary. https://wiki.factorio.com/Types/ElectricUsagePriority
That's why my creative items mod has 4 additional energy interfaces instead of 2. :roll:

Mods trying to use accus alongside steam need to utilize composite entities, see Transformers.
I think the most ups friendly way to achive transformers would be to have an assembler producing hot fluid requiring only power at lowest input priority and a steam engine consuming that fluid outputting power at highest priority.
The mod would have to do is set the recipe for the assembler when the entity is placed down and make sure each entity is in a different power network.

User avatar
eradicator
Smart Inserter
Smart Inserter
Posts: 5206
Joined: Tue Jul 12, 2016 9:03 am
Contact:

Re: Modded Accumulators Which Can Charge/Be Charged By Other Accumulators?

Post by eradicator »

Concerning your post in simplify primary and secondary electric usage priority.

You can impose arbitrary limits onto the energy_source via input_flow_limit and output_flow_limit. I do this in Hand Crank Generator (see signature) to use a "tertiary" accumulator as an output-only generator. If you want to power other things you should try an electric-energy-interface with your prefered priority and gui_mode="none" and allow_copy_paste = false. You'll still have to deal with all the annoyances (performance, energy graph spikes) that happen when you're trying to large-scale teleport electricity around.
rainboy wrote: I could also model it as some multi-entity structure (probably a visible accumulator which I manipulate and a hidden assembler which does the actual work mechanically), but the devs have repeatedly said (and I happen to agree) that such structures are bad form in general since they introduce issues with keeping the various parts of the structure in sync with each other (eg, one part is deleted/destroyed and the other part has to do the same, which can be difficult).
This is bullshit btw. Without composite entities many of the most interesting mods would be impossible and everything would just be "Assembler Mk X". It's a sad state of affairs that certain devs would rather declare it bad practice than start properly supporting it (and as the lack of official support is the only reason they're supposedly "bad" that is a self-fullfilled prophecy).
Author of: Belt Planner, Hand Crank Generator, Screenshot Maker, /sudo and more.
Mod support languages: 日本語, Deutsch, English
My code in the post above is dedicated to the public domain under CC0.

User avatar
Rainboy
Inserter
Inserter
Posts: 22
Joined: Sat Sep 17, 2016 11:46 pm
Contact:

Re: Modded Accumulators Which Can Charge/Be Charged By Other Accumulators?

Post by Rainboy »

eradicator wrote:
Mon Jul 01, 2019 5:12 pm
You can impose arbitrary limits onto the energy_source via input_flow_limit and output_flow_limit. I do this in Hand Crank Generator (see signature) to use a "tertiary" accumulator as an output-only generator. If you want to power other things you should try an electric-energy-interface with your prefered priority and gui_mode="none" and allow_copy_paste = false. You'll still have to deal with all the annoyances (performance, energy graph spikes) that happen when you're trying to large-scale teleport electricity around.
Thanks! I've already abused the flow_limit controls to no end, and actually have an algorithm which generates a smooth power curve while still being efficient as far as I can tell at O(n) complexity per tick using a cache of entities to deal with so I don't have to iterate over every entity in the game. I haven't done any particularly large scale tests and I have a good PC, but I'd estimate the performance impact of what I'm doing to be pretty low right now based on the code, unless accessing/manipulating an entity's power is itself expensive. My only problem right now is not being able to interface with accumulators.

I'm intrigued by the electric-energy-interface suggestion... I'll have to give that a whirl and see what happens. I'd assumed that it shared the same tertiary-only lock that accumulators had for the buffer (unless you're just referring to the energy_production / energy_usage aspects, which would indeed be annoying to deal with).

User avatar
eradicator
Smart Inserter
Smart Inserter
Posts: 5206
Joined: Tue Jul 12, 2016 9:03 am
Contact:

Re: Modded Accumulators Which Can Charge/Be Charged By Other Accumulators?

Post by eradicator »

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?
Author of: Belt Planner, Hand Crank Generator, Screenshot Maker, /sudo and more.
Mod support languages: 日本語, Deutsch, English
My code in the post above is dedicated to the public domain under CC0.

User avatar
darkfrei
Smart Inserter
Smart Inserter
Posts: 2904
Joined: Thu Nov 20, 2014 11:11 pm
Contact:

Re: Modded Accumulators Which Can Charge/Be Charged By Other Accumulators?

Post by darkfrei »

You are need a modded electric energy interface with disabled GUI and primary priority, that takes the energy from the accumulator by the script.

User avatar
eradicator
Smart Inserter
Smart Inserter
Posts: 5206
Joined: Tue Jul 12, 2016 9:03 am
Contact:

Re: Modded Accumulators Which Can Charge/Be Charged By Other Accumulators?

Post by eradicator »

darkfrei wrote:
Tue Jul 02, 2019 7:47 am
You are need a modded electric energy interface with disabled GUI and primary priority, that takes the energy from the accumulator by the script.
Could you at least read the thread before mindlessly repating what has already been said?
Author of: Belt Planner, Hand Crank Generator, Screenshot Maker, /sudo and more.
Mod support languages: 日本語, Deutsch, English
My code in the post above is dedicated to the public domain under CC0.

User avatar
Rainboy
Inserter
Inserter
Posts: 22
Joined: Sat Sep 17, 2016 11:46 pm
Contact:

Re: Modded Accumulators Which Can Charge/Be Charged By Other Accumulators?

Post by Rainboy »

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!").

User avatar
darkfrei
Smart Inserter
Smart Inserter
Posts: 2904
Joined: Thu Nov 20, 2014 11:11 pm
Contact:

Re: Modded Accumulators Which Can Charge/Be Charged By Other Accumulators?

Post by darkfrei »

Rainboy wrote:
Tue Jul 02, 2019 12:12 pm
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!").
table.remove(table, last_index) is pretty fast, but the table.remove(table, 1) will be very slow for big tables

User avatar
eradicator
Smart Inserter
Smart Inserter
Posts: 5206
Joined: Tue Jul 12, 2016 9:03 am
Contact:

Re: Modded Accumulators Which Can Charge/Be Charged By Other Accumulators?

Post by eradicator »

Rainboy wrote:
Tue Jul 02, 2019 12:12 pm
Also, protip, never use table.remove. It's horribly inefficient and has plagued my performance.
Also protip: Use tools appropriate for the task. That guy on stackexchange tests some tables with 20 *million* entries of which over a million are removed at once. So he's comparing his "smart" approach of ~1.5million assignments to the "dump" approach of roughtly 15 *trillion* key=value assignments per table, with the obvious outcome that doing 5 order of magnitude fewer assignments is faster. I'm not sure what you do internally, but i'd expected the only events where you have to insert/remove table entries would be entity de/construction, which happens one at a time. And for one-removal-at-a-time table.remove() - as a C++ side function - should certainly be faster than a lua-side reimplementation. Futhermore...if you don't actually care about the *order* of a table (as is usually the case with lists of entities) you can just shove the last element of a table into the new position when you want to delete something:

Code: Select all

table.remove(tbl,i) --standard
tbl[i] = tbl[#tbl] --don't care about order
Well. If you're touching every tesseract every tick then getting no spikes is natural. My 0.15 algorythm was similar to your "first" i think, except that with runtime control of flow_limit it didn't produce spikes, but sadly that's not an option anymore. I wonder if your "simple read+dump" instead of calculating stuff is actually relevantly faster. I should look at factorissimo to see how it handles this. But then again it only has to handle a maximum of 32 pairs or so,....

Btw, i can't get your mod to work. I built a bunch of tier-3s, and the input side shows internal power, but the output side doesn't. Also the entitys have wildly different descriptions than the items/recipes.
difference.png
difference.png (160.95 KiB) Viewed 3760 times
bunch.png
bunch.png (385.06 KiB) Viewed 3760 times
Author of: Belt Planner, Hand Crank Generator, Screenshot Maker, /sudo and more.
Mod support languages: 日本語, Deutsch, English
My code in the post above is dedicated to the public domain under CC0.

User avatar
Rainboy
Inserter
Inserter
Posts: 22
Joined: Sat Sep 17, 2016 11:46 pm
Contact:

Re: Modded Accumulators Which Can Charge/Be Charged By Other Accumulators?

Post by Rainboy »

eradicator wrote:
Tue Jul 02, 2019 3:03 pm
Btw, i can't get your mod to work. I built a bunch of tier-3s, and the input side shows internal power, but the output side doesn't. Also the entitys have wildly different descriptions than the items/recipes.
Everything works well for me (remember that there's a 30MW global drain if there are any mk3s on the map; try it with mk1s which only have a 300kW global drain). Are you sure you're producing enough power? Edit: Scratch that, my code has a bug that breaks things when you drop down mk3s or mk2s without having ever dropped down mk1s that save... excuse me, my ineptitude at programming (and testing) is showing.

The descriptions for the items are indeed off. EEI doesn't display throughput, so I added it to the description... and forgot to also add it to the item. I'll fix that now; thanks for the heads up.

User avatar
Rainboy
Inserter
Inserter
Posts: 22
Joined: Sat Sep 17, 2016 11:46 pm
Contact:

Re: Modded Accumulators Which Can Charge/Be Charged By Other Accumulators?

Post by Rainboy »

eradicator wrote:
Tue Jul 02, 2019 3:03 pm
Also protip: Use tools appropriate for the task. That guy on stackexchange tests some tables with 20 *million* entries of which over a million are removed at once. So he's comparing his "smart" approach of ~1.5million assignments to the "dump" approach of roughtly 15 *trillion* key=value assignments per table, with the obvious outcome that doing 5 order of magnitude fewer assignments is faster. I'm not sure what you do internally, but i'd expected the only events where you have to insert/remove table entries would be entity de/construction, which happens one at a time. And for one-removal-at-a-time table.remove() - as a C++ side function - should certainly be faster than a lua-side reimplementation. Futhermore...if you don't actually care about the *order* of a table (as is usually the case with lists of entities) you can just shove the last element of a table into the new position when you want to delete something:

Code: Select all

table.remove(tbl,i) --standard
tbl[i] = tbl[#tbl] --don't care about order
He does also does a test where he removes only one item, which seems algorithmically comparable to a single table.remove. His algorithm still wins, provided you're using LuaJIT. Not sure if Factorio uses the just in time compiler or not, so that point may be moot.

And regardless, I suppose at the end of the day order doesn't really matter... so you're right in that I should just be running around slapping the last element in that slot. I don't think that will matter too much since I still have to evaluate every entity, which will probably dwarf the cost of the extra assignments that code makes.

Internally, I was previously doing the really stupid approach of iterating forwards (causing the maximum number of things to be shifted) and only evaluating removal when I was actually going to evaluate the entity (which in previous versions was once every X ticks, meaning that quite a few tesseracts could get removed from the list at a time if, for example, I deconstructed a few hundred test tesseracts with robots), causing some slightly noticeable jitter in frame time occasionally.

User avatar
eradicator
Smart Inserter
Smart Inserter
Posts: 5206
Joined: Tue Jul 12, 2016 9:03 am
Contact:

Re: Modded Accumulators Which Can Charge/Be Charged By Other Accumulators?

Post by eradicator »

Does it break the savegame for good if i place mk3's first? I tried placing some 1's and 2's and then rebuild the 3's, but that didn't seem to help. Got it to work after removing your mod from the save and adding it again. Also it seems like you're not handling the script_raised_* group of events, so you're incompatible with scripted ghost reviving, which might have caused additional problems (due to my testing mod doing instant de/construction).

Ah yes, right at the bottom there's a single run of a one-item-removal with a difference so marginal it could just as well be measurement error - not saying it is, just saying that testing a single iteration is meh. Performance is weird and magic is all i can say. Factorio does not use LuaJit, but @Rseding did try to do stuff and made a post about it on reddit.

And yea, if you're doing list management in on_tick then you'd care about removal times. But as you've noticed yourself seperating list-management out of on_tick is an easy optimization :).

So, now that i could actually test the performance of your mod in action i'm kinda desillusioned. It's still not anywhere near what i would find acceptable for any single mod to consume in my own playthroughs. Measuring 1.5ms for 100 pairs of mk3's on an empty map, and my stripped down testing implementation (see below) only gets to 0.9ms either even though it has less overhead due to not being a full mod.

I did test storing some intermediate calculations like the demand for each accu in a list, but the overhead of managing the list seems to be more expensive than reading and recalculating the values. So the only conclusion i can draw is that larger buffers and less tick processing are the only way to better performance - which is the same conclusion i drew a few years back ^_^.

Btw, i also dug up the power transfer from factorissimo, which is much more simplistic as it only deals with pairs of EEIs.

Code: Select all

local function transfer_power(from, to)
	if not (from.valid and to.valid) then return end
	local energy = from.energy+to.energy
	local ebs = to.electric_buffer_size
	if energy > ebs then
		to.energy = ebs
		from.energy = energy - ebs
	else
		to.energy = energy
		from.energy = 0
	end
end

Stripped down sample implementation:

Build some accus on west/east sides of y-axis, then scan the surface:

Code: Select all

/c

global.ins = {}
global.outs = {}

local function update_list()
  global.list_in  = {}
  global.list_out = {}
  global.total_out_capacity = 0
  global.total_in_capacity  = 0

  for _,this in pairs(global.ins) do
    local max = this.prototype.electric_energy_source_prototype.buffer_capacity
    table.insert(global.list_in,{this,max})
    global.total_in_capacity  = global.total_in_capacity  + max
    end
  for _,this in pairs(global.outs) do
    local max = this.prototype.electric_energy_source_prototype.buffer_capacity
    table.insert(global.list_out,{this,max})
    global.total_out_capacity = global.total_out_capacity + max
    end
  end

local function add(this)
  if this.type == 'accumulator' then
    if this.position.x < 0 then 
      print('add','in',this.unit_number)
      global.ins[this.unit_number] = this
    else
      print('add','out',this.unit_number)
      global.outs[this.unit_number] = this
      end
    end
  update_list()
  end
    
local p = game.player
for _,accu in pairs(p.surface.find_entities_filtered{type = 'accumulator',force = p.force}) do
  add(accu)
  end
The tick handler:

Code: Select all

/c
--[[V2]]
local list_out = global.list_out
local list_in  = global.list_in
script.on_event(defines.events.on_tick,function(e)

  local total_supply = 0
  local total_demand = 0
  
  for i=1,#list_out do
    total_demand = total_demand + list_out[i][2] - list_out[i][1].energy
    end
    
  for i=1,#list_in do
    total_supply = total_supply + list_in[i][1].energy
    end

  local in_factor  = (total_supply <= 0) and 0 or math.min(1, total_demand / total_supply)
  local out_factor = (total_demand <= 0) and 0 or math.min(1, total_supply / total_demand)

  for i=1,#list_out do
    list_out[i][1].energy = list_out[i][1].energy
      + (list_out[i][2] - list_out[i][1].energy) * out_factor
    end
    
  for i=1,#list_in do
    list_in[i][1].energy = list_in[i][1].energy - (list_in[i][1].energy * out_factor)
    end

  if e.tick % 3600 == 0 then
    print(total_supply,total_demand,in_factor,out_factor,#list_in,#list_out)
    end
    
  end)
  
Author of: Belt Planner, Hand Crank Generator, Screenshot Maker, /sudo and more.
Mod support languages: 日本語, Deutsch, English
My code in the post above is dedicated to the public domain under CC0.

User avatar
Rainboy
Inserter
Inserter
Posts: 22
Joined: Sat Sep 17, 2016 11:46 pm
Contact:

Re: Modded Accumulators Which Can Charge/Be Charged By Other Accumulators?

Post by Rainboy »

eradicator wrote:
Wed Jul 03, 2019 9:18 am
Does it break the savegame for good if i place mk3's first? I tried placing some 1's and 2's and then rebuild the 3's, but that didn't seem to help. Got it to work after removing your mod from the save and adding it again. Also it seems like you're not handling the script_raised_* group of events, so you're incompatible with scripted ghost reviving, which might have caused additional problems (due to my testing mod doing instant de/construction).
My latest update from yesterday should've fixed the bug. I'll release something today that has a little bit of cleanup (eg, more efficient access from entity to config, using your trick of moving the last entity in the list to the front when one needs to be deleted) as well as handling those script_raised_* events. Didn't realize those existed. I don't expect performance will be significantly better than it was before, but I don't have a very good handle on what individual calls are expensive yet.
eradicator wrote:
Wed Jul 03, 2019 9:18 am
Stripped down sample implementation:

Build some accus on west/east sides of y-axis, then scan the surface:

Code: Select all

/c

global.ins = {}
global.outs = {}

local function update_list()
  global.list_in  = {}
  global.list_out = {}
  global.total_out_capacity = 0
  global.total_in_capacity  = 0

  for _,this in pairs(global.ins) do
    local max = this.prototype.electric_energy_source_prototype.buffer_capacity
    table.insert(global.list_in,{this,max})
    global.total_in_capacity  = global.total_in_capacity  + max
    end
  for _,this in pairs(global.outs) do
    local max = this.prototype.electric_energy_source_prototype.buffer_capacity
    table.insert(global.list_out,{this,max})
    global.total_out_capacity = global.total_out_capacity + max
    end
  end

local function add(this)
  if this.type == 'accumulator' then
    if this.position.x < 0 then 
      print('add','in',this.unit_number)
      global.ins[this.unit_number] = this
    else
      print('add','out',this.unit_number)
      global.outs[this.unit_number] = this
      end
    end
  update_list()
  end
    
local p = game.player
for _,accu in pairs(p.surface.find_entities_filtered{type = 'accumulator',force = p.force}) do
  add(accu)
  end
The tick handler:

Code: Select all

/c
--[[V2]]
local list_out = global.list_out
local list_in  = global.list_in
script.on_event(defines.events.on_tick,function(e)

  local total_supply = 0
  local total_demand = 0
  
  for i=1,#list_out do
    total_demand = total_demand + list_out[i][2] - list_out[i][1].energy
    end
    
  for i=1,#list_in do
    total_supply = total_supply + list_in[i][1].energy
    end

  local in_factor  = (total_supply <= 0) and 0 or math.min(1, total_demand / total_supply)
  local out_factor = (total_demand <= 0) and 0 or math.min(1, total_supply / total_demand)

  for i=1,#list_out do
    list_out[i][1].energy = list_out[i][1].energy
      + (list_out[i][2] - list_out[i][1].energy) * out_factor
    end
    
  for i=1,#list_in do
    list_in[i][1].energy = list_in[i][1].energy - (list_in[i][1].energy * out_factor)
    end

  if e.tick % 3600 == 0 then
    print(total_supply,total_demand,in_factor,out_factor,#list_in,#list_out)
    end
    
  end)
  
Woo! That looks cool. How did you measure performance? With some things, I'm shooting in the dark and it would be cool to be able to produce numbers to back up my intuition.

Also, I have a couple useful debug prints which you can enable in the per-player settings. It duplicates most of what you're testing there.

User avatar
eradicator
Smart Inserter
Smart Inserter
Posts: 5206
Joined: Tue Jul 12, 2016 9:03 am
Contact:

Re: Modded Accumulators Which Can Charge/Be Charged By Other Accumulators?

Post by eradicator »

I updated the mod, and starting with (or rebuilding) mk3's does work now. However it looks like you're not handling script_raised_revived (no debug messages and tesseract doesn't work)? Btw, be careful when handling script_raised_* events, as they are not sanitized by the engine, so a mod can send you garbage data like events with invalid entity wrappers, or no entity at all (just saying, didn't look at code).

I'm not doing anything fancy, i just built a bunch of stuff and then looked at the debug screen timings. Since 0.17 there's also game.create_profiler() to manually measure things ingame. But you always have to keep in mind that printing any sort of message has a significant cost by itself (at least to "cmd.exe" on windows, not sure how bad in-game printing is in that respect).

The code also basically does the same as yours except for the "if is_input" check. Measure total demand/supply, then apply it. My initial test was to try and minimize the entity interaction by only reading and writing energy exactly once per entity, but the additional management overhead makes it actually slower, if not by much. Which means that reading .energy is much faster than i assumed. But also means there's really not much left to optimize if you want to balance over all in/outputs. Whereas even one second worth of buffering could theoretically improve speed by factor 60.

(I modded the EEI to primary-output, thus the radars for consumption.)

Code: Select all

--[[V1]]
--[[list construction is more expensive than expected]]
--[[so instead of using a new one every tick i reuse them which seems a tiny bit faster]]
local _out = {
  list = global.list_out,
  demand = {},
  supply = {},
  }
local _in = {
  list = global.list_in,
  demand = {},
  supply = {},
  }
script.on_event(defines.events.on_tick,function(e)

  _out.list = global.list_out
  _in.list = global.list_in

  local total_supply = 0
  local total_demand = 0

  for i=1,#_out.list do
    _out.supply[i] = _out.list[i][1].energy
    _out.demand[i] = _out.list[i][2] - _out.supply[i]
    total_demand = total_demand + _out.demand[i]
    end

  for i=1,#_in.list do
    _in.supply[i] = _in.list[i][1].energy
    total_supply = total_supply + _in.supply[i]
    end

  local in_factor  = (total_supply <= 0) and 0 or math.min(1, total_demand / total_supply)
  local out_factor = (total_demand <= 0) and 0 or math.min(1, total_supply / total_demand)

  for i=1,#_out.list do
    _out.list[i][1].energy = _out.supply[i] + (_out.demand[i] * out_factor)
    end

  for i=1,#_in.list do
    _in.list[i][1].energy = _in.supply[i] - (_in.supply[i] * in_factor)
    end
 
  end)
Attachments
screenshot_45305.small.jpg
screenshot_45305.small.jpg (759.45 KiB) Viewed 3696 times
Author of: Belt Planner, Hand Crank Generator, Screenshot Maker, /sudo and more.
Mod support languages: 日本語, Deutsch, English
My code in the post above is dedicated to the public domain under CC0.

User avatar
eradicator
Smart Inserter
Smart Inserter
Posts: 5206
Joined: Tue Jul 12, 2016 9:03 am
Contact:

Re: Modded Accumulators Which Can Charge/Be Charged By Other Accumulators?

Post by eradicator »

Regarding the whole "animated/not animated" thing: At this point you're not using any special features of the EEI, so you might as well use some other kind of entity, like assemblers/generators that produce/consume actual fuel. On the other hand if you keep the EEI you could experiment with actually producing/consuming power, removing most script overhead for tesseracts running at full capacity, but introducing lots of complicated overhead code for dealing with the transitional states between not-used-at-all and fully-used.

Btw, were you thinking about supporting channels like actual TE tesseracts?
Author of: Belt Planner, Hand Crank Generator, Screenshot Maker, /sudo and more.
Mod support languages: 日本語, Deutsch, English
My code in the post above is dedicated to the public domain under CC0.

User avatar
Rainboy
Inserter
Inserter
Posts: 22
Joined: Sat Sep 17, 2016 11:46 pm
Contact:

Re: Modded Accumulators Which Can Charge/Be Charged By Other Accumulators?

Post by Rainboy »

eradicator wrote:
Wed Jul 03, 2019 3:35 pm
Regarding the whole "animated/not animated" thing: At this point you're not using any special features of the EEI, so you might as well use some other kind of entity, like assemblers/generators that produce/consume actual fuel. On the other hand if you keep the EEI you could experiment with actually producing/consuming power, removing most script overhead for tesseracts running at full capacity, but introducing lots of complicated overhead code for dealing with the transitional states between not-used-at-all and fully-used.

Btw, were you thinking about supporting channels like actual TE tesseracts?
I was thinking of trying out something like consuming/producing 1W of power to make the animations run. However, my understanding is that the animations just stop when the entity isn't "working," whereas I need it to switch to a seperate picture (otherwise there'd be frozen electric zaps in the air if it stopped mid animation).

As far as channels, I was thinking of looking at allowing channels via virtual signals once I finished vehicle tesseracts and stopped getting new bug reports for a while (as this is my first mod, there are many, MANY things I didn't expect to be problems that have been). I'm thinking that may end up adding quite a bit of overhead to the per tick calculations, but I won't really know until I try it. Thanks for the pointers regarding script event sanitation and create_profiler(), I really appreciate all the help you've been to me here.

User avatar
eradicator
Smart Inserter
Smart Inserter
Posts: 5206
Joined: Tue Jul 12, 2016 9:03 am
Contact:

Re: Modded Accumulators Which Can Charge/Be Charged By Other Accumulators?

Post by eradicator »

I thought a bit about optimization, but the best thing i came up with was to have all tiers have the exact same buffer size, which should make almost all calculations slightly cheaper at the cost of having rather large buffers for lower tiers.

To get an idea of what we're dealing with i tried a minimalistic handler that simply deletes all energy in the setup from the above screenshot, and that still uses 0.65ms. This is probably more than factorissimo uses for *all* of it's handling in a medium sized factory. So i can only recommend again you switch back to a buffered solution that only runs once per second or so if you have any desire to "grow your userbase" (:p), as this sort of change will introduce all sorts of bugs again so the earlier you do it the better.

Code: Select all

/c 

accus = {}
for _,accu in pairs(game.player.surface.find_entities_filtered{type='accumulator'}) do
  table.insert(accus,accu)
  end

script.on_event(defines.events.on_tick,function()
  for i=1,#accus do accus[i].energy = 0 end
  end)
Author of: Belt Planner, Hand Crank Generator, Screenshot Maker, /sudo and more.
Mod support languages: 日本語, Deutsch, English
My code in the post above is dedicated to the public domain under CC0.

User avatar
Rainboy
Inserter
Inserter
Posts: 22
Joined: Sat Sep 17, 2016 11:46 pm
Contact:

Re: Modded Accumulators Which Can Charge/Be Charged By Other Accumulators?

Post by Rainboy »

eradicator wrote:
Thu Jul 04, 2019 3:56 pm
I thought a bit about optimization, but the best thing i came up with was to have all tiers have the exact same buffer size, which should make almost all calculations slightly cheaper at the cost of having rather large buffers for lower tiers.

To get an idea of what we're dealing with i tried a minimalistic handler that simply deletes all energy in the setup from the above screenshot, and that still uses 0.65ms. This is probably more than factorissimo uses for *all* of it's handling in a medium sized factory. So i can only recommend again you switch back to a buffered solution that only runs once per second or so if you have any desire to "grow your userbase" (:p), as this sort of change will introduce all sorts of bugs again so the earlier you do it the better.
I'm rather loathe to give up the smooth accumulator/power graphs that come with full evaluation each tick. The mod is designed to not need very many Tesseracts on the map at a given time. The energy costs preclude tesseracts from being a particularly useful way to transfer power around your main base, and it aims to be a solution for rail worlds and other "I need to have a very far away outpost" situations. I'd be a little surprised if a vanilla game needed to transfer more than a couple GW of power (especially since the change to EEI now allows tesseracts to charge accumulators), which would equate to a couple hundred total tesseracts at most. The increased power generation that comes with bobpower warrants a 4th tier which is 8 times more powerful and I'd expect puts us in the same state of only needing a few. I'm open to making "UPS Friendly Mode" a setting again, but there was quite a bit of code complexity involved in supporting two radically different methods of transferring power and the code is a lot easier to manage without that. I'm also open to adding more tiers and/or increasing throughput in an effort to limit the number of tesseracts a player will realistically need. I'll give it some thought, and I'm still open to your advice.

I did go in and do a more comprehensive optimization analysis on per tick operations. I think I milked a little bit more out of it by making some commonly used variables local instead of accessing the global version, but I doubt that will make a particularly huge impact. I'm currently looking at optimizing the debug print statements (specifically, skipping a bunch of the calculations if there are no players with the setting enabled), but I also doubt that will have a large impact especially since they are only calculated once every five seconds. These changes will be uploaded for 0.0.11, along with the change which switches invalid entities with the end of the list. I don't expect we'll reach what you consider acceptable, but I'd be curious to see what performance you get out of it (I didn't do any profiling on the old build).

User avatar
eradicator
Smart Inserter
Smart Inserter
Posts: 5206
Joined: Tue Jul 12, 2016 9:03 am
Contact:

Re: Modded Accumulators Which Can Charge/Be Charged By Other Accumulators?

Post by eradicator »

(Please excuse the non-linear nature of this answer :p)

Preword: I know modding is always about compromises. Only the most simplistic modded concepts get a "perfectly clean solution" and everyone else always has to deal with compromises here and there. So it's only normal that you make different compromises than i "would" theoretically if i tried to make the same mod. So...

I'm pretty sure that you don't have to sacrifice the smoothness of the power graph even with "buffered updates" (60-tick buffer but 1-tick flow_limit). You would however have to sacrifice the smoothness of the "Energy:" bar in the entity-info (the thing you see when you hover your mouse over something). I think there was some weird configuration for EEIs to make that bar vanish completely (if it still exists), which would hide the fluctuation from the player at the cost of less visual feedback. If you had a great graphic for the transformers you could even make the delay visual by having them produce some sort of energy pulse every second.

As for my own "what i find acceptable" i already said above that i consider that impossible with the each-tick approach right now.

1GW of transfer would be 32 pairs of mk3s as far as i can see. You say "the mod is designed to only require a few of these". But factorio is a game about spamming stuff like there's no tomorrow! If i could choes in an ideal world i'd prefer if there was only one low powered tier that you had to build large fields of :p. Also i wouldn't be suprised if ppl start using them for cross-surface transfers.

Ofc "just adding more tiers" is an easy optimization *if* the player acts like you want them to and replaces the lower ones with *fewer* higher ones (instead of replacing the lower ones with *the same amount* of higher ones :p).

Ofc optimizataions also make the code more complicated and i totally understand if you don't want to go that road - especially if this is your first mod.
Author of: Belt Planner, Hand Crank Generator, Screenshot Maker, /sudo and more.
Mod support languages: 日本語, Deutsch, English
My code in the post above is dedicated to the public domain under CC0.

Post Reply

Return to “Modding help”