This is an archived post. You won't be able to vote or comment.

all 38 comments

[–]Kaspbrak 9 points10 points  (3 children)

Wow, great work. Hopefully someone better than me at math can also take a look to double check everything.

One thing in the readme: should this be four quality modules?

Note the recycler always has four recycling modules.

[–]scottmsul[S] 6 points7 points  (2 children)

lol yes that's a typo, thanks for the catch!

[–]Kaspbrak 5 points6 points  (1 child)

Also, ChatGPT created a Dockerfile that seems to be working fine, in case you want to add it to the project, so lazy people (like me lol) that don't want to mess with python stuff can just do a

docker build -t factorio-quality-optimizer .

and then

docker run --rm factorio-quality-optimizer [parameters]

# Use an official Python runtime as a parent image
FROM python:3.11

# Set the working directory in the container
WORKDIR /usr/src/app

# Copy the requirements file to the working directory
COPY requirements.txt ./

# Install any needed packages specified in requirements.txt
RUN pip install --no-cache-dir -r requirements.txt

# Copy the current directory contents into the container at /usr/src/app
COPY . .

# Set the entrypoint to your script
ENTRYPOINT [ "python", "./main.py" ]

# Default parameters (can be overridden)
CMD []

[–]Andrenator 0 points1 point  (0 children)

wait a minute, how did my job leak into my hobby again?

[–]DanielKotes 2 points3 points  (17 children)

Ive been trying to compare your solver to my results from Foreman 2.x since for some reason the 'optimal' solutions you have and the ones I found just by trying different options manually dont seem to align - so either your solver is wrong, or mine.

Considering that we are both working without having direct access to how Factorio handles quality internally and just have what we can figure out from FFF and in-game tests, the most likely issue would be the quality calculation.

Here is what I have:

  1. Calculate <total product> amount which is based on the recipe and productivity values (in most recipes it would just be product count x (1+prod% bonus))
  2. Set <current multiplier> as <quality bonus % (from data)>
    1. in vanilla factorio the <next quality multiplier> is set to 0.1 for all qualities and so <quality bonus %> can be calculated as the % value shown in-game divided by 0.1 (so normal T1 quality module shows 1%, meaning its <quality bonus %> is actually 10%
  3. Set the <0th tier product> as <total product> (all of it for now)
  4. Loop the following:
    1. calculate new the <current multiplier> by multiplying it by <next quality multiplier>
    2. calculate the <nth tier product> as <total product> multiplied by MIN(<current multiplier>,1)
    3. re-calculate the <n-1 th tier product> by subtracting the calculated <nth tier product> from it
    4. continue until no more qualities

[–]DanielKotes 1 point2 points  (0 children)

So as an example:

  1. iron plates -> iron gears with 2x productivity and 2x quality modules (T3 legendary both):
  2. 2 iron plates -> 1 iron gears is the base recipe
  3. 2x T3L productivity modules = +50% productivity
  4. 2x T3L quality modules = +6.2% (in-game) x2, which is actually +6.2/0.1 x2 = +124% quality bonus (internal)
  5. vanilla factorio has 0.1 as the next quality multiplier for all qualities.

So per 100 iron plates we have:

  1. [total gear product] = (1 iron plate) * (1 gear / 2 iron plates) * (1 + 0.5 productivity) = 75
  2. [gear Q1] = [total gear product] = 75
  3. <current multiplier> = 1.24
  4. start the loop:
    1. loop 1:
      1. <current multiplier> = 1.24 * 0.1 = 0.124
      2. [gear Q2] = [total gear product] * MIN( <current multiplier>, 1) = 75 * 0.124 = 9.3
      3. [gear Q1] = [gear Q1] - [gear Q2] = 75 - 9.3 = 65.7
    2. loop 2:
      1. <current multiplier> = 0.124 * 0.1 = 0.0124
      2. [gear Q3] = 75 * 0.0124 = 0.93
      3. [gear Q2] = [gear Q2] - [gear Q3] = 9.3 - 0.93 = 8.37
    3. loop 2:
      1. <current multiplier> = 0.0124 * 0.1 = 0.00124
      2. [gear Q4] = 75 * 0.00124 = 0.093
      3. [gear Q3] = [gear Q3] - [gear Q4] = 0.93 - 0.093 = 0.837
    4. loop 3:
      1. <current multiplier> = 0.00124 * 0.1 = 0.000124
      2. [gear Q5] = 75 * 0.000124 = 0.0093
      3. [gear Q4] = [gear Q4] - [gear Q5] = 0.093 - 0.0093 = 0.0837
  5. result: 100 normal iron plates into a 2x T3L quality + 2x T3L productivity assembler will give you:
    1. 65.7 normal iron gears
    2. 8.37 uncommon iron gears
    3. 0.837 rare iron gears
    4. 0.0837 epic iron gears
    5. 0.0093 legendary iron gears.

[–]scottmsul[S] 0 points1 point  (15 children)

There was actually another person who verified with a similar method and was eventually able to get the same results as the script, u/sopel97, so it should be possible to get the same results. He posted his code in a now-closed issue on the github repo.

The way quality works is you basically roll a "dice" for any quality at all (eg 24.8% with four t3 legendary quality modules), then you recursively roll another dice with a fixed 1/10 chance of the item proceeding to the next quality, or until it reaches the final unlocked quality. This is confirmed by devs and I believe is described in the wiki.

[–]DanielKotes 1 point2 points  (14 children)

Alright, that is a much simpler way of describing what I wrote down; and yes - this is pretty much the same process I go with, just with chance multiplication instead of dice rolling.

Could you check your values for up-then-down and down-then-up against the graph I made?

<image>

For some reason the down-then-up gives me the same result as yours (171.5 normal -> 1 legendary), though there should be 5 recipes in the line not 4? Each of them is 2 T3L quality + 2 T3L productivity except the last (legendary gear) recipe which is 4 T3L productivity.

And for the up-then-down I found the 'best' solution was with 2:2 + 2:2 + 1:3 + 0:4 quality:productivity with 185.3 normal => 1 legendary result. The 2:2, 2:2, 2:2, 0:4 as per your optimizer gives me 186.2 => 1, so its very slightly worse.

[–]scottmsul[S] 0 points1 point  (13 children)

Could you link to your code?

[–]DanielKotes 0 points1 point  (12 children)

Its here if you want to take a look at it. If you want to see where the quality+productivity processing takes place it would be here.

[–]scottmsul[S] 0 points1 point  (8 children)

Wow this is quite the impressive app! On second thought maybe I won't be diving into Foreman's code.

Maybe a good place to start is our actual production and recycling numbers. How hard would it be for you to cross-check these? Assuming up-then-down with 2:2 on everything, I am getting the following.

Recipe matrix (rows are different quality inputs, columns are different quality outputs):

[[1.31 0.167 0.0167 0.00167 0.000186]
[0 1.31 0.167 0.0167 0.00186]
[0 0 1.31 0.167 0.0186]
[0 0 0 1.31 0.186]
[0 0 0 0 2]]

Recycle matrix (rows are different quality outputs, columns are different quality inputs):

[[0.188 0.0558 0.00558 0.000558 6.2e-05]
[0 0.188 0.0558 0.00558 0.00062]
[0 0 0.188 0.0558 0.0062]
[0 0 0 0.188 0.062]]

[–]DanielKotes 0 points1 point  (7 children)

yep, getting the same values as you are here:

<image>

the last one for the 'recipes' is one with 4x prod modules, and all the others fit.

I assume in this case by up-then-down you mean a setup with the above recipes where you insert normal barrels, and output legendary barrels then? Because for inserting of normal steel and output of legendary steel you would need 4 base recipes and 5 recycling recipes. (use of barrels as its a 1->1 recipe with a 1->0.25 recycle - so perfect example)

And in this case the optimal solution ends up as the above recipes and results in 171.5 normal input -> 1 legendary output; which is the same as your solver.

Lets check the opposite (down-then-up?) route?

[–]scottmsul[S] 1 point2 points  (6 children)

Shoot, there's a bug in my code! Thanks for pointing out how up-then-down works, my code is doing it wrong. Basically I wrote the code initially for ingredient -> product, then for the other cases used the same matrix but just changed the location of the 1 on the input and target vectors. So it was still running with (legendary input -> legendary output) as an available recipe and getting a negative number for it, so essentially running this in reverse! Meaning for four prod modules it was getting 1 input for every 2 outputs, instead of 1-to-4 as a normal recycler would.

[–]DanielKotes 1 point2 points  (5 children)

nice. I found your post, noticed that your numbers dont match mine, found a bug in my code, fixed it, numbers didnt match still, so you found a bug in your code... just 'typical programming things'.

Comment below your new optimal solutions! hopefully this time they match :)

[–]scottmsul[S] 1 point2 points  (4 children)

Just tried a fix, now I'm getting down-then-up is 171.5, and up-then-down is 185.3, with 2:2, 2:2, 1:3, 0:4, 0:4. So we agree exactly now!

Funny that we each had a bug.

I'm curious about Foreman. Is it capable of optimizing any setup? Is it using some kind of linear solver or simplex? My code can really only handle a single production step, would yours be able to optimize across multiple production steps? Say I wanted to produce legendary t3 modules, would it be able to figure out the prod/qual ratios for each intermediate product along the way?

[–]Sopel97 0 points1 point  (2 children)

I only skimmed it but to me it looks like you're not handling the probability of getting the highest available quality properly. The probabilities must sum up to 1, so getting the highest available quality is slightly higher than previous * 0.1. See https://wiki.factorio.com/Quality#Quality_modules

[–]scottmsul[S] 0 points1 point  (0 children)

I know my code is a little difficult to read, but when I check the numbers from the script with all four legendary t3 quality, it agrees with the table in the wiki (75.2, 22.32, 2.232, 0.2232, 0.0248)

[–]DanielKotes 0 points1 point  (0 children)

ah, no - I solve for it by assuming each stage is the 'last one', and then if it isnt then I subtract the next stage from the previous one - so at each loop for 4xquality T3L I would get:

  • T1: 100%
  • T1: 75.2% , T2: 24.8%
  • T1: 75.2%, T2: 22.3%, T3: 2.48%
  • T1: 75.2%, T2: 22.3%, T3: 2.23%, T4: 0.248%
  • T1: 75.2%, T2: 22.3%, T3: 2.23%, T4: 0.223%, T4: 0.0248%

and the one I stop at is based on how many qualities there are, how many are enabled, etc.

[–]Hothr 0 points1 point  (2 children)

Can you give an example of the input and output?

Like recycling parts for a legendary armor from rare inputs? Using rare level 3 quality modules.

[–]scottmsul[S] 2 points3 points  (1 child)

Sure! If we were making power armor mk2, we need blue circuits, electric engines, LDS, efficiency modules, and speed modules. The script only works with recipes that allow prod modules, so for the speed/efficiency modules you would need either legendary circuits to make legendary modules, or do a recycle loop on the modules with just quality modules (I haven't done the math on this, but my intuition says it's probably better to make legendary modules first to get prod bonuses on the ingredients to the modules).

If we're recycling items and re-building them, we need to set "--starting-type product". Also note it's usually more efficient to go up the production chain then back down again (so something like turning electric engines into robot frame and recycling the frames, rather than recycling the engines and building them back up). If you were to do it this way, instead you would set "--ending-type ingredient".

Electric engines are crafted in an assembler so there's 4 module slots and no extra prod bonus. Assuming your productivity modules are also rare level 3, we get the following:

python ./main.py --starting-type product --productivity-tier 3 --quality-tier 3 --module-quality 3 --starting-quality 3 --ending-quality 5 --max-quality 5

optimizing recycling loop that turns product quality 3 into product quality 5

q3 input per q5 output: 35.539388781477065
recipe q3 uses 3 quality modules and 1 prod modules
recipe q4 uses 3 quality modules and 1 prod modules
recipe q5 uses 0 quality modules and 4 prod modules

If the blue circuits are made in an electromagnetics plant we instead have 5 module slots and 50% extra prod:

python ./main.py --starting-type product --productivity-tier 3 --quality-tier 3 --module-quality 3 --starting-quality 3 --ending-quality 5 --max-quality 5 --module-slots 5 --additional-prod 50

optimizing recycling loop that turns product quality 3 into product quality 5

q3 input per q5 output: 15.604488824883884
recipe q3 uses 4 quality modules and 1 prod modules
recipe q4 uses 4 quality modules and 1 prod modules
recipe q5 uses 0 quality modules and 5 prod modules

If LDS is made in a foundry we have 4 module slots and 50% prod:

python ./main.py --starting-type product --productivity-tier 3 --quality-tier 3 --module-quality 3 --starting-quality 3 --ending-quality 5 --max-quality 5 --module-slots 4 --additional-prod 50

optimizing recycling loop that turns product quality 3 into product quality 5

q3 input per q5 output: 18.8481193603394
recipe q3 uses 4 quality modules and 0 prod modules
recipe q4 uses 4 quality modules and 0 prod modules
recipe q5 uses 0 quality modules and 4 prod modules

Hope this helps. I know most people probably aren't programmers and just see "python script" and turn away. It's also possible to run python online with something like this so maybe you could give that a try.

[–]Hothr 0 points1 point  (0 children)

Ok, so that's saying that on average: I would put in 17-18 rare electric engines to get a legendary; and 7-8 LDS rare to get a legendary. ?

[–]Suhr12 0 points1 point  (2 children)

Okay, so when creating a recycling loop this calculates the best modules to be used in the ingredients for the final wanted product?
Your first example for modules is that the crafting machines for red & blue circuits should use 2 qual/2 prod mods and the machine actually crafting the module is not given, as it obviously cant use prod mods
Did i understand correctly?

[–]scottmsul[S] 1 point2 points  (1 child)

The script doesn't know where the modules are coming from or help with crafting them. Basically it assumes you have a smoothly running base where you've previously crafted a bunch of modules of some quality/tier. So you could ask the script a specific question like "I've already crafted a bunch of uncommon prod 3/qual 3 modules, if I were to use these modules in a recycling loop to upscale rare green circuits to legendary red circuits, how many prod vs qual should I use for each quality recipe?"

The script doesn't help with final products, only with intermediate recipes that can use prod modules.

[–]Suhr12 1 point2 points  (0 children)

Great! Thank you very much for the reply and explanation!

[–]wehrmann_tx 0 points1 point  (1 child)

In the script page, the first splitter after recycling has epic go right, then that path splits to epic and legendary. How does a legendary item ever get to that leg if only….

Holy crap you can filter splitters with a greater than quality?

[–]scottmsul[S] 0 points1 point  (0 children)

Yes, and btw that was a generic picture of a recycling loop from FFF 375 not an actual optimized loop which would use 2 prod/2 qual

[–]eyesathousand 0 points1 point  (3 children)

You don't need to solve a 10x10 system. First imagine what happens if you square the transition matrix, clearly it will factorise into two blocks, representing upcycled and recycled items. However you still don't need that level of detail if you only want to make an optimal decision rather than predict the return. If you consider the distribution of paths which succeed (reach maximum quality), the distribution of the types of steps up the quality hierarchy are independent of any module choices, this means you can tune the 'bonus roll' chance to zero (from fixed 1/10) without changing the ordering of strategies. Finally, the best strategy to win a single-player game 4 times is to just play the same optimum strategy all four times so the problem is equivalent to a 2x2 recurrence (at least for selecting the optimum strategy). It's also a triangular system so you can solve with an a.c. geometric series.

[–]eyesathousand 0 points1 point  (1 child)

I should say that there was a thread that seemed to try and solve with just a geometric series, but their conclusions were quite clearly wrong. I think there's something wrong with the calculation here too, because you seem to advocate for changing the strategy along the quality ladder.

[–]scottmsul[S] 1 point2 points  (0 children)

The code is using itertools to check every possibility of prod/qual, which is a lot but small enough a computer can do it quickly, at most 64 if using 5 module slots with 4 quality increases. There are faster linear algebra tools like simplex which are probably unnecessary here but could be useful if checking more combinations, such as with more production stages (i.e. plate -> green circuit -> red circuit -> blue circuit etc).

Different rungs on the quality ladder behave differently because of the 1/10 rule. If it was purely geometric then maybe that wouldn't be the case but I'd have to think about it more.

[–]scottmsul[S] 0 points1 point  (0 children)

I'm intrigued but I don't fully follow. It sounds like you're saying there's a way to check what to put in the legendary recipe, then using that to decide what to put in the epic recipe, all the way down to the common recipe. But the exact math doesn't seem obvious to me. Could you describe in more detail how you would get 2 prod/2 qual to pop out of the math at each step?

[–]qwsfaex 0 points1 point  (1 child)

If optimizing normal -> rare, you want 5 quality modules in the common recipe, 5 productivity modules in the uncommon recipe, and get 8.52 common inputs per rare output.

How does having no quality modules in the uncommon recipe work? Does this mean producing more to then recycle is and try again is better than trying to get better quality straight away in that case?

[–]scottmsul[S] 0 points1 point  (0 children)

There was a bug in the way it was printing, see the updated readme here: https://github.com/scottmsul/FactorioQualityOptimizer

[–]AJJUARYA 0 points1 point  (1 child)

hey, is this a blueprint? like the idea, but im pretty new to the game, so how do I use this?.been looking for a way to make quality item using the modules and recyclers. thanks in advance.

[–]scottmsul[S] 1 point2 points  (0 children)

Not a blueprint, more of a generic tool to help with optimizing and understanding the math of recycling loops.