Published
Edited
Jun 19, 2022
Importers
Insert cell
Insert cell
md ` # Examples`
Insert cell
md `Examples of using the Experiment class to run system dynamic models can be found in the [TokenCAD Examples](https://observablehq.com/@tannr/tokencad-examples) notebook

Below are a collection of examples for using the Component-based approach to model building


`
Insert cell
md`
## Example 1: System Dynamics of food-population ecosystem
Below is an example of constructing a simple food-population model using the system composition approach with the Component class

Note that the foodPopulationDynamics system defined with a higher order component updates the component states sequentially. Therefore, the dynamics produced will be similar to a food population model with Exmperiment wherein each value is updated in a different partial state update block. This also means that the dynamics are dependent on the order of the array of components ([food, population]) passed to the foodPopulationDynamics component.

`
Insert cell
{
// In the component-based approach, system parameters are passed as 'state' objects
// to the top-level component that represents the whole system in the systemDynamic model

// Example question one could ask from model: "How long does it take for food supply to begin declining?"
// Run the following model and look at the return output above this cell to find out.
// If the model runs for long enough time, does food supply ever get higher again? When?
// What does this say about the population size and the carrying capacity of the ecosystem being modeled here?
let sysParams = {
reproduction_rate: {state: 0.02}, // Beta
consumption_rate: {state: 0.01}, // Alpha
food_growth_rate: {state: 0.01}
}


// 1. Define state update functions
function foodStep(context) {
let {t, s, i, h} = context
let inObj = {}
Object.keys(i).forEach((iKey) => {
inObj[iKey] = i[iKey].state
})
let stateObj = {}
stateObj[s.id] = s.state

let mathConext = {...inObj, ...stateObj}
return math.evaluate('food + (food * food_growth_rate) - (consumption_rate * population)', mathConext)
}
function populationStep(context) {
let {t, s, i, h} = context
let inObj = {}
Object.keys(i).forEach((iKey) => {
inObj[iKey] = i[iKey].state
})
let stateObj = {}
stateObj[s.id] = s.state
let mathConext = {...inObj, ...stateObj}
return math.evaluate('population + reproduction_rate * food', mathConext)

}
function ecoStep(context) {
let {t, s, i, h} = context
// Could also pass an object to subSystem.next here.
// in step 3, we set up relationships between food & population subsystems
// If we wanted to model a policy, that e.g., decreased the effects of the food on the
// population size, we could grab the food's state, decrease that value by some factor, and pass
// {food: decreased_value} to subsytem.next({food: decreased_value}) when the subsytem.id is
// 'population' (since that is the id of the population component initialized in step 2)
let ecosystemState = s.state.map((subSystem) => {
subSystem.next(i)
return subSystem
})
return ecosystemState
}

// 2. Initialize sub components / sub systems (in this case, food and population would be the stocks in a stock-flow diagram
let food = new Component('food', foodStep, 1000, {}, 100)
let population = new Component('population', populationStep, 50, {}, 100)
// 3. Set up relationships between components (the flows in a stock-flow diagram)
food.setInput(population)
population.setInput(food)
// 4. Setup the higher order component (the overall system in this case)
let foodPopulationDynamic = new Component('ecosystem', ecoStep, [food, population],{}, 100)

// 5. Run a simulation

let states = {

}
for (let i = 0; i < 50; i++) {
let state = foodPopulationDynamic.getState()
state.state.forEach((subsys) => {
let subState = subsys.getState()
if (!states[subsys.id]) {
states[subsys.id] = []
}
states[subsys.id].push(subState.state)
foodPopulationDynamic.next(sysParams)
})
}
return states
}
Insert cell
class Experiment {
constructor(timesteps, initialState, updateFunctions, sysParams, updateBlocks) {
this.timesteps = timesteps;
this.initialState = initialState;
this.currState = {...initialState}
this.updateFunctions = updateFunctions
this.runs = 1
this.states = {}
this.statesPerRun = []
this.pastSteps = []
this.currStep = 0;
this.sysParams = sysParams
if (updateBlocks) {
this.updateBlocks = updateBlocks
} else {
this.updateBlocks = [[]]
Object.keys(initialState).forEach((n) => {
this.updateBlocks[0].push(n)
})
}
Object.keys(this.updateFunctions).forEach((stateVar) => {
let fString = this.updateFunctions[stateVar]
this.updateFunctions[stateVar] = function(s, t) {
let context = {...s, ...sysParams, timestep: t}
return math.evaluate(fString, context)
}
})
this.initStateTransitions()
}
initializeState() {
Object.keys(this.initialState).forEach((stateKey) => {
this.states[stateKey] = [{key: stateKey, value: this.initialState[stateKey], time: 0}]
})
}
getAllStates() {
return this.states
}
initStateTransitions() {
this.transition = function(currState, currStep) {
const snapshotState = {...currState}
var newState = Object.assign({}, currState)
Object.keys(newState).forEach((name) => {
if (this.updateFunctions.hasOwnProperty(name)) {
let nextState = this.updateFunctions[name].call(this, {...snapshotState}, currStep)
newState[name] = snapshotState[name] + nextState
} else {
throw new Error("No update function for state variable " + name)
}
})
return newState
}
}
update() {
this.updateBlocks.forEach((block) => {
let context = {
currState: {},
currStep: this.currStep
}
let currState = {...this.currState}
block.forEach((stateVarName) => {
context.currState[stateVarName] = currState[stateVarName]
})
context.currState = {...this.transition(currState, context.currStep)}
this.currState = {...context.currState}
})
Object.keys(this.currState).forEach((stateKey) => {
this.states[stateKey].push({key: stateKey, value: this.currState[stateKey], time: this.currStep})
})
this.pastSteps.push(this.currStep)
this.currStep += 1;
}
run() {
if (Array.isArray(this.initialState)) {
let results = []
this.initialStateSequence = [...this.initialState]
this.initialStateSequence.forEach((state) => {
this.initialState = {...state}
this.currState = {...state}
this.initializeState()
for (let i = 1; i <= this.timesteps; i++) {
this.currStep = i;
this.update();
}
results.push(this.getAllStates())
this.states = {}
})
return results
} else {
this.initializeState()
for (let i = 1; i <= this.timesteps; i++) {
this.currStep = i;
this.update();
}
}
}
plotTimeSeries(chartName){
let stateTraces = this.getAllStates()
let traces = Object.keys(stateTraces).map((traceKey) => {
let trace = stateTraces[traceKey]
let traceObj = {type: 'scatter', x: [], y: []}
trace.forEach((stateObj) => {
traceObj.x.push(stateObj.time)
traceObj.y.push(stateObj.value)
})
return traceObj
})
//return traces
return plotly.newPlot(chartName, traces)
}
plotPhaseSpace(chartName, stateTraces) {
let traceObj = {
type: 'scatter',
x: [],
y: []
}
let traces = []
let stateVarNames = Object.keys(this.initialState);
if (!!stateTraces && Array.isArray(stateTraces)) {
traces = stateTraces.map((stateTraceObject) => {
let objectForChart = Object.assign({}, traceObj)
stateVarNames.forEach((n, idx) => {
if (idx === 0) {
objectForChart.x = stateTraces[n].map(stateAtTime => stateAtTime.value)
} else if (idx === 1) {
objectForChart.y = stateTraces[n].map(stateAtTime => stateAtTime.value)
} else if (idx === 2) {
objectForChart.z = stateTraces[n].map(stateAtTime => stateAtTime.value)
}
})
return objectForChart
})
} else {
if (!stateTraces) {
stateTraces = this.getAllStates();
}
stateVarNames.forEach((n, idx) => {
if (idx === 0) {
traceObj.x = stateTraces[n].map(stateAtTime => stateAtTime.value)
} else if (idx === 1) {
traceObj.y = stateTraces[n].map(stateAtTime => stateAtTime.value)
} else if (idx === 2) {
traceObj.z = stateTraces[n].map(stateAtTime => stateAtTime.value)
}
})
traces = [traceObj]
}
//return traces
return plotly.newPlot(chartName, traces)
}

}

Insert cell
/**
initialStateTemplate:

{
variableNames: [string],
stateVariables: [{
name: string,
min: num,
max: num,
interval: num
}]
}
**/

function generateInitialStatesSequence(template) {
let initStatesSequence = []
let instanceTemplate = {}
let startStatesForEachVar = {}
template.variableNames.forEach((n) => {instanceTemplate[n] = 0})
template.variableNames.forEach((n) => {startStatesForEachVar[n] = []})

template.stateVariables.forEach((varTemplate) => {
let startStates = []
for (let i = varTemplate.min; i <= varTemplate.max; i += varTemplate.interval) {
startStates.push(i)
}
startStatesForEachVar[varTemplate.name] = [...startStates]
})
Object.values(startStatesForEachVar)[0].forEach((val, idx) => {
let startStateInstance = {...instanceTemplate}
Object.keys(startStatesForEachVar).forEach((varName) => {
startStateInstance[varName] = startStatesForEachVar[varName][idx]
})
initStatesSequence.push({...startStateInstance})
})
return initStatesSequence
}
Insert cell
class Component {
constructor(id, step, initialState, validation, timesteps) {
this.timeStep = 0;
this.inputs = {}
this.timesteps = timesteps
this.pre = validation.pre ? validation.pre : () => {return true};
this.post = validation.post ? validation.post : () => {return {result: true}};
this.halt = validation.halt ? validation.post : () => {return false};
this.ready = true
this.error = false
this.busy = false
this.step = step
this.incoming = []
this.id = id
this.halted = false
if (Array.isArray(initialState)) {
this.state = [...initialState]
this.states = [[...initialState]]
} else if (typeof initialState === 'object' && initialState !== null) {
this.state = {...initialState}
this.states = [{...initialState}]
} else {
this.state = initialState
this.states = [initialState]
}
}
preCheck(context) {
return this.ready && this.pre.call(this, context)
}
getState() {
let stateObj = {
id: this.id
}
let myState = this.state
if (Array.isArray(myState)) {
stateObj.state = [...myState]
} else if (typeof myState === 'object' && myState !== null) {
stateObj.state = {...myState}
} else {
stateObj.state = myState
}
return stateObj
}
setState(newState) {
let myState = this.state
if (Array.isArray(myState)) {
this.state = [...newState]
this.states.push([...newState])
} else if (typeof myState === 'object' && myState !== null) {
this.state = {...newState}
this.states.push({...newState})
} else {
this.state = newState
this.states.push(newState)
}
}
setInput(component) {
let name = component.id
this.inputs[name] = () => {
return component.getState()
}
}
postCheck(context, nextState) {
return this.post.call(this, context, nextState)
}
getInputs() {
let inputVals = {}
Object.keys(this.inputs).forEach((i) => {
inputVals[i] = this.inputs[i].call(this.inputs[i])
})
return inputVals
}
next(environment) {
// if (!this.input) {
// this.ready = true
// return
// }
if (this.halted){
return this.getState()
}
let inputs = this.getInputs()
if (environment) {
inputs = {...inputs, ...environment}
}
this.busy = true
let currStep = this.timeStep
let currState = this.getState();
let history = [...this.states]
let context = {t: currStep, s: currState, i: inputs, h: history, id: this.id}
if (!this.preCheck(context)) {
this.error = true
return new Error("Precheck failed");
}
let nextState = this.step(context)
let postValid = this.postCheck(context, nextState)
if (this.halt) {
this.halted = this.halt()
}
if (postValid.result) {
this.setState(nextState)
this.timeStep += 1
this.ready = true
return nextState
} else {
this.error = true
throw new Error(postValid.message)
}
}
// return() {
// return this.currState
// }
}
Insert cell
import {Button, Checkbox, Toggle, Radio, Range, Select, Text, Search, Table} from "@observablehq/inputs"
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
require("https://cdn.jsdelivr.net/gh/opensource9ja/danfojs@latest/lib/bundle.js")
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more