Skip to content

Inputs and parameters

Inputs and parameters have a similar but distinct role in a model. Parameters are static features intrinsic to the model that do not change during a simulation. Inputs are external influences that might change during a simulation. E.g., a pendulum model might have the length and weight of the pendulum as parameters, and a (constant) external driving force as an input. Inputs are always numerical, while parameters might also be non-numerical (e.g., booleans to activate or deactivate certain features of the model, or strings to indicate what is calculated by the model).

Static vs. dynamic inputs

Inputs can be static (constant during a simulation) or dynamic (changing during a simulation). Dynamic inputs are provided as a function of time, parameters and other inputs. For instance, a forced oscillator might have an external force as a sine wave that varies over time (\(F_0\sin\left(\omega t\right)\)), and a constant external torque.

def external_force_input(time, parameters):
    F0 = parameters['F0']
    omega = parameters['omega']
    return F0 * np.sin(omega * time)

model = Model(
    ..., 
    inputs={"external_force": external_force_input, "external_torque": 0.5},
    parameters={"F0": 1.0, "omega": 2.0 * np.pi},
)

Dynamic inputs can use values from other inputs. All static inputs are directly available to dynamic input functions. Values from other dynamic inputs functions are available in order:

def variable_input_1(inputs):
    return inputs['static_input'] * 2

def variable_input_2(inputs):
    return inputs['variable_input_1'] + 3

model = Model(
    ...,
    inputs={
        "static_input": 5.0,
        # the order here matters!
        "variable_input_1": variable_input_1, 
        "variable_input_2": variable_input_2,
    },
)

Modularity: inputs from other functions or models

Inputs can also be provided by other dynamics or models. This allows for modular design of complex models, where different components provide inputs to each other. For instance, a heart model might have a function that determines the heart rate and a second function that determines the cardiac output, which is in turn provided as an input to a circulatory system model.

def heart_rate_function(state, parameters):
    ...
    return {"heart_rate": heart_rate_value}


def cardiac_output_function(state, inputs, parameters):
    heart_rate = inputs["heart_rate"]
    ...
    return {"cardiac_output": cardiac_output_value}


def circulatory_system_function(state, inputs, parameters):
    cardiac_output = inputs["cardiac_output"]
    ...
    return {"d_blood_pressure": blood_pressure_value}


heart_model = Model(
    [heart_rate_function, cardiac_output_function], 
    ...
)

circulatory_model = Model(
    circulatory_system_function, 
    ...
)

complete_model = Model([heart_model, circulatory_model])

The cardiac output function can use the heart_rate returned by the heart rate function as input (reuses values returned within the same model), and the circulatory model can access the cardiac_output input provided by the cardiac output function in the heart model.

Note that it is also possible to run it separately by providing a static or dynamic input for cardiac_output when running the simulation. This enables modular development and testing, and dynamic combination of individual components of a larger model.

result = circulatory_model.run_simulation(
    time=100, 
    inputs={"cardiac_output": 5.0}  # static input
)

Using parameters to reuse functions

Parameters can be used to make dynamics more generic and reusable. You might want to calculate the behavior of multiple similar models that are mathematically indistinct. Since dynamics uses and returns named values (through dictionaries), you can use parameters to indicate what names to use and return.

def dynamics_animal_growth_rate(state, parameters):
    animal = parameters['animal']
    state_label = f"{animal}_population"
    growth_rate = parameters['growth_rate']
    current_population = state[state_label]
    d_population = growth_rate * current_population
    return {f"d_{state_label}": d_population}

rabbit_model = Model(
    dynamics=dynamics_animal_growth_rate,
    state_components=["rabbit_population"],
    parameters={"animal": "rabbit", "growth_rate": 0.1},
)
fox_model = Model(
    dynamics=dynamics_animal_growth_rate,
    state_components=["fox_population"],
    parameters={"animal": "fox", "growth_rate": 0.05},
)
complete_model = Model([rabbit_model, fox_model])

This example reuses the same dynamics to create two different models: one for rabbits and one for foxes. The behavior of each model is determined by the parameters provided during model instantiation. Similarly, entire models can be reused.

def function_a(state, parameters):
    state_label = parameters['state_label']
    ...
    return {f"d_{state_label}": value}

some_complex_model = Model([function_a, function_b, function_c])

foo_model = Model(
    some_complex_model,
    parameters={"param1": 10, "param2": 20, "state_label": "foo"},
)
bar_model = Model(
    some_complex_model,
    parameters={"param1": 5, "param2": 15, "state_label": "bar"},
)
complete_model = Model([foo_model, bar_model])

Using parameters to switch features on/off

Parameters can also be used to enable or disable certain features of a model. For instance, a model might have a parameter that indicates whether to include friction or not.

def dynamics(time, state, inputs, parameters):
    friction_enabled = parameters.get('enable_friction', False)  # disabled by default

    if friction_enabled:
        ...
    else:
        ...

model = Model(
    dynamics=dynamics,
    ...
)

result_without_friction = model.run_simulation(time=100)
result_with_friction = model.run_simulation(
    time=100,
    parameters={"enable_friction": True},
)