Using agentpy, how to create an agent during a simulation - python

I have a simple simulation of a company using agentpy. It is doing a great job of modelling promotions, but I'd also like to be able to model people joining and leaving the company.
Leaving is easy, I can just set a flag to inactive or something, but joining is more complicated. Do I need to make a bunch of agents during setup and set their state to as yet unknown, or can I create an agent during a step and add them in?
The person class is defined like this:
class PersonAgent(ap.Agent):
def setup(self):
p = PEOPLE_DF.iloc[self.id] # 👈 the existing people are in a spreadsheet
self.name = p.full_name
self.gender = get_gender(p.gender)
self.bvn_rank = get_rank(p.b_rank)
# self.capability = float(p.capability)
print()
def rank_transition(self):
self.bvn_rank = transition(self.b_rank, self.gender)
I'm guessing I'd do something with the __init__, but I've had no luck figuring that out.

Yes, you can initiate new agents during a simulation step.
Here are some examples:
class MyModel(ap.Model):
def setup(self):
# Initiate agents at the start of the simulation
self.agents = ap.AgentList(self, 10, PersonAgent)
def step(self):
# Create new agents during a simulation step
self.single_new_agent = PersonAgent(self)
self.list_of_new_agents = ap.AgentList(self, 10, PersonAgent)
# Add them to the original list (if you want)
self.agents.append(self.single_new_agent)
self.agents.extend(self.list_of_new_agents)

Related

How to assign variable variables in Python3 based on another variable

I am working on building an AI in Python3 to play Starcraft II using python-sc2 in BurnySC2 . How do I concisely assign values to different variables based on another variable? I've kicked around a few things, but none of them seem to do what I want. All of the buildings and units are stored as a UnitTypeId with different names for similar unit/building types based on the race.
For instance, I want to do something like:
ai_race = 1
def ai_tech_tree(ai_type)
switcher = {
Race.1: ai_townhall = UnitTypeId.townhall1;
ai_worker = UnitTypeId.worker1;
ai_fighter = UnitTypeId.fighter1,
Race.2: ai_townhall = UnitTypeId.townhall2;
ai_worker = UnitTypeId.worker2;
ai_fighter = UnitTypeId.fighter2
}
ai_tech_tree(ai_race)
class MyBot(BotAI): # inherits from BotAI
async def on_step(self, iteration: int): # method called every step of the game
if self.townhalls: # does AI have a townhall?
town_hall = self.townhalls.random # select random AI-owned townhall
if town_hall.is_idle and self.can_afford(ai_worker): # is AI's townhall idle and can AI afford a worker?
town_hall.train(ai_worker) # train a worker
but you can't seem to declare a variable in the CASES statement. The closest I got to something that at least worked is doing a CASES for each unit/building type, but that was large and clunky.
ai_race = 1
def get_ai_townhall(ai_type)
switcher = {
Race.1: UnitTypeId.townhall1,
Race.2: UnitTypeId.townhall2,
Race.3: UnitTypeId.townhall3
...
}
def get_ai_worker(ai_type)
switcher = {
Race.1: UnitTypeId.worker1,
Race.2: UnitTypeId.worker2,
Race.3: UnitTypeId.worker3
...
}
...
ai_townhall = get_ai_townhall(ai_race)
ai_worker = get_ai_worker(ai_race)
...
I am hoping there is a more elegant solution.

Handling multiple objects in pytransitions

https://github.com/pytransitions/transitions
I was trying with the batman = NarcolepticSuperhero("Batman") example in the documentation.
But let's say if we have 100 batmans to handle, My approach is to create a list of every batman object and track their states individually using a loop.
IMO this is very naive.
Can some-one please suggest some effective solution.
Thanks
Assuming all NarcolepticSuperheros should behave the same, I recommend to use one Machine for all 'batmen' instead of instantiating NarcolepticSuperhero a hundred times. This way, you can use Machine.dispatch to trigger an event on ALL models instead of looping through a list of superheroes (under the hood Machine also just loops through the model list though). I condensed the mentioned example a bit (all transitions in one list), removed the machine that was bound to NarcolepticSuperheros and introduced a SuperheroDen. Concerning state checking, you could use a dictionary to keep track of how many heroes are currently in what state. I added a hero_log to NarcolepticSuperhero and every time a hero changes its state, it will first log out of its current state and then log in with its new state.
from transitions import Machine
from collections import defaultdict
import random
class NarcolepticSuperhero(object):
hero_log = defaultdict(list)
def __init__(self, name):
self.name = name
self.kittens_rescued = 0
def update_journal(self):
self.kittens_rescued += 1
#property
def is_exhausted(self):
return random.random() < 0.5
#staticmethod
def change_into_super_secret_costume():
print("Beauty, eh?")
def log_out(self):
self.hero_log[self.state].remove(self)
def log_in(self):
self.hero_log[self.state].append(self)
class SuperheroDen(Machine):
states = ['asleep', 'hanging out', 'hungry', 'sweaty', 'saving the world']
transitions = [
['wake_up', 'asleep', 'hanging out'],
['work_out', 'hanging out', 'hungry'],
['eat', 'hungry', 'hanging out'],
['nap', '*', 'asleep'],
dict(trigger='distress_call', source='*', dest='saving the world', before='change_into_super_secret_costume'),
dict(trigger='complete_mission', source='saving the world', dest='sweaty', after='update_journal'),
dict(trigger='clean_up', source='sweaty', dest='asleep', conditions=['is_exhausted']),
['clean_up', 'sweaty', 'hanging out'],
]
def __init__(self):
super().__init__(model=[NarcolepticSuperhero('Clone warrior') for i in range(100)],
states=self.states,
transitions=self.transitions,
# tell each model to 'log_out' right before state change
before_state_change='log_out',
# and to 'log_in' right after
after_state_change='log_in',
initial='asleep')
# since our super heroes are asleep (and 'spawn' in their state), we have to log them in the first time
for model in self.models:
NarcolepticSuperhero.hero_log[self.initial].append(model)
machine = SuperheroDen()
# trigger event 'wake_up' on all models
machine.dispatch('wake_up')
assert len(NarcolepticSuperhero.hero_log['asleep']) == 0
assert len(NarcolepticSuperhero.hero_log['hanging out']) == 100
for i in range(10):
machine.models[i].work_out()
assert len(NarcolepticSuperhero.hero_log['hanging out']) == 90
assert len(NarcolepticSuperhero.hero_log['hungry']) == 10
assert machine.models[0] in NarcolepticSuperhero.hero_log['hungry']
What you suggest sounds reasonable to me.
heros = [NarcolepticSuperhero(f"Batman{i:02d}")
for i in range(100)]
Then either loop over all of them,
or use a while True: loop that sleeps a moment
then picks a hero at random.
Either way, you will have a hero
that you can trigger some new transition on.

How to call a function when instances are being dynamically created?

I'm a Java newbie, transitioning into Python and I've been trying to develop a simplistic market-like interaction between companies and investors. Basically, there are 100 companies and 100 investors been created dynamically with random values at initialization.
class Company:
def __init__(self):
""" Constructor for the Company object """
self.id: str = uuid.uuid4().hex
self.shares_num = random.randint(500, 1000)
self.share_price = Decimal(random.randint(10, 100))
class Investor:
def __init__(self):
""" Constructor for the Investor object """
self.id_inv: str = uuid.uuid1().hex
self.budget: Decimal = random.randint(50_000, 100_000)
self.acq_shares = 0
def sell_shares(self):
trade = self.budget - Company.get_share_price
Company.get_shares_num -= 1
self.acq_shares += 1
self.set_budget = trade
return self
The function sell_shares() performs the actual sale, i.e. makes the trade between the investor's money and the company's shares.
The companies and investors are then placed into a list with their ID and a dict with share's data for companies and budget's for investors.
# dynamic instances of Companies and Investors
comp = [Company() for _ in range(10)]
companies = {c.id: c for c in comp}
inv = [Investor() for _ in range(10)]
investors = {i.id_inv: i for i in inv}
The problem is when I call the sell_shares() method Investors.sell_shares neither the instances of companies or shares are affected. I don't really see anything wrong, apart from the fact that it isn't working.
Any suggestions on how I could try and fix this?
Thanks a million :)
P.S.: I'm using Python 3.8.2 with standard libraries.
Probably the biggest source of your pain is your use of mutable global variables inside of a class method. Please don't do that. Every time you do, somewhere, a baby kitten dies.
You could fix this problem quite easily by making trade() accept a keyword argument for a company, and then just operate using self (referring to that particular investor), and the keyword argument, (referring to a particular instance of a company). Don't try to access all the investors and companies at once, being that your method only depends on one of each.
import uuid
import random
from decimal import Decimal
class Company:
def __init__(self):
""" Constructor for the Company object """
self.id: str = uuid.uuid4().hex
self.shares_num = random.randint(500, 1000)
self.share_price = Decimal(random.randint(10, 100))
class Investor:
def __init__(self):
""" Constructor for the Investor object """
self.id: str = uuid.uuid1().hex
self.budget: Decimal = random.randint(50000, 100000)
self.acq_shares = 0
def trade(self, company):
trading = self.budget - company.share_price
company.share_price -= 1
self.acq_shares += 1
# this equation is wrong, but I'm not sure entirely what you want to do here
# this is equivalent to -company.share_price
company.share_price = trading - self.budget
self.budget = trading
return self.id, self.budget
Note that this also removes any need to create get_share_price() or get_budget() methods.
If, somehow, I've misunderstood your setup, and your trade function does depend on all investors and all companies at once, then don't make it a class method. Make it a regular function, outside of a class, where it takes two arguments like so def trade(investors, companies):, because in that case, it doesn't have to do with that single class instance of investor.
You can test out your classes like so:
# Generate companies and investors
comp = [Company() for _ in range(10)]
companies = {c.id: c for c in comp} # <-- not sure what this is used for
inv = [Investor() for _ in range(10)]
investors = {i.id: i for i in inv} # <-- not sure what this is used for
# Select an investor and a company
some_investor = inv[random.randint(0,9)]
some_company = comp[random.randint(0,9)]
# Make a trade
some_investor.trade(some_company)
print(some_company.share_price) # prints a negative number
Note that your current formula for share price seems very wrong to me, being that it's equivalent to self.budget - company.share_price - self.budget.
Hope this helps.

How to add tags to an existing state object in Pytransitions?

In the project I'm assigned to we use pytransitions. Our states are created, get equipped with additional attributes and added to a list one by one as objects first. Then this list of State objects is passed to a Machine object.
Here's a simple example:
from transitions import State
states = []
state_initial = State("initial", on_exit="some_callback")
text = "this is some text"
state.text = text
states.append(state)
This is how a machine is created:
from transitions import Machine
from some_library import SomeClass
from my_module import user_transitions
class User:
states = states
initial_state = states[0]
def __init__(self, some_param: str, another_param: SomeClass = default_param):
self.machine = Machine(model=self,
states=User.states,
initial=User.initial_state,
transitions=user_transitions,
prepare_event="preparing_callback",
after_state_change="ending_callback")
What I'd like to do is to add tags to my states at the time of or after state object creation. I mean the tags in transitions.extensions.states, so I could get them with is_tag kind of methods like in docs. Something like state_initial.add_tags(["tag1", "tag2"]) or
state_initial = State("initial", on_exit="some_callback", tags=["tag1", "tag2"])
or in any other way considering my legacy setup. How do I go about this?
My first suggestion would be to check whether you can streamline the state creation process by using a dedicated TextState instead of just assigning an additional attribute. This way you can keep your state configuration a bit more comprehensible. Reading machine configurations from yaml or json files gets way easier as well.
from transitions import Machine, State
from transitions.extensions.states import Tags
# option a) create a custom state class and use it by default
# class TextState and CustomState could be combined of course
# splitting CustomState into two classes decouples tags from the
# original state creation code
class TextState(State):
def __init__(self, *args, **kwargs):
self.text = kwargs.pop('text', '')
super(TextState, self).__init__(*args, **kwargs)
class CustomState(Tags, TextState):
pass
class CustomMachine(Machine):
state_cls = CustomState
states = []
state_initial = CustomState("initial", text="this is some text")
# we can pass tags for initialization
state_foo = dict(name="foo", text="bar!", tags=['success'])
states.append(state_initial)
states.append(state_foo)
# [...] CustomMachine(model=self, states=User.states, initial=User.initial_state)
But your question was about how you can inject tag capability AFTER states have been created. Probably because it would need major refactoring and deep digging to alter state creation. Adding state.tags = ['your', 'tags', 'here'] is fine and should work out of the box for graph and markup creation. To get state.is_<tag> working you can alter its __class__ attribute:
from transitions import Machine, State
from transitions.extensions.states import Tags
# option b) patch __class__
states = []
state_initial = State("initial")
state_initial.text = "this is some text"
# we can pass tags for initialization
state_foo = State("foo")
state_foo.text = "bar!"
state_foo.tags = ['success']
states.append(state_initial)
states.append(state_foo)
# patch all states
for s in states:
s.__class__ = Tags
s.tags = []
# add tag to state_foo
states[1].tags.append('success')
class User:
states = states
initial_state = states[0]
def __init__(self):
self.machine = Machine(model=self,
states=User.states,
initial=User.initial_state)
user = User()
user.to_foo()
assert user.machine.get_state(user.state).is_success # works!
assert not user.machine.get_state(user.state).is_superhero # bummer...
But again, from my experience code becomes much more comprehensible and reusable when you strive to separate machine configuration from the rest of the code base. Patching states somewhere in the code and assigning custom paramters might be overlooked by the next guy working with your code and it surely is surprising when states change their class between two debugging breakpoints.

Python Multiprocessing.Pool in a class

I am trying to change my code to a more object oriented format. In doing so I am lost on how to 'visualize' what is happening with multiprocessing and how to solve it. On the one hand, the class should track changes to local variables across functions, but on the other I believe multiprocessing creates a copy of the code which the original instance would not have access to. I need to figure out a way to manipulate classes, within a class, using multiprocessing, and have the parent class retain all manipulated values in the nested classes.
A simple version (OLD CODE):
function runMultProc():
...
dictReports = {}
listReports = ['reportName1.txt', 'reportName2.txt']
tasks = []
pool = multiprocessing.Pool()
for report in listReports:
if report not in dictReports:
dictReports[today][report] = {}
tasks.append(pool.apply_async(worker, args=([report, dictReports[today][report]])))
else:
continue
for task in tasks:
report, currentReportDict = task.get()
dictReports[report] = currentFileDict
function worker(report, currentReportDict):
<Manipulate_reports_dict>
return report, currentReportDict
NEW CODE:
class Transfer():
def __init__(self):
self.masterReportDictionary[<todays_date>] = [reportObj1, reportObj2]
def processReports(self):
self.pool = multiprocessing.Pool()
self.pool.map(processWorker, self.masterReportDictionary[<todays_date>])
self.pool.close()
self.pool.join()
def processWorker(self, report):
# **process and manipulate report, currently no return**
report.name = 'foo'
report.path = '/path/to/report'
class Report():
def init(self):
self.name = ''
self.path = ''
self.errors = {}
self.runTime = ''
self.timeProcessed = ''
self.hashes = {}
self.attempts = 0
I don't think this code does what I need it to do, which is to have it process the list of reports in parallel AND, as processWorker manipulates each report class object, store those results. As I am fairly new to this I was hoping someone could help.
The big difference between the two is that the first one build a dictionary and returned it. The second model shouldn't really be returning anything, I just need for the classes to finish being processed and they should have relevant information within them.
Thanks!

Categories

Resources