# Wind Tunnel Mk2

{% columns %}
{% column %}

<div data-full-width="true"><figure><img src="https://3492874807-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUqYDL9yvLTNUYW7H1Q6t%2Fuploads%2F5PDrqgk4mQsoOhqNs8tM%2Fwt_mk2_light_background_150dpi.png?alt=media&#x26;token=14504755-5714-426a-8924-a353b5cd9c86" alt=""><figcaption></figcaption></figure></div>
{% endcolumn %}

{% column %}
The wind tunnel produces **time-series data** from a **dynamical system**.

The tunnel consists of two controllable fans that push air through it, and a variety of sensors to measure variables such as fan speed, power, and air pressure at different locations. A hatch regulates an additional opening to the outside, creating an additional flow of air.
{% endcolumn %}
{% endcolumns %}

The chamber produces time-series data from up to 41 variables, including sensor measurements, control inputs, and sensor parameters. See the [variables table](#variables-table) for a description of each variable, and the [map of effects](#map-of-effects) between them.

<div align="center" data-full-width="false"><figure><picture><source srcset="https://3492874807-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUqYDL9yvLTNUYW7H1Q6t%2Fuploads%2Fjw5jN3KncMcyNgMf7dpg%2Fwt-impulse-dark.png?alt=media&#x26;token=8a67f1d4-d56d-415e-bb9a-604678781e47" media="(prefers-color-scheme: dark)"><img src="https://3492874807-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUqYDL9yvLTNUYW7H1Q6t%2Fuploads%2FhMGrb60YXzYCAPEolHeA%2Fwt-impulse-light.png?alt=media&#x26;token=c0d5a198-2b06-4485-8142-91bfa36b4d0c" alt="" width="563"></picture><figcaption><p>Example of time-series data from a subset of the chamber variables, collected after applying an impulse to <code>load_in</code>, the control signal of the intake fan. See the <a href="#variables-table">variables table</a> for a description of these variables.</p></figcaption></figure></div>

<details>

<summary>Chamber diagram</summary>

{% hint style="info" %}
See the [variables table](#variables-table) for a description of all variables.
{% endhint %}

<figure><img src="https://3492874807-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUqYDL9yvLTNUYW7H1Q6t%2Fuploads%2Fm30ZXKTEGxHnPm1H7pJl%2Fwt_diagram_light_background.png?alt=media&#x26;token=092e1b6f-3406-433e-b56a-8ea98cd8b0eb" alt=""><figcaption><p>Right click to download the image (available under a <a href="https://creativecommons.org/licenses/by-nc/4.0/">CC BY-NC 4.0</a> non-commercial license).</p></figcaption></figure>

</details>

<details>

<summary>Simulators</summary>

See the [Simulator Index](https://github.com/juangamella/causal-chamber-package/tree/main/causalchamber/simulators) for a list of the simulators we offer for this chamber, including documentation and example code.

</details>

### Hardware configurations

Like all chambers, the wind tunnel can automatically load different [hardware configurations](https://docs.causalchamber.ai/how-they-work#hardware-configurations), exposing different variables and behaviors of the underlying physical system. For each configuration, see the corresponding PDF for a chamber diagram, a complete description of all variables, and the causal ground-truth graph between them.

<table><thead><tr><th width="173.5126953125">Name</th><th width="396.19677734375">Description</th><th width="160.1365966796875">Documentation</th></tr></thead><tbody><tr><td><code>full</code></td><td>Full configuration with all variables.</td><td><a href="https://cchamber-box.s3.eu-central-2.amazonaws.com/config_doc_wt_mk2_full.pdf" class="button secondary">.pdf</a></td></tr></tbody></table>

### Map of effects

We provide a detailed description of all effects between chamber variables, together with additional experiments and figures. Throughout, we use an edge A $$\longrightarrow$$ B to denote that a variable A has an effect on variable B.

> **Short-hand notation**
>
> * A1/2 $$\longrightarrow$$ B, C is equivalent to the edges A1 $$\longrightarrow$$ B, A1 $$\longrightarrow$$ C, A2 $$\longrightarrow$$ B and A2 $$\longrightarrow$$ C
> * We can also express this as A\* $$\longrightarrow$$ B,C

Some text and figures in this section are adapted from the [original paper](https://www.nature.com/articles/s42256-024-00964-x) (Gamella et al. 2025, [Appendix III](https://static-content.springer.com/esm/art%3A10.1038%2Fs42256-024-00964-x/MediaObjects/42256_2024_964_MOESM1_ESM.pdf)).

{% tabs %}
{% tab title="Graph" %}

<figure><img src="https://3492874807-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUqYDL9yvLTNUYW7H1Q6t%2Fuploads%2Fqhk191K36w6Wv0VgRkou%2Fwt_causal_graph_light.png?alt=media&#x26;token=2afdb630-8118-47c7-8c25-e5c0c16b4374" alt=""><figcaption><p>Right click to download the image (available under a <a href="https://creativecommons.org/licenses/by-nc/4.0/">CC BY-NC 4.0</a> non-commercial license).</p></figcaption></figure>
{% endtab %}
{% endtabs %}

<details>

<summary>Causal ground-truth</summary>

The graph above can be interpreted as a **causal ground truth**, as formalized in Gamella et al. (2025, [Appendix V](https://cchamber-box.s3.eu-central-2.amazonaws.com/nature_paper_appendices.pdf)), i.e., an edge X $$\longrightarrow$$ Y signifies that—for some value of the other chamber inputs—an intervention on X will change the distribution of subsequent measurements of Y. The graph should <mark style="color:$danger;">**not**</mark> be taken as a graphical model of statistical dependencies, as [external influences](#external-influences) on the system may create additional correlations between variables.

</details>

In what follows, we provide a detailed description and visualization of each edge (physical effect) in the above graph.

{% hint style="info" %}
See the [variables table](#variables-table) for a description of all variables.
{% endhint %}

***

#### `load_in/out` $$\longrightarrow$$ `rpm_in/out`, `current_in/out`, `current_in/out_raw`

The fan loads (`load_in`, `load_out`) define the [duty cycle](https://en.wikipedia.org/wiki/Pulse-width_modulation#Duty_cycle) of the control signal sent to the fans, affecting their speed (measured by `rpm_in/out`) and the calibrated (`current_in/out`) and uncalibrated (`current_in/out`) measurements of the drawn electrical current. The fans operate in an open-loop configuration. In steady-state—and keeping all other variables constant—the load has a quasi-linear effect on the load and a cubic effect on the current ([Figure 1](#figure-1), left/center). A justification from first principles is provided in Gamella et al. (2025, [Appendix IV.1.1](https://cchamber-box.s3.eu-central-2.amazonaws.com/nature_paper_appendices.pdf)). The effect of the fan load on its speed and current is not instantaneous, as the fan requires time to accelerate ([Figure 1](#figure-1), right).

{% tabs %}
{% tab title="Figure 1" %}

<figure><picture><source srcset="https://3492874807-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUqYDL9yvLTNUYW7H1Q6t%2Fuploads%2FCmmZmywjgVzV53jffRwZ%2Floads_on_speeds_current_dark.svg?alt=media&#x26;token=d23aae91-66b8-4127-b812-74386a154bf9" media="(prefers-color-scheme: dark)"><img src="https://3492874807-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUqYDL9yvLTNUYW7H1Q6t%2Fuploads%2FBg3OO2TL5KCRSEqvdjuI%2Floads_on_speeds_current_light.svg?alt=media&#x26;token=4fb51b88-52d0-456f-9c4c-14ff3923d578" alt=""></picture><figcaption><p><strong>Left:</strong> steady-state measurements of the calibrated fan current (<code>current_in</code>) for different values of the load <code>load_in</code>. <strong>Center:</strong> steady-state measurements of the fan speed (<code>rpm_in</code>) for different values of <code>load_in</code>. Due to their intended application, unless completely powered off (i.e., <code>load_in/out</code> = 0) the fans never operate below a certain speed, corresponding to a minimum load of 0.1 (shown by the gray line). <strong>Right:</strong> time-series data after a step increase in <code>load_in</code>, showing a lagged effect on fan speed (<code>rpm_in</code>) and current (<code>current_in</code>).</p></figcaption></figure>
{% endtab %}

{% tab title="Experiment" %}
To recreate the data for the left and center panels (steady state) using the [Remote Lab](https://docs.causalchamber.ai/remote-lab):

{% code overflow="wrap" %}

```python
# Connect to the lab
import causalchamber.lab as lab
rlab = lab.Lab(credentials_file = '.credentials')

# Declare experiment
experiment = rlab.new_experiment('wt-h0pe-i4ug', 'full')

# Wait for fans to stabilize after start
experiment.wait(8000)

# Collect steady state measurements
for load in np.linspace(0.01, 1, 100):
    experiment.set('load_in', load)
    experiment.wait(2000)
    experiment.measure(n=5)

# Submit
experiment.submit(tag='steady-state-fan-load')
```

{% endcode %}

For the right panel (impulse):

{% code overflow="wrap" %}

```python
# Connect to the lab
import causalchamber.lab as lab
rlab = lab.Lab(credentials_file = '.credentials')

# Declare experiment
experiment = rlab.new_experiment('wt-h0pe-i4ug', 'full')

# Wait for fans to stabilize after start
experiment.wait(8000)

# Run impulse
experiment.measure(20)
experiment.set('load_in', 1)
experiment.measure(30)
experiment.set('load_in', 0.01)
experiment.measure(50)

# Submit
experiment.submit(tag='impulse-fan-load')
```

{% endcode %}
{% endtab %}
{% endtabs %}

When the fan load is set to zero, the fan is completely powered off and no longer produces a [tachometer](https://en.wikipedia.org/wiki/Tachometer) signal; the resulting speed measurement (`rpm_in/out`) corresponds to the last measured speed ([Figure 2](#figure-2)).

{% tabs %}
{% tab title="Figure 2" %}

<div align="center"><figure><picture><source srcset="https://3492874807-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUqYDL9yvLTNUYW7H1Q6t%2Fuploads%2FIUWh2DRPBckZ2kesU8Cj%2Fzero_load_dark.svg?alt=media&#x26;token=3fb02566-fbbf-4a1a-bb8d-cdf60937fd4c" media="(prefers-color-scheme: dark)"><img src="https://3492874807-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUqYDL9yvLTNUYW7H1Q6t%2Fuploads%2Fi0D9CUuTc8G1OfWoJCfh%2Fzero_load_light.svg?alt=media&#x26;token=8b9afb46-3bf0-4a88-b87e-7a5e755effbe" alt="" width="563"></picture><figcaption><p>By setting a fan load to zero (e.g., <code>load_out</code> ← 0 at t=125), the fan is completely powered off and will decelerate until it stops rotating. It will no longer produce a tachometer signal, and the resulting speed measurement will be the last measured speed (see <code>rpm_out</code> above for 125 &#x3C; t &#x3C; 200). When powered up again (t=200) the fan draws full power for an instant, accelerating before returning to the level specified by the load.</p></figcaption></figure></div>
{% endtab %}

{% tab title="Experiment" %}
To replicate the experiment with the [Remote Lab](https://docs.causalchamber.ai/remote-lab):

{% code overflow="wrap" %}

```python
# Connect to the lab
import causalchamber.lab as lab
rlab = lab.Lab(credentials_file = '.credentials')

# Declare experiment for a Wind Tunnel Mk2.
experiment = rlab.new_experiment(chamber_id = 'wt-h0pe-i4ug', config = 'full')

# Wait for fans to stabilize after start
experiment.wait(8000)

# Run experiment
experiment.measure(100, 0)       # initial conditions
experiment.set("load_in", 1)     # intake fan to max
experiment.measure(25, 0)        
experiment.set("load_out", 0)    # power off exhaust fan
experiment.measure(25, 0)
experiment.set("load_in", .01)   # idle intake fan
experiment.measure(50, 0)
experiment.set("load_out", .01)  # power on exhaust fan
experiment.measure(100, 0)

# Submit
experiment.submit(tag='zero_load')
```

{% endcode %}
{% endtab %}
{% endtabs %}

Because both fans are connected to the same power supply, the load of one fan affects the current drawn by the other, specially when both are running at high loags ([Figure 3](#figure-3)).

{% tabs %}
{% tab title="Figure 3" %}

<div align="center"><figure><img src="https://3492874807-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUqYDL9yvLTNUYW7H1Q6t%2Fuploads%2FvO4uqaLkEsmkwTjKpCNp%2Fcurrent_mutual.svg?alt=media&#x26;token=9cdc89de-6558-4787-9956-a9ab5fc504a0" alt="" width="563"><figcaption><p>Calibrated fan currents (<code>current_in/out</code>, top) under step changes to the fan loads (<code>load_in/out</code>, bottom). Because both fans share the same power supply, when a fan is operating close to its maximum load, its drawn current is affected by large changes to the load of the other fan, e.g., at <code>t=200,300</code>.</p></figcaption></figure></div>
{% endtab %}

{% tab title="Experiment" %}
To replicate the experiment with the [Remote Lab](https://docs.causalchamber.ai/remote-lab):

{% code overflow="wrap" %}

```python
import causalchamber.lab as lab
rlab = lab.Lab(credentials_file = '.credentials', verbose=False)

# Define experiment
experiment = rlab.new_experiment('wt-demo-ch4lu', 'full')

# Wait for fan speed to stabilize after reset
experiment.wait(8_000)

experiment.measure(n=100)
experiment.set('load_in', 1)
experiment.measure(n=100)
experiment.set('load_out', 1)
experiment.measure(n=100)
experiment.set('load_in', 0.01)
experiment.measure(n=100)
experiment.set('load_out', 0.01)
experiment.measure(n=100)

# Submit
experiment.submit(tag='current-effects')
```

{% endcode %}
{% endtab %}
{% endtabs %}

***

#### `res_rpm_in/out` $$\longrightarrow$$ `rpm_in/out`

Changing the resolution (`res_rpm_in`, `res_rpm_out`) of the timers used in the fan [tachometers](https://en.wikipedia.org/wiki/Tachometer) also changes the resolution of the resulting speed measurement (`rpm_in`, `rpm_out`). Using a resolution of microseconds (e.g. `res_rpm_in` = 1) allows measuring smaller changes in the fan speed ([Figure 4](#figure-4)).

{% tabs %}
{% tab title="Figure 4" %}

<figure><img src="https://3492874807-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUqYDL9yvLTNUYW7H1Q6t%2Fuploads%2FWEVJ7mjPGMpRmNjlEzoo%2Fres_rpm_light.svg?alt=media&#x26;token=673654c3-b9ad-4a19-bac3-8dc60aabf2c6" alt="" width="563"><figcaption><p>Measurements of fan speed (<code>rpm_in</code>) for different resolutions of the underlying tachometer (<code>res_rpm_in</code>), for increasing values of the fan load <code>load_in</code>. The quantization error is larger for higher speeds, when tachometer pulses occur at shorter intervals. The results for <code>rpm_out</code> are the same and not shown.</p></figcaption></figure>
{% endtab %}

{% tab title="Experiment" %}
To replicate the experiment with the [Remote Lab](https://docs.causalchamber.ai/remote-lab):

```python
# Connect to the lab
import causalchamber.lab as lab
rlab = lab.Lab(credentials_file = '.credentials')

# Declare experiment
experiment = rlab.new_experiment(chamber_id = 'wt-demo-ch4lu', config='full')
loads = [0.1, 0.4, 0.7, 1]
for res in [0,1]:
    experiment.set('res_rpm_in', res) # Set speed sensor resolution
    # Set initial load and wait for fan speed to stabilize
    experiment.set('load_in', loads[0])
    experiment.wait(8_000)
    # Increase the load in steps
    for load in loads:
        experiment.set('load_in', load)
        experiment.measure(n=100)

# Submit
experiment.submit(tag='res-rpm')
```

{% endtab %}
{% endtabs %}

***

#### `hatch` $$\longrightarrow$$ `rpm_in/out`

The two fans in the chamber operate in tandem to drive air through the tunnel (see [diagram](#chamber-diagram)). Thus, their speeds are coupled, i.e., if one fan accelerates, the other will as well, even if no additional power is applied to it. The strength of this coupling is modulated by the hatch position, which controls a third path for air to flow into or out of the chamber; larger openings result in a weaker coupling ([Figure 5](#figure-5)).

{% tabs %}
{% tab title="Figure 5" %}

<figure><img src="https://3492874807-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUqYDL9yvLTNUYW7H1Q6t%2Fuploads%2FVjCOZzZacK17BnMm6eBq%2Fcombined_hatch_figure.svg?alt=media&#x26;token=1c0a81a6-6e0b-4067-8ccf-1457171b55e5" alt=""><figcaption><p><strong>Left</strong>: effect on the fan speed of changing the hatch position (bottom) under constant fan loads (top and middle plot). <strong>Right:</strong> effect of applying a short impulse to <code>load_in</code> on the fan speeds <code>rpm_in/out</code> for different hatch positions. The coupling between between the fan speeds decreases as the hatch is opened. The hatch is closed at 0º, and fully open at ±45º.</p></figcaption></figure>
{% endtab %}

{% tab title="Experiment" %}
To replicate the experiment for the left panel using the [Remote Lab](https://docs.causalchamber.ai/remote-lab):

{% code overflow="wrap" %}

```python
# Connect to the lab
import causalchamber.lab as lab
rlab = lab.Lab(credentials_file = '.credentials')

# Declare experiment
experiment = rlab.new_experiment(chamber_id = 'wt-demo-ch4lu', config='full')

for flag, (load_in, load_out) in enumerate([(1, 0.1), (0.1, 1)]):
    # Set initial conditions and wait for system to stabilize
    experiment.set('flag', flag)
    experiment.set('load_in', load_in)
    experiment.set('load_out', load_out)
    experiment.set('hatch', 0)
    experiment.wait(8_000)    
    # Measure at step increments of the hatch
    for hatch in [0, 22, 45]:
        experiment.set('hatch', hatch)
        experiment.measure(n=200)

# Submit
experiment.submit(tag='hatch-on-speeds')
```

{% endcode %}

For the right panel:

{% code overflow="wrap" %}

```python
# Connect to the lab
import causalchamber.lab as lab
rlab = lab.Lab(credentials_file = '.credentials')

# Declare experiment
experiment = rlab.new_experiment(chamber_id = 'wt-demo-ch4lu', config='full')

# Apply impulses to load_in at different hatch positions
for hatch in [0, 5, 10, 22, 45]:
    # Set hatch and initial fan load
    experiment.set('load_in', 0.01)
    experiment.set('hatch', hatch)
    experiment.wait(8_000) # Wait for system to stabilize
    # Apply impulse
    experiment.measure(20)
    experiment.set('load_in', 1)
    experiment.measure(30)
    experiment.set('load_in', 0.01)
    experiment.measure(50)
    
# Submit
experiment.submit(tag='hatch-on-speeds-impulse')
```

{% endcode %}
{% endtab %}
{% endtabs %}

***

#### `hatch` $$\longrightarrow$$ `hatch_angle`

A magnetic encoder measures the actual position of the hatch, producing the measurement `hatch_angle` (in degrees). Under normal operating conditions (default values of `mot_enabled/steps/max`), the position of the hatch (as measured by `hatch_angle`) closely follows the position set by `hatch` ([Figure 6](#figure-6), top left).

Lowering the resolution of the motor (`mot_steps`) results in a coarser hatch placement. If we lower the current delivered to the motor (`mot_max`) or power it off completely (`mot_enabled = 0`), the motor will cease to function properly, creating a mismatch between `hatch` and the actual hatch position measured by `hatch_angle`.

{% tabs %}
{% tab title="Figure 6" %}

<figure><img src="https://3492874807-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUqYDL9yvLTNUYW7H1Q6t%2Fuploads%2FDueAtVFYBDvOXiPXdXzr%2Fmotor_parameters.svg?alt=media&#x26;token=e8aa40b6-3ab8-4dba-a520-89528b3982c8" alt=""><figcaption><p>Position of the hatch (<code>hatch_angle</code>) along a trajectory (dotted black line) defined by the input <code>hatch</code> that sets the desired hatch position. We show trajectories for the default motor parameters (top left) and different values of the motor parameters <code>mot_steps/max/enabled</code>. Lower motor resolutions (<code>mot_steps</code>) result in a coarser hatch placement and potential accumulation of errors. Lowering the current delivered to the motor (<code>mot_max</code>), or powering it off completely (<code>mot_enabled = 0</code>) cause the motor to miss steps, creating a mismatch between the set position (<code>hatch</code>) and the actual position of the hatch (<code>hatch_angle</code>).</p></figcaption></figure>
{% endtab %}

{% tab title="Experiment" %}
To replicate the experiments for the plot using the [Remote Lab](https://docs.causalchamber.ai/remote-lab):

{% code overflow="wrap" %}

```python
# Connect to the lab
import causalchamber.lab as lab
rlab = lab.Lab(credentials_file = '.credentials', verbose=False)

settings = [None, # default settings
            {'mot_max': 1024},
            {'mot_max': 512},
            {'mot_enabled': 0},
            {'mot_steps': 1600},
            {'mot_steps': 800},
            {'mot_steps': 400},
            {'mot_steps': 200},
           ]

# Run a separate experiment for each setting
for setting in settings:    
    experiment = rlab.new_experiment(chamber_id = 'wt-demo-ch4lu', config='full')

    # Set experiment settings
    if setting is None:
        label = "default"
    else:
        label = "-".join([f'{p}:{v}' for p,v in setting.items()])
        for var,value in setting.items():
            experiment.set(var, value)
    
    # Trajectory for the hatch
    t = 4 * np.cos(np.linspace(0, 2*np.pi, 25)) - 4
    for hatch in (t):
        experiment.set('hatch', hatch)        
        experiment.measure(n=1)
    
    # Submit    
    experiment.submit(tag=label)
```

{% endcode %}
{% endtab %}
{% endtabs %}

***

#### `load_in/out`, `hatch` $$\longrightarrow$$ `pressure_upwind/downwind/intake`

The fan loads (`load_in`, `load_out`) and the hatch position (`hatch`) affect the air pressure measured by the barometers inside the wind tunnel (`pressure_upwind`, `pressure_downwind`) and at its intake (`pressure_intake`). See the [chamber diagram](#chamber-diagram) for the location of these barometers.

An increase in the load of the intake fan (`load_in`) results in more air being pumped into the tunnel, increasing `pressure_downwind` and `pressure_upwind`; increasing the load of the exhaust fan (`load_out`) has the opposite effect ([Figure 7](#figure-7), left & center). Opening the `hatch` creates an additional flow of air into or out of the chamber, also affecting the inner pressure measurements. While all three variables affect `pressure_intake`, the effect is very weak for `load_out` and `hatch` ([Figure 6](#figure-6)).

{% tabs %}
{% tab title="Figure 7" %}

<figure><img src="https://3492874807-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUqYDL9yvLTNUYW7H1Q6t%2Fuploads%2FYGGIMivk0uh6BKYzVRrE%2Floads_hatch_pressure.svg?alt=media&#x26;token=0f6e8398-6fe8-4c3b-a1ac-42afed7a56ea" alt=""><figcaption><p><strong>Left</strong>: change in the tunnel pressures after applying an impulse to <code>load_in</code> (gray dashed line), causing a change in the speed of the intake fan (light gray), and creating a pressure wave inside the chamber. The hatch is kept closed (<code>hatch=0</code>), and the exhaust fan is held at a constant load of <code>load_out=0.1</code>. <strong>Center</strong>: change in the tunnel pressures after applying an impulse to <code>load_out</code> (gray dashed line), causing the exhaust fan to accelerate and decelerate (light gray). As before, the hatch and exhaust fan load are kept contant (<code>hatch=0</code>, <code>load_in=0.1</code>). <strong>Right:</strong> change on the tunnel pressures by opening and closing the hatch; the fans are kept at a constant load of <code>load_in=1</code> and <code>load_out=0.1</code>.</p></figcaption></figure>
{% endtab %}

{% tab title="Experiment" %}
To replicate the experiment with the [Remote Lab](https://docs.causalchamber.ai/remote-lab):

{% code overflow="wrap" %}

```python
# Connect to the lab
import causalchamber.lab as lab
rlab = lab.Lab(credentials_file = '.credentials', verbose=False)

# Define experiment
experiment = rlab.new_experiment('wt-ptdm-73iz', 'full')

# Set barometers to maximum precision (oversampling)
for bar in ['upwind', 'downwind', 'intake', 'ambient']:
    experiment.set(f'osr_pressure_{bar}', 3)

# --------------------------------------------
# Left panel: impulse on load_in
#   Set initial conditions and wait for the system to stabilize
experiment.set('flag', 1)
experiment.set('load_in', 0.01)
experiment.set('load_out', 0.01)
experiment.wait(8_000) 
#   Apply impulse
experiment.measure(20)
experiment.set('load_in', 1)
experiment.measure(30)
experiment.set('load_in', 0.01)
experiment.measure(50)

# --------------------------------------------
# Center panel: impulse on load_out
#   Set initial conditions and wait for the system to stabilize
experiment.set('flag', 2)
experiment.set('load_in', 0.01)
experiment.set('load_out', 0.01)
experiment.wait(8_000) 
#   Apply impulse
experiment.measure(20)
experiment.set('load_out', 1)
experiment.measure(30)
experiment.set('load_out', 0.01)
experiment.measure(50)

# --------------------------------------------
# Right panel: constant loads, steps on hatch
#   Set initial conditions and wait for the system to stabilize
experiment.set('flag', 3)
experiment.set('load_in', 1)
experiment.set('load_out', 0.1)
experiment.wait(8_000) 
#   Apply impulse
experiment.measure(20)
experiment.set('hatch', 45)
experiment.measure(30)
experiment.set('hatch', 0)
experiment.measure(50)

# Submit
experiment.submit(tag='pressure-vs-loads-hatch')
```

{% endcode %}
{% endtab %}
{% endtabs %}

***

#### `osr_*` $$\longrightarrow$$ `pressure_*`

The [oversampling rate](https://www.microchip.com/en-us/about/media-center/blog/2024/what-is-oversampling) of the barometers (`osr_pressure_upwind/downwind/intake/ambient`) determines how many readings are averaged to produce a single measurement of the air pressure. Thus, a higher oversampling rate increases the precision of these sensors, increasing the signal-to-noise ratio of the resulting measurements ([Figure 8](#figure-8)).

{% tabs %}
{% tab title="Figure 8" %}

<figure><img src="https://3492874807-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUqYDL9yvLTNUYW7H1Q6t%2Fuploads%2FCJsrHmAhbM6v6hzj7A2D%2Fbarometers_osr_light.svg?alt=media&#x26;token=603dda9f-0734-4d0b-a7c7-c214d404d52a" alt="" width="563"><figcaption><p>Effect of the barometer oversampling rate (<code>osr_pressure_upwind/downwind/ambient/intake</code>) on the resulting measurement (<code>pressure_upwind/downwind/ambient/intake</code>). For all barometers, the oversampling rate is increased at t=200,400, 800, while keeping all other chamber inputs and sensor parameters constant.</p></figcaption></figure>
{% endtab %}

{% tab title="Experiment" %}
To replicate the experiment with the [Remote Lab](https://docs.causalchamber.ai/remote-lab):

{% code overflow="wrap" %}

```python
# Connect to the lab
import causalchamber.lab as lab
rlab = lab.Lab(credentials_file = '.credentials')

# Declare experiment
experiment = rlab.new_experiment(chamber_id = 'wt-demo-ch4lu', config='full')
for osr in [0,1,2,3]:    
    [experiment.set(f'osr_pressure_{bm}', osr) for bm in ['upwind', 'downwind', 'intake', 'ambient']]
    experiment.measure(n=200)

# Submit
experiment.submit(tag='barometers-osr')
```

{% endcode %}
{% endtab %}
{% endtabs %}

***

#### `offset/sps/res_current_*` $$\longrightarrow$$ `current_*`

The chamber produces calibrated measurements (in Amperes) of the electrical current drawn by the fans (`current_in/out`) and the hatch motor (`current_mot`). For each measurement, the chamber also returns the underlying raw, uncalibrated measurements (`current_in/out_raw`, `current_mot_raw`), which take values in the range \[-2¹⁵, 2¹⁵] (the output of the sensor's [ADC](https://en.wikipedia.org/wiki/Analog-to-digital_converter)).

We can independently control three parameters in each sensor:

* `offset_current_*` : the reference voltage. Changing it creates an additive shift in the uncalibrated measurements (`*_raw`) but is largely compensated for in the calibrated measurements ([Figure 9](#figure-9), right).
* `sps_current_*` : the [oversampling rate](https://www.microchip.com/en-us/about/media-center/blog/2024/what-is-oversampling), i.e., how many readings are averaged to produce a single measurement. Lower values correspond to higher oversampling rates, increasing the noise-to-signal ratio of the resulting measurements. Both the calibrated and uncalibrated measurements are affected ([Figure 9](#figure-9), center).
* `res_current_*` : the measurement range—and thus the resolution—of the sensor. Higher values correspond to smaller measurement ranges, increasing the resolution but saturating the sensor if the actual values falls outside this range ([Figure 9](#figure-9), right). Changes to `res_*` result in a shift and scaling of the uncalibrated measurements (`*_raw`).

{% tabs %}
{% tab title="Figure 9" %}

<figure><img src="https://3492874807-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUqYDL9yvLTNUYW7H1Q6t%2Fuploads%2FtCy4uSEADtylMMltHNlH%2Foffset_sps_res_current_in.svg?alt=media&#x26;token=941b1726-9607-4d49-9fbd-cd6edd1ff4da" alt=""><figcaption><p>Effect of the sensor parameters <code>offset/sps/res_current_in</code> (resp. left, center, right) on the calibrated (<code>current_in</code>) and uncalibrated (<code>current_in_raw</code>) of the intake fan (top and bottom row, respectively). The behaviour for <code>current_out</code> and <code>current_mot</code> is the same and not shown. The calibrated measurements (in Amps) largely compensate for changes in the reference voltage (<code>offset_</code>, left) and sensor resolution (<code>res_</code>, right), unless sensor saturation occurs. For example, in the right plot, the resolution (<code>res_current_in = 2</code>) is increased to the point where the measurements fall outside of the sensor range. Both calibrated and uncalibrated measurements are affected by changes in the oversampling rate (<code>sps_</code>), which affects their signal-to-noise ratio (i.e., variance, precision).</p></figcaption></figure>
{% endtab %}

{% tab title="Experiment" %}
You can replicate the experiments for this plot with the [Remote Lab](https://docs.causalchamber.ai/remote-lab). For the left panel (varying `offset_current_in`):

{% code overflow="wrap" %}

```python
# Connect to the lab
import causalchamber.lab as lab
rlab = lab.Lab(credentials_file = '.credentials', verbose=False)

# Declare experiment
experiment = rlab.new_experiment(chamber_id = 'wt-demo-ch4lu', config='full')

# Set initial load and wait for fan speed to stabilize
experiment.set('load_in', 0.5)
experiment.wait(8_000)

# Iterate over offset values and take measurements
for offset in [0, 100, 200, 300]:
    experiment.set('offset_current_in', offset)
    experiment.measure(n=500)

# Submit
experiment.submit(tag='offset-current-in')
```

{% endcode %}

For the center panel (varying `sps_current_in`):

{% code overflow="wrap" %}

```python
# Connect to the lab
import causalchamber.lab as lab
rlab = lab.Lab(credentials_file = '.credentials', verbose=False)

# Declare experiment
experiment = rlab.new_experiment(chamber_id = 'wt-demo-ch4lu', config='full')

# Set initial load and wait for fan speed to stabilize
experiment.set('load_in', 0.5)
experiment.wait(8_000)

# Iterate over offset values and take measurements
for sps in [0, 2, 5, 7]:
    experiment.set('sps_current_in', sps)
    experiment.measure(n=100)

# Submit
experiment.submit(tag='sps-current-in')
```

{% endcode %}

For the right panel (varying `res_current_in`):

{% code overflow="wrap" %}

```python
# Connect to the lab
import causalchamber.lab as lab
rlab = lab.Lab(credentials_file = '.credentials', verbose=False)

# Declare experiment
experiment = rlab.new_experiment(chamber_id = 'wt-demo-ch4lu', config='full')

# Set initial load and wait for fan speed to stabilize
experiment.set('load_in', 0.5)
experiment.wait(8_000)

# Iterate over offset values and take measurements
for res in np.arange(3):
    experiment.set('res_current_in', res)
    experiment.measure(n=500)

# Submit
experiment.submit(tag='res-current-in')
```

{% endcode %}
{% endtab %}
{% endtabs %}

***

#### `offset/sps/res_mic` $$\longrightarrow$$ `mic`, `mic_raw`

The chamber produces calibrated measurements (`mic`, in Volts) of the signal produced by the tunnel microphone (see [diagram](#chamber-diagram)). It also returns the raw, uncalibrated measurements (`mic_raw`) produced by the underlying analog sensor, which produces values in the range \[-2¹⁵, 2¹⁵] (the output of its [ADC](https://en.wikipedia.org/wiki/Analog-to-digital_converter)).

As for the [current measurements](#offset-sps-res_current_-current), we can individually control three parameters of the sensor:

* `offset_mic` : the reference voltage. Changing it creates an additive shift in the uncalibrated measurements (`mic_raw`) but is compensated for—up to a small effect—in the calibrated measurements ([Figure 10](#figure-10), right).
* `sps_mic` : the [oversampling rate](https://www.microchip.com/en-us/about/media-center/blog/2024/what-is-oversampling), i.e., how many readings are averaged to produce a single measurement. Lower values correspond to higher oversampling rates, increasing the noise-to-signal ratio (i.e., precision) of the resulting measurements. Both the calibrated and uncalibrated measurements are affected ([Figure 10](#figure-10), center).
* `res_mic` : the measurement range—and thus the resolution—of the sensor. Higher values correspond to smaller measurement ranges, increasing the resolution but saturating the sensor if the actual value falls outside this range ([Figure 10](#figure-10), right). Changes to `res_mic` result in a shift and scaling of the uncalibrated measurements (`mic_raw`).

{% tabs %}
{% tab title="Figure 10" %}

<figure><img src="https://3492874807-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUqYDL9yvLTNUYW7H1Q6t%2Fuploads%2F3FxBsV8odbbtABjV4EWn%2Foffset_sps_res_mic.svg?alt=media&#x26;token=b13f5d90-b103-41a1-9d70-832ad9451dc9" alt=""><figcaption><p>Effect of the sensor parameters <code>offset/sps/res_mic</code> (resp. left, center, right) on the calibrated (<code>mic</code>) and uncalibrated (<code>mic_raw</code>) measurements from the tunnel microphone (top and bottom row, respectively). The calibrated measurements (in Volts) largely compensate for changes in the reference voltage (<code>offset_mic</code>, left) and sensor resolution (<code>res_mic</code>, right), unless sensor saturation occurs. For example, in the right plot, at the smallest measurement range (<code>res_mic = 6</code>) some measurements fall outside of the sensor range. Saturation can be achieved with lower values of <code>res_mic</code> by shifting the reference voltage of the sensor through <code>offset_mic</code>. Both calibrated and uncalibrated measurements are affected by changes in the oversampling rate (<code>sps_mic</code>), which affects their signal-to-noise ratio (i.e., variance, precision).</p></figcaption></figure>
{% endtab %}

{% tab title="Experiment" %}
You can replicate the experiments for this plot with the [Remote Lab](https://docs.causalchamber.ai/remote-lab). For the left panel (varying `offset_mic`):

{% code overflow="wrap" %}

```python
# Connect to the lab
import causalchamber.lab as lab
rlab = lab.Lab(credentials_file = '.credentials', verbose=False)

# Declare experiment
experiment = rlab.new_experiment(chamber_id = 'wt-demo-ch4lu', config='full')

# Wait for fans to stabilize after reset
experiment.wait(8_000)

# Randomize over offset values and take measurements
import pandas as pd
inputs = pd.DataFrame({'offset_mic': [offset for _ in range(200) for offset in [0, 100, 200, 300, 400]]})
inputs = inputs.sample(n = len(inputs))
experiment.from_df(inputs, n=1)

# Submit
experiment.submit(tag='offset-mic')
```

{% endcode %}

For the center panel (varying `sps_mic`):

{% code overflow="wrap" %}

```python
# Connect to the lab
import causalchamber.lab as lab
rlab = lab.Lab(credentials_file = '.credentials', verbose=False)

# Declare experiment
experiment = rlab.new_experiment(chamber_id = 'wt-demo-ch4lu', config='full')

# Wait for fans to stabilize after reset
experiment.wait(8_000)

# Randomize over sps values and take measurements
import pandas as pd
inputs = pd.DataFrame({'sps_mic': [offset for _ in range(200) for offset in [0, 2, 5, 7]]})
inputs = inputs.sample(n = len(inputs))
experiment.from_df(inputs, n=1)

# Submit
experiment.submit(tag='sps-mic')
```

{% endcode %}

For the right panel (varying `res_mic`):

{% code overflow="wrap" %}

```python
# Connect to the lab
import causalchamber.lab as lab
rlab = lab.Lab(credentials_file = '.credentials', verbose=False)

# Declare experiment
experiment = rlab.new_experiment(chamber_id = 'wt-demo-ch4lu', config='full')

# Wait for fans to stabilize after reset
experiment.wait(8_000)

# Randomize over res values and take measurements
import pandas as pd
inputs = pd.DataFrame({'res_mic': [offset for _ in range(200) for offset in range(6)]})
inputs = inputs.sample(n = len(inputs))
experiment.from_df(inputs, n=1)

# Submit
experiment.submit(tag='res-mic')
```

{% endcode %}
{% endtab %}
{% endtabs %}

***

#### `load_in/out`,`hatch` $$\longrightarrow$$ `mic`

The speed of the fans, controlled by the loads `load_in/out`, affect the overall noise level and the amount of air flowing through the exhaust and over the tunnel microphone, affecting its calibrated and uncalibrated measurements `mic`, `mic_raw` ([Figure 11](#figure-11), left). The position of the hatch also modulates the amount of air flowing over the microphone, affecting its readings ([Figure 11](#figure-11), right).

{% tabs %}
{% tab title="Figure 11" %}

<figure><img src="https://3492874807-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUqYDL9yvLTNUYW7H1Q6t%2Fuploads%2FBUEE2tccTDGx4DwvnRKs%2Floads_hatch_mic.svg?alt=media&#x26;token=8ca537e3-2443-4200-8f75-2048e8040aa6" alt=""><figcaption><p><strong>Left:</strong> time-series data of the microphone output <code>mic</code> (top) collected under varying inputs (bottom) to the fan loads <code>load_in/out</code> and the hatch position <code>hatch</code>. All three inputs affect the microphone measurements. <strong>Right:</strong> marginal distribution of the microphone output <code>mic</code> for the colored regions on the left plot. The hatch modulates the amount of air that flows through the tunnel exhaust and over the microphone, having a slight effect on the distribution of its measurements. The effect depends on the fan loads, e.g., opening the hatch (<code>hatch=45</code>) decreases the airflow over the microphone when <code>load_in=1, load_out=0.01</code> (top), but increases it when <code>load_in=0.01, load_out=1</code> (bottom). The results for the uncalibrated measurement <code>mic_raw</code> are the same and not shown.</p></figcaption></figure>
{% endtab %}

{% tab title="Experiment" %}
To replicate the experiment for the figure using the [Remote Lab](https://docs.causalchamber.ai/remote-lab):

{% code overflow="wrap" %}

```python
# Connect to the lab
import causalchamber.lab as lab
rlab = lab.Lab(credentials_file = '.credentials', verbose=False)

# Declare experiment
experiment = rlab.new_experiment(chamber_id = 'wt-demo-ch4lu', config='full')

# Wait for fans to stabilize after reset
experiment.wait(8_000)

# Randomize over res values and take measurements
import pandas as pd
inputs = pd.DataFrame({'load_in': [0.01] * 500 + [1] * 1000 + [0.01] * 2500,
                       'load_out': [0.01] * 2500 + [1] * 1000 + [0.01] * 500,
                       'hatch': [0] * 1000 + [45] * 500 + [0] * 1500 + [45] * 500 + [0] * 500})
experiment.from_df(inputs, n=1)

# Submit
experiment.submit(tag='loads-hatch-mic')
```

{% endcode %}
{% endtab %}
{% endtabs %}

***

#### `mot_enabled/max` $$\longrightarrow$$ `current_mot` , `current_mot_raw`

The variable `mot_max` controls the amount of electrical current delivered to the hatch motor, and `mot_enabled` switches the motor on or off. Thus, both affect the calibrated (`current_mot`) and uncalibrated (`current_mot_raw`) measurements of the electrical current drawn by the motor. As opposed to the [fan currents](#load_in-out-rpm_in-out-current_in-out-current_in-out_raw), the effect of `mot_max/enabled` on the current measurements is instantaneous, i.e., faster than the measurement rate ([Figure 12](#figure-12), left). The relationship between `mot_max` and `current_mot`, `current_mot_raw` is non-linear ([Figure 12](#figure-12), right).

{% tabs %}
{% tab title="Figure 12" %}

<figure><img src="https://3492874807-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUqYDL9yvLTNUYW7H1Q6t%2Fuploads%2FYrMqHvejd25p6a5tRFCo%2Fmot_max.svg?alt=media&#x26;token=dc45ef1f-c8ac-43fe-8320-e960f14afbf1" alt=""><figcaption><p><strong>Left:</strong> calibrated motor current (<code>current_mot</code>) under an impulse on the input <code>mot_max</code> , when the motor is enabled (<code>mot_enabled=1</code>, blue) and when it is disabled (<code>mot_enabled=0</code>, yellow). <strong>Right:</strong> measurements of the calibrated motor current (<code>current_mot</code>) for different values of <code>mot_max</code>, when the motor is enabled (<code>mot_enabled=1</code>, blue) and when it is disabled (<code>mot_enabled=0</code>, yellow). The behaviour of the uncalibrated measurement <code>current_mot_raw</code> is the same and not shown.</p></figcaption></figure>
{% endtab %}

{% tab title="Experiment" %}
To replicate the experiment for the left panel (impulse) using the [Remote Lab](https://docs.causalchamber.ai/remote-lab):

{% code overflow="wrap" %}

```python
# Connect to the lab
import causalchamber.lab as lab
rlab = lab.Lab(credentials_file = '.credentials', verbose=False)

# Declare experiment
experiment = rlab.new_experiment(chamber_id = 'wt-ptdm-73iz', config='full')

# Toggle motor on and off
for enabled in [0,1]:
    experiment.set('mot_enabled', enabled)
    #   Apply impulse
    experiment.set('mot_max', 0)
    experiment.measure(20)
    experiment.set('mot_max', 4095)
    experiment.measure(30)
    experiment.set('mot_max', 0)
    experiment.measure(50)

# Submit
experiment.submit(tag='mot-max-impulse')
```

{% endcode %}

For the right panel:

{% code overflow="wrap" %}

```python
# Connect to the lab
import causalchamber.lab as lab
rlab = lab.Lab(credentials_file = '.credentials', verbose=False)

# Declare experiment
experiment = rlab.new_experiment(chamber_id = 'wt-ptdm-73iz', config='full')

# Set motor current at random
rng = np.random.default_rng(134)
for enabled in [0,1]:
    experiment.set('mot_enabled', enabled)
    for i in range(1000):
        experiment.set('mot_max', rng.choice(np.arange(4096)))
        experiment.measure(n=1)

# Submit
experiment.submit(tag='mot-max-random')
```

{% endcode %}
{% endtab %}
{% endtabs %}

### External influences

The tunnel barometers producing the measurements `pressure_upwind/downwind/intake/ambient` are all affected by natural variations in [local atmospheric pressure](https://barometricpressure.app/zurich#History) at our location in Zurich ([Figure 13](#figure-13), left). In other words, atmospheric pressure acts as a confounding factor between these measurements.

**Controlling for local atmospheric pressure & sensor drift**

The measurements produced by the ambient barometer (`pressure_ambient`) are unaffected by the other chamber variables (excluding [osr\_pressure\_ambient](#osr_-pressure)), and act as a proxy for the local atmospheric pressure. Subtracting them from the other measurements can partially remove its effect. However, since all barometers experience sensor drift ([Figure 13](#figure-13), right), the effect cannot be completely removed by this simple approach.

The sensor drift, which over time converges to a stable point ([Figure 13](#figure-13), right), can create an additional source of correlation between the barometer measurements.

{% tabs %}
{% tab title="Figure 13" %}

<figure><img src="https://3492874807-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUqYDL9yvLTNUYW7H1Q6t%2Fuploads%2Fj6msi5I4HVSQDb0zsIlp%2Fambient_pressure_and_drift.png?alt=media&#x26;token=de39f230-ffec-4d72-9df4-aec428ca4c15" alt=""><figcaption><p><strong>Left</strong>: measurements from the tunnel barometers over the span of 3 hours, showing the effect of variations in the <a href="https://barometricpressure.app/zurich#History">local atmospheric pressure</a> at our facility in Zurich. The fans are powered off, and all chamber inputs and parameters are kept constant. <strong>Right</strong>: drift in the barometer sensors, visible when controlling for the effect of ambient atmospheric pressure, i.e., by subtracting the <code>pressure_ambient</code> measurement, which is unaffected by the other chamber variables (see also <a href="#figure-13">Figure 13</a>).</p></figcaption></figure>
{% endtab %}

{% tab title="Experiment" %}
To replicate the experiment for the plot using the [Remote Lab](https://docs.causalchamber.ai/remote-lab):

{% code overflow="wrap" %}

```python
import causalchamber.lab as lab
rlab = lab.Lab(credentials_file = '.credentials', verbose=False)

# Define experiment
experiment = rlab.new_experiment('wt-ptdm-73iz', 'full')

# Set barometers to maximum precision (oversampling)
for bar in ['upwind', 'downwind', 'intake', 'ambient']:
    experiment.set(f'osr_pressure_{bar}', 3)

# Turn off fans and wait for them to stop turning
experiment.set('load_in', 0)
experiment.set('load_out', 0)
experiment.wait(30_000)

# Take measurements
experiment.measure(n=100_000)

# Submit
experiment.submit(tag='barometers-influences')
```

{% endcode %}
{% endtab %}
{% endtabs %}

### Variables table

Below is a glossary of the chamber variables discussed on this page.

{% hint style="info" %}
**Note:** see the documentation of each [hardware configuration](#hardware-configurations) for its complete list of variables and their default & valid values.
{% endhint %}

<table data-header-hidden="false" data-header-sticky><thead><tr><th width="206.615478515625" align="right">Variable</th><th>Description</th></tr></thead><tbody><tr><td align="right"><code>hatch</code></td><td>The set position of the hatch, in degrees. The hatch is closed at 0º and open at ± 45º.</td></tr><tr><td align="right"><code>hatch_angle</code></td><td>The position of the hatch, in degrees, as measured by the encoder of the motor.</td></tr><tr><td align="right"><code>mot_steps</code></td><td>The steps-per-revolution of the stepper motor controlling the hatch. Higher values mean a higher motor resolution, i.e., more precise positioning (<a href="#figure-6">Figure 6</a>).</td></tr><tr><td align="right"><code>mot_enabled</code></td><td>Enables (1) or disables (0) the motor controlling the hatch. If the motor is disabled (0), setting <code>hatch</code> will have no effect on the actual position of the hatch (<a href="#figure-6">Figure 6</a>).</td></tr><tr><td align="right"><code>mot_max</code></td><td>Regulates the maximum current drawn by the motor controlling the hatch. At low current levels the motor may lose torque and start missing steps, resulting in a mismatch between the set position <code>hatch</code> and the actual hatch angle (<a href="#figure-6">Figure 6</a>).</td></tr><tr><td align="right"><code>current_mot</code></td><td>The measurement (in Amperes) of the electric current drawn by the motor controlling the hatch (<a href="#figure-12">Figure 12</a>).</td></tr><tr><td align="right"><code>current_mot_raw</code></td><td>The uncalibrated measurement, i.e., the raw ADC output, corresponding to the measurement <code>current_mot</code>.</td></tr><tr><td align="right"><code>offset_current_mot</code></td><td>The reference voltage (offset) of the ADC producing the <code>current_mot</code> and <code>current_mot_raw</code> measurements. The actual reference voltage (in Volts) is given by <span class="math">5 \times \frac{\text{offset\_current\_mot}}{4095}.</span> Because the signal from the current sensor is passed through an inverting amplifier, higher values of <code>offset_current_mot</code> result in higher values of <code>current_mot_raw</code> (<a href="#figure-9">Figure 9</a>).</td></tr><tr><td align="right"><code>sps_current_mot</code></td><td>The data rate of the ADC producing the <code>current_mot</code> and <code>current_mot_raw</code> measurements. Lower values mean the ADC accumulates more readings to produce a single measurement, reducing noise but also lowering the measurement speed (<a href="#figure-9">Figure 9</a>).</td></tr><tr><td align="right"><code>res_current_mot</code></td><td>The resolution of the ADC producing the <code>current_mot</code> and <code>current_mot_raw</code> measurements. Higher values mean a higher resolution, where a smaller voltage range is mapped to the ADC output range <span class="math">\{-32768, \ldots, 32767\}</span>. The reading will saturate, i.e., clamp at <code>-32768</code> or <code>32767</code>, if the input voltage exceeds the set range (<a href="#figure-9">Figure 9</a>).</td></tr><tr><td align="right"><code>load_in</code></td><td>The load of the intake fan, corresponding to the <a href="https://en.wikipedia.org/wiki/Pulse-width_modulation#Duty_cycle">duty cycle</a> of the pulse-width-modulation (PWM) signal that controls its speed. At higher values, the fan consumes more power and turns faster. At 0, the complete fan is powered off, including the tachometer; the measurement of fan speed (<code>rpm_in</code>) remains constant at the last measured value.</td></tr><tr><td align="right"><code>rpm_in</code></td><td>The speed of the intake fan in revolutions per minute.</td></tr><tr><td align="right"><code>res_rpm_in</code></td><td>The resolution of the <a href="https://en.wikipedia.org/wiki/Tachometer">tachometer</a> that measures the speed of the intake fan (<a href="#figure-4">Figure 4</a>), where 1 corresponds to microseconds (higher resolution) and 0 to milliseconds (lower resolution).</td></tr><tr><td align="right"><code>load_out</code></td><td>The load of the exhaust fan, corresponding to the <a href="https://en.wikipedia.org/wiki/Pulse-width_modulation#Duty_cycle">duty cycle</a> of the pulse-width-modulation (PWM) signal that controls its speed. At higher values, the fan consumes more power and turns faster. At 0, the complete fan is powered off, including the tachometer; the measurement of fan speed (<code>rpm_out</code>) remains constant at the last measured value.</td></tr><tr><td align="right"><code>rpm_out</code></td><td>The speed of the exhaust fan in revolutions per minute.</td></tr><tr><td align="right"><code>res_rpm_out</code></td><td>The resolution of the <a href="https://en.wikipedia.org/wiki/Tachometer">tachometer</a> that measures the speed of the exhaust fan (<a href="#figure-4">Figure 4</a>), where 1 corresponds to microseconds (higher resolution) and 0 to milliseconds (lower resolution).</td></tr><tr><td align="right"><code>pressure_intake</code></td><td>The air pressure, in pascals, measured by the barometer placed at the tunnel intake.</td></tr><tr><td align="right"><code>osr_pressure_intake</code></td><td>The oversampling rate of the intake barometer, which determines how many consecutive readings are averaged to produce a single measurement (<a href="#figure-8">Figure 8</a>).</td></tr><tr><td align="right"><code>pressure_ambient</code></td><td>The air pressure, in pascals, measured by the outer barometer. This is the ambient pressure outside the chamber.</td></tr><tr><td align="right"><code>osr_pressure_ambient</code></td><td>The oversampling rate of the ambient barometer, which determines how many consecutive readings are averaged to produce a single measurement (<a href="#figure-8">Figure 8</a>).</td></tr><tr><td align="right"><code>pressure_downwind</code></td><td>The air pressure, in pascals, measured by the barometer inside the tunnel placed facing away from the airflow.</td></tr><tr><td align="right"><code>osr_pressure_downwind</code></td><td>The oversampling rate of the downwind barometer, which determines how many consecutive readings are averaged to produce a single measurement (<a href="#figure-8">Figure 8</a>).</td></tr><tr><td align="right"><code>pressure_upwind</code></td><td>The air pressure, in pascals, measured by the barometer inside the tunnel placed facing into the airflow.</td></tr><tr><td align="right"><code>osr_pressure_upwind</code></td><td>The oversampling rate of the upwind barometer, which determines how many consecutive readings are averaged to produce a single measurement (<a href="#figure-8">Figure 8</a>).</td></tr><tr><td align="right"><code>current_in</code></td><td>The measurement of electric current drawn by the intake fan, in Amperes.</td></tr><tr><td align="right"><code>current_in_raw</code></td><td>The uncalibrated measurement, i.e., the raw ADC output, corresponding to the measurement <code>current_in</code>.</td></tr><tr><td align="right"><code>offset_current_in</code></td><td>The reference voltage (offset) of the ADC producing the <code>current_in</code> and <code>current_in_raw</code> measurements. The actual reference voltage (in Volts) is given by <span class="math">5 \times \frac{\text{offset\_current\_in}}{4095}.</span> Because the signal from the current sensor is passed through an inverting amplifier, higher values of <code>offset_current_in</code> result in higher values of <code>current_in_raw</code> (<a href="#figure-9">Figure 9</a>).</td></tr><tr><td align="right"><code>sps_current_in</code></td><td>The data rate of the ADC producing the <code>current_in</code> and <code>current_in_raw</code> measurements. Lower values mean the ADC accumulates more readings to produce a single measurement, reducing noise but also lowering the measurement speed (<a href="#figure-9">Figure 9</a>).</td></tr><tr><td align="right"><code>res_current_in</code></td><td>The resolution of the ADC producing the <code>current_in</code> and <code>current_in_raw</code> measurements. Higher values mean a higher resolution, where a smaller voltage range is mapped to the ADC output range <code>[-32768, 32767]</code>. The reading will saturate, i.e., clamp at <code>-32768</code> or <code>32767</code>, if the input voltage exceeds the set range (<a href="#figure-9">Figure 9</a>).</td></tr><tr><td align="right"><code>current_out</code></td><td>The measurement of electric current drawn by the exhaust fan, in Amperes.</td></tr><tr><td align="right"><code>current_out_raw</code></td><td>The uncalibrated measurement, i.e., the raw ADC output, corresponding to the measurement <code>current_out</code>.</td></tr><tr><td align="right"><code>offset_current_out</code></td><td>The reference voltage (offset) of the ADC producing the <code>current_out</code> and <code>current_out_raw</code> measurements. The actual reference voltage (in Volts) is given by <span class="math">5 \times \frac{\text{offset\_current\_out}}{4095}.</span> Because the signal from the current sensor is passed through an inverting amplifier, higher values of <code>offset_current_out</code> result in higher values of <code>current_out_raw</code> (<a href="#figure-9">Figure 9</a>).</td></tr><tr><td align="right"><code>sps_current_out</code></td><td>The data rate of the ADC producing the <code>current_out</code> and <code>current_out_raw</code> measurements. Lower values mean the ADC accumulates more readings to produce a single measurement, reducing noise but also lowering the measurement speed (<a href="#figure-9">Figure 9</a>).</td></tr><tr><td align="right"><code>res_current_out</code></td><td>The resolution of the ADC producing the <code>current_out</code> and <code>current_out_raw</code> measurements. Higher values mean a higher resolution, where a smaller voltage range is mapped to the ADC output range <code>[-32768, 32767]</code>. The reading will saturate, i.e., clamp at <code>-32768</code> or <code>32767</code>, if the input voltage exceeds the set range (<a href="#figure-9">Figure 9</a>).</td></tr><tr><td align="right"><code>mic</code></td><td>The measurement of the sound level captured by the microphone, in Volts.</td></tr><tr><td align="right"><code>mic_raw</code></td><td>The uncalibrated measurement, i.e., the raw ADC output, corresponding to the measurement <code>mic</code>.</td></tr><tr><td align="right"><code>offset_mic</code></td><td>The reference voltage (offset) of the ADC producing the <code>mic</code> and <code>mic_raw</code> measurements. The actual reference voltage (in Volts) is given by <span class="math">5 \times \frac{\text{offset\_mic}}{4095}.</span> Higher values of <code>offset_mic</code> result in lower values of <code>mic_raw</code> (<a href="#figure-10">Figure 10</a>).</td></tr><tr><td align="right"><code>sps_mic</code></td><td>The data rate of the ADC producing the <code>mic</code> and <code>mic_raw</code> measurements. Lower values mean the ADC accumulates more readings to produce a single measurement, reducing noise but also lowering the measurement speed (<a href="#figure-10">Figure 10</a>).</td></tr><tr><td align="right"><code>res_mic</code></td><td>The resolution of the ADC producing the <code>mic</code> and <code>mic_raw</code> measurements. Higher values mean a higher resolution, where a smaller voltage range is mapped to the ADC output range <code>[-32768, 32767]</code>. The reading will saturate, i.e., clamp at <code>-32768</code> or <code>32767</code>, if the input voltage exceeds the set range (<a href="#figure-10">Figure 10</a>).</td></tr></tbody></table>

### Citation

If you use this documentation, our [open-source datasets](https://github.com/juangamella/causal-chamber), or [Remote Lab](https://docs.causalchamber.ai/remote-lab) in your scientific work, please consider citing:

{% code overflow="wrap" %}

```bibtex
﻿@article{gamella2025chamber,
  author={Gamella, Juan L. and Peters, Jonas and B{\"u}hlmann, Peter},
  title={Causal chambers as a real-world physical testbed for {AI} methodology},
  journal={Nature Machine Intelligence},
  doi={10.1038/s42256-024-00964-x},
  year={2025},
}
```

{% endcode %}

### References

> \[[PDF](https://www.nature.com/articles/s42256-024-00964-x)] Gamella, J.L., Peters, J. & Bühlmann, P. Causal chambers as a real-world physical testbed for AI methodology. *Nat Mach Intell* 7, 107–118 (2025).
