genetic alg for building optimization

Place to post guides, observations, things related to modding that are not mods themselves.
Post Reply
blazespinnaker
Filter Inserter
Filter Inserter
Posts: 665
Joined: Wed Sep 16, 2020 12:45 pm
Contact:

genetic alg for building optimization

Post by blazespinnaker »

Writing some GA stuff for building optimization

Still some work to go on simulation. Writing about 5-10 lines of code / day as I have lots of other (irl) work to do. If anyone is interested in helping out, would be great.

Here's the code so far. 99% of the effort is in simulating factorio feeding/crafting in order to get proper fitness evaluation. Some work needs to be done in the mating/cross over alg to ideal subsequences from parents, but I don't imagine that will be that hard.


--

Code: Select all


import random

ASSEMBLER=0
IRON=1
STONE=2
COPPER=3
COIL=4
GEAR=5
GS=6
FURNACE=7
MINER=8
COAL=9

IRON_MFP=0
STONE_M=1
COAL_M=2
COPPER_MFP=3
GEAR_ASM=4
FURNACE_ASM=5
MINER_ASM=6
COIL_ASM=7
GS_ASM=8
AS_ASM=9

CRAFT=0
PLACE=1
FEED_Z=2 #Z drop
FEED_HALF=3 #right click
FEED_FULL=4 #left click
GRAB=5 #left click

def toAction(action):
    if action==CRAFT:
        return 'CRAFT'
    elif action==PLACE:
        return 'PLACE'
    elif action==FEED_Z:
        return 'FEED_Z'
    elif action==FEED_HALF:
        return 'FEED_HALF'
    elif action==FEED_FULL:
        return 'FEED_FULL'
    elif action==GRAB:
        return 'GRAB'

def toAct_on(action, act_on):
    if action==CRAFT:
        if act_on==ASSEMBLER:
            return 'ASSEMBLER'
        elif act_on==GEAR:
            return 'GEAR'
        elif act_on==COIL:
            return 'COIL'
        elif act_on==GS:
            return 'GS'
        elif act_on==FURNACE:
            return 'FURNACE'
        elif act_on==MINER:
            return 'MINER'
    else:
        if act_on==IRON_MFP:
            return 'IRON_MFP'
        elif act_on==STONE_M:
            return 'STONE_M'
        elif act_on==COAL_M:
            return 'COAL_M'
        elif act_on==COPPER_MFP:
            return 'COPPER_MFP'
        elif act_on==GEAR_ASM:
            return 'GEAR_ASM'
        elif act_on==FURNACE_ASM:
            return 'FURNACE_ASM'
        elif act_on==MINER_ASM:
            return 'MINER_ASM'
        elif act_on==COIL_ASM:
            return 'COIL_ASM'
        elif act_on==GS_ASM:
            return 'GS_ASM'
        elif act_on==AS_ASM:
            return 'AS_ASM'




INGREDIENTS={
    #resource:[components,time,output,craftedin]
    #components is [(resource,amount),..]
    ASSEMBLER:[[(IRON,9),(GEAR,5),(GS,3)],1,1,[],50],
    IRON:[[(COAL,1/6)],3,1,[], 100],
    STONE:[[(COAL,1/6)],3,1,[], 50],
    COPPER:[[(COAL,1/6)],3,1,[], 100],
    GEAR:[[(IRON,2)],1,1,[], 100],
    COIL:[[(COPPER,1)],1,2,[], 200],
    GS:[[(IRON,1),(COIL,3)],1,1,[]], 200],
    FURNACE:[[(STONE,5)],1,1,[], 50],
    MINER:[[(IRON,3),(GEAR,3),(FURNACE,1)],4,1,[], 50],
    COAL:[[],3,1,[], 50]
}

s

ENTITIES={
    IRON_MFP:{'prod':IRON, 'requires':[MINER, FURNACE]},
    STONE_M:{'prod':STONE,'requires':[FURNACE]},
    COAL_M:{'prod':COAL, 'requires':[FURNACE]},
    COPPER_MFP:{ 'prod':COPPER,'requires':[MINER, FURNACE]},
    GEAR_ASM:{ 'prod':GEAR, 'requires':[ASSEMBLER]},
    FURNACE_ASM:{ 'prod':FURNACE, 'requires':[ASSEMBLER]},
    MINER_ASM:{ 'prod':MINER,'requires':[ASSEMBLER]},
    COIL_ASM:{'prod':COIL,'requires':[ASSEMBLER]},
    GS_ASM:{'prod':GS,'requires':[ASSEMBLER]},
    AS_ASM:{'prod':ASSEMBLER,'requires':[ASSEMBLER]}
}


def check_rules(rules, ruled_on):
    return True

def got_rule(res):
    #check current rule to see if res has been crafted or grabbed
    return True

CRAFTABLES=[GEAR, COIL, GS, FURNACE, MINER, ASSEMBLER]
PLACEABLES=list(ENTITIES.keys())
FEEDABLES=PLACEABLES

#(craft, acton)  (will craft as much as possible)
#(place, acton) (wil place as much as possible)
#(feed, acton, %) (will feed up to % or as much as possible)
#(grab, acton) (will grab as much as possible)

TW=1 #time window 1 second
CRLPS=5 #(default 5 per second)
PRLPS=5 #placing 5 per second
FRLPS=5 #feeding 5 person
GRLPS=5 #grabbing 5 per second


craft_queue=[]
placed={}


def reset():
    _=[inv_count[x]=0 for x in INGREDIENTS.keys()]



def has_entity(entity):
    return True

def has_ingredients(ingredient):
    return True

def remove_ingredients(ingredient):
    return True

def remove_entity(entity):
    return True

def none():
    return None

def rng_perc():
    return (int(random.random()*4)+1)*.25

def craft_act(act_on):
    ing = INGREDIENTS[act_on]
    for _ in range(0, TW * CRLPS):
        if has_ingredients(ing):
            craft_queue.append((ing, ing[1]))
            remove_ingredients(ing)
        else:
            break
    return True

def place_act(act_on):
    ent = ENTITIES[act_on]
    entities = get_entities(act_on)
    count = TW * PRLPS
    count = remove_entity_from_inv(count)
    if entities == None:
        entities={"count": 0}
    entities['count'] = entities['count']+count
    placed[act_on]=entities

def get_entities(act_on)
    if act_on in placed.keys():
        return placed['act_on']
    else:
        return None

def get_ingredient(entities):
    ings=entities['ingredients']
    ings.sort(key=lambda x:x[1]) #asc by default
    for x in ings:
        if has_inv(x[0]):
            return x
    #get entities act_on
    #get ingredient counts
    #sort by ingredient counts asc
    #loop through ingredients
    #if ingredient in inventory
    #    use that ingredient
    return None,None#entities, ingredient

def feed_entities(entities, incredient, amount)
    #increase entities ssby FRPL*TW*amount if in inventory
    #decrease inventory

def feed_z_act(act_on):
    entities = get_entities(act_on)
    ingredient = get_ingredient(entities)
    feed_entities(entities, ingredient, 1)
    #loop FRPLS*t
    #   add one to entitie
    #   remove 1 from inventory
    return True

def feed_half_act(act_on):
    entities = get_entities(act_on)
    ingredient = get_ingredient(entities)
    feed_entities(entities, ingredient, ingredient[4]/2)
    return True

def feed_full_act(act_on):
    entities = get_entities(act_on)
    ingredient = get_ingredient(entities)
    feed_entities(entities, ingredient, ingredient[4])
    return True

def grab_act(act_on):
    entities = get_entities(act_on)
    #loop on GRPPLS
    #remove from entity
    #add to inventory
    return True

def inc_inventory(ing, amt):
    inv[ing]=inv[ing]+amt

def process_craft_queue(tick):
    time_used=0
    time_left=tick
    while True
        if len(craft_queue)==0:
            return
        top=craft_queue[0]
        if top[1] > time_left:
            top[1]=top[1]-time_left
            return
        inc_inventory(top[0], top[2])
        del(craft_queue(0))
        time_left=time_left-top[1]

def process_entity_crafting():
    return True

def process_tick(tick):
    process_craft_queue(tick)
    process_entity_crafting(tick)
    


ACTIONS={
    CRAFT:{'acton':CRAFTABLES, 'params':none, 'act':craft_act,'rules':{}},
    PLACE:{'acton':PLACEABLES, 'params':none, 'act':place_act,'rules':{}},
    FEED_Z:{'acton':FEEDABLES, 'params':rng_perc, 'act': feed_z_act,'rules':{}},
    FEED_HALF:{'acton':FEEDABLES,'params':rng_perc, 'act':feed_half_act,'rules':{}},
    FEED_FULL:{'acton':FEEDABLES,'params':rng_perc, 'act':feed_full_act,'rules':{}},
    GRAB:{'acton':FEEDABLES,'params':none, 'act':grab_act,'rules':{}},
}

for craftable in CRAFTABLES:
  ACTIONS[CRAFT]['rules'][craftable]=[(got_rule, INGREDIENTS[craftable][0])]

for placeable in PLACEABLES:
  ACTIONS[PLACE]['rules'][placeable]=[(got_rule, ENTITIES[placeable]['requires'])]

for x in ENTITIES.keys():
    INGREDIENTS[ENTITIES[x]['prod']][3].append(x)

def gen_random_build_step(curl_rules, ruled):
    #return random action, 
    action=None
    act_on=None
    for _ in range(0,1000):
        action_key=list(ACTIONS.keys())[int(len(ACTIONS)*random.random())]
        action=ACTIONS[action_key]
        act_ons=action['acton']
        act_on=act_ons[int(len(act_ons)*random.random())]
        rules = action['rules']
        if act_on in rules.keys() and check_rules(rules[act_on], ruled) == False:
            continue
        else:
            break
    return (action_key, act_on, action['params']())

def to_string(rule):
    return toAction(rule[0])+":"+toAct_on(rule[0], rule[1])+":"+str(rule[2])

for i in range(0,100):
    print(to_string(gen_random_build_step(None,None)))
OptimaUPS Mod, pm for info.

blazespinnaker
Filter Inserter
Filter Inserter
Posts: 665
Joined: Wed Sep 16, 2020 12:45 pm
Contact:

Re: genetic alg for building optimization

Post by blazespinnaker »

I finished the first version, fairly different from the above. I'll post to github soon as well as initial results.

GA is a lot of fun, thinking through some of the hinting heuristics and evolutionary concepts is pleasant. Not entirely sure if my crossover alg is best, but seems to be finding optimal solutions so I suppose that's good enough.

Not so much fun is the level of compute power required to converge a precise simulation of Factorio. I have a rack at home, but they are older HP proliant servers and waiting around for answers turns out to be kinda boring - especially when you discover an hour later there was a bug in the sim which lead to an incorrect answer. On top of all that, the only thing you can really precisely simulate is handfeeding. Rather limiting.

However, it wasn't too hard to adopt the concepts to a coarser grained approach. In fact, it turns out to be better as it allows for using modular factories instead of single assemblers. Solutions converge quickly due to fewer moving parts.

Some effort is required offline to create different types of meta factories though for the GA stuff to chew on. But, that is a fun and creative effort, so I doubt it'll be much of a chore. And of course, can still just go the hand feeding route by considering a single factory as a representation of a group of assemblers.

There is a balance, of course, reduce too many moving parts and you don't really need GA to solve it.
OptimaUPS Mod, pm for info.

Post Reply

Return to “Modding discussion”