***CONTrOL*** *is an agent-based model developed for the purpose of studying behavioral mechanisms influencing social learning and performance within and across business organisations. CONTrOL stands for Complex Organisational and Network-driven Transmissions resulting in Organisation Learning.* # Organisation package This section contains the description of the code objects that are part of the organisation package. These objects are clustered as separate python submodules (.py files) within this package. The [AgentPy library](https://agentpy.readthedocs.io/en/latest/) is used for the creation of the agents and their attributes. ```python import agentpy as ap ``` ## organisation.Individual module ```{eval-rst} .. automodule:: organisation.Individual :members: :undoc-members: :show-inheritance: ``` ### Class definition for `Individual` This is a **superclass** definition for an agent in the AgentPy library. * The class is called Individual and it inherits from ap.Agent, which is a base class in the AgentPy library for defining agents. * The setup method is defined within the class and it initializes various attributes of the agent, such as its personal competency, current location, path, and knowledge. The `initPersonalCompetency`, `initLocation`, and `initKnowledge` methods are defined in the next code section of this module and are called to set the initial values for these attributes. * The code also randomly sets the size of the agent's memory and creates a new `Memory` object with that size, which is used to store the agent's past visited locations. The `remember` method of the `Memory` object is called to remember the agent's current location. * Lastly, the code initializes empty lists for the agent's departmental colleagues, organizational colleagues, and friends. These lists are populated later in the code. ```python class Individual(ap.Agent): def setup(self, initialLocation): self.initPersonalCompetency() self.currentLocation = self.initLocation(initialLocation) self.path = [self.currentLocation] self.knowledge = self.initKnowledge(initialLocation) memorySize = self.model.random.randint(self.model.p.minMemory, self.model.p.maxMemory) self.memory = Memory(memorySize) self.memory.remember(self.currentLocation) self.deptColleaguesList = [] self.orgColleaguesList = [] self.friends = [] ``` Other initializations included in the class definition for `Individual` are: * The `initPersonalCompetency(self)` function, which involves: * energy initialization `self.totalEnergy` for the agents to have at least enough energy for a month (default setting = `reimbursementWindow` parameter, see [main.py](#main.py) to propose a one-step move every day, and max 10 times that; * `self.maxNumFriends` which sets randum number of maximum friends an agent can have (network size) between 0 and `maxMaxNumFriends`; * `self.innovativenss` and `self.predictionCompetence` both of which are random number with triangular distribution between 0 and 1. [Triangular distribution](https://en.wikipedia.org/wiki/Three-point_estimation) is used since the outcome range has a minimum (0), a maximum (1) and a peak parameter (i.e. a most-likely parameter). * The `initLocation(self, initialLocation)` fuction which initializes an `IndividualLocationPerception` object based on the `initialLocation` parameter and the number of multiplex layers defined in the landscape (**add link later**).The `IndividualLocationPerception` object stores the realized fitness values per layer for the initial location. The function records the realized fitness value for each layer using the `recordRealisedFitness` method, which can be found [here](#indivLP) Finally, the function returns the `IndividualLocationPerception` object representing the initial location. * The `initKnowledge (self, sourceNode)` function: * This function initializes the knowledge of an agent in a model. It takes a sourceNode as an input, which is the initial location of the agent; * The function generates a list of layer numbers using the `random.choices method`, which selects `k` layer numbers from the range of 0 to the number of layers in the model's landscape; * The function then creates a new `Knowledge` object using these parameters along with the number of initial knowledge nodes, initial knowledge graph degree, and mind capacity specified in the model parameters (see [main.py[(#main.py)]). * The `Knowledge` object contains information about the agent's knowledge, including the nodes and edges in their mental representation of the landscape. ```python class Individual(ap.Agent): CONTINUED def initPersonalCompetency(self): self.totalEnergy = self.model.random.randint(self.model.p.reimbursementWindow, self.model.p.reimbursementWindow*10) self.maxNumFriends = self.model.random.randint(0, self.model.p.maxMaxNumFriends) self.innovativeness = self.model.nprandom.triangular(0, self.model.p.modeInnovativeness, 1) self.predictionCompetence = self.model.nprandom.triangular(0, self.model.p.modePredictionCompetence, 1) def initLocation(self, initialLocation): location = IndividualLocationPerception(initialLocation, self.model.landscape.num_layers) for l_idx in range(self.model.landscape.num_layers): location.recordRealisedFitness(l_idx, self.model.landscape.getFitnessForLayer(l_idx, initialLocation)) return location def initKnowledge(self, sourceNode): layers = self.model.random.choices([i for i in range(0, self.model.landscape.num_layers)], k=self.model.p.initKnowledgeLayerNum) return Knowledge(self.model.landscape, sourceNode, layers, self.model.p.initKnowledgeNodeNum, self.model.p.initKnowledgeGraphDegree, self.model.p.mindCapacity) ``` (soc)= Next, several functions represent initial social attributes and behavior of the `individual`: * The `getFriends(self)` function determines which neighbors are already a friend (as opposed to organisational superior and/or subordinate relationships): * The function first creates a list of all of the `individual`'s neighbors in the social graph using a list comprehension (i.e. nodes that are directly connected to the agent); * It then iterates through this list, checking the type of the edge connecting the agent to each neighbor in the social graph; * If the type of the edge is "friendRelation", meaning that the neighbor is a friend of the agent, it appends the neighbor to a list called "friends"; * Finally, the function returns the "friends" list. * The `getColleagues(self)` function evaluates every other `individual` in the landscape to identify whether it is a colleague within the agent's organisation and the agent's department. If so, the respective `individual` is added (`.append`) to the list of the agent's organisational colleagues (`orgColleaguesList`) and departmental colleagues (`deptColleaguesList`). *Note: CEO's have no department and therefore do not have departmental colleagues, only organisational*. * The `socialise(self)` fuction is a function to initialise the creation of a social network for every `individual` in the form of a list that is composed of every other `individual` in the landscape, not being an organisational and/or departmental colleague. Initially, all nodes are eligible to befriend. Later, this set is reduced (**add link**). The logic is as follows: * Find out which nodes are colleagues; don't befriend them (except that employees can befriend the CEO); * Don't befriend people that are already a neighbor, i.e. current friends, or employees' managers, or managers' CEOs or employees, or self; * *[Source of inspiration](https://stackoverflow.com/questions/10929269/how-to-select-two-nodes-pairs-of-nodes-randomly-from-a-graph-that-are-not-conn)* * Direct neighbors and colleagues within the department won't be befriended; this is registered in the `exclude_list`. * First make a list of which nodes these are, and then remove them from the set of eligible nodes * *Note: the orgColleaguesList is not used here* ```python class Individual(ap.Agent): CONTINUED def getFriends(self): self.neighborList = [n for n in self.model.socialGraph.neighbors(self)] friends = list() for neighbor in self.neighborList: if self.model.socialGraph.edges[(self, neighbor)]["type"] == "friendRelation": friends.append(neighbor) return friends def getColleagues(self): deptColleaguesList = list() orgColleaguesList = list() for node in self.model.socialGraph.nodes: if node.organisation == self.organisation and node.type != "CEO": if self.type == "CEO": orgColleaguesList.append(node) if self.model.debugFriendship: print('I am node', self, 'and I am orgcolleagues with', node) elif node.department == self.department: deptColleaguesList.append(node) if self.model.debugFriendship: print('I am node', self, 'and I am depcolleagues with', node) else: orgColleaguesList.append(node) if self.model.debugFriendship: print('I am node', self, 'and I am orgcolleagues with', node) return deptColleaguesList, orgColleaguesList def socialise(self): self.friends = self.getFriends() self.numFriends = len(self.friends) self.deptColleaguesList, self.orgColleaguesList = self.getColleagues() while self.numFriends < self.maxNumFriends: eligible_nodes = set(self.model.socialGraph.nodes()) exclude_list = [*self.neighborList, *self.deptColleaguesList] exclude_list.append(self) eligible_nodes.difference_update(exclude_list) if len(eligible_nodes) > 0: new_friend = self.model.random.choice(list(eligible_nodes)) else: break self.model.socialGraph.add_edge(self, new_friend, type="friendRelation", color = "green") self.numFriends += 1 self.friends.append(new_friend) ``` ### Class object definition for `Memory` Class object definition of `Memory` and the initialization of two agent attributes: 1. a new empty list of locations 2. memory size Next, the function `remember` is created to update the agent's memory with location `l`. * The function checks whether the agent has the location already stored in its memory. If so, the memory is updated. If not, the location is added to the agent's memory. * The function also ensures that the locations list does not exceed a certain size, by removing the oldest location from the list if it does, see function `forget(self)`. Lastly, several functions are created to mark when memory `isEmpty` and which locations are `oldest` and `latest`. ```python class Memory: def __init__(self, size): self.locations = [] self.size = size def remember(self, l): updated = False for i in range(len(self.locations)): if self.locations[i].get() == l.get(): self.locations[i] = l updated = True if not updated: self.locations.insert(0, l) if len(self.locations) > self.size: self.forget() def forget(self): return self.locations.pop() def isEmpty(self): return len(self.locations) == 0 def oldest(self): return self.locations[-1] def latest(self): return self.locations[0] def search(self, location): results = [r for r in filter(lambda l: l.get() == location, self.locations)] if len(results) > 0: return results[0] else: return None ``` ### Class object definition for `IndividualLocalPerception` Landscape fitness perception is defined and registered at the individual level. For this purpose, a separate class `IndividualLocationPerception` is created. For this class object, *three attributes* are initialized that belong to each agent (`self`) and depends two paramaters: (1) their location (`location`) and (2) the number of landscape layers the agent can interpret (`num_layers`): * coordinates of a landscape location (not necessarily the agent's actual current location) (`self.location`); * the fitness they estimate for that location (relevant for locations where the agent considers moving to (`self.estimatedFitness`)); * the realised fitness value of a location where the agent actually is (`self.realisedFitness`). If the fitness is unknown or undefined, the value -1 is provided. Ex-ante fitness estimation and ex-post fitness realization by the agent are recorded at the level of the separate layers in the multiplex landscape by means of functions `recordFitnessEstimation (self, layer, fitness)` and `recordRealisedFitness(self,layer,fitness)`. Lastly, the function `getFitness(self, weights)` calculates **the total average fitness** of an agent based on the estimated fitness and realised fitness values of the agent and a set of weights. The function takes two arguments: `self`, which refers to the current instance of the class that the function is defined in, and `weights`, which is a list of weights assigned per landscape layer and thus for each fitness value. 1. The function first initializes a variable called `result` to 0. 2. It then iterates through the estimated fitness values of the agent using a for loop, with `l_idx` as the loop index variable. For each fitness value, the function checks if the realised fitness value of the agent at the same index is greater than -1. If it is, then the function uses the realised fitness value instead of the estimated fitness value for that index. The function then multiplies the fitness value at that index by the corresponding weight from the `weights` list and adds it to the `result` variable. 3. Finally, the function returns the average value of `result` by dividing it by the length of the `weights` list. *Note: Realised fitness will only be recorded for a particular location when the agent actually moves to that location. This is further defined in the [Employee module.](#employee-module)* (indivLP)= ```python class IndividualLocationPerception: def __init__(self, location, num_layers): self.location = location self.estimatedFitness = [-1 for e in range(num_layers)] self.realisedFitness = [-1 for e in range(num_layers)] def recordFitnessEstimation(self, layer, fitness): self.estimatedFitness[layer] = fitness def recordRealisedFitness(self, layer, fitness): self.realisedFitness[layer] = fitness def get(self): return self.location def getFitness(self, weights): result = 0 for l_idx in range(len(self.estimatedFitness)): fitness_value = self.estimatedFitness[l_idx] if self.realisedFitness[l_idx] > -1: fitness_value = self.realisedFitness[l_idx] result += weights[l_idx]*fitness_value return result/len(weights) ``` (employee-module)= ## organisation.Employee module ```{eval-rst} .. automodule:: organisation.Employee :members: :undoc-members: :show-inheritance: ``` ### Class definition for `NodeLayer` This function initializes a class `NodeLayer` that takes `node` and `layer` as two landscape-based parameters. The `self` argument refers to the object of the class being created, i.e. the agent (the employee). It initializes two object variables, `node` and `layer` in the landscape. The purpose of the `NodeLayer` object is to easily access node-layer pairs. ```python class NodeLayer: def __init__(self, node, layer): self.node = node self.layer = layer ``` ### Class definition for `Proposal` This function initializes a class object `Proposal` and takes `employee` as a parameter. It initializes several object variables: * `self.employee`: this object variable is assigned the value of the `employee` argument passed to the class construction method. It represents the employee that submitted the proposal. * `self.source`: this object variable is initialized to "None". It will be used to store the current location of the employee. * `self.target`: this object variable is initialized to "None". It will be used to store the proposed new location for the employee. * `self.improvement`: this object variable is initialized to 0. It will be used to store the improvement score of the proposal. * `self.cost`: this object variable is initialized to 0. It will be used to store the costs related to the proposal. * `self.isApproved`: this object variable is initialized to False. It will be used to store a Boolean value indicating whether the proposal has been approved or not. ```python class Proposal: def __init__(self, employee): self.employee = employee self.source = None self.target = None self.improvement = 0 self.cost = 0 self.isApproved = False ``` ### Class definition for `EmployeeReport` This function initializes a class `EmployeeReport` to record the state of the employee. It initializes several (self-explanatory) employee-level object variables. The `clear` function is to clear the variables of the EmployeeReport object and set them to zero. ```python class EmployeeReport: def __init__(self, employee): self.employee = employee self.proposal = Proposal(employee) self.energyUsed = 0 self.newEdgesLearnt = 0 self.experimentsConducted = 0 self.innovativenessIncrease = 0 self.predictionCompetenceIncrease = 0 def clear(self): self.energyUsed = 0 self.newEdgesLearnt = 0 self.experimentsConducted = 0 self.innovativenessIncrease = 0 self.predictionCompetenceIncrease = 0 ``` ### Class definition for `Employee` This code defines a class `Employee` which inherits from the `Individual` superclass. Given that this class definition has an extensive number of functions and methods, these are separately covered in the following subsections. #### Setup functions The `setup` method is defined within the`Employee` class, and it takes one argument `managerInitialLocation`. The purpose of this method is to initialize the agent and pick an initial landscape location for the employee that is close to the manager. * The method starts by initializing an empty list called `potential_nodes`. * Then, for each path length in the `path_length` attribute of the `landscape` object in the model, the method iterates over the nodes in the first layer of the landscape. * For each node, it checks if the path length from the `managerInitialLocation` to that node is within a certain distance range (defined by the `depEmployeeInitialDistance` attribute of the model's parameters set in the [main module](#main.py), and if so, it adds the node to the `potential_nodes` list. * Finally, the method selects an initial location for the employee from the potential nodes using NumPy's random choice function `nprandom` if there are any potential nodes, otherwise, it assigns the `managerInitialLocation` as the initial location for the employee. ```python class Employee(Individual): def setup(self, managerInitialLocation): potential_nodes = [] for path_length in self.model.landscape.path_length: for node in self.model.landscape.layers[0].nodes: if path_length[managerInitialLocation][node] >= (self.model.p.depEmployeeInitialDistance - 1) and \ path_length[managerInitialLocation][node] <= (self.model.p.depEmployeeInitialDistance + 1): if node not in potential_nodes: potential_nodes.append(node) employee_initial_location = self.model.nprandom.choice(potential_nodes) if len(potential_nodes) > 0 else managerInitialLocation ``` The `super().setup(employee_initial_location)` has the purpose of creating of the agent type `Employee` as a subclass of `Individual` This code it calls the setup method of its superclass using the `super()` function. By calling the `setup` method of the `Individual` class, the agent-type `Employee` inherits some of the initialization properties and methods defined in the Individual class. Additionally, the code assigns the string value `"employee"` to the type attribute of the `Employee` object, which indicates the role of the individual. Also, it adds several status variables to the agent-type `Employee`, such as the PCSs initiated by the agent (`self.ownedPCSes`) and PCSs in which the agent participates (`self.PCSes`). Several of these state variables are parameters set in [main.py](#main.py). ```python class Employee(Individual): CONTINUED super().setup(employee_initial_location) self.type = "employee" self.reimbursement = 0 self.report = EmployeeReport(self) self.proposalHistory = [] self.proposalAveragingWindow = self.model.p.proposalAveragingWindow self.ownedPCSes = list() self.PCSes = list() self.PCSInvitations = list() self.ERIThreshold = self.model.p.ERIThreshold ``` (setupbookkeeping=) The purpose of `setupBookkeeping` is to create an administrative method within the `Employee` class where the agent's (`self`)place in the organisation is stored. ```python class Employee(Individual): CONTINUED def setupBookkeeping(self, ceo, organisation, manager, department): self.ceo = ceo self.organisation = organisation self.manager = manager self.department = department ``` #### Landscape exploration The `exploreLandscape` function defines how agents of the type `Employee` behave to explore the landscape. * It initializes `energy_left` with the value of `availableEnergyForExploration` and `experiments_left` with the value of `availableEnergyForExperimenting`; * It retrieves attributes (`knowledge`, `innovativeness`, `predictionCompetence`) from the instance of the Employee class; * It checks if PCSes are enabled for this model run; * If it does, then use the aggregated knowledge and competences instead of that of the agent itself; * If not, it skips the subsequent logic; * It iterates over the layers of the landscape to establish a list of `weights` based on `organisation.beliefSystem` and the available knowledge of the employee (which is either its own or an aggregation of knowledge available in the PCS whereto the employee belongs); * It records the locations already visited in this exploration round to keep track of visited and potential locations for revisiting during the exploration of the landscape. The following loop continues as long as `energy_left` is greater than 0. * It checks if there are potential locations to revisit (`potential_locations_to_revisit`). If there are no potential locations to revisit, it breaks out of the loop; * It determines the source node for exploration based on a random choice, considering the employee's innovativeness. By default, it starts looking from the current location. If the employee is innovative enough, a node is selected beyond the default starting node; * It identifies potential nodes for exploration based on the neighbors of the source node within each layer of knowledge. * If there are potential new nodes that haven't been visited before, it randomly selects a node for exploration. * Check whether the agent has already visited this node and still remembers its fitness value. If not, make a prediction on its fitness value. This prediction is biased by an error (`prediction_error`) depending on personal competencies (`predictionCompetence`). This error can be both positive and negative; * It updates the employee's `memory` and tracks visited and `potential_locations_to_revisit` accordingly. * It decrements the energy available for exploration (`energy_left`). Next, the function prepares a proposal for moving to another node that maximizes the employee's expected fitness: * `proposed_move`: It determines the proposed move (location) by selecting the location with the highest fitness based on the given weights. The `max` function is used to find the location with the maximum fitness, and the key for comparison is defined as the fitness of each location weighted by the `weights` list. * `energy_used`: It calculates the energy used for exploration by subtracting the `energy_left` from the initial available energy for exploration (`availableEnergyForExploration`). * `expected_improvement`: This calculates how much the fitness is expected to improve by making the proposed move by subtracting the fitness of the proposed move from the fitness of the current location. Then, if the ERIThreshold of the employee is not met and the employee is not part of a PCS, the employee initiates an experimentation phase: * By default, it initializes a list `experiment_locations` with the last proposed location. * It starts a loop where experiments are conducted as long as there's energy (experiments_left > 0). * It selects a source node for experimentation. Based on the agent's `innovativeness`, a node beyond the default starting node is selected. * It identifies potential nodes for experimentation based on unknown neighbours from the location the agent currently is exploring. *Note: The agent cannot learn about edges in unknown layers. Layers can only be discovered when learnt from someone else* * It randomly chooses a node from the potential nodes for experimentation. * It performs a fitness estimation for the chosen node and appends it to experiment_locations. Because the employee lacks knowledge about this node, it assumes the fitness is equal (so not worse, nor better) to that of its prior 'best' proposal. It essentially 'bets' on the fitness of this location being better than its best available alternative. * It adds an edge to the knowledge graph representing the connection between the source node and the chosen node. The employee has learned about the node's existence, hence add an edge hereto in its knowledge graph. Finally, several updates are recorded: * The last experiment location becomes the new proposed move. * It calculates the distance between the current location and the proposed move based on knowledge. * It calculates the cost of the move based on the organization's decision cost function. * Various attributes and reports related to the exploration process are updated, including energy usage, the proposed move, improvement, cost, and more. * The proposal is added to the proposal history. * The total energy used for exploration is deducted from the employee's total energy. ```python class Employee(Individual): CONTINUED def exploreLandscape(self, availableEnergyForExploration, availableEnergyForExperimenting): energy_left = availableEnergyForExploration experiments_left = availableEnergyForExperimenting knowledge = self.knowledge innovativeness = self.innovativeness predictionCompetence = self.predictionCompetence if self.model.p.pcs: if len(self.PCSes) > 0: pcs = self.PCSes[0] knowledge = pcs.assembledKnowledge innovativeness = pcs.combinedInnovativeness predictionCompetence = pcs.combinedPredictionCompetence weights = [] for l_idx in range(self.model.landscape.num_layers): if knowledge.isLayerActive[l_idx]: weights.append(self.organisation.beliefSystem.get(l_idx)) else: weights.append(0) locations_visited = [self.currentLocation] potential_locations_to_revisit = [self.currentLocation.get()] while energy_left > 0: if len(potential_locations_to_revisit) == 0: break source_node = potential_locations_to_revisit[0] if self.model.random.random() < innovativeness: source_node = potential_locations_to_revisit[-1] potential_nodes = [] for layer in knowledge.layers: neighbors = layer.neighbors(source_node) for node in neighbors: if node not in potential_nodes: potential_nodes.append(node) if len(potential_nodes) > 0: potential_new_nodes = [node for node in potential_nodes if node not in map(lambda l: l.get(), locations_visited)] if len(potential_new_nodes) > 0: chosen_node = self.model.nprandom.choice(potential_new_nodes) memory_result = self.memory.search(chosen_node) if memory_result == None: fitness_list = self.model.landscape.getFitness(chosen_node) prediction_error_abs = self.model.nprandom.uniform(0, 1 - predictionCompetence) prediction_error = prediction_error_abs if self.model.random.random() < 0.5 else -1 * prediction_error_abs estimated_fitness_list = [f + prediction_error for f in fitness_list] new_location_memory = IndividualLocationPerception(chosen_node, self.model.landscape.num_layers) for l_idx in range(len(knowledge.layers)): if knowledge.isLayerActive[l_idx]: new_location_memory.recordFitnessEstimation(l_idx, estimated_fitness_list[l_idx]) self.memory.remember(new_location_memory) locations_visited.append(new_location_memory) potential_locations_to_revisit.append(new_location_memory.get()) energy_left -= 1 else: locations_visited.append(memory_result) potential_locations_to_revisit.append(memory_result.get()) else: potential_locations_to_revisit.remove(source_node) else: potential_locations_to_revisit.remove(source_node) if energy_left > 0: energy_left -= 1 proposed_move = max(locations_visited, key=lambda l: l.getFitness(weights)) energy_used = availableEnergyForExploration - energy_left expected_improvement = proposed_move.getFitness(weights) - self.currentLocation.getFitness(weights) if len(self.PCSes) == 0: if expected_improvement / self.currentLocation.getFitness(weights) < self.ERIThreshold: experiment_locations = [proposed_move] while experiments_left > 0: source_node = experiment_locations[0].get() if self.model.random.random() < self.innovativeness: source_node = experiment_locations[-1].get() potential_nodes = [] for l_idx, layer in enumerate(self.knowledge.layers): neighbors = self.knowledge.unknownNeighbors(source_node, l_idx) for node in neighbors: if node not in potential_nodes: potential_nodes.append(NodeLayer(node, l_idx)) if len(potential_nodes) > 0: chosen_node = self.model.nprandom.choice(potential_nodes) node_experiment = IndividualLocationPerception(chosen_node.node, self.model.landscape.num_layers) node_experiment.estimatedFitness = proposed_move.estimatedFitness experiment_locations.append(node_experiment) self.knowledge.addEdge((source_node, chosen_node.node,), chosen_node.layer) experiments_left -= 1 proposed_move = experiment_locations[-1] self.report.experimentsConducted = len(experiment_locations[1:]) distance = knowledge.distance(self.currentLocation.get(), proposed_move.get()) cost_of_move = self.organisation.decisionCostFunction(distance) self.report.proposal.isApproved = False self.report.energyUsed = energy_used self.report.proposal.source = self.currentLocation self.report.proposal.target = proposed_move self.report.proposal.improvement = expected_improvement self.report.proposal.cost = cost_of_move self.proposalHistory.append(self.report.proposal) self.totalEnergy -= energy_used ``` The `assessExpectedPerformance(self)` function computes the Expected Relative Improvement (ERI), which represents the expected improvement in the current proposal relative to the average improvement over a specified window of past proposals. * It loops through the proposal history in reverse, starting from the last proposal (-1) and going back up to `self.proposalAveragingWindow+2`. * In the first iteration (i == -1), it calculates the `current_expected_improvement` based on the improvement of the last proposal. * In subsequent iterations (i != -1), it calculates the sum of improvements over the averaging window (excluding the last proposal) to calculate the `average_improvement`. * It computes the final `average_improvement` by adding 1 and dividing by the number of proposals in the averaging window, effectively averaging the improvements. * It calculates `eri` by dividing `current_expected_improvement` by `average_improvement`. ```python class Employee(Individual): CONTINUED def assessExpectedPerformance(self): current_expected_improvement = 0 average_improvement = 0 for i in range(-1, -1*(self.proposalAveragingWindow+2), -1): if i == -1: current_expected_improvement = 1 + self.proposalHistory[i].improvement else: average_improvement += self.proposalHistory[i].improvement average_improvement = 1 + average_improvement / (self.proposalAveragingWindow-1) eri = current_expected_improvement/average_improvement return eri ``` #### PCS participation The function `participateInPCSes(self, PCSToEvaluate)` manages the employee's continued participation in a PCS. It evaluates conditions for potential PCS disbanding, as well as the possibility of inviting new participants to an existing PCS based on the ERI (obtained from the `assessExpectedPerformance` function) and the ERI Threshold (i.e. the minimum ERI for the employee in casu). * It first checks the age of the given PCS (`PCSToEvaluate`) by calling the `checkAge` method on it. * It then checks if the PCS is active (`PCSToEvaluate.alive`). * If the PCS is active and has only one member (`len(PCSToEvaluate.members)` == 1; i.e. the employee is the owner and the sole participant) and the employee has PCS invitations (`len(self.PCSInvitations)` > 0), the PCS is disbanded using `PCSToEvaluate.disband()`. * Otherwise, if the PCS has more than one member or the employee does not have PCS invitations, the function proceeds to evaluate if the employee should continue to participate in the PCS. * If the employee is the owner of the PCS (`PCSToEvaluate.owner` == `self`) and there is sufficient history of proposals (`len(self.proposalHistory)` > `self.proposalAveragingWindow`), it calculates the expected relative improvement using `self.assessExpectedPerformance()`. * If the expected relative improvement is below the ERI threshold (`expected_relative_improvement` < `self.ERIThreshold`), the PCS invites new participants using `PCSToEvaluate.inviteNewParticipants(self.manager)` (see PCS module), passing on the employee's manager as an argument to enable PCS extension beyond its maximum capacity - if needed. Please note that the function can only assess the ERI if there is enough historical data available. ```python class Employee(Individual): CONTINUED def participateInPCSes(self, PCSToEvaluate): PCSToEvaluate.checkAge() if PCSToEvaluate.alive: if len(PCSToEvaluate.members) == 1 and len(self.PCSInvitations) > 0: PCSToEvaluate.disband() else: if PCSToEvaluate.owner == self and len(self.proposalHistory) > self.proposalAveragingWindow: expected_relative_improvement = self.assessExpectedPerformance() if expected_relative_improvement < self.ERIThreshold: PCSToEvaluate.inviteNewParticipants(self.manager) ``` The `actOnPCSInvitations(self)` function allows the employee to act on PCS invitations by joining a randomly selected PCS from the available invitations, becoming a member of that PCS, and adding the PCS to their list of PCSes. * It first updates the employee's PCS invitations by calling the `updatePCSInvitations` method. * It checks if there are any available PCS invitations (`len(self.PCSInvitations)` > 0). * If there are available PCS invitations, it randomly selects one PCS invitation to respond to using `self.model.nprandom.choice(self.PCSInvitations)`. * Then, it adds the employee as a member to the selected PCS using `PCSToJoin.addMember(self)`. * Finally, it appends the selected PCS to the employee's list of PCSes using `self.PCSes.append(PCSToJoin)`. ```python class Employee(Individual): CONTINUED def actOnPCSInvitations(self): self.updatePCSInvitations() if len(self.PCSInvitations) > 0: PCSToJoin = self.model.nprandom.choice(self.PCSInvitations) PCSToJoin.addMember(self) self.PCSes.append(PCSToJoin) ``` The `initiatePCS` function handles the initiation of a PCS for the employee. If the initiation is successful, it adds the PCS to the employee's owned and active PCS lists. If the initiation fails (initial invitation count is 0), it cleans up and adjusts the organization's PCS counter accordingly. * It creates a new instance of the `PCS` class and assigns it to the variable `initiatedPCS`. * It checks if the `initialInvitationCount` of the newly created PCS is equal to 0. * If the `initialInvitationCount` is 0, it deletes the `initiatedPCS` instance and decrements the organization's PCS counter (self.organisation.pcsCounter) since the creation was not successful. * If the initialInvitationCount is not 0, it proceeds to the next step. * It appends the `initiatedPCS` to the employee's list of owned PCSes (`self.ownedPCSes`), indicating that the employee owns this PCS. It also appends the initiatedPCS to the employee's list of active PCSes (`self.PCSes`), indicating that the employee is a member of this PCS. ```python class Employee(Individual): CONTINUED def initiatePCS(self): initiatedPCS = PCS(self) if initiatedPCS.initialInvitationCount == 0: del initiatedPCS self.organisation.pcsCounter -= 1 else: self.ownedPCSes.append(initiatedPCS) self.PCSes.append(initiatedPCS) ``` #### Learning The `developCompetencies(self, availableEnergy)` function is responsible for the employee's development of competencies, specifically innovativeness and prediction competence, based on the available energy and the relative importance the employee's manager has assigned to innovativeness and prediction as competencies for development. * It calculates the amount of energy to allocate for improving innovativeness and prediction competence based on the available energy and personal skill preferences defined in the manager's policy. This policy is part of a DCS. Note: the `math.floor` method rounds a given number down to the nearest integer that is less than or equal to the given number. It essentially truncates the decimal part of the number. * It updates the employee's innovativeness and prediction competence based on the calculated energy for improving each competency and a competency learning step defined in the model parameters [main.py](#main.py). * It ensures that the innovativeness and prediction competence are capped at 1 (maximum value). * It updates the employee's development report with the increase in innovativeness and prediction competence, reflecting the energy allocated for improvement multiplied by the `competencyLearningStep`. ```python class Employee(Individual): CONTINUED def developCompetencies(self, availableEnergy): energy_for_improving_innovativeness = math.floor(availableEnergy * self.manager.dcs.policy.personalSkillPreferences[0]) energy_for_improving_prediction = math.floor(availableEnergy * self.manager.dcs.policy.personalSkillPreferences[1]) self.innovativeness += energy_for_improving_innovativeness * self.model.p.competencyLearningStep self.predictionCompetence += energy_for_improving_prediction * self.model.p.competencyLearningStep if self.innovativeness > 1: self.innovativeness = 1 if self.predictionCompetence > 1: self.predictionCompetence = 1 self.report.innovativenessIncrease = energy_for_improving_innovativeness*self.model.p.competencyLearningStep self.report.predictionCompetenceIncrease = energy_for_improving_prediction*self.model.p.competencyLearningStep ``` The `allocateEnergy(self)` function distributes the total energy available to the employee across different aspects of their work, based on specified policy parameters. * It calculates the energy divider based on the reimbursement window (`self.model.p.reimbursementWindow`) and the current time step (`self.model.t`). The remainder of the division is used as the energy divider, and if the remainder is zero, the reimbursement window itself is used. * It calculates the energy allocated per time step (`step_energy`) by dividing the total energy available (`self.totalEnergy`) by the energy divider. * It calculates and allocates energy for different goals based on the policy parameters defined by the manager as part of a DCS (`self.manager.dcs.policy.goalTermImportances` and `self.manager.dcs.policy.learningModePreferences`). Energy is allocated for exploration, social learning, competency development, and experimenting, based on the DCS-based weightings and preferences. * It returns a tuple containing the allocated energies for exploration, social learning, competency development, and experimenting at the employee level (`self`). ```python class Employee(Individual): CONTINUED def allocateEnergy(self): energy_divider = self.model.p.reimbursementWindow % self.model.t if energy_divider == 0: energy_divider = self.model.p.reimbursementWindow step_energy = self.totalEnergy / energy_divider energy_to_explore = math.ceil(step_energy*self.manager.dcs.policy.goalTermImportances[0]) energy_for_social_learning = math.ceil(step_energy * self.manager.dcs.policy.goalTermImportances[1] * self.manager.dcs.policy.learningModePreferences[0]) energy_for_competency_development = math.ceil(step_energy * self.manager.dcs.policy.goalTermImportances[1] * self.manager.dcs.policy.learningModePreferences[1]) energy_for_experimenting = math.ceil(step_energy * self.manager.dcs.policy.goalTermImportances[1] * self.manager.dcs.policy.learningModePreferences[2]) return (energy_to_explore, energy_for_social_learning, energy_for_competency_development, energy_for_experimenting,) ``` The `receiveProposalApproval(self)` function updates various attributes and records in response to a proposal being approved, including updating the current location, recording realized fitness, marking the proposal as approved, and remembering the current location in the employee's memory. * It calculates weights for each layer in the landscape based on whether the layer is active in the employee's knowledge. If a layer is active, the weight is set to 1; otherwise, it's set to 0. * It updates the employee's current location to the target location of the approved proposal (`self.report.proposal.target`). It appends this location to the employee's path. * It sets the isApproved attribute of the proposal in the report to True. * It retrieves the realized fitness for the current location from the landscape using `self.model.landscape.getFitness`. It records the realized fitness for each active layer in the employee's knowledge and the proposal's target location. * It updates the current location in the employee's memory. ```python class Employee(Individual): CONTINUED def receiveProposalApproval(self): weights = [] for l_idx in range(self.model.landscape.num_layers): if self.knowledge.isLayerActive[l_idx]: weights.append(1) else: weights.append(0) self.currentLocation = self.report.proposal.target self.path.append(self.currentLocation) self.report.proposal.isApproved = True realised_fitness_list = self.model.landscape.getFitness(self.currentLocation.get()) for l_idx in range(len(self.knowledge.layers)): if self.knowledge.isLayerActive[l_idx]: self.currentLocation.recordRealisedFitness(l_idx, realised_fitness_list[l_idx]) self.proposalHistory[-1].target.recordRealisedFitness(l_idx, realised_fitness_list[l_idx]) self.memory.remember(self.currentLocation) ``` The `learningCostFunction(self)` is a method that returns a value representing the learning 'cost', i.e. the amount of energy used for learning, as set as a model parameter in [main.py](#main.py). ```python class Employee(Individual): CONTINUED def learningCostFunction(self): return self.model.p.learningCost ``` The `learn(self, availableEnergy)` function represents the learning process of an employee, where they attempt to learn knowledge from potential 'teachers' based on the available energy and learning cost. * It initializes variables such as `learning_cost` based on the learning cost function, `available_energy` based on the available energy parameter, and `num_edges_learnt` to keep track of the edges learned. * It checks if there is enough energy available for the learning process (i.e. if `available_energy` > `learning_cost`). * It gathers potential 'teachers' from the employee's friends and peers in PCSs for potential knowledge sharing. * It randomly selects potential teachers for knowledge sharing based on the specified learning attempts or the available number of potential teachers. * For each potential teacher selected, it checks if the employee and the potential teacher have overlapping knowledge by checking if they are connected. * If they are connected, it identifies shareable edges of knowledge that can be learned from the potential teacher. * It randomly chooses an edge and layer to learn from the shareable edges and adds the learned edge to the employee's knowledge. * It updates the available energy based on the learning cost and the energy used for learning. * It updates the total energy of the employee. * It returns the number of edges learned (`num_edges_learnt`). ```python class Employee(Individual): CONTINUED def learn(self, availableEnergy): learning_cost = self.learningCostFunction() available_energy = availableEnergy num_edges_learnt = 0 if available_energy > learning_cost: pcs_peers = [] if self.model.p.pcs: if len(self.PCSes) > 0: for PCS in self.PCSes: pcs_peers = pcs_peers + PCS.members pcs_peers.remove(self) friends_and_pcs_peers = [*self.friends, *pcs_peers] potential_teachers_to_select = self.model.p.learningAttempts if len(friends_and_pcs_peers) > 0: if len(friends_and_pcs_peers) < potential_teachers_to_select: potential_teachers_to_select = len(friends_and_pcs_peers) potential_teachers = self.model.random.sample(friends_and_pcs_peers, potential_teachers_to_select) for potential_teacher in potential_teachers: if available_energy > learning_cost: knowledge_list_to_test = [self.knowledge, potential_teacher.knowledge] is_connected = hasOverlap(knowledge_list_to_test) if is_connected: shareable_edges = getShareableKnowledge(self.knowledge, potential_teacher.knowledge, False) if len(shareable_edges) > 0: edge_and_layer_to_learn = self.model.random.choice(shareable_edges) that will be learnt; format is [(node start, node end), layer] edge_to_learn = edge_and_layer_to_learn[0] layer_to_learn = edge_and_layer_to_learn[1] self.knowledge.addEdge(edge_to_learn, layer_to_learn) num_edges_learnt += 1 available_energy -= learning_cost energy_used = availableEnergy - available_energy self.totalEnergy -= energy_used return num_edges_learnt ``` The `updatePCSInvitations(self)` function removes disbanded PCSs from the employee's list of invitations (`self.PCSInvitations`) ```python class Employee(Individual): CONTINUED def updatePCSInvitations(self): # Remove disbanded PCSes from my invitations self.PCSInvitations[:] = [PCS for PCS in self.PCSInvitations if PCS.alive] ``` #### Timestep (simulation execution) Lastly, the `step(self)` function represents a single step in the employee's actions and interactions within the simulation, including socialization, competency development, landscape exploration, participation in PCS, learning, and reporting. Each step involves updating the employee's state and gathering data for reporting purposes. * It clears the employee's report to prepare for new data for this step. * It initiates the socialization process for the employee (`self.socialise`). * It calculates and allocates energy for different activities, including exploring, social learning, and competency development. * It calls the `developCompetencies` function to develop the employee's competencies. * It calls the `exploreLandscape` function to explore the landscape. * If PCS is enabled, it evaluates participation in existing PCSes or initiates a new one based on certain conditions. * It calls the `actOnPCSInvitations` function to handle the employee's response to PCS invitations. * It calls the `learn` function to allow the employee to learn from potential teachers in their network. * It records various pieces of information such as organization ID, current location, and PCS logs for the report. ```python class Employee(Individual): CONTINUED def step(self): self.report.clear() initiatedPCS = None self.socialise() energy_to_explore, energy_for_social_learning, energy_for_competency_development, energy_for_experimenting = self.allocateEnergy() self.developCompetencies(energy_for_competency_development) self.exploreLandscape(energy_to_explore, energy_for_experimenting) if self.model.p.pcs: in_PCS = False self.updatePCSInvitations() if len(self.PCSes) > 0: for PCS_to_evaluate in self.PCSes: self.participateInPCSes(PCS_to_evaluate) if PCS_to_evaluate.alive: in_PCS = True if not in_PCS: if len(self.proposalHistory) > self.proposalAveragingWindow: expected_relative_improvement = self.assessExpectedPerformance() if expected_relative_improvement < self.ERIThreshold: if len(self.PCSInvitations) == 0: self.initiatePCS() self.actOnPCSInvitations() self.report.newEdgesLearnt = self.learn(energy_for_social_learning) self.record('org_id', self.organisation.ceo.id) self.record('current_location', self.currentLocation.location) pcs_logs = [] for pcs in self.ownedPCSes: if len(pcs.members) > 1: pcs_logs.append(pcs.log()) self.record('pcs_logs', pcs_logs) ``` ## organisation.Manager module ```{eval-rst} .. automodule:: organisation.Manager :members: :undoc-members: :show-inheritance: ``` ### Class object definition `ManagerReport` This method simply initializes an empty list of proposals. ```python class ManagerReport: def __init__(self): self.proposals = [] ``` ### Class definition `Manager` Similar to the `Employee` class definition, this section is broken down into the separate functions and methods that define the agent-type `Manager` class. Also similar to `Employee`, this class inherits from the superclass `Individual`. #### Setup functions The `setup` function serves to setting up the Manager's initial location in the landscape and establishing attributes for reporting and a diagnostic control system. * It takes `organisationInitialLocation` as a parameter, which represents the initial location of the organization. * The method first creates a list called `potential_nodes`. * It iterates over path lengths in the model's landscape and checks for nodes that are within a certain distance (which is a parameter set in [main.py](#main.py) from the initial organization location (`organisationInitialLocation`). If a node satisfies the distance condition, it is added to the `potential_nodes` list. * It then selects the manager's initial location randomly from the potential nodes or sets it to the organization's initial location if no potential nodes are found. * The method calls the setup method of the superclass `Individual` and passes the initial location to set up the manager's initial state. * It sets the manager's agent-type to "manager". * It creates a report object with an instance of the `ManagerReport` class. * It creates a DCS object with an instance of the `DiagnosticControlSystem` class. ```python class Manager(Individual): def setup(self, organisationInitialLocation): potential_nodes = [] for path_length in self.model.landscape.path_length: for node in self.model.landscape.layers[0].nodes: if path_length[organisationInitialLocation][node] >= (self.model.p.orgDepInitialDistance - 1) and \ path_length[organisationInitialLocation][node] <= (self.model.p.orgDepInitialDistance + 1): if node not in potential_nodes: potential_nodes.append(node) manager_initial_location = self.model.nprandom.choice(potential_nodes) if len(potential_nodes) > 0 else organisationInitialLocation super().setup(manager_initial_location) self.type = "manager" self.report = ManagerReport() self.dcs = DiagnosticControlSystem() ``` The purpose of `setupBookkeeping` is to create an administrative method within the `Manager` class where the agent's (`self`)place in the organisation is stored. ```python class Manager(Individual): CONTINUED def setupBookkeeping(self, ceo, organisation): self.ceo = ceo self.organisation = organisation self.department = Department(self.ceo, self.organisation, self) self.setupCreateEmployees() ``` The `setupCreateEmployees(self)` method is responsible for setting up and creating a specified number of employees for the manager. * It sets the number of employees (`self.numEmployees`) to the value specified as a parameter in the model's [main module](#main.py) (`self.p.numEmployeesPerDept`). * It creates a list of employees, specifying the agent-class as Employee. Each employee is initialized with the manager's initial location (`managerInitialLocation=self.currentLocation.get()`). * It iterates through each employee and calls the `setupBookkeeping` [function](#setupbookkeeping) on the employee, passing relevant IDs and information (such as CEO, organization, manager, and department IDs) to the employee. * For each employee, it establishes a manager-employee relationship in the social graph by adding an edge with a type of "managerRelation" and a specified color "black". * It adds each employee to the organization's list of employees (`self.organisation.employees`) and the department's list of employees (`self.department.employees`). ```python class Manager(Individual): CONTINUED def setupCreateEmployees(self): self.numEmployees = self.p.numEmployeesPerDept self.employees = ap.AgentList(self.model, self.numEmployees, Employee, managerInitialLocation=self.currentLocation.get()) for employee in self.employees: employee.setupBookkeeping(self.ceo, self.organisation, self, self.department) # Pass own IDs to the employee self.model.socialGraph.add_edge(self, employee, type = "managerRelation", color = 'black') self.organisation.employees.append(employee) self.department.employees.append(employee) ``` #### Employee Compensation The `employeeRewardFunction` function calculates the reward for an employee, taking into account various factors such as energy usage, fitness improvement, competency improvement, learning improvement, and policy parameters. The reward is influenced by the employee's actions and the organization's overall fitness improvement. * It starts by setting the reward to the energy used by the employee, obtained from the employee's report (`employee.report.energyUsed`). * If the employee's proposal was approved and they have a path of at least two locations, it calculates the actual fitness improvement (`actual_improvement`)based on the fitness of the last two locations in the path and the layer-level priorities (weights) in the organisation as set by the belief system. * It calculates the competency improvement and learning improvement based on the policy preferences as set by the manager in the DCS and the employee's reports on innovativeness increase, prediction competence increase, new edges learnt, and experiments conducted. * It calculates a `multiplier` based on goal term importances, actual fitness improvement, and learning improvement. The multiplier is capped at a maximum value defined by the model parameters (self.model.p.maxBonusMultiplier) as set in the [main module](#main.py). * It calculates a bonus as the product of the multiplier and the maximum bonus defined by the model parameters (self.model.p.maxBonus) as set in the model's [main module](@main.py). * It records the organizational fitness improvement, `actual_improvement` by the manager's employees, and the calculated reward in the Diagnostic Control System. * It returns the final reward, which includes the energy used and the bonus based on the calculated multiplier. ```python class Manager(Individual): CONTINUED def employeeRewardFunction(self, employee, organisationImprovement): reward = employee.report.energyUsed weights = self.organisation.beliefSystem.weights actual_improvement = 0 if employee.report.proposal.isApproved and len(employee.path) > 1: actual_improvement = employee.path[-1].getFitness(weights) - employee.path[-2].getFitness(weights) competence_improvement = employee.report.innovativenessIncrease * self.dcs.policy.personalSkillPreferences[0] + \ employee.report.predictionCompetenceIncrease * self.dcs.policy.personalSkillPreferences[1] learning_improvement = (self.dcs.policy.learningModePreferences[0] * employee.report.newEdgesLearnt / employee.report.energyUsed \ if employee.report.energyUsed > 0 else 0) + \ (self.dcs.policy.learningModePreferences[1] * competence_improvement) + (self.dcs.policy.learningModePreferences[2]*employee.report.experimentsConducted / employee.report.energyUsed \ if employee.report.energyUsed > 0 else 0) multiplier = self.dcs.policy.goalTermImportances[0] * actual_improvement + self.dcs.policy.goalTermImportances[1] * learning_improvement if math.isnan(multiplier): multiplier = 0 if multiplier > self.model.p.maxBonusMultiplier: multiplier = self.model.p.maxBonusMultiplier bonus = math.floor(multiplier * self.model.p.maxBonus) reward = reward + bonus self.dcs.recordOrganisationFitnessImprovement(organisationImprovement) self.dcs.recordFitnessImprovement(actual_improvement) self.dcs.recordReward(reward) return reward ``` The `rewardEmployees` function processes each employee, calculates their reward based on their performance and the organization's improvement, and updates their reimbursement accordingly. * It iterates through each employee in the list of employees. * For each employee, it calculates the reward using the `employeeRewardFunction`, passing the employee and the organization's improvement (`organisationImprovement`) as parameters. * It adds the calculated reward to the employee's reimbursement. ```python class Manager(Individual): CONTINUED def rewardEmployees(self, organisationImprovement): for employee in self.employees: reward = self.employeeRewardFunction(employee, organisationImprovement) employee.reimbursement += reward ``` The `reimburseEmployees` function is responsible for actually reimbursing employees by adding their rewards to their total energy. * It iterates through each employee in the list of employees. * For each employee, it adds the employee's reimbursement to their total energy. * After reimbursing the employees, it sets the employee's reimbursement back to 0 to prepare for the next reward calculation cycle. ```python class Manager(Individual): CONTINUED def reimburseEmployees(self): for employee in self.employees: employee.totalEnergy += employee.reimbursement employee.reimbursement = 0 ``` #### Setting Diagnostic Control System (DCS) Policy The `assessDiagnosticControlSystem(self)` function assesses the DCS policy based on reward improvement, departmental fitness improvement, and organizational fitness improvement, and it potentially adjusts the policy to better align with the department's objectives. Finally, it applies the policy change to the DCS. * It calculates the reward improvement, department fitness improvement, and organizational fitness improvement based on the DCS policy and its history. * It creates a `DepartmentPolicyChange` object to track the changes to the DCS policy. * If there's a positive reward improvement and the DCS policy history has more than one entry, it retrieves the last policy change. * If there's no reward improvement or the DCS policy history has only one entry, it generates a new random policy change based on specified gradient steps for various policy parameters. * If the department fitness improvement is less than the organizational fitness improvement, it adjusts the policy change to favor the department's short-term goal priority. * It applies the determined policy change to the DCS by calling `self.dcs.applyPolicyChange(change)`. ```python class Manager(Individual): CONTINUED def assessDiagnosticControlSystem(self): reward_improvement = self.dcs.policy.getAverageReward() - self.dcs.policyHistory[-1].getAverageReward() dep_fitness_improvement = self.dcs.policy.getAverageFitnessImprovement() org_fitness_improvement = self.dcs.policy.getAverageOrganisationFitnessImprovement() change = DepartmentPolicyChange() if reward_improvement > 0 and len(self.dcs.policyHistory) > 1: change = self.dcs.policyChangelog[-1] else: self.dcs.policy = copy.deepcopy(self.dcs.policyHistory[-1]) change = DepartmentPolicyChange() item_to_update = self.model.random.randint(0, 6) if item_to_update == 0: change.goalTermImportances[0] = self.model.p.mcsGradientStep change.goalTermImportances[1] = -1 * self.model.p.mcsGradientStep if item_to_update == 1: change.goalTermImportances[0] = -1 * self.model.p.mcsGradientStep change.goalTermImportances[1] = self.model.p.mcsGradientStep if item_to_update == 2: change.learningModePreferences[0] = self.model.p.mcsGradientStep change.learningModePreferences[1] = (-1/2) * self.model.p.mcsGradientStep change.learningModePreferences[2] = (-1/2) * self.model.p.mcsGradientStep if item_to_update == 3: change.learningModePreferences[0] = (-1/2) * self.model.p.mcsGradientStep change.learningModePreferences[1] = self.model.p.mcsGradientStep change.learningModePreferences[2] = (-1/2) * self.model.p.mcsGradientStep if item_to_update == 4: change.learningModePreferences[0] = (-1/2) * self.model.p.mcsGradientStep change.learningModePreferences[1] = (-1/2) * self.model.p.mcsGradientStep change.learningModePreferences[2] = self.model.p.mcsGradientStep if item_to_update == 5: change.personalSkillPreferences[0] = self.model.p.mcsGradientStep change.personalSkillPreferences[1] = -1 * self.model.p.mcsGradientStep if item_to_update == 6: change.personalSkillPreferences[0] = -1 * self.model.p.mcsGradientStep change.personalSkillPreferences[1] = self.model.p.mcsGradientStep if dep_fitness_improvement < org_fitness_improvement: change.goalTermImportances[0] += self.model.p.mcsGradientStep change.goalTermImportances[1] += -1 * self.model.p.mcsGradientStep self.dcs.applyPolicyChange(change) ``` #### Timestep (simulation execution) Lastly, the `step(self)` function represents a single step in the manager's actions and interactions within the simulation. It orchestrates the actions for the manager in each time step, including recording employee proposals, reimbursing employees, assessing the DCS if enabled, and recording relevant information for reporting. * It iterates through each employee and appends their proposals to the manager's report. * If the current time step is a multiple of the reimbursement window defined in the model parameters, as set in the [main module](#main.py) (`self.model.p.reimbursementWindow`), it calls the `reimburseEmployees` method to reimburse employees. * If the DCS is enabled in the model (`self.model.p.mcs`), and the current time step is a multiple of the DCS evaluation window defined in the model parameters (`self.model.p.mcsEvaluationWindow`), it calls the `assessDiagnosticControlSystem` method to assess and potentially update the DCS policy. * It records various pieces of information related to the organization, DCS policy, average reward, and average fitness improvement for reporting purposes. ```python class Manager(Individual): CONTINUED def step(self): for employee in self.employees: self.report.proposals.append(employee.report.proposal) if self.model.t % self.model.p.reimbursementWindow == 0: self.reimburseEmployees() if self.model.p.mcs: if self.model.t % self.model.p.mcsEvaluationWindow == 0: self.assessDiagnosticControlSystem() self.record('org_id', self.organisation.ceo.id) self.record('dcs_goal_term_importances', self.dcs.policy.goalTermImportances) self.record('dcs_learning_mode_preferences', self.dcs.policy.learningModePreferences) self.record('dcs_personal_skill_preferences', self.dcs.policy.personalSkillPreferences) self.record('dcs_average_reward', self.dcs.policy.getAverageReward()) self.record('dcs_average_fitness_improvement', self.dcs.policy.getAverageFitnessImprovement()) ``` ## organisation.CEO module ```{eval-rst} .. automodule:: organisation.CEO :members: :undoc-members: :show-inheritance: ``` ### Class definition `CEO` #### Setup functions The setup method for the `CEO` class sets up the initial conditions and state for the CEO and their organization in the simulation. This class inherits from the `Individual` class. * It randomly selects an initial location for the CEO from the nodes in the first layer of the landscape. * It calls the setup method of the superclass (`Individual`) and passes the initial location to set up the CEO's initial state. * It sets the CEO's type to "CEO". * It creates an instance of the Organisation class and associates it with the CEO (`self`). * It creates a specified number of managers (`self.numManagers`), specifying the manager class as `Manager`. * For each manager, it calls the `setupBookkeeping` method to set up administrative organisation, passing the CEO and the organization. * It establishes a "CEORelation" edge in the social graph between the CEO and each manager, marked in red. * It adds each manager to the organization's list of managers (`self.organisation.managers`). ```python class CEO(Individual): def setup(self): initialLocation = self.model.nprandom.choice(self.model.landscape.layers[0].nodes) super().setup(initialLocation) self.type = "CEO" self.organisation = Organisation(self.model, self) self.numManagers = self.p.numManagersPerOrg self.managers = ap.AgentList(self.model, self.numManagers, Manager, organisationInitialLocation=initialLocation) for manager in self.managers: manager.setupBookkeeping(self, self.organisation) self.model.socialGraph.add_edge(self, manager, type = "CEORelation", color = 'red') self.organisation.managers.append(manager) ``` #### Assess Management Control Systems (MCSs) The `assessBoundarySystem` function assesses the boundary system of the organization, particularly focusing on the resource margin in comparison to competitors who are performing better. * It checks if there are competitors (i.e. other organisations in the landscape) that are performing better based on some criteria. * If there are competitors performing better, it calculates a weighted margin difference towards the better performing competitors' boundary systems. * For each competitor, it calculates the difference in resource margin with respect to the organization's boundary system and weighs it based on the competitor's position in the list of competitors performing better. * The weighted margin difference is then calculated as the weighted sum of these differences divided by the total weight. * It changes the boundary system towards the resulting gradient, by a pre-determined step defined as a model parameter set in [main.py](#main.py) (`self.model.p.mcsGradientStep`). ```python class CEO(Individual): CONTINUED def assessBoundarySystem(self): if len(self.organisation.competitorsPerformingBetter) > 0: weighted_margin_difference = 0 denominator = 0 for i, competitor in enumerate(self.organisation.competitorsPerformingBetter): weighted_margin_difference += (i+1) * (competitor.boundarySystem.resourceMargin - self.organisation.boundarySystem.resourceMargin) denominator += i+1 weighted_margin_difference = weighted_margin_difference/denominator self.organisation.boundarySystem.resourceMargin += self.model.p.mcsGradientStep * weighted_margin_difference ``` The `assessBeliefSystem` function assesses the belief system of the organization, particularly focusing on the resource margin in comparison to competitors who are performing better. * It checks if there are competitors (i.e. other organisations in the landscape) that are performing better based on some criteria and appends these to a list of `gradients` * For each competitor performing better, it calculates the gradient difference between the competitor's belief system weights and the organization's belief system weights. * For each element in the gradient array, a weighted sum is calculated based on the position of the competitor (indexed by `j`) and the corresponding gradient value at that position (`gradients[j][i]`). * The weighted sum `c` is calculated by summing up `(j+1) * gradients[j][i]` for each competitor. * The `denominator` is updated by adding `j + 1` for each competitor, which will be used later to calculate the average. * The calculated weighted sum `(c)` is appended to the `gradient_weighted_average` list. * After iterating through all gradient elements, the code divides each element in the `gradient_weighted_average` list by the denominator to compute the average. * It calculates a `gradient_weighted_average` for each belief system weight by considering the gradient differences and weighing them based on the position of the competitor in the list of competitors performing better, as such to approach the ideal belief system for the organisation. * It changes the belief system towards the resulting gradient, by a pre-determined step defined as a model parameter set in [main.py](#main.py) (`self.model.p.mcsGradientStep`). ```python class CEO(Individual): CONTINUED def assessBeliefSystem(self): if len(self.organisation.competitorsPerformingBetter) > 0: gradients = [] gradient_weighted_average = [] for competitor in self.organisation.competitorsPerformingBetter: gradients.append(list(map(sub, competitor.beliefSystem.weights, self.organisation.beliefSystem.weights))) denominator = 0 for i in range(len(gradients[0])): c = 0 for j in range(len(self.organisation.competitorsPerformingBetter)): c += (j+1) * gradients[j][i] denominator += j + 1 gradient_weighted_average.append(c) gradient_weighted_average = [x/denominator for x in gradient_weighted_average] for i, w in enumerate(self.organisation.beliefSystem.weights): self.organisation.beliefSystem.weights[i] = w + gradient_weighted_average[i] * self.model.p.mcsGradientStep ``` Finally, the `assessMCS` function involves the execution of the the benchmarking function `requestOrganisationRanking` and the assessment of the Belief (`assessBeliefSystem`) and Boundary System (`assess.BoundarySystem`). ```python class CEO(Individual): CONTINUED def assessMCS(self): self.model.requestOrganisationRanking() self.assessBeliefSystem() self.assessBoundarySystem() ``` #### Proposal evaluation The `assessProposals` function handles the assessment and decision-making process for proposals by the CEO as the ultimate decision-maker in the organisation. * It initializes an empty list `proposals` to store proposals. * It iterates through each manager in the `self.managers`. * For each manager, collect their proposals and append them to the proposals list. * Clear the manager's report of proposals (`manager.report.proposals = []`). * It sorts the proposals based on the expected fitness improvement. * It initializes variables for `decisionCost`, `currentAbsoluteFitness`, and `resourceMargin`. * It obtains the current absolute fitness of the organization (`self.organisation.getAbsoluteFitness()`). * It calculates the resource margin (i.e. the budget) based on the organization's resources and a defined margin. * It then iterates through the sorted proposals: * If the proposal's cost is less than the available resource margin, approve the proposal. * It executes the `employee.receiveProposalApproval()` function. * Update resource-related variables and decision cost accordingly. * Finally, it calculates the new absolute fitness after the proposals are executed by the employees and updates fitness-related data records in the organization's history. * It calculates the improvement in realised (i.e. absolute) fitness and determines the reward based on realised cost and fitness improvement. * It updates the organization's resources based on the calculated reward. * It provides input to each manager for the employee rewards function (`manager.rewardEmployees`) with organisational fitness `improvement` as an input variable. ```python class CEO(Individual): CONTINUED def assessProposals(self): proposals = [] for manager in self.managers: for p in manager.report.proposals: proposals.append(p) manager.report.proposals = [] proposals.sort(key=lambda p: p.improvement, reverse=True) decisionCost = 0 currentAbsoluteFitness = self.organisation.getAbsoluteFitness() resourceMargin = self.organisation.resources * (1 - self.organisation.boundarySystem.resourceMargin) for p in proposals: if resourceMargin > p.cost: p.employee.receiveProposalApproval() self.organisation.resources -= p.cost resourceMargin -= p.cost decisionCost += p.cost newAbsoluteFitness = self.organisation.getAbsoluteFitness() self.organisation.absoluteFitnessHistory.append(newAbsoluteFitness) self.organisation.fitnessHistory.append(self.organisation.getFitness()) improvement = newAbsoluteFitness - currentAbsoluteFitness reward = self.organisation.decisionRewardFunction(decisionCost, improvement) self.organisation.resources += reward for manager in self.managers: manager.rewardEmployees(improvement) ``` #### Timestep (simulation execution) Lastly, the `step(self)` function represents a single step in the CEO's actions and interactions within the simulation. It orchestrates the execution of the proposal evaluation and decision-making by the CEO, the periodic assessment of the MCSs, the recording of various performance statistics and calls the `step` function of the PCS module. * It calls the `assessProposals` method to assess and make decisions on proposals. * It evaluates the MCSs periodically with a frequency that is defined in the [main module](#main.py) * It records various performance-related statistics involving e.g. energy, fitness, competence and resources. * It calls the `pcs.step()` function for each PCS in the organisation ```python class CEO(Individual): CONTINUED def step(self): self.assessProposals() if self.model.p.mcs: if self.model.t % self.model.p.mcsEvaluationWindow == 0: self.assessMCS() energy_list = list(map(lambda e: e.totalEnergy, self.organisation.employees)) innovativeness_list = list(map(lambda e: e.innovativeness, self.organisation.employees)) pc_list = list(map(lambda e: e.predictionCompetence, self.organisation.employees)) self.record('fitness', self.organisation.getFitness()) self.record('abs_fitness', self.organisation.getAbsoluteFitness()) self.record('resources', self.organisation.resources) self.record('avg_energy', sum(energy_list)/len(energy_list)) self.record('avg_innovativeness', sum(innovativeness_list)/len(innovativeness_list)) self.record('avg_prediction_competence', sum(pc_list)/len(pc_list)) self.record('belief_system', copy.deepcopy(self.organisation.beliefSystem).weights) self.record('boundary_system_resource_margin', self.organisation.boundarySystem.resourceMargin) self.record('num_pcs', self.organisation.pcsCounter) for manager in self.managers: for employee in manager.employees: for pcs in employee.ownedPCSes: pcs.step() ``` ## organisation.Department module ```{eval-rst} .. automodule:: organisation.Department :members: :undoc-members: :show-inheritance: ``` The class definition `Department` only involves the initialisation of an instance with 3 arguments: `CEO`, `Organisation`, and `Manager`. It initialises a list of employees as an instance variable and assigns the agent-type "dep" to the class instance. The unique department ID is a combination of the agent-type "dep" and the respective manager ID. ```python class Department: def __init__(self, ceo, organisation, manager): self.ceo = ceo self.organisation = organisation self.manager = manager self.employees = list() self.type = "dep" self.id = self.type + str(self.manager.id) ``` ## organisation.Organisation module ```{eval-rst} .. automodule:: organisation.Organisation :members: :undoc-members: :show-inheritance: ``` ### Class definitions #### Instance initialisation The class definition `Organisation` initialises a class instance based on the CEO's in the model. It takes `model` and `ceo` as an argument to initialise the organisations in the model. * It assigns "org" as the agent-type to this class * It generates an organisational ID which is composed of "org" and the unique CEO ID. * It generates various empty lists to store its managers, departments and employees. * It set the `pcsCounter` to zero * It generates various empty lists to record fitness-based performance data * It assesses how many competitors (i.e. other organisations in the model) it will benchmark against by generating a random integer value between 1 (inclusive) and the maximum number of competitors to consider (self.model.p.maxCompetitorsToConsider, as set in the [main module](#main.py)) using the `randint` function from the `random` module. * It calculates its resource size based on the number of managers per organisation (`numManagersPerOrg`), the number of employees per department (`numEmployeesPerDept`) -both of which are set in the [main module(#main.py)] - and the 'resource margin' (`resourceMargin`) which is a floating point number between 0 and 1, denoting which percentage of the organisational budget may not be spent on decisions [**ADD LINK**]. * It initialises a `BeliefSystem` by taking the number of landscape layers as an argument, and a `BoundarySystem` by taking the maxResourceMargin (which is set in the [main module](#main.py) as an argument. ```python class Organisation: def __init__(self, model, ceo): self.model = model self.ceo = ceo self.type = "org" self.id = self.type + str(self.ceo.id) self.managers = list() self.departments = list() self.employees = list() self.pcsCounter = 0 self.fitnessHistory = [] self.absoluteFitnessHistory = [] self.competitorsToConsider = self.model.random.randint(1, self.model.p.maxCompetitorsToConsider) self.competitorsPerformingBetter = [] self.model.organisations.append(self) self.resources = (self.model.p.numManagersPerOrg + self.model.p.numEmployeesPerDept) * self.model.p.resourceMargin self.beliefSystem = BeliefSystem(self.model.landscape.num_layers) self.boundarySystem = BoundarySystem(self.model.p.maxResourceMargin) ``` #### Cost and benefit functions The cost of executing a proposal to move to a different location in the landscape is equaled to the distance in terms of number of edges. The `decisionRewardFunction` takes `decisionCost` and `fitnessImprovement` as arguments to calculate the net rewards yielded by moving to a location with a higher fitness. The `getFitness` function derives a subjective organisation fitness value based on the weights assigned in the Belief System to each landscape layer. It calculates the fitness for each employee based on their current location's fitness (using weights from the belief system), aggregates these fitness values, calculates the average fitness, and returns the average as the fitness for the organization: ```python class Organisation: CONTINUED def decisionCostFunction(self, distance): return distance def decisionRewardFunction(self, decisionCost, fitnessImprovement): return decisionCost * (1 + fitnessImprovement) ``` #### Organisational fitness functions The `getFitness` function derives a subjective organisation fitness value based on the weights assigned in the Belief System to each landscape layer. It calculates the fitness for each employee based on their current location's fitness (using weights from the belief system), aggregates these fitness values, calculates the average fitness, and returns the average as the fitness for the organization: * It initializes a variable named `fitness` to store the cumulative fitness of the employees. * It iterates through each employee in the list `self.employees`. * For each employee, it calls the `getFitness` method on the employee's current location, passing the `beliefSystem.weights` as an argument. * It adds the fitness value obtained for each employee to the `fitness` variable. * It divides the total fitness by the number of employees (`len(self.employees)`), obtaining the average fitness of the organisation. ```python def getFitness(self): fitness = 0 for employee in self.employees: fitness += employee.currentLocation.getFitness(self.beliefSystem.weights) fitness = fitness / len(self.employees) return fitness ``` Almost similarily, absolute ('objective') organisational fitness is calculated by the `getAbsoluteFitness` function, however with the weights assigned to each layer as set to 1. The `getAverageAbsoluteFitness` function then calculates the average absolute fitness over a certain period. ```python def getAbsoluteFitness(self): weights = [1 for i in range(self.model.landscape.num_layers)] fitness = 0 for employee in self.employees: fitness += employee.currentLocation.getFitness(weights) fitness = fitness/len(self.employees) return fitness def getAverageAbsoluteFitness(self, startPeriod, endPeriod): result = 0 for f in self.absoluteFitnessHistory[startPeriod:endPeriod]: result += f return result/(endPeriod - startPeriod) ``` #### Timestep (simulation execution) The class `Organisation` does not execute any agent-based actions in the simulation, it merely functions to create entities in which the agents operate. The `step` function therefore contains a `pass` statement which allows the code to be valid syntax without executing any specific actions. ```python def step(self): pass ``` ## organisation.PCS module ```{eval-rst} .. automodule:: organisation.PCS :members: :undoc-members: :show-inheritance: ``` ### PCS class definition #### PCS instance initialisation The `__init__` method serves to construct the `PCS` class instances, taking the `owner` as a parameter. In this method, the following attributes are assigned to each class instance: * `self.id`: It constructs a unique ID for the PCS instance based on the owner's ID and a timestamp (`owner.model.t`). The owner is the agent (`employee`) that initialises the creation of the PCS. * `self.participants`: Initializes an empty list to store participants. * `self.owner`: Assigns the owner passed to the constructor. * `self.managerOfOwner`: Assigns the manager of the owner. * `self.lifetime`: Assigns a random lifetime to the PCS instance using the owner's model and predefined maximum lifetime (maxLifetime) as set in [main.py](#main.py). * `self.dateOfBirth`: Assigns the creation timestamp of the PCS. * `self.members`: Initializes a list with the owner as the initial member. * `self.desiredNumMembers`: Sets a desired number of members for the PCS instance, randomly chosen within a minimum of 2 and a maximum (`maxPCSSize`) as set in the [main module](#main.py). * `self.assembledKnowledge`: Initializes `assembledKnowledge` based on the owner's `knowledge`. * `self.combinedInnovativeness`: Initializes `combinedInnovativeness` based on the owner's `innovativeness`. * `self.combinedPredictionCompetence`: Sets `combinedPredictionCompetence` based on the owner's `predictionCompetence`. * `self.managerOverrideUsed`: Initializes a flag indicating whether a manager override has been used (`False`). * `self.proposedMove`: Initializes a null value (`None`) for `proposedMove`. * `self.alive`: Sets a flag to indicate whether the PCS is active (`alive`). * `self.owner.organisation.pcsCounter`: Increments a counter for the number of PCS instances associated with the owner's organization. * `self.initialInvitationCount`: Initializes a count for initial invitations to 0. * `self.inviteNewParticipants(self.owner)`: Invites the owner (initial participant) to the PCS. ```python class PCS: def __init__(self, owner): self.id = '{0}_{1}'.format(owner.id, owner.model.t) self.participants = [] self.owner = owner self.managerOfOwner = self.owner.manager self.lifetime = self.owner.model.random.randint(1, self.owner.model.p.maxLifetime) self.dateOfBirth = owner.model.t self.members = [owner] self.desiredNumMembers = self.owner.model.random.randint(2, self.owner.model.p.maxPCSSize) self.assembledKnowledge = Knowledge(owner.knowledge) self.combinedInnovativeness = owner.innovativeness self.combinedPredictionCompetence = owner.predictionCompetence self.managerOverrideUsed = False self.proposedMove = None self.alive = True self.owner.organisation.pcsCounter += 1 self.initialInvitationCount = 0 self.inviteNewParticipants(self.owner) ``` #### PCS Instance development The `addMember` function adds new members to the list of PCS members. ```python class PCS: CONTINUED def addMember(self, member): self.members.append(member) ``` The `inviteNewParticipants` method manages the invitation of new participants to the PCS, considering various conditions such as capacity, eligibility, and knowledge overlap. It ensures that invitations are sent based on the specified criteria and updates relevant counters. * The method takes `inviter` as an argument, which can be both the initialising employee (`owner`) of the PCS or, under specific conditions, its manager. * The method initializes several state variable to keep track of the number of invitations, the potential size of the PCS, the number of spots available in the PCS, and a boolean indicating whether the capacity of the PCS has been extended by the owner's manager. * It checks if the `inviter` is the manager of the owner and the PCS has reached its desired capacity. If true, it allows for a manager override by reserving one spot for a new member. * It determines `eligible_invitees` based on the type of `inviter` (either the manager or the owner). Eligible invitees are individuals from specific lists (`deptColleaguesList`, `orgColleaguesList`, and `friends`) associated with the inviter. * It checks if the eligible invitees are of type "employee" and adds them to the list of eligible invitees (`eligible_invitees`). * It invites eligible invitees by checking if they are not the `inviter` and not already a `member` of the PCS. * It checks if there is a knowledge overlap between the inviter and the eligible invitee using a function `hasOverlap`. * If there's an overlap, an invitation is sent to the eligible invitee. * It updates counters for potential members and invitation count accordingly. * It handles the case where a manager override was used to ensure only one additional member is invited in this case. * It sets the `initialInvitationCount` attribute of the PCS to the final invitation count. ```python class PCS: CONTINUED def inviteNewParticipants(self, inviter): invitation_count = 0 num_potential_members = len(self.members) num_spots_for_new_members = self.desiredNumMembers - num_potential_members manager_override_capacity = False if inviter == self.managerOfOwner and num_spots_for_new_members <= 0 and not self.managerOverrideUsed: manager_override_capacity = True self.managerOverrideUsed = True num_spots_for_new_members = 1 if num_potential_members < self.desiredNumMembers or manager_override_capacity: eligible_invitees = [] eligible_invitees_all_types = [] if inviter == self.managerOfOwner: eligible_invitees_all_types = [*self.managerOfOwner.deptColleaguesList, *self.managerOfOwner.orgColleaguesList, *self.managerOfOwner.friends] for eligible_invitee in eligible_invitees_all_types: if eligible_invitee.type == 'employee': eligible_invitees.append(eligible_invitee) elif inviter == self.owner: eligible_invitees_all_types = [*self.owner.deptColleaguesList, *self.owner.orgColleaguesList, *self.owner.friends] for eligible_invitee in eligible_invitees_all_types: if eligible_invitee.type == 'employee': eligible_invitees.append(eligible_invitee) if len(eligible_invitees) > 0: selected_invitees = eligible_invitees for eligible_invitee in selected_invitees: if eligible_invitee != inviter and eligible_invitee not in self.members: knowledge_list_to_test = [self.owner.knowledge, eligible_invitee.knowledge] is_connected = hasOverlap(knowledge_list_to_test) if is_connected: eligible_invitee.PCSInvitations.append( self) num_potential_members += 1 invitation_count += 1 if manager_override_capacity: manager_override_capacity = False break if invitation_count >= num_spots_for_new_members: break self.initialInvitationCount = invitation_count ``` The `disband` method executes the dissolvement of a PCS by removing references to the PCS from the member and owner's lists, and marking the PCS as not alive. Additionally, it updates the PCS count associated with the owner's organization. * It uses a for loop to iterate over each member in the `self.members` list, which contains all the members of the PCS. * For each member, it removes the PCS from their `PCSes` list. * If the owner of the PCS is equal to the current member being processed, it also removes the PCS from the member's `ownedPCSes` list. * It sets the `alive` attribute of the PCS instance to `False`. This action flags that the PCS is now considered inactive. * It decrements the `pcsCounter` attribute of the owner's organization. ```python class PCS: CONTINUED def disband(self): for member in self.members: member.PCSes.remove(self) if self.owner == member: member.ownedPCSes.remove(self) self.alive = False self.owner.organisation.pcsCounter -= 1 ``` The `log` function provides a simple way to retrieve and log specific information about the PCS, including the number of members and the PCS's lifetime. ```python class PCS: CONTINUED def log(self): return [len(self.members), self.lifetime] ``` #### Timestep (simulation execution) The `step` method function represents a single step within the PCS's actions and interactions within the simulation. It orchestrates the collective assemblance of knowledge and compentency development with regards to innovativeness and prediction. * It calculates the `assembledKnowledge` attribute of the PCS. This attribute is updated by taking the `union` of the knowledge of all members in the `self.members` list. It uses the `union` function to perform this operation, which combines the knowledge from all members into a single knowledge structure associated with the PCS (`self.assembledKnowledge`). * It calculates the `combinedInnovativeness` attribute by: * Creating a list called `innovativeness_list` containing the innovativeness values of all members. * Calculating the sum of the innovativeness values in this list. * Dividing the sum by the number of members (`len(innovativeness_list)`) to compute the average innovativeness among the members. * The result is then assigned to the `self.combinedInnovativeness` attribute of the PCS. * Similarly to the `combinedInnovativeness` calculation, it calculates the `combinedPredictionCompetence` attribute. ```python class PCS: CONTINUED def step(self): self.assembledKnowledge = union([m.knowledge for m in self.members]) # innovativeness_list = list(map(lambda m: m.innovativeness, self.members)) self.combinedInnovativeness = sum(innovativeness_list)/len(innovativeness_list) pc_list = list(map(lambda m: m.predictionCompetence, self.members)) self.combinedPredictionCompetence = sum(pc_list)/len(pc_list) ``` Finally, the `checkAge` function verifies how long the PCS has been operating and whether it exceeds the maximum lifetime of a PCS (as parametrised in the [main module](#main.py)). If it exceeds this maximum, it is disbanded. ```python class PCS: CONTINUED def checkAge(self): if self.owner.model.t - self.dateOfBirth >= self.lifetime: self.disband() ``` ## Module contents ```{eval-rst} .. automodule:: organisation :members: :undoc-members: :show-inheritance: ```