# ############################################################################# # # Project: Model of Immunological Memory # Module: AIS.easel # Date: April 10, 2002 # Author: Chris Lord (clord@cmu.edu) # # Revision History # # Version Edit Date Who Description # ------- ---------- --- ------------------------------------------------ # 0.0.000 2002-04-10 CCL Created # 0.0.001 2002-04-18 CCL Change probabilistic bind to account for lack of competition # 0.0.002 2002-04-19 CCL Fix expressions to fix precedence of monadic functions # 0.0.003 2002-04-20 CCL Add parameter to control epitope duplication and uniquness # Use probabilistic encounters if clusters set to one # 0.0.004 2002-04-21 CCL Add graveyard position for dead cells and antigens # Partial actor recycling support # Add clonal generation limit # Ensure segments don't include 0 points # # # Copyright (C) 2002 by Chris Lord. All Rights Reserved. # # ############################################################################# # ############################################################################# # # Simulation # # ############################################################################# Simulation: simulation type is # # Simulation Parameters # SpatialXSize :: number := 500.0; # Width of spatial playground for depictions SpatialYSize :: number := 400.0; # Height of spatial playground for depictions SpatialXLimit :: number := # Limit including size of actors SpatialXSize + 20; SpatialYLimit :: number := # Limit including size of actors, graphs drawn below this SpatialYSize + 20; MoveRadiusSize :: number := 15; # Maximum radius for movement MoveHeadingDrift :: number := pi/4; # Maximum amount of rotational drift per move ClusterXSize :: number := 50; # Cluster width in whole spatial units ClusterYSize :: number := 40; # Cluster height in whole spatial units SpatialXClusters :: number := # Number of clusters along X axis SpatialXSize/ClusterXSize; SpatialYClusters :: number := # Number of clusters along Y axis SpatialYSize/ClusterYSize; ClusterCount :: int := # Total number of clusters (note: special case if total is one) SpatialXClusters*SpatialYClusters; # # Model Parameters # SymbolSet :: number := 255; # Our shape space is a 255*255*255 world SegmentLength :: int := 3; # Sequences of length 3, or 3 dimensions SpacePoints :: number := # Total discrete points (hence, total unique antibodies/epitopes) SymbolSet^SegmentLength; SpaceXSize :: number := SymbolSet; SpaceYSize :: number := SymbolSet; SpaceZSize :: number := SymbolSet; AffinityThreshold :: number := 8; # Euclidean distance to be considered 'close enough' to bind SpaceCoverage :: number := (2*AffinityThreshold+1)^SegmentLength; SpaceMinSize :: number := SpacePoints/SpaceCoverage; SpaceMaxDistance :: number := sqrt(SegmentLength*SymbolSet^2); SpaceMinPositions :: number := 28; # Number of points in one dimension for minimal coverage SpaceMinIncrement :: number := # Amount of space covered by each point. SymbolSet/SpaceMinPositions; ProbMemoryCell :: number := 0.25; # Probability that an B-cell differentiates into memory cell ProbMutation :: number := 0.67; # Probability that an activated memory cell will mutate InitialBCells :: int := 10; # Initial population of B-cells PathogenCount :: int := 4; # Number of pathogens to introduce PathogenLength :: int := 20; # Number of antigens (epitopes) in a pathogen PathogenDuplicates :: int := 5; # Duplicates of each antigen in pathogen PathogenUniqueAntigens :: int := PathogenLength/PathogenDuplicates; PathogenInterval :: int := 30; # Time units between introductions PathogenRepeats :: int := 3; # Repeat introductions before advancing BCellGenesisTime :: int := 2; # Time units between new naive B-cell production AbProductionRate :: int := 1; # Antibodies produced each time unit BCellMemoryClones :: int := 3; # Number of clones from activated memory cell (2^n-1) BCellActiveClones :: int := 3; # Number of clones from stimulated B-cell # # Simulation Attributes # BarHeight :: int := 15; # Height of each graph bar BarLabel :: int := 50; # Width of bar label BarValue :: int := 25; # Width of bar value BarOffset :: int := BarLabel+BarValue; # Distance from left edge (space for text) BarXLimit :: int := SpatialXLimit - BarOffset; BarSpacing :: int := 3; # Space between each bar graph View :: view := ?; # One and only view Clusters :: list := new list Cluster; # List of neighborhood clusters BCells :: list := new list BCell; # List of all B-Cells in the simulation BCellTotalCount :: int := 0; # Current count of all B-Cells BCellCounts :: list int := (list int)'[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; BCellStateCounters: list string := new list string; BCellLastId :: int := 0; # Last id assigned to B-cell (results are 1-based) Antigens :: list := new list Antigen; # List of all antigens in the simulation AntigenCount :: int := 0; # Current count of all antigens AntigenLastId :: int := 0; # Last id assigned to antigen (results are 1-based) ClusterLastId :: int := -1; # Last id assigned to cluster (results are 0-based) GraveyardXPos :: number := 500.0; # X position outside normal view for dead actors GraveyardYPos :: number := 575.0; # Y position outside normal view for dead actors DeadAntigens :: list := new list Antigen; DeadBCells :: list := new list BCell; # # Counter Acecssors # IncrementAntigens(): action is sim.AntigenCount := sim.AntigenCount + 1; IncrementBCells(): action is sim.BCellTotalCount := sim.BCellTotalCount + 1; DecrementAntigens(): action is sim.AntigenCount := sim.AntigenCount - 1; DecrementBCells(): action is sim.BCellTotalCount := sim.BCellTotalCount - 1; # # Id Functions # # These assign globally unique ids to each example so we can keep track of # various activities in debug output. They are not required by the model. # GetAntigenId(): int is sim.AntigenLastId := sim.AntigenLastId + 1; return sim.AntigenLastId; GetBCellId(): int is sim.BCellLastId := sim.BCellLastId + 1; return sim.BCellLastId; GetClusterId(): int is sim.ClusterLastId := sim.ClusterLastId + 1; return sim.ClusterLastId; # # Simulate # Simulate(): action is World :: Simulation := new Simulation; World.View := new view(World, "Immunological Memory Model", white, nil); outln(World); # # Initialize # # Our actor initalization occurs in the context of our simulation # null new(World, Initialize()); wait World; Simulate(); # # BCellState, Times, Life # # The following are each of our cell states. The transitions are handled in the # actor. The wait times for each state determine how many time units elapse # in a state before transitioning to the next state. This feature is primarily # to improve the depictions (so there is time to see B-cells and antigens in a # bound state). The lifetime is the maximum amount of time that a B-cell will # live in any one of these states. If it doesn't transition to a new state # within this number of units, it dies. There are several intermediate states # where we don't expect to persist, so these have lifetimes of 1. # BCellState: type is enum(StateNaive, StateBound, StateStimulated, StateActivated, StatePlasma, StateAntibody, StateConsumed, StateMemory, StateSecondary, StateDead); BCellStateNames: type is list '["Naive", "Bound", "Stim", "Active", "Plasma", "Antibody", "Munch", "Memory", "Second", "Dead"]; BCellStateColors: type is list '[green, blue, pink, red, orange, yellow, black, purple, plum, black]; BCellWait: type is list '[1, 5, 2, 1, 1, 1, 5, 1, 1]; BCellLife: type is list '[200, 50, 5, 5, 5, 25, 20, 100000, 5]; # '[1000, 1000, 1, 1, 500, 500, 10, 10000000, 1]; # # Initialize # # Our initialization occurs in the context of a simulation so that all simulation and # model parameters are available. # Initialize(): actor type is Pathogens :: list := new list any; PathogenSet :: list := ?; BCellCountdown :: number := ?; NextPathogen :: int := 0; PathogenCountdown :: number := ?; PathogenRepeats :: number := ?; Frame :: frame := ?; # # Sanity check model and simulation parameters # Assert sim.PathogenUniqueAntigens >= 1; for i: each 1..2000 do outln("."); # # Create each of our neighborhood clusters, numberd 0...ClusterCount-1 # for i: each 1..sim.ClusterCount do push(sim.Clusters, new Cluster); # # Create a library of random pathogens for experiments # for i: each 1..sim.PathogenCount do PathogenSet := new list any; # BUG: for j: each 1..sim.PathogenUniqueAntigens do for j: each 1..5 do Seg :: Segment := GetNewRandomSegment(); push(PathogenSet, Seg); for k: each 1..sim.PathogenDuplicates-1 do push(PathogenSet, CloneSegment(Seg)); outln("Debug: Pathogen ",i-1, "=", PathogenSet); push(Pathogens, PathogenSet); # # Create the initial set of B-cells. # for i: each 1..sim.InitialBCells do push(sim.BCells, new(sim, BCell(StateNaive, GetNewSpanningSegment(), GetNewRandomPosition()))); # # Draw a thick line separating the interaction area from our graphs. # TODO: Why doesn't this work with polyline() # depict(sim.View, paint(polygon( 0, sim.SpatialYLimit+sim.BarSpacing, sim.SpatialXLimit, sim.SpatialYLimit+sim.BarSpacing, sim.SpatialXLimit, sim.SpatialYLimit+sim.BarSpacing+2, 0, sim.SpatialYLimit+sim.BarSpacing+2), black)); # # Draw our bar graphs for each possible B-Cell state. # for i: each StateNaive..StateSecondary do # # Create text frames for the static labels # Frame := new frame(rectangle(0,sim.SpatialYLimit+(i+1)*sim.BarHeight+i*sim.BarSpacing, sim.BarLabel,sim.SpatialYLimit+(i+1)*sim.BarHeight+i*sim.BarSpacing+sim.BarHeight)); style(Frame, #" font-family: Arial; font-size: 11pt; font-weight: bold; font-style: normal; color: black; background-color: white; "#); # TODO: The layout trashes memory (fine if we just add Frame) depict(sim.View, layout(BCellStateNames[i], "", Frame)); # # Create text frames for reporting the current values. There is no nice way to # have dynamic strings from integers without updating the strings from the # background (which we do in our count routine). # Frame := new frame(rectangle(sim.BarLabel,sim.SpatialYLimit+(i+1)*sim.BarHeight+i*sim.BarSpacing, sim.BarLabel+sim.BarValue,sim.SpatialYLimit+(i+1)*sim.BarHeight+i*sim.BarSpacing+sim.BarHeight)); style(Frame, #" font-family: Arial; font-size: 11pt; font-weight: normal; font-style: normal; color: black; background-color: white; "#); push(sim.BCellStateCounters, new string); UpdateCounter(i); # TODO: The layout trashes memory (fine if we just add Frame) depict(sim.View, layout(sim.BCellStateCounters[i], "", Frame)); # # Create dynamically updated percentage bars based on our cell counts # depict(sim.View, var paint(polygon( sim.BarOffset, sim.SpatialYLimit+(i+1)*sim.BarHeight+i*sim.BarSpacing, sim.BarOffset+((var sim.BCellCounts[i])/(var sim.BCellTotalCount))*sim.BarXLimit, sim.SpatialYLimit+(i+1)*sim.BarHeight+i*sim.BarSpacing, sim.BarOffset+((var sim.BCellCounts[i])/(var sim.BCellTotalCount))*sim.BarXLimit, sim.SpatialYLimit+(i+1)*sim.BarHeight+i*sim.BarSpacing+sim.BarHeight, sim.BarOffset, sim.SpatialYLimit+(i+1)*sim.BarHeight+i*sim.BarSpacing+sim.BarHeight), BCellStateColors[i] )); # # Action # BCellCountdown := sim.BCellGenesisTime; PathogenCountdown := 1; PathogenRepeats := 0; for every true do wait(1.0); # # Is it time to generate a new BCell? If so, each new B-Cell is based on a point # from our minimum spanning set. # BCellCountdown := BCellCountdown - 1; if BCellCountdown = 0 then push(sim.BCells, new(sim, BCell(StateNaive, GetNewSpanningSegment(), GetNewRandomPosition()))); BCellCountdown := sim.BCellGenesisTime; # # Is it time to introduce a new pathogen? # PathogenCountdown := PathogenCountdown - 1; if PathogenCountdown = 0 then outln("Introduce Pathogen ", NextPathogen, " ", PathogenSet); for i: each Pathogens[NextPathogen] do push(sim.Antigens, new(sim, Antigen(i, GetNewRandomPosition()))); PathogenRepeats := mod(PathogenRepeats + 1, sim.PathogenRepeats); if PathogenRepeats = 0 then NextPathogen := mod(NextPathogen+1, sim.PathogenCount); PathogenCountdown := sim.PathogenInterval; # # List Manipulators # # The actors in this simulation are constantly moving between lists. These # manipulators allow self insertion and deletion on the specified list with # localized sanity checks. # PrintList( List: list ): action is Index :: int := 0; for i: each List do output(" ",Index,": "); if i isa BCell then outln("Ab ", i.Id, " ", i.State, " Seg=(", i.Antibody.x, ",", i. Antibody.y,",",i. Antibody.z,") Pos=(", i.Pos.x,",", i.Pos.y, ") Hood=", i.Hood.Id, " Life=", i.Life); else if i isa Antigen then outln("Ag ", i.Id, " Seg=(", i.Epitope.x, ",",i.Epitope.y,",",i.Epitope.z,") Pos=(", i.Pos.x, ",", i.Pos.y, ") Hood=", i.Hood.Id); else outln("Unknown list element type"); Index := Index + 1; # # BUG: index_of does not always find elements on a list so we roll our own # SearchList( List: list, Element: any ): indexer is Index :: int := 0; Assert List isa list; for i: each List do if i.Id = Element.Id then return Index; Index := Index + 1; return Index; RemoveFromList( List: list ): action is Index :: indexer := ?; Assert List isa list; Assert (self isa Antigen) | (self isa BCell); if (length List) > 0 then Index := SearchList(List, self); if Index = -1 then output("Error: RFL invalid list - "); PrintActor("RFL1"); else if Index = (length List) then trace true; output("Error: RFL self not found index=",Index," - "); PrintActor("RFL2"); PrintList(List); else remove(List, Index); AddToList( List: list ): action is Index :: indexer := ?; Assert List isa list; Assert (self isa Antigen) | (self isa BCell); # # If we're already on a list, then there is a problem. # if (length List) > 0 then Index := SearchList(List, self); if Index = -1 then output("Error: ATL invalid list - "); PrintActor("RFL1"); else if Index != (length List) then output("Error: ATL self found - "); PrintActor("ATL2"); else push(List, self); else push(List, self); # # Cluster # # A cluster is a neighborhood of antibodies, B-cells and antigens. Actors # move from one cluster to an adjacent cluster. This provides a very simple # mechanism to simulate spacial interactions for depiction without the overhead # of maintaining or calculating neighbor relations. The cluster that an actor # belongs to is determined by their position as follows: # # Cluster[trunc(Pos.y/ClusterYSize)*SpatialXClusters + trunc(Pos.x/ClusterXSize)] # # This gives us a coarse measure of 'nearby' that provides the same dynamics # even as it fails to provide the same physical fidelity. It, importantly, # lets us create realms of competition between antigens. Note that it assumes # that the only legal positions are (0..SpatialXSize-1, 0..SpatialYSize-1); # # There is a special case of one cluster which forces us into a probablistic encounter # mode with much better performance but unintuitive visualization. # # BUG: index_of returns -1 for non-lists (undocumented return value) # BUG: index_of doesn't find actors on list if they've been internally reallocated # Cluster: type is Id :: int := GetClusterId(); BCells :: list := new list any; Antigens :: list := new list any; # # GetCluster # # Return the cluster occupied based on the specified position. # GetCluster( Pos: Position ): Cluster is Index :: int := (trunc (trunc Pos.y)/sim.ClusterYSize)*sim.SpatialXClusters + (trunc (trunc Pos.x)/sim.ClusterXSize); Assert (Pos.x < sim.SpatialXSize) & (Pos.y < sim.SpatialYSize); if (Index < 0) | (Index >= sim.ClusterCount) then output("Error: Cluster index=", Index, " YSize=", sim.ClusterYSize," XSize=", sim.ClusterXSize, " XClusters=",sim.SpatialXClusters, " - "); PrintActor("GErr"); Index := sim.ClusterCount - 1; return sim.Clusters[Index]; # # InsertCluster # # Insert the caller in the appropriate cluster. This should only be called at actor # initialization after which MigrateCluster should be used to switch clusters. # InsertCluster( Pos: Position ): action is NewHood :: Cluster := GetCluster(Pos); Assert NewHood isa Cluster; self.Hood := NewHood; if self isa Antigen then AddToList(NewHood.Antigens); else AddToList(NewHood.BCells); # # MigrateCluster # # Move from one cluster to another. This requires hat the caller be either a BCell # or an Antigen and have a Hood attribute which is their current cluster. We only # move if our new cluster is different than our old one. # MigrateCluster( Pos: Position ): action is NewHood :: Cluster := GetCluster(Pos); Assert self.Hood isa Cluster; Assert NewHood isa Cluster; if self.Hood.Id != NewHood.Id then if self isa Antigen then RemoveFromList(self.Hood.Antigens); else RemoveFromList(self.Hood.BCells); self.Hood := NewHood; if self isa Antigen then AddToList(NewHood.Antigens); else AddToList(NewHood.BCells); # # Position # Position: type is x :: number := ?; y :: number := ?; ClonePosition( Copy: Position ): Position is Pos :: Position:= new Position; Pos.x := Copy.x; Pos.y := Copy.y; return Pos; GetNewRandomPosition(): Position is Pos :: Position := new Position; Pos.x := random(uniform, 0, sim.SpatialXSize); Pos.y := random(uniform, 0, sim.SpatialYSize); return Pos; WrapPosition( Pos: Position ): Position is if ( Pos.x > sim.SpatialXSize ) then Pos.x := Pos.x - sim.SpatialXSize; else if ( Pos.x < 0 ) then Pos.x := Pos.x + sim.SpatialXSize; if ( Pos.y > sim.SpatialYSize ) then Pos.y := Pos.y - sim.SpatialYSize; else if ( Pos.y < 0 ) then Pos.y := Pos.y + sim.SpatialYSize; return Pos; GetRandomMove( Pos: Position, Heading: number ): Position is Pos.x := Pos.x + random(uniform, 0, sim.MoveRadiusSize)*cos(Heading); Pos.y := Pos.y + random(uniform, 0, sim.MoveRadiusSize)*sin(Heading); return WrapPosition(Pos); GetRandomHeading( Heading: number ): number is if random(uniform, 0, 1) < 0.5 then Heading := Heading - random(uniform, 0, sim.MoveHeadingDrift); if Heading < 0 then Heading := 2*pi + Heading; else Heading := Heading + random(uniform, 0, sim.MoveHeadingDrift); if Heading > 2*pi then Heading := 2*pi - Heading; return Heading; # ############################################################################# # # Model # # ############################################################################# # # Assert # Assert( Result: boolean ): action is if Result = false then outln("Error: Assertion failed"); trace true; # # PrintActor # PrintActor( Desc: string ): action is if self isa Antigen then PrintAntigen(Desc); else if self isa BCell then PrintAntibody(Desc); else outln("Error: PrintActor(",Desc,") unknown type"); outln(self); # # Segment # # This is the base type of an antibody or epitope. It represents a point in # 3D shape space. Conveniently, it also has an RGB color. The affinity between # any two points is the Euclidean distance. # Segment: type is x :: 1..255 := ?; y :: 1..255 := ?; z :: 1..255 := ?; # # BUG: Overload resolution of Clone(Segment) and Clone(Position) always matches Position # CloneSegment( Copy: Segment ): Segment is Seg :: Segment := new Segment; Seg.x := Copy.x; Seg.y := Copy.y; Seg.z := Copy.z; return Seg; # # BUG: "0+" is required for rgb_color expressions or Easel crashes # GetSegmentColor( Seg: Segment ): pattern is return rgb_color(0+Seg.x, 0+Seg.y, 0+Seg.z); GetNewSpanningSegment(): Segment is Seg :: Segment := new Segment; Seg.x := round(random(uniform, 0, sim.SpaceMinPositions)*sim.SpaceMinIncrement,1); Seg.y := round(random(uniform, 0, sim.SpaceMinPositions)*sim.SpaceMinIncrement,1); Seg.z := round(random(uniform, 0, sim.SpaceMinPositions)*sim.SpaceMinIncrement,1); return Seg; GetNewRandomSegment(): Segment is Seg :: Segment := new Segment; Seg.x := round(random(uniform, 1, sim.SymbolSet),1); Seg.y := round(random(uniform, 1, sim.SymbolSet),1); Seg.z := round(random(uniform, 1, sim.SymbolSet),1); return Seg; GetNearAffinitySegment( Seg: Segment ): Segment is Seg.x := mod(Seg.x+round(random(uniform, 0, sim.AffinityThreshold),1),sim.SymbolSet-1)+1; Seg.y := mod(Seg.y+round(random(uniform, 0, sim.AffinityThreshold),1),sim.SymbolSet-1)+1; Seg.z := mod(Seg.z+round(random(uniform, 0, sim.AffinityThreshold),1),sim.SymbolSet-1)+1; return Seg; GetNearSpanningSegment( Seg: Segment ): Segment is Seg.x := mod(Seg.x+round(random(uniform, 0, sim.SpaceMinIncrement),1),sim.SymbolSet-1)+1; Seg.y := mod(Seg.y+round(random(uniform, 0, sim.SpaceMinIncrement),1),sim.SymbolSet-1)+1; Seg.z := mod(Seg.z+round(random(uniform, 0, sim.SpaceMinIncrement),1),sim.SymbolSet-1)+1; return Seg; GetSegmentDistance( Seg1: Segment, Seg2: Segment ): number is return sqrt((Seg1.x-Seg2.x)^2+(Seg1.y-Seg2.y)^2+(Seg1.z-Seg2.z)^2); # # Shapes # # The antibody (bcell) and antigen shapes are designed to merge at a common # origin 0,0 providing they are both at the same orientation. This allows us # to easily show binding without altering depictions. # # AntibodyShape: drawing is polygon(0,0, 3,4, 3,-2, -3,-2, -3,4); # AntigenShape: drawing is polygon(0,0, 3,4, 3,10, -3,10, -3,4); AntibodyShape: drawing is polygon(0,0, 6,8, 6,-4, -6,-4, -6,8); AntigenShape: drawing is polygon(0,0, 6,8, 6,20, -6,20, -6,8); # # BCell: Actor # BCell( InitialState: BCellState, InitialAntibody: Segment, InitialPos: Position): actor type is Id :: int := GetBCellId(); State :: BCellState := InitialState; Antibody :: Segment := InitialAntibody; Pos :: Position := InitialPos; Shape:: drawing := paint(AntibodyShape, GetSegmentColor(Antibody)); Orientation :: direction := random(uniform, 0, 2*pi); Hood :: Cluster := ?; IsBound :: boolean := false; BoundAg :: Antigen := ?; Move :: boolean := ?; Life :: number := ?; # # PrintAntibody # PrintAntibody( Desc: string ): action is outln("Ab ", self.Id, " ", Desc, ": ", self.State, " Seg=(", self.Antibody.x, ",", self. Antibody.y,",",self. Antibody.z,") Pos=(", self.Pos.x,",", self.Pos.y, ") Hood=", self.Hood.Id, " Life=", self.Life); # # CloneBCell # CloneBCell( NewState: BCellState ): action is NewPos :: Position := ClonePosition(self.Pos); NewPos := GetRandomMove(NewPos, random(uniform, 0, 2*pi)); NewAb :: Segment := CloneSegment(self.Antibody); push(sim.BCells, new(sim, BCell(NewState, NewAb, NewPos))); # # UpdateCounter # UpdateCounter( State: BCellState ): action is Spaces :: int := ?; if State != StateDead then truncate(sim.BCellStateCounters[State],0); Spaces := 3 - (trunc (log sim.BCellCounts[State])); for i: each 1..Spaces do push(sim.BCellStateCounters[State], 32); format(sim.BCellStateCounters[State], sim.BCellCounts[State]); # outln("Format: ", State,"='", sim.BCellStateCounters[State],"'"); # # CountState # CountState( OldState: BCellState, NewState: BCellState ): action is sim.BCellCounts[OldState] := sim.BCellCounts[OldState] - 1; if sim.BCellCounts[OldState] < 0 then outln("Error: State ", OldState, " count negative: ", sim.BCellCounts); sim.BCellCounts[OldState] := 0; sim.BCellCounts[NewState] := sim.BCellCounts[NewState] + 1; if (NewState != StateDead) & (sim.BCellCounts[NewState] > sim.BCellTotalCount) then outln("Error: State ", NewState, " count above limit ", sim.BCellTotalCount,": ", sim.BCellCounts); sim.BCellCounts[NewState] := sim.BCellTotalCount; UpdateCounter(OldState); UpdateCounter(NewState); # # Depiction # depict(sim.View, var offset_by(rotate(Shape, 0.0, 0.0, var (Orientation)), var Pos.x, var Pos.y)); # # Initialization # State := InitialState; IncrementBCells(); sim.BCellCounts[State] := sim.BCellCounts[State] + 1; UpdateCounter(State); Life := BCellLife[InitialState]; InsertCluster(Pos); # # Action # # PrintAntibody("New "); for every true do # PrintAntibody("Main"); # # Wait an amount of time based on our current state. If we expire, then purge ourselves # gracefully from the world. We should only expire as a Naive, Plasma or Antibody or Consumed cells; # all others are transition states. # for i: each 1.. BCellWait[State] do wait(1.0); Life := Life - BCellWait[State]; if Life <= 0 then # PrintAntibody("Dead"); CountState(State, StateDead); DecrementBCells(); RemoveFromList(Hood.BCells); RemoveFromList(sim.BCells); # # There currently is no way to remove the depiction of a dead actor from a # view. To work around this, we move the actor to a graveyard position that # is outside the normal view area we use. We also add them to a dead list # for potential recycling. # AddToList(sim.DeadBCells); Pos.x := sim.GraveyardXPos; Pos.y := sim.GraveyardYPos; wait(1.0); terminate(); # # Default is to move at the end of the state processing. # Move := true; # # Perform whatever we're supposed to in our current state and handle state transitions. # Note: select/case isn't implemented, so we handle via a series of conditions. # if (State = StateNaive) | (State = StateAntibody) | (State = StateMemory) then # # Naive, Antibody and Memory # # These cells behave very much the same in our model: they look in their immediate # neighborhood for antigens within the affinity threshold and bind with the best # match with a probability proportional to the affinity (which provides the element # of competition between cells). # if (IsBound = false) & ((length Hood.Antigens) > 0) then Distance :: number := sim.SpaceMaxDistance; TempDistance :: number := ?; # # Search our neighborhood for an antigen that is within the affinity threshold. # BoundAg := nil; for Ag: each Hood.Antigens do if Ag.IsBound = false then TempDistance := GetSegmentDistance(Antibody, Ag.Epitope); if (TempDistance < sim.AffinityThreshold) & (TempDistance < Distance) then Distance := TempDistance; BoundAg := Ag; # # If we found an antigen to bind with, determine the probability of a bind occuring # based on our proximity to the affinity threshold. Given that d < AffinityThreshold, # the probability of binding in this time unit is Pr[bind] = 1 - d/(2*Affinity). A # distance of zero means we always bind. The probability is then always in the range # of 1.0 to 0.5 for our affinity from exact match out to the limit. # # This is adjusted based on the number of other B-cells in the cluster to model the # differing attractions and competition between cells based on the following: # # Pr[bind] = trunc(Pr[bind] - (0.99-log10(n))) # # The adjustment adds .99 when there is a single cell (no competition) which ensures # we always bind, decreasing logarithmically to ~0 at 10 cells. At 30 cells, the # adjustment is .5 which means cells with the weakest affinity will not bind. # if BoundAg != nil then Pr :: number := 1 - (Distance/(sim.AffinityThreshold*2)); # # Adjust based on the number of B-cells in the cluster. This may result in negative # probabilities so we invert our usual condition so that these are equivalent to zero. # Pr := (trunc (Pr - (0.99 - log length Hood.BCells))); # # The process of binding with an antigen triggers its countdown to termination # if (1-random(uniform, 0, 1.0)) > Pr then # PrintAntibody("Bind"); # outln(" Ag=", BoundAg.Id, " Seg=", BoundAg.Epitope, " Dist=", Distance); # # We successfully bound. If we're an antibody (not a real B-cell) then we # transition into the consumed state where we hangout until our life expires. # A memory cell that binds divides. On cell follows the traditional path of # a bound cell (since we don't have a stimulate process), the other follows # a mutation path back to a naive cell. # if State = StateAntibody then CountState(State, StateConsumed); State := StateConsumed; else if State = StateMemory then for i: each 1..sim.BCellMemoryClones do CloneBCell(StateSecondary); CountState(State, StateBound); State := StateBound; BoundAg.Pos.x := Pos.x; BoundAg.Pos.y := Pos.y; BoundAg.Orientation := Orientation; BoundAg.IsBound := true; BoundAg.Life := BCellLife[State]; IsBound := true; Move := false; else if State = StateBound then # # Bound # # Bound B-cells consume the antigen and become an antigen presenting cell bearing antigen # fragments on the surface. Normally we would wait until we are stimulated by a T-cell # which also recognizes the same antigen. We don't currently implement this process # and instead move straight to the stimulated state. The antigen is consumed at this # point because we triggered its destruction above. # CountState(State, StateStimulated); State := StateStimulated; else if State = StateStimulated then # # A stimulated cell divides, each of which becomes either a plasma or memory cell. # for i: each 1..sim.BCellActiveClones do CloneBCell(StateActivated); CountState(State, StateActivated); State := StateActivated; else if State = StateActivated then # # Differentiate into either a memory or plasma cell. These start off new lifetimes. # if random(uniform, 0, 1) < sim.ProbMemoryCell then CountState(State, StateMemory); State := StateMemory; # PrintAntibody("Mem "); else CountState(State, StatePlasma); State := StatePlasma; # PrintAntibody("Plas"); Life := BCellLife[State]; else if State = StatePlasma then # # Plasma # # Plasma cells generate large quantities of identical antibodies during their # relatively short lifetime. In our model, antibodies are just inactive B-cells # which simplifies processing. Each antibody is created nearby in some random # direction from our plasma cell. # for i: each 1..sim.AbProductionRate do CloneBCell(StateAntibody); wait(1.0); else if State = StateSecondary then # # Secondary # # Secondary response cells are the result of activated memory cells. We go through a # probabilistic mutation on our way to becoming a naive cell. # if random(uniform, 0, 1.0) < sim.ProbMutation then Antibody := GetNearAffinitySegment(Antibody); CountState(State, StateNaive); State := StateNaive; # # Should we move? Some states we like to leave put for a while (like right after we # bind). But for the most part we like to move (and potentially update our cluster). # if Move then Orientation := GetRandomHeading(Orientation); Pos := GetRandomMove(Pos, Orientation); MigrateCluster(Pos); # # Antigen # # Currently antigens have only a single epitope which simplifies tracking of bind state # and improves performance. # Antigen( InitialEpitope: Segment, InitialPos: Position): actor type is Id :: int := GetAntigenId(); Epitope :: Segment := InitialEpitope; Pos :: Position := InitialPos; Shape :: drawing := paint(AntigenShape, GetSegmentColor(Epitope)); Orientation :: direction := random(uniform, 0, 2*pi); IsBound :: boolean := false; Life :: number := 0; Hood :: Cluster := ?; # # Depiction # depict(sim.View, var offset_by(rotate(Shape, 0.0, 0.0, var (Orientation)), var Pos.x, var Pos.y)); # # PrintAntigen # PrintAntigen( Desc: string ): action is outln("Ag ", self.Id, " ", Desc, ": Seg=(", self.Epitope.x, ",",self.Epitope.y,",",self.Epitope.z,") Pos=(", self.Pos.x, ",", self.Pos.y, ") Hood=", self.Hood.Id); # # Initialization # IncrementAntigens(); InsertCluster(Pos); # # Action # # PrintAntigen("New "); for every true do # PrintAntigen("Main"); # # An antigen is passive in this model. We exist until we are bound after which we die. # IsBound and Life are set by the B-cell during binding. # if IsBound = true then # PrintAntigen("Dead"); DecrementAntigens(); RemoveFromList(Hood.Antigens); RemoveFromList(sim.Antigens); # # There currently is no way to remove the depiction of a dead actor from a # view. To work around this, we move the actor to a graveyard position that # is outside the normal view area we use. We also add them to a dead list # for potential recycling. # AddToList(sim.DeadAntigens); Pos.x := sim.GraveyardXPos; Pos.y := sim.GraveyardYPos; for i: each 1..Life do wait(1.0); terminate(); # # In an unbound state, we wander aimlessly through our world. # Orientation := GetRandomHeading(Orientation); Pos := GetRandomMove(Pos, Orientation); MigrateCluster(Pos); wait(1.0);