Hi there!
Before we begin: disclaimer. I've never played this game, but a friend of mine pointed me towards this blog post, and I had quite a lot to say about it. I've discussed the game mechanics pretty in-depth with him as it pertains to these pipes, so I think I have a decent enough handle on how this works to proceed.
Basic Concept
In real life, flow is driven by pressure. If you try to simulate flow without using pressure as your driving parameter (like using volume and velocity as noted in the blog post), you'll run into all sorts of issues down the road due to how those parameters don't inherently describe the behavior of the flow. Therefor, this implementation suggestion is based around using pressure as your primary driving parameter.
Basic Assumptions
To get started, we'll need to make a couple of assumptions. These cover some basic details about how this simulation gets implemented, and allow us to really simplify a lot of the calculation steps that are necessary.
Pipes start empty, but once pressurized they become and remain full for the duration of the use of the pipes.
This is a pretty important assumption because it's essential to doing reasonable calculations for flow in pipes and also is how pipes actually work. In real life, a pipe filled partially with water and partially with air isn't moving fluid: it's either unused or is a pressure bomb waiting to explode. Once you've turned the pressure on in a piping system, all the air will be forced out of the system and remain that way. No pipe system allows for backflow of air because it is both inefficient and extremely dangerous.
Minor and Major fluid losses will not be accounted for
Minor and Major losses are the names for what happens as fluid flows through a pipe. As it rushes around corners, passes down long tubes, or does spins while going through a T-bend, it loses pressure, and that's what drives a lot of pipe design. However, in a system like what factorio has, simulating these doesn't make a lot of sense. It would feel really bad to put a bunch of elbows on a pipe system to make it go where it needs to go and end up with a dribble at the end because of the minor losses. Therefor, this concept will assume that all minor and major losses in the pipe system are zero.
The Bernoulli Equation describes the flow
This probably doesn't mean a whole lot yet, but the implication is that the flow in these pipes can be described fully by a particular equation that is very well-behaved and easy to solve. This is a very reasonable assumption to make as it is true for the vast majority of pipe flow that any of us encounter regularly.
All pipes are flat *
It was explained to me that there is no elevation change in factorio. Therefor, all pipes operate at the same height, meaning that there will never be a difference in height between the inlet and the outlet of the pipe. I've included an asterisk here because once we discuss the implementation of tanks, a limited form of height may come in handy when applied specifically to them.
All outlets are ambient pressure **
Ok, so this might not mean a whole lot yet, but it will end up mattering. We're going to assume that all of our pipe outlets are at ambient pressure for this analysis. I've included a couple asterisks here because this is something that can be tweaked later on for some more complex simulation if we want to.
Ok, so that's it for our starting assumptions. Now, a more detailed dive on how fluid mechanics in pipes work, and how this helps us for implementation.
Pipe Implementation Details
Flow Velocity
In a pipe, flow is driven by just 2 parameters: pressure and density of the fluid. Normally there's more, but our previous assumptions cut it down to just those 2. The way you calculate flow through a pipe is by taking the starting and ending points of the pipe, finding out those 2 parameters, and calculating from there. Density of the fluid never changes, and that means that the only important parameter is pressure at the starting point and pressure at the ending point. Remember that we assumed earlier that pressure at the ending point is ambient, and so we already know one of those. This makes a very easy equation to solve, so I'll show it here.
Start: velocity = 0, pressure = PumpPressure, density = rho
Exit: velocity = unknown, pressure = ambient, density = rho
Velocity at exit = sqrt( 2 * (PumpPressure-ambient)/rho)
Ok, so that's pretty easy. There's really only 2 things that can change here: density and pump pressure. Density is the term you can use to play around with how different liquids act. A more dense liquid will flow slower and a less dense liquid will flow faster, so you can implement that to make different liquids feel different. Pump pressure is the other part of this. As you increase pressure, you increase the velocity of the flow. This is also where we reach our first pipe system physical limitation!
Maximum pressure is a limiting parameter that cannot be exceeded.
A pipe has a maximum pressure that it cannot increase past. This puts a hard limit on the velocity of the flow and allows you to implement a gameplay limit to how much liquid can be moved. It also allows you to do things like implement different kinds of pipes with higher pressure limits to allow for fast fluid flow.
Flow Rate
Right, so that tells us how fast the liquid is flowing. Now we need to turn it into what's called "volumetric flow rate", which is a description of how much liquid is being moved. This looks like "cubic feet per second", or something similar. For this we'll need another parameter: Pipe area.
Volumetric flow rate = Velocity * Pipe area
Alright, so that's really easy. However, there's a bit more to think about here. If you put junctions on your pipe, you're effectively increasing the amount of pipe area that you have, but you won't be getting a flow rate increase from that. The pipe area in this calculation is the smallest area in the system, making it our second pipe system physical limitation!
Maximum pipe area is limited by the smallest point in the system.
As you can see in the equation, the bigger the pipe, the more flow you get. This allows you, if you choose, to implement different size pipes that put different limits on the flow rate of the liquid. However, there's another hidden system limiting parameter here!
Maximum flow rate is limited by the source generation rate.
You very well might be able to build a pipe system that is capable of flowing faster than your source generates fluid. This is necessarily impossible, so the maximum flow rate simply needs to be limited by what the source can actually produce. In certain conditions, it could go over that (once we discuss tanks), but over the long term, this parameter will be a strict limiter, and will be applied to the system to cap the flow rate just like the pipe pressure limit caps the pressure in the system.
Junctions and Outlets
Next, lets talk about junctions and outlets. I saw from your post earlier that you want to basically divide the flow proportionally based on how many splits it's seen. The way this can be implemented is that each outlet will be given a specific proportion based on the upstream junctures. You can do a running tally of junctures once for each outlet to get this number. For each 2-way split, apply 1/2 multiplier. For each 3-way split, apply 1/3 multiplier, etc. Do this for each junction it hits passing upstream, and get the total proportion. This calculation only needs to be done once for each outlet for any given configuration of pipe, so once the system is built, you only have to redo that calculation if the upstream junctions change. Here's what the calculation looks like:
Outlet Flow Rate = Volumetric flow rate * Calculated Proportion
Again, pretty easy. You perform this calculation for each outlet point, and the total flow rate will be guaranteed to match the initial total flow rate. You don't have to worry specifically about how the upstream junctures are behaving because that's already accounted for by the proportion calculation.
Dead Ends and System Startup
First, dead ends. A dead end in a pipe is interesting. It's filled with fluid, it's pressurized, but the velocity of the flow is zero. This means that once the system is fully up and running, a dead end effectively stops existing, but it does matter for how long the system takes to start up. However, since it has no actual outlet, a dead end has absolutely zero relevance on any of the fluid flow calculations that we've gone through already, so that keeps it nice and clean.
Next, system startup. In order to start up the system in a fairly reasonable manner, the pipes first have to fill up before the flow gets going properly. There's a pretty simple way to simulate this though, and it only takes a couple of parameters. We need to know the total length of pipe in the system, and from this we calculate the volume.
Total system volume = Pipe Area * total system length
Earlier we calculated the volumetric flow rate, so now we apply that to the total system volume to figure out how long it takes the system to fill up.
System Fill Time = Total System Volume / Volumetric flow rate
Now that you've got the fill time, you can simply use all the pre-calculated outlet flow rates and scale them linearly from 0 to full across the duration of the startup time. Alternatively, you could do something like keep them zero for the first 50%, then scale them twice as fast for the 2nd 50% to kind of mock-up how you'll get more of a rush of liquid out of the pipe towards the end of the system pressurization. That implementation would be up to you, but both would be simple. The important thing to remember here is that this is a calculation that happens once, at system startup. Once the pipe system is fully pressurized, this transient simulation drops out, leaving us with just the previous steady calculations.
Pumps and Pressure
Lastly, we come to pumps and pressure. The purpose of a pump is to increase the pressure of a fluid. This drives flow through a system, and allows your whole scheme to run. There's really 2 main ways you could choose to implement this, and both of them would be appropriate. You could have the source machines generate an inherent pressure that drives the system or you could requires a pump to be placed on the pipe to drive the system. Alternatively, you could do both: source machines generate an inherent pressure, and inline pumps add pressure for faster flow. Regardless of how you implement it, this portion will generate that starting pressure value that drives this whole simulation. The strength of your pumps will allow you to control the strength that the players are able to start off their pipe system with.
Summary
Right, here's a quick summary of what I just discussed. First, the steady behavior of the system can be described with 3 basic equations.
- Velocity at exit = sqrt( 2 * (PumpPressure-ambient)/rho)
- Volumetric flow rate = Velocity * Pipe area
- Outlet Flow Rate = Volumetric flow rate * Calculated Proportion
These equations are quite simple and easy to calculate. Additionally, they describe the entire system without needing any granularity along the pipes and junctures past the initial setup of the calculations. Lastly, they only need to be recalculated when there is a change in one of the driving parameters, meaning that once you've done the initial system calculation, you can simply keep the output values as parameters for your larger factory without needing to recalculate them each cycle.
Next, the startup behavior of the system can be described with 2 more equations.
- Total system volume = Pipe Area * total system length
- System Fill Time = Total System Volume / Volumetric flow rate
Just like the others, these are both easy to calculate and only need to be performed once, at system startup.
As a whole, this is an extremely light-weight simulation to run. It describes the whole system with one set of equations, is easily adjusted to account for flow-limiting characteristics in the game mechanics, and properly describes the way a real-world system behaves (mostly anyway). While I'm sure your current implementation does work, I'm fairly confident that this implementation or a variant of it will be far more usable both from an in-game perspective and an actual development standpoint.
Final Comments
So some things in particular I haven't addressed yet are tanks and the 2 asterisked assumptions at the start. All 3 of those things basically go together, but they get us into a bit more of a complex realm. One of the nice things about this implementation, though, is that it naturally accounts for tanks and how they function if you choose to implement that. I'm not going to address that right now, but I can easily discuss that further if you want an explanation of how that could be done.
Feel free to question me or point out how I've brutally missed some basic mechanics of factorio here. I'm pretty confident that this is a good way to go about it, but this is far from a perfect implementation, so good suggestions always make improvements.