agentscript0 icon indicating copy to clipboard operation
agentscript0 copied to clipboard

Allow breeds based on subclasses

Open bennlich opened this issue 10 years ago • 7 comments

Previously: model.createBreeds() would determine if a new breed was a sub-breed (i.e. a breed based on agents, links, or patches) by looking at the breed attribute of the base class's prototype. This worked if your base class was one of model.Agent, model.Patch, or model.Link, but it wouldn't work with a custom subclass:

class Ant extends ABM.Agent
  # define Ant class

class MyModel extends ABM.Model
  setup: ->
    @createBreeds("ants", Ant, @Agents)

The problem is that the breed attribute is only assigned to a class's prototype when that class is used to build an agentset. So our Ant class never gets a breed attribute, and createBreeds() fails to pass a mainSet into the agentset constructor.

I changed createBreeds() to accept mainSet as a fourth argument, and I refactored patchBreeds(), agentBreeds(), and linkBreeds() to default to using the appropriate mainSet.

Now the above example works with this syntax:

class Ant extends ABM.Agent
  # define Ant class

class MyModel extends ABM.Model
  setup: ->
    @agentBreeds("ants", Ant)

This is in preparation for writing more object-oriented models. Is this what we want? The ultimate goal is to make behaviors modular, and I'm thinking that may be easiest if behaviors are a part of an agent's prototype. Then we can swap out behaviors by swapping prototypes, or by adding behaviors to agent prototypes (instead of to model objects). What do you think?

bennlich avatar Dec 19 '14 06:12 bennlich

Well, generally one doesn't extend any of the main critters. Instead, you add variables to the critters and give them different behavior in your step method.

In other words, NL is not OO. Instead it has a protocol for adding variables to the base agent, patch, link, and give them different breeds which simply mean they may have their own additional variables. The "cars own speed" idea.

So NL achieves subclassing via breeds and via variables in the agents and their breeds. The behavior is simply managed in step by asking the cars to behave in a different way than drivers.

I'm reluctant to fuss with the agentset class for two reasons: it creates a potentially fragile class hierarchy, and we want to re-architect them into a choice of OofA and AofO.

Actually, I've never been really happy with breeds. We started out with them simply being a property of Agent, Patch, Link but that looked like a performance problem. So I did the current subclass stunt. But it means we keep two copies of critters, one in the Agents array, and a pointer to it in the Driver breed array. Makes me nervous.

Does this make sense? I'm OK with the PR if you find it compelling and not too confusing. But I just wanted to give you the NL and AS history.

-- Owen

On Thu, Dec 18, 2014 at 11:06 PM, Benny Lichtner [email protected] wrote:

Previously: model.createBreeds() would determine if a new breed was a sub-breed (i.e. a breed based on agents, links, or patches) by looking at the breed attribute of the base class's prototype https://github.com/backspaces/agentscript/blob/master/src/model.coffee#L252. This worked if your base class was one of model.Agent, model.Patch, or model.Link, but it wouldn't work with a custom subclass:

class Ant extends ABM.Agent

define Ant class

class MyModel extends ABM.Model setup: -> @createBreeds("ants", Ant, @Agents)

The problem is that the breed attribute is only assigned to a class's prototype when that class is used to build an agentset https://github.com/backspaces/agentscript/blob/master/src/agentset.coffee#L61. So our Ant class never gets a breed attribute, and createBreeds() fails to pass a mainSet into the agentset constructor.

I changed createBreeds() to accept mainSet as a fourth argument, and I refactored patchBreeds(), agentBreeds(), and linkBreeds() to default to using the appropriate mainSet.

Now the above example works with this syntax:

class Ant extends ABM.Agent

define Ant class

class MyModel extends ABM.Model setup: -> @agentBreeds("ants", Ant)

This is in preparation for writing more object-oriented models. Is this what we want? The ultimate goal is to make behaviors modular, and I'm thinking that may be easiest if behaviors are a part of an agent's prototype. Then we can swap out behaviors by swapping prototypes, or adding behaviors to agent prototypes (instead of to model objects). What do you

think?

You can merge this Pull Request by running

git pull https://github.com/backspaces/agentscript modular-behavior

Or view, comment on, or merge it at:

https://github.com/backspaces/agentscript/pull/78 Commit Summary

  • Add mainSet argument to createBreeds; refactor agentBreeds, patchBreeds, linkBreeds

File Changes

  • M src/model.coffee https://github.com/backspaces/agentscript/pull/78/files#diff-0 (16)

Patch Links:

  • https://github.com/backspaces/agentscript/pull/78.patch
  • https://github.com/backspaces/agentscript/pull/78.diff

— Reply to this email directly or view it on GitHub https://github.com/backspaces/agentscript/pull/78.

backspaces avatar Dec 19 '14 16:12 backspaces

Does this make sense? I'm OK with the PR if you find it compelling and not too confusing. But I just wanted to give you the NL and AS history.

Yeah, after I made the PR, I realized that subclassing Agent isn't the NL way to do things. The effect, though, is just to change where behaviors are stored: in the agent prototype or in the model prototype. You can still let the model step function control all your agents. The syntax just changes from:

for turtle in turtles
  wiggleUphill(turtle)
  dropPheromone(turtle)

to

for turtle in turtles
  turtle.wiggleUphill()
  turtle.dropPheromone()

neither of which, by the way, is all that NL-y:

ask turtles [
  wiggleUphill
  dropPheromone
]

Aside: I suppose if we really wanted to be NL-y, we could rewrite the ask function to take a list of functions and apply them in the context of the agent:

Agents.ask = (fnList) ->
  for fn in fnList
    for a in @
      fn.apply(a)

Then model.step would look like:

@agents.ask([
  @wiggleUphill,
  @dropPheromone
])

End aside.


One nice thing about storing behaviors in an agent subclass is that it becomes straightforward to modularize agents into their own files. E.g. Ant.coffee:

class Ant extends ABM.Agent
  reset: (withFood) =>
    @carryingFood = withFood
    if @breed.useSprites
      @sprite = if withFood then @model.nestSprite else @model.foodSprite
    else
      @color = if withFood then @model.nestColor else @model.foodColor
    @pheromone = @model.maxPheromone
  wiggleUphill: () =>
    if @p.isOnEdge()
      @rotate u.degToRad(180)
    else
      nAhead = @inCone @p.n, u.degToRad(180), 2
      [p, max] = nAhead.maxOneOf ((n) => @targetPheromone(n)), true
      @face p if max > .001/@model.maxPheromone
    @rotate u.randomCentered(@model.wiggleAngle)
    @forward 1
  targetPheromone: (p) =>
    if @carryingFood then p.nestPheromone else p.foodPheromone
  dropPheromone: (a) =>
    if (not @carryingFood and @p.isFood) or (@carryingFood and @p.isNest)
      @reset(not @carryingFood)
    if @carryingFood
    then @p.foodPheromone += 0.1*@pheromone
    else @p.nestPheromone += 0.1*@pheromone
    @pheromone *= 0.9

Is this desirable? My instinct is that it is because it becomes easier to understand models structured into components like this, as opposed to filtering through tons of model functions.

You can break models into components without subclassing agents, but it's uglier. For example, in the seismogram project, I have a bunch of files that look like this:

_.extend(ABM.Model.prototype, {
  // behavior pertaining to a certain breed of agent (i.e. skiers, rollers, floods)
});

Actually, I've never been really happy with breeds. We started out with them simply being a property of Agent, Patch, Link but that looked like a performance problem. So I did the current subclass stunt. But it means we keep two copies of critters, one in the Agents array, and a pointer to it in the Driver breed array. Makes me nervous.

The two copies thing makes sense to me--if you want to access all your agents, you go to model.agents, and if you want just your ants, you go to model.ants. Why does it make you nervous?

The main goal that motivated this change is wanting to see modular agent behaviors that can be loaded into agents at runtime. So I think the question I want to answer is: Is subclassing agents the best way to modularize agent behavior? Or should we keep behavior functions in the model class? Does it matter?

bennlich avatar Dec 19 '14 17:12 bennlich

Note that NetLogo supports turtles and links changing breeds at runtime, using set breed. It's not uncommon for user code to take advantage of this. If AS wants to support it, that may have implications for you represents breeds at the JS level.

SethTisue avatar Dec 19 '14 19:12 SethTisue

Mmm, yeah. Owen's done a really good job porting over NetLogo functionality, so we actually do already have setBreed in AgentSet, but this is an important point.

I think the approaches we're deciding between in the above comments use the same JS representation for breeds (javascript prototypes). The question I'm trying to answer, I think, is more about syntax and what kind of programming style we want to encourage when building models.

bennlich avatar Dec 19 '14 20:12 bennlich

In other words, NL is not OO. Instead it has a protocol for adding variables to the base agent, patch, link, and give them different breeds which simply mean they may have their own additional variables. The "cars own speed" idea. ... I'm reluctant to fuss with the agentset class for two reasons: it creates a potentially fragile class hierarchy, and we want to re-architect them into a choice of OofA and AofO.

Ah, I see what you mean now. The architectural difference between the two approaches is that currently a breed's prototype is a copy of Agent, Patch, or Link with extra attributes added through AgentSet.setDefault(). This is in contrast to the above class Ant extends ABM.Agent example, where an ant's prototype is class Ant, whose prototype is class Agent.

So, yes, if we really want to avoid prototype chains, let's not use coffeescript subclassing. Instead, though, let's maybe write a util.extend() function that clones a class and adds functions to the clone's prototype? So instead of:

class Ant extends ABM.Agent
  # ant behaviors

we'd have:

Ant = util.extend ABM.Agent, {
  # ant behaviors
}

Also might be worth taking a look at Leaflet's Class class, which has an extend function.

bennlich avatar Dec 20 '14 07:12 bennlich

@backspaces What do you think about adding some kind of extend function to AS? I'd like to start supporting modular models, where a set of behaviors can be packaged up as JSON and transported to other models.

Does this interfere with you wanting to restructure class AgentSet?

bennlich avatar Mar 02 '15 22:03 bennlich

Extend is popular amongst the prototypal inheritance crowd. It seems that Object.create is the tool of choice. That would fail if you don't need a standard object, however. For example, typedColor uses a typed array as its "object" via the basic Proto stunts. It has no need for an Object as the instance .. it uses a typedarray instead.

I would like to consider a different approach to AgentSet and the individual instances with AgentSet (Patch/Agent/Link). It would likely resolve into the layers conversation. I really want a good flyweight, high performance critter, maybe even asm.js

The color work has made a renderer separation more likely, btw. A mixin is used for all colors now, and converts them all into typedColors and colorMaps. The mixin could be converted to a more complete object of some sort that each AgentSet item refers to.

Hopefully this would make webgl use more likely. I admittedly am biased towards Cesium/3D rather than tiles and slippy maps. But that could be tough.

On Mon, Mar 2, 2015 at 3:08 PM, Benny Lichtner [email protected] wrote:

@backspaces https://github.com/backspaces What do you think about adding some kind of extend function to AS? I'd like to start supporting modular models, where a set of behaviors can be packaged up as JSON and transported to other models.

Does this interfere with you wanting to restructure class AgentSet?

— Reply to this email directly or view it on GitHub https://github.com/backspaces/agentscript/pull/78#issuecomment-76837098.

backspaces avatar Mar 05 '15 17:03 backspaces