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.
Related
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)
I'm trying to create a DnD style dungeon crawler game. I'm using the 5E SRD and other publicly available information as the base for my characters and gameplay.
Currently I'm working on the character generator, and it seems to be going well, but I've hit a roadblock when trying to assign the racial bonuses. I've got the races set up as their own subclasses, each with it's unique bonuses. When I try to assign the appropriate bonuses based on the character's race I get a (Classname)has no attribute (attribute) error.
python
class Race:
def __init__(self, race):
self.name = race
self.racial_str_bonus = 0
self.racial_char_bonus = 0
class Dragonborn(Race):
def __init__(self):
super()
self.name = "Dragonborn"
self.racial_str_bonus = +2
self.racial_char_bonus = +1
def get_racial_bonus(race):
race = race
racial_str_bonus = 0
racial_char_bonus = 0
if race == "Dragonborn":
racial_str_bonus = Dragonborn.racial_str_bonus
racial_char_bonus = Dragonborn.racial_char_bonus
return racial_str_bonus, racial_char_bonus
class BaseCharacter:
def __init__(self, racial_str_bonus, racial_char_bonus):
self.racial_str_bonus = racial_str_bonus
self.racial_char_bonus = racial_char_bonus
#classmethod
def generate_player_character(cls):
cls.race = input("Race: ")
get_racial_bonus(cls.race)
BaseCharacter.generate_player_character()
What I'm looking for is something along the line of:
'''
Race: Dragonborn
print(my_player_char.racial_str_bonus)
2
'''
Where am I goofing up?
Thanks, everyone for the feedback. In cleaning up the code to get it minimally reproducible, I figured out the issue. Per Jonrsharpe's note, I corrected the inhertance invocation to 'super().init(self)'. Once that was correct, I realized that the way they had been defined, I had to include parentheses in the property call: "Dragonborn().racial_str_bonus".
Thanks again, and I will remember to improve my submissions in the future.
I have been trying my hand at a NASCAR project, where I would have to use a class to create 20 unique vehicles and then have them race ( or to see who would reach 500 miles first, through the means of repeatedly choosing a different speed between 1 and 120 and adding it to an increasing odometer). I made what you see below and ran it, and it boots well into the Python IDLE. However, it will always tell me that NameError: name 'Driver_sponsor' is not defined. See, I have been facing this error for a while now, and I have tried placing the Driver_sponsor list into a class, placing it into the Main def and placing the keyword self. before it. No matter what I did, I faced this error. I am going to go back into my class book to see what I can do, but I am hoping that someone here can tell me what I am missing within my code, since, really, I am extremely lost.
from random import randint
import time
class Car:
def __init__(self,Driver_Name,Sponsor):
self.__Total_Odometer_Miles = 0
self.__Speed_Miles_Per_Hour = 0
self.__Driver_Name = Driver_Name
self.__Sponsor = Sponsor
self.__Driver = ('Drivers name Missing')
self.__Sponsor = ('Sponsor Missing')
self.__Driver_sponsor = {'A.J.Allmendinger:3M','Aric Almirola:Allegiant ','Trevpr Bayne:AMR ','Ryan Blaney:Camping World ','Clint Bowyer:Chevrolet ',
'Chris Buesher:Coca-Cola','Kurt Busch:Coca-light ','Kyle Busch:Credit One ','Landon Cassill:Ford','Matt DiBenedetto:FDP',
'Austin Dillon:','Ty Dillon:','Dale Earnhardt:Jacob Companies ','Chase Elliott: M & M ','Denny Hamlin: Microsoft ',
'Kevin Harvick:GoodYear ','Jimmie Johnson:Nationwide','Erik Jones:SUNOCO','Kasey Kahne:Toyota','Matt Kenseth:Visa ' }
def Name(self,Driver_Name):
self.__Driver_Name = Driver_Name
def Retrieve_Name(self):
return self.__Driver_Name
def __mutualize__(self):
self.__Total_Odometer_Miles = 0
self.__Speed_Miles_Per_Hour = 0
def sponsors(self):
self.__Driver_sponsor = {'A.J.Allmendinger:3M','Aric Almirola:Allegiant ','Trevpr Bayne:AMR ','Ryan Blaney:Camping World ','Clint Bowyer:Chevrolet ',
'Chris Buesher:Coca-Cola','Kurt Busch:Coca-light ','Kyle Busch:Credit One ','Landon Cassill:Ford','Matt DiBenedetto:FDP',
'Austin Dillon:','Ty Dillon:','Dale Earnhardt:Jacob Companies ','Chase Elliott: M & M ','Denny Hamlin: Microsoft ',
'Kevin Harvick:GoodYear ','Jimmie Johnson:Nationwide','Erik Jones:SUNOCO','Kasey Kahne:Toyota','Matt Kenseth:Visa ' }
def Retrieve_sponsor(self,Driver_sponsor):
return self.__Driver_sponsor
def main():
for key in Driver_sponsor():
CurrentCar = Car()
CurrentCar.Driver = key
CurrentCar.Sponsor = val
CurrentCar.MPH = randint(1,120)
time.sleep(.05)
time = 5
currentCar.ODT = 5
CurrentCar.ODT = CurrentCar.ODT + CurrentCar.MPH*Time
print(CurrentCar.Driver,CurrentCar.ODT)
if CurrentCar.ODT >= 500:
print('\ the winner is',key,'t\ sponsored by',val)
main()
There are a few issues with your code.
First, you're getting this error because you're calling a variable that isn't set.
But more importantly, you're trying to access the driver-sponsor dict before you've initialized an instance of Car (which currently only happens inside the loop that iterates over Driver_sponsor!).
If you want to loop over driver-sponsor pairs and initialize a new Car for each one, then do you really need the full Driver_sponsor dict initialized for every Car? If so, just pass it as an argument when constructing Car and populate self.__Driver_sponsor.
For example:
driver_sponsor_pairs = {'A.J.Allmendinger:3M',...,'Matt Kenseth:Visa'}
class Car:
def __init__(self, driver_sponsor):
# ...
self.driver_sponsor = driver_sponsor
CurrentCar = Car(driver_sponsor=driver_sponsor_pairs)
# now refer to CurrentCar.driver_sponsor
Second, you are only asking for key when looping over the Driver_sponsor dict, but you call on both key (for Driver) and val (for Sponsor) in each loop . Extract both key and val in your loop creation. You'll need the .items() dict method to get both values:
for key, val in driver_sponsor_pairs.items():
...
Third, your Car __init__ expects Driver and Sponsor arguments, but you try to define CurrentCar = Car() and then populate CurrentCar.Driver and CurrentCar.Sponsor afterwards. Continuing with the above updates, try instead:
CurrentCar = Car(Driver=key, Sponsor=val)
Fourth, you won't need the Retrieve_sponsor() method if you already have the .Sponsor attribute set.
There are a lot of misunderstandings here about Object syntax and design. You may find it frustrating to try and debug at this level of complexity. I would suggest starting very simply, say, with Car() having just one attribute. Test that, make sure it works as you want, and then build more attributes and methods from there.
I've seen several posts about this, so I know it is fairly straightforward to do, but I seem to be coming up short. I'm not sure if I need to create a worker pool, or use the Queue class. Basically, I want to be able to create several processes that each act autonomously (which is why they inherit from the Agent superclass).
At random ticks of my main loop I want to update each Agent. I'm using time.sleep with different values in the main loop and the Agent's run loop to simulate different processor speeds.
Here is my Agent superclass:
# Generic class to handle mpc of each agent
class Agent(mpc.Process):
# initialize agent parameters
def __init__(self,):
# init mpc
mpc.Process.__init__(self)
self.exit = mpc.Event()
# an agent's main loop...generally should be overridden
def run(self):
while not self.exit.is_set():
pass
print "You exited!"
# safely shutdown an agent
def shutdown(self):
print "Shutdown initiated"
self.exit.set()
# safely communicate values to this agent
def communicate(self,value):
print value
A specific agent's subclass (simulating an HVAC system):
class HVAC(Agent):
def __init__(self, dt=70, dh=50.0):
super(Agent, self).__init__()
self.exit = mpc.Event()
self.__pref_heating = True
self.__pref_cooling = True
self.__desired_temperature = dt
self.__desired_humidity = dh
self.__meas_temperature = 0
self.__meas_humidity = 0.0
self.__hvac_status = "" # heating, cooling, off
self.start()
def run(self): # handle AC or heater on
while not self.exit.is_set():
ctemp = self.measureTemp()
chum = self.measureHumidity()
if (ctemp < self.__desired_temperature):
self.__hvac_status = 'heating'
self.__meas_temperature += 1
elif (ctemp > self.__desired_temperature):
self.__hvac_status = 'cooling'
self.__meas_temperature += 1
else:
self.__hvac_status = 'off'
print self.__hvac_status, self.__meas_temperature
time.sleep(0.5)
print "HVAC EXITED"
def measureTemp(self):
return self.__meas_temperature
def measureHumidity(self):
return self.__meas_humidity
def communicate(self,updates):
self.__meas_temperature = updates['temp']
self.__meas_humidity = updates['humidity']
print "Measured [%d] [%f]" % (self.__meas_temperature,self.__meas_humidity)
And my main loop:
if __name__ == "__main__":
print "Initializing subsystems"
agents = {}
agents['HVAC'] = HVAC()
# Run simulation
timestep = 0
while timestep < args.timesteps:
print "Timestep %d" % timestep
if timestep % 10 == 0:
curr_temp = random.randrange(68,72)
curr_humidity = random.uniform(40.0,60.0)
agents['HVAC'].communicate({'temp':curr_temp, 'humidity':curr_humidity})
time.sleep(1)
timestep += 1
agents['HVAC'].shutdown()
print "HVAC process state: %d" % agents['HVAC'].is_alive()
So the issue is that, whenever I run agents['HVAC'].communicate(x) within the main loop, I can see the value being passed into the HVAC subclass in its run loop (so it prints the received value correctly). However, the value never is successfully stored.
So typical output looks like this:
Initializing subsystems
Timestep 0
Measured [68] [56.948675]
heating 1
heating 2
Timestep 1
heating 3
heating 4
Timestep 2
heating 5
heating 6
When in reality, as soon as Measured [68] appears, the internal stored value should be updated to output 68 (not heating 1, heating 2, etc.). So effectively, the HVAC's self.__meas_temperature is not being properly updated.
Edit: After a bit of research, I realized that I didn't necessarily understand what is happening behind the scenes. Each subprocess operates with its own virtual chunk of memory and is completely abstracted away from any data being shared this way, so passing the value in isn't going to work. My new issue is that I'm not necessarily sure how to share a global value with multiple processes.
I was looking at the Queue or JoinableQueue packages, but I'm not necessarily sure how to pass a Queue into the type of superclass setup that I have (especially with the mpc.Process.__init__(self) call).
A side concern would be if I can have multiple agents reading values out of the queue without pulling it out of the queue? For instance, if I wanted to share a temperature value with multiple agents, would a Queue work for this?
Pipe v Queue
Here's a suggested solution assuming that you want the following:
a centralized manager / main process which controls lifetimes of the workers
worker processes to do something self-contained and then report results to the manager and other processes
Before I show it though, for the record I want to say that in general unless you are CPU bound multiprocessing is not really the right fit, mainly because of the added complexity, and you'd probably be better of using a different high-level asynchronous framework. Also, you should use python 3, it's so much better!
That said, multiprocessing.Manager, makes this pretty easy to do using multiprocessing. I've done this in python 3 but I don't think anything shouldn't "just work" in python 2, but I haven't checked.
from ctypes import c_bool
from multiprocessing import Manager, Process, Array, Value
from pprint import pprint
from time import sleep, time
class Agent(Process):
def __init__(self, name, shared_dictionary, delay=0.5):
"""My take on your Agent.
Key difference is that I've commonized the run-loop and used
a shared value to signal when to stop, to demonstrate it.
"""
super(Agent, self).__init__()
self.name = name
# This is going to be how we communicate between processes.
self.shared_dictionary = shared_dictionary
# Create a silo for us to use.
shared_dictionary[name] = []
self.should_stop = Value(c_bool, False)
# Primarily for testing purposes, and for simulating
# slower agents.
self.delay = delay
def get_next_results(self):
# In the real world I'd use abc.ABCMeta as the metaclass to do
# this properly.
raise RuntimeError('Subclasses must implement this')
def run(self):
ii = 0
while not self.should_stop.value:
ii += 1
# debugging / monitoring
print('%s %s run loop execution %d' % (
type(self).__name__, self.name, ii))
next_results = self.get_next_results()
# Add the results, along with a timestamp.
self.shared_dictionary[self.name] += [(time(), next_results)]
sleep(self.delay)
def stop(self):
self.should_stop.value = True
print('%s %s stopped' % (type(self).__name__, self.name))
class HVACAgent(Agent):
def get_next_results(self):
# This is where you do your work, but for the sake of
# the example just return a constant dictionary.
return {'temperature': 5, 'pressure': 7, 'humidity': 9}
class DumbReadingAgent(Agent):
"""A dumb agent to demonstrate workers reading other worker values."""
def get_next_results(self):
# get hvac 1 results:
hvac1_results = self.shared_dictionary.get('hvac 1')
if hvac1_results is None:
return None
return hvac1_results[-1][1]['temperature']
# Script starts.
results = {}
# The "with" ensures we terminate the manager at the end.
with Manager() as manager:
# the manager is a subprocess in its own right. We can ask
# it to manage a dictionary (or other python types) for us
# to be shared among the other children.
shared_info = manager.dict()
hvac_agent1 = HVACAgent('hvac 1', shared_info)
hvac_agent2 = HVACAgent('hvac 2', shared_info, delay=0.1)
dumb_agent = DumbReadingAgent('dumb hvac1 reader', shared_info)
agents = (hvac_agent1, hvac_agent2, dumb_agent)
list(map(lambda a: a.start(), agents))
sleep(1)
list(map(lambda a: a.stop(), agents))
list(map(lambda a: a.join(), agents))
# Not quite sure what happens to the shared dictionary after
# the manager dies, so for safety make a local copy.
results = dict(shared_info)
pprint(results)
For brevity, I'm just showing what can/must occur in states. I haven't run into any oddities in the state machine framework itself.
Here is a specific question:
Do you find it confusing that we have to return StateChange(...) and StateMachineComplete(...) whereas some of the of the other actions like some_action_1(...) and some_action_2(...) need not be returned - they're just direct method invocations?
I think that StateChange(...) needs to return because otherwise code beyond the StateChange(...) call will be executed. This isn't how a state machine should work! For example see the implementation of event1 in the ExampleState below
import abc
class State(metaclass=abc.ABCMeta):
# =====================================================================
# == events the state optionally or must implement ====================
# =====================================================================
# optional: called when the state becomes active.
def on_state_entry(self): pass
# optional: called when we're about to transition away from this state.
def on_state_exit(self): pass
#abc.abstractmethod
def event1(self,x,y,z): pass
#abc.abstractmethod
def event2(self,a,b): pass
#abc.abstractmethod
def event3(self): pass
# =====================================================================
# == actions the state may invoke =====================================
# =====================================================================
def some_action_1(self,c,d,e):
# implementation omitted for brevity
pass
def some_action_2(self,f):
# implementation omitted for brevity
pass
class StateChange:
def __init__(self,new_state_type):
# implementation omitted for brevity
pass
class StateMachineComplete: pass
class ExampleState(State):
def on_state_entry(self):
some_action_1("foo","bar","baz")
def event1(self,x,y,z):
if x == "asdf":
return StateChange(ExampleState2)
else:
return StateChange(ExampleState3)
print("I think it would be confusing if we ever got here. Therefore the StateChange calls above are return")
def event2(self,a,b):
if a == "asdf":
return StateMachineComplete()
print("As with the event above, the return above makes it clear that we'll never get here.")
def event3(self):
# Notice that we're not leaving the state. Therefore this can just be a method call, nothing need be returned.
self.some_action_1("x","y","z")
# In fact we might need to do a few things here. Therefore a return call again doesn't make sense.
self.some_action_2("z")
# Notice we don't implement on_state_exit(). This state doesn't care about that.
When I need a state machine in Python, I store it as a dictionary of functions. The indices into the dictionary are the current states, and the functions do what they need to and return the next state (which may be the same state) and outputs. Turning the crank on the machine is simply:
state, outputs = machine_states[state](inputs)
By putting the outgoing state changes in code you're obfuscating the whole process. A state machine should be driven by a simple set of tables. One axis is the current state, and the other is the possible events. You have two or three tables:
The "next-state" table that determines the exit state
The "action" table that determines what action to take
The "read" table that determines whether you stay on the current input event or move on to the next.
The third table may or may not be needed depending on the complexity of the input "grammar".
There are more esoteric variations, but I've never found a need for more than this.
I also struggled to find a good state_machine solution in python. So I wrote state_machine
It works like the following
#acts_as_state_machine
class Person():
name = 'Billy'
sleeping = State(initial=True)
running = State()
cleaning = State()
run = Event(from_states=sleeping, to_state=running)
cleanup = Event(from_states=running, to_state=cleaning)
sleep = Event(from_states=(running, cleaning), to_state=sleeping)
#before('sleep')
def do_one_thing(self):
print "{} is sleepy".format(self.name)
#before('sleep')
def do_another_thing(self):
print "{} is REALLY sleepy".format(self.name)
#after('sleep')
def snore(self):
print "Zzzzzzzzzzzz"
#after('sleep')
def big_snore(self):
print "Zzzzzzzzzzzzzzzzzzzzzz"
person = Person()
print person.current_state == person.sleeping # True
print person.is_sleeping # True
print person.is_running # False
person.run()
print person.is_running # True
person.sleep()
# Billy is sleepy
# Billy is REALLY sleepy
# Zzzzzzzzzzzz
# Zzzzzzzzzzzzzzzzzzzzzz
print person.is_sleeping # True