Fasow is a project that contains two subprojects: fasow-api
and fasow-monorepo
. This repository includes both the new version of the Fasow library, exposed as an HTTP API, and the legacy library along with its client for interacting with experiments.
The Fasow project is divided into three main parts:
-
fasow-api: This folder contains the new version of the Fasow library, which has been refactored and exposed as an HTTP API. This allows the experiments and functionalities of the library to be accessed through HTTP requests.
-
fasow-monorepo: This subproject includes the legacy version of the Fasow library along with a client that provides the necessary interface to interact with the experiments. It is useful for maintaining compatibility with previous versions and for users who still rely on the legacy implementation. The
fasow-monorepo
was developed with Node.js 16 and has been run with Node.js 22, but we are not sure of its correct execution. -
fasow-ui: This folder contains the new version of the Fasow UI that works as console to select, initialize and run models implemented on Fasow.
Fasow/
│
├── fasow-api/ # New version of the Fasow library exposed as an HTTP API
│ ├── .eslintrc.js # ESLint configuration
│ ├── .prettierrc # Prettier configuration
│ ├── fasowLoader.ts # Main loader script that allows users to register new clases for Fasow
│ ├── nest-cli.json # NestJS CLI configuration
│ ├── package.json # Project dependencies and scripts
│ ├── README.md # Documentation specific to fasow-api
│ ├── tsconfig.build.json # TypeScript configuration for build
│ ├── tsconfig.json # General TypeScript configuration
│ └── src/ # Source code for the API
│
└── fasow-monorepo/ # Legacy library and client for experiments
├── packages/ # Packages and modules of the legacy library
├── .gitignore # Git ignore file for unnecessary files
├── FASOWState.json # State file for the legacy library
├── lerna.json # Lerna configuration for the monorepo
├── LICENSE # Project license
├── package.json # Dependencies and scripts for the monorepo
├── README.md # Documentation specific to fasow-monorepo
└── yarn.lock # Dependency lock file for Yarn
To clone the project and access its subprojects, follow these steps:
git clone https://github.com/pragmaticslaboratory/fasow.git
cd modules
cd modules-api
npm install
npm run start:dev
cd modules-monorepo
yarn install
yarn start
cd modules-ui
npm install
npm run dev
The Fasow architecture is based on the idea of the reflection tower, and is composed by
3 principal modules, the Experiment
, the TowerHandler
and the DataHandler
. Thus, Fasow provides us
a way to implement and create simulations of an Agent Based model of a Word of Mouth campaign on a Social
Network Site (SNS), managing a flexible architecture easy to learn (easy to reach more users?) and an output generator.
Fasow Architecture
This experiment represent a selected Agent-Based model on Fasow where we can change them, by selecting other one, initialize them to prepare Fasow to start a simulation by instantiating the required entities on the model, running it by their execution and finally get the output of each iteration and repetition time of the simulation to get the result of the simulation.
// This both modules interact laterally with the Experiment by interacting with the selected experiment on execution time, the datahandler is notified by each time on the simulation of the selected model by the use of the observer pattern, and the TowerHandler using the idea of the reflection tower allows to instantiate the required extensions of the selected model.
The DataHandler modules uses the observer pattern that where agents, environments, and experiments notifies and updates the datahandler each time where a tick or a repetition happen. So the DataHandler, that had a list of the references of all the instances of the classes that had decorated his attributs with a DataHandler decorator like:
//Todo: Agregar ejemplos de los diferentes decoradores de atributos que existen disponibles en modules.
/**
* For each tick of the clock, it counts all the agents that have the decorated property,
* to later display it in the output on the column with name @name. Users could count
* false or true values according to the value of countFalse.
*
* @param name : string : The column name of the property being registered
* @param countFalse : boolean : specify if the count was being to true or false values.
* If countFalse = true, agents with a false value will be counted
* If countFalse = false, agents with a true value will be counted
*/
export function AgentCountBoolean(name: string, countFalse: boolean);
/**
* For each period, it counts all the agents whose agent.state is equal to the
* value entered and then displays a column with the name @name in the output.
*
* @param name : string : The column name of the property being registered
* @param value : number : The value that is registered as possible agent status and that will be used
* to count the agents that have this value as status.
*/
export function AgentStateIntegerCount(name: string, value: number);
/**
* For each period, add the values of each agent that have this property, to then display a column named @name in the output.
* @param name : string : The column name of the property being registered.
*/
export function AccumulateAgentValue(name: string);
/**
* For each period, it adds the old values with the current value of the property for the corresponding period, to then display a column named @name in the output.
* @param name : string : The column name of the property being registered.
*/
export function AccumulateEnvironmentValue(name: string);
/**
* For each period it records the marked parameter, and then it is recorded in the output in a column named @name
* @param name : string : The column name of the property being registered.
*/
export function EnvironmentCount(name: string);
/**
* For each repetition it records the marked parameter, and then it is recorded in the output in a column named @name
* @param name : string : The column name of the property being registered.
*/
export function ExperimentCount(name: string);
The TowerHandler is a module that allows us to encapsulate and expose the implementation of certain ABMs concerns of Fasow, where each concern is managed with a specific abstraction level, that we will name as a Fasow level, where each one of thems will be implemented by the TowerHandler. In this way, we can handle and manage how each concern will be instantiated on execution time.
A level in Fasow is an abstraction that handles a specific concern of the ABMs and is composed principally by three modules and extensions modules.
Fasow Level X
-
MetaAPI
: A Metaprogramming API that exposes the implementation of the Level interface. The MetaLevel API provides methods to managed, define or interact with the instantiation of the particularities of the level on execution time, and provides the capability to register new extensions for the level. -
MetaLevel
: Is a Configuration object which communicate and connect the MetaLevelAPI with the Level Interface. This objects had certain information that is required to pass through the Level constructor when we will instantiate them on execution time. -
LevelCore Module
: The core module of the level, that can be abstract or not, but that defines the base functionality for the level, this interface is the entity that the MetaLevel Interface will instantiate on execution time. -
Level Extensions
Level Modules: These modules are entities that extends the functionality that provides the level interface, and allows to users to implements other requirements that cant be provided by the base level interface.
We created fourth fasow levels where each level had less particularity knowledge of the abm concern (or model?),
these levels were injecting on the TowerHandler, to facade theirs implementations and centralize the concerns in one class
by this wey we can start to see the Reflective Tower
.
The Reflective Tower
Is based on the idea of the reflection tower that is present in programming languages and allow us to segment a
software architecture by abstraction levels of different granularity. On this case, the Fasow architecture
is segmented by 4 levels (Calibration, Environment, Agent and Actions), where each one handles a specific concern of
the Agent Based Models.
Reflective Tower
The Calibration level manage the experiments
on Fasow, that represents a model to study, implement and simulate. The level
is composed by the ExperimentAPI
, the MetaExperimentConfig
and the Abstract Experiment
class with his extended particularities modules.
The Experiments
allow us to introduce the input of the model and define a strategy to follow during the simulation, but,
before, we need to register all the modules that will require on the simulation by the use of the TowerHandle.
abstract class Experiment {
name: string;
description: string;
repetition: number;
maxRepetitions: number;
strategy(): void;
run();
setConfig(config: MetaExperimentConfig): void;
loadConfig(): void;
//..getters and setters
}
The CalibrationAPI
allow us to register, manage and instantiate Experiments
, also allow us to set and change
the configuration on the MetaExperimentConfig, which represents part of the information required to instantiate and
initialize the model to run the simulation.
interface CalibrationAPI {
selectExperimentByName(experiment: string): void;
registerNewExperiment(exp: typeof Experiment): void;
setExperimentName(name: string): void;
setExperimentDescription(description: string): void;
setExperimentMaxRepetitions(maxRepetitions: number): void;
getExperimentConfig(): MetaExperimentConfig;
createSelectedExperiment(): Experiment;
selectExperiment(selected: typeof Experiment): void;
getSelectedExperiment(): typeof Experiment;
getState(): any;
selectExperimentByName(experiment: string): void;
}
The MetaCalibration
help us to define a name, a description and what to instantiate for the experiment
to simulate, and also, we can define a number of times which the simulation will be repeated to handles the
stochastic effect.
export default interface MetaCalibration {
// Calibration Metadata
readonly id: number;
name: string;
description: string;
type: typeof Experiment;
maxRepetitions: number;
// Scenario Metadata
environmentConfig: MetaEnvironmentConfig;
}
The environment level manages the Environments
which are the abstraction of a Social Network Site (SNS) and the simulation.
An environment allow us to define and configure a simulation on Fasow. This level is composed by the EnvironmentAPI
, the MetaEnvironment
and the Abstract Environment
.
The Environments
being the abstraction of the simulation and a Social Network Site, enable us to set the size of the
simulation, the types of Agents to create and his relationships, and the duration of the simulation. Also, provides
a place to define the behavior of a social network site, his users, his followers and his followings. On the line of
the WOM communication process on FASOW, we can have two types of agents, the seeds and the non-seeds, where a seed agent is
an agent that start the WOM communication process.
export default abstract class Environment implements EnvironmentConfig, IEnvironmentCreator, Ticks {
id: number;
initialized: boolean;
seedSize: number;
networkSize: number;
seeds: Agent[];
agents: Agent[];
maxTick: number;
tick: number;
setConfig(config: MetaEnvironmentConfig): Environment;
public abstract step(): void;
public run(): void;
initialize(): void;
createAgents(): void;
addFollowers(): void;
addFollowings(): void;
isDone(): boolean;
resetAgentStates(): void;
resetSeedStates(): void;
abstract createEnvironment(environmentConfig: MetaEnvironmentConfig): Environment;
//...getters and setters
}
The MetaEnvironment
establish the configuration of the simulation like the size of agents to create, the duration
of the simulation, the SNS to use and the configuration of the agents to instantiate.
export default interface MetaEnvironmentConfig {
networkSize: number;
maxTick: number;
environmentType: typeof Environment;
metaAgentsConfigs: MetaAgentConfig[];
}
The EnvironmentAPI
manages the registration of the new Environments extensions and the configuration of the
Environment that will be instantiated on the execution time.
export default interface EnvironmentAPI {
//...
registerNewEnvironment(newEnvironmentType: typeof Environment)
private getEnvironment(environmentType: typeof Environment): typeof Environment //Why is private ?
generateEnvironment(config: MetaEnvironmentConfig): Environment
setNetworkToScenario(environment: typeof Environment)
addAgentToScenario(agentConfig: MetaAgentConfig)
setNetworkSizeToScenario(size: number)
setPeriodsToScenario(max: number)
setScenarioConfig(scenarioConfig: MetaEnvironmentConfig)
getScenarioConfig(): MetaEnvironmentConfig
resetScenarioConfig(): MetaEnvironmentConfig
getState(): any
}
//Todo: Describe the level as a general way, then describe the Abstract Class, The MetaConfig, and the MetaAPI
The Agent level manage the Agents
which are the abstraction of the users of a SNS, and allow us to create new
types of agents and defining specific behaviors that they can do on the simulation. Agents are connected with
other Agents that they can follow, connect or subscribe to catch up some information that the other Agents
shares with his connections. By this way the Agent level is composed by AgentAPI
, the MetaAgent
and the abstract Agent
The AgentAPI
manages the registration of the new Agent extensions, and the correct instantiation of the MetaAgent
export default class AgentAPI {
private agentsFactories: Map<string, typeof Agent>;
private agentConfigs: MetaAgentConfig[];
registerNewAgent(type: typeof Agent);
registerNewMetaAgentConfig(agentConfig: MetaAgentConfig);
registerMetaConfigs(agentConfigs: MetaAgentConfig[]);
generateAgentList(): Agent[];
generateAgentsByConfigs(metaConfigs: MetaAgentConfig[]): Agent[];
getMetaAgentConfigById(id: number);
getState(): any;
}
MetaAgent
had the information required to instantiate an Agent on execution time, so had their
name, the percentage of type of Agents to instantiate that compose the SNS, their initial state, their behavior
and if these instances will seed or not.
export default interface MetaAgent {
// Metadata
readonly id: number;
name: string;
percentage: number;
followersPercentage: number;
actionsConfigs: MetaAction[];
type: typeof Agent;
// Normal data to create a agentConfig ?
state?: AgentState;
isSeed: boolean;
}
The abstract Agent
is the abstraction of a user of a SNS, can have followers and followings, creating connections
with other Agents, and making a subscription with these agents, being available to send and receive the messages that they publish on the Network.
Thus, way agents have states that are determined by some event, behavior o Action related with the Word of Mouth (WOM) communication process
where they can READ a message or SHARE a message, between others agents ... (Read more info in WOM Communication Process in Fasow
)
export default abstract class Agent implements AgentConfig, IAgentCreator, Observer, Subject {
id: number;
state?: AgentState | undefined;
isSeed: boolean;
actions: Action[];
followers: Agent[];
followings: Agent[];
indexMetaAgentConfig: number;
abstract step(): void;
addFollower(agent: Agent)
addFollowing(agent: Agent)
removeFollower(agentId: number)
removeFollowing(agentId: number)
receiveMessage(): void
resetState(): void
abstract createAgent(id: number, agentData: MetaAgentConfig): Agent;
setConfig(id: number, config: MetaAgentConfig): Agent
share(): void
abstract update(message: any): any;
The Action level manage the Actions
, that are behaviors that the Agents do or not.
Actions are fundamentals on the WOM Communication Process on Fasow, because they give us the capability to define behaviors,
that can modify the state of the agents. This level is composed by the ActionAPI
, the MetaAction
and the abstract Action
The ActionAPI
manage the registration of the new Action extensions and the instantiation of the new Actions,
and provides methods to manages the MetaActions.
export default class ActionAPI {
// private actionFactories: Map<typeof Action, IActionCreator>;
private actionConfigs: MetaActionConfig[];
// private actionFactories : typeof Action[];
private actionFactories: Map<string, typeof Action>;
registerNewAction(newActionType: typeof Action)
registerMetaActionConfig(actionConfig: MetaActionConfig)
private getAction(type: typeof Action): typeof Action
generateActionList(): Action[]
generateActions(metaConfigs: MetaActionConfig[]): Action[]
getMetaConfigs(): MetaActionConfig[]
getState(): any
the MetaAction
allows to assign a probability to happen for an Action to instantiate and to identify them.
export default interface MetaActionConfig {
readonly id: number;
name: string;
probability: number;
type: typeof Action;
}
The abstract Action is very simply and uses the inversion of control with the method execute, thus way Actions receive the instance of the Agent that execute the action and can interact with the Agent. Also, the Action is a representation of the command design pattern where the Agents are the Invokers and the Actions the function to execute!
export default abstract class Action implements ActionConfig, IActionCreator {
name: string;
probability: number;
idMetaActionConfig: number;
abstract execute(agent: Agent): void;
public getRandom(): number;
public setConfig(actionConfig: MetaActionConfig): Action;
abstract createAction(actionData: MetaActionConfig): Action;
}
The WOM communication process in Fasow depends on the interacting of the different components of fasow and by two principally types of iterations, the repetitions and the ticks. Where each one manages a different concept of the time.
-
Repetitions: Allows us to avoid the stochastic effect by repeating a simulation over and over again to generate different and random outputs to then weighting and standardize them.
-
Ticks: Represent a discrete unit of time like one day, one hour, one second, one week, but usually is one day.
Word of mouth communication process on Fasow
With the use of the interaction of the different extensions of fasow, we can start to draw a basic flow for their WOM communication process, because we define a path to get a message, thinking about them, make the decision to share, and share them with his subscribers. As this way we created a method to handle what an agent can do before, during and after they communicate information.
However, we also need to know and understand how this process beginnings, so is:
- Select an experiment and runit
- Initialize the repetitions from zer (0)
- Initialize the Calibration of the model by execute his strategy and start to instantiate the Environment, Agent and Action extensions.
- Start to iterate through the repetition of the model to avoid the stochastic effect.
- Start validation of a correct initialization of the simulation
- Run a simulation to iterate through the ticks of the simulation to understand the past of the time
- Executes and applies the social network site logic and the seeds agents sends the first message.
- Executes and applies the user logic to prepare them to receive information.
- Executes the Actions of the Agents by handling the reception of a message.
- Agents takes the decision to read the message.
- Agents takes the decision to share the message
- DataHandler is notified of the end of the tick and next tick is assigned
- DataHandler query decorated variables and generate an iteration output row
- DataHandler is notified of the end of the repetition, a new iteration output row is created and next repetition is assigned
- DataHandler generates the output
Experiment.Run()
initializeRepetitions()
Calibration.initialize():
Iteration type, Repetition:
Simulation.isDone()?
if !done:
end repetitions;
simulation.run():
nextRepetition(): // repetitions+=1, and notifies data handler module to start querying the decorated variables.
validateCanNextRepetition():
initialize the Calibration model:
Calibration.initialize():
Simulation.isDone():
is done is:
the agents instances maintain the networkSize;
the instances of seed maintain the seedSize;
for each agent validate:
followers or followings was mutated? from the percentage of the networkSize;
Simulation.run():
this.environment.run():
Environment.canNextTick(): //Tick iterations
Environment.Step(): //And print step data
Execute Environment extension.Step():
Environment.agents.forEach(agent => agent.step())
Agent.Step()
Execute Agent extension.Step():
Agent.forEach(action => action.execute())
EnvironmentExtension.Step():
A way to define the sharing information process of the social network site, the
simples way to define this process is do:
step(): void {
this.agents.forEach((agent) => {
agent.step();
});
}
Agent.Step():
this.agent.Actions.forEach((action) => {
action.execute();
});
We have two principal actions defined that are part of the WOM communication process of Fasow. The first is The READ action and the SHARE action,
that are logically the same, events that depends on the states of the agents, and a probability to happen or not. Thus, way we can define
different types of actions to create behaviors that adapts to our needs, and modify our Agents
and the Environment
.
-
ActionRead
: Check the state of the agent that receives a message, and handles the switch of the state toAgenState.READ
if the state of the agent isAgenState.NOT_READ
-
ActionShare
: Check the state of the agent that receives the message and if he had his state asAgentState.READ
, indicates that the agent already has executed theActionRead
, and now we can handle how to send a message
/** Model/Action/WOM/ActionRead **/
export default class ActionRead extends Action {
execute(agent: Agent): void {
const aux: TwitterAgent = <TwitterAgent>agent;
if (aux.state === AgentState.NOT_READ) {
const p1: number = this.getRandom();
if (p1 > 100 - this.probability) {
aux.state = AgentState.READ;
}
}
}
}
/** Model/Action/WOM/ActionShare **/
export default class ActionShare extends Action {
execute(agent: Agent): void {
const aux: TwitterAgent = <TwitterAgent>agent;
if (aux.state === AgentState.READ) {
const p1: number = this.getRandom();
if (p1 > 100 - this.probability) {
aux.state = AgentState.READY_TO_SHARE;
}
}
}
}
- NOT_READ: Indicates that the agent no have read some message and are be able to receive a message and read one.
- READ: Indicates that the agent already have read some message and that's it and now are be able to decide if SHARE or not with other the message.
- READY_TO_SHARE: Indicates that the Agent take the decision to share the message with their followers.
- SHARED: Indicates that the agent already shared a message with their followers.
/**
* Enumeration of the most simply states of an Agent in a WOM communication process.
* NOT_READ = 0,
* READ = 1,
* READY_TO_SHARE = 2,
* SHARED = 3
*/
export enum AgentState {
NOT_READ,
READ,
READY_TO_SHARE,
SHARED,
}
With the use of the Reflection Tower and with the Four Levels of abstraction that provides Fasow we can extend the functionality of Fasow by the creation of:
- New Experiments: To Implement a new Model to simulate on Fasow!.
- New Environments: To Adding new Social Network Sites (like the Reddit Social Network) or a specific Agent management rule.
- New Agents: To Adding new behaviors, logic or states that could have an Agent.
- New Actions: To Adding new ways to send or receive a message or change the state of the agent in some circumstance.
Whichever will be the approach to follow, always we will have to Register this new Behavior on Fasow with the use of the TowerHandler.
//..experiments/ExampleExperiment.ts
class ExampleExperiment extends Experiment {
// ... other logic
Strategy(): void {
Fasow.TowerHandler.registerNewAgent(TwitterAgent); //Registering a new Agent on Fasow
Fasow.TowerHandler.registerNewAction(ActionRead); //Registering a new Action on Fasow
Fasow.TowerHandler.registerNewAction(ActionShare); //Registering a new Action on Fasow
Fasow.TowerHandler.registerNewEnvironment(EnvironmentTwitter); // Registering a new Action on Fasow
}
}
However, the Experiments
must be Registered on Fasow, by importing them manually and adding to the fasowLoader.ts
file
as the following way:
//..fasowLoader.ts
import ExperimentAgentCombination from 'src/experiments/ExperimentAgentCombinatio/ExperimentAgentCombination';
import ExperimentAgentCombinationBestSeed from 'src/experiments/ExperimentAgentCombinatio/ExperimentAgentCombinationBestSeed';
import ExampleExperiment from './experiments/ExampleExperiment';
import TestExperiment from './experiments/TestExperiment/TestExperiment';
const fasowConfig = [
ExperimentAgentCombination,
ExperimentAgentCombinationBestSeed,
ExampleExperiment,
/** Add your Experiments below to register them on Fasow**/
TestExperiment, //Here you are registering your Experiments
];
export default fasowConfig;
By extending the funtionality of the action layers we can add new behaviors to handle how to send, receive the message or change some state in the Agents by the execution of some rules.
To do this we need to create a new Action
that extends the Abstract Action
like this:
class TestAction extends Action {
createAction(actionData: MetaActionConfig): Action {
return new TestAction().setConfig(actionData);
}
execute(agent: Agent): void {
agent.receiveMessage();
console.log('TestAction specialized Behavior');
}
}
Then we need to register this new action with the TowerHandler to allow to Fasow can use them.
// TODO: Imports must be fixed because Fasow not exists in that path xd
import Fasow from "./Fasow";
Also, to maintain the Fasow logic this must be done in definition of the Strategies on the Experiments.
import Fasow from "./Fasow";
import Experiment from "./Experiment";
class ExampleExperiment extends Experiment {
// ... other logic
Strategy(): void {
Fasow.TowerHandler.registerNewAction(TestAction); // Register the new Action on the Experiment Strategy
}
}
This project is licensed under the MIT License. See the LICENSE file for more details.