For the complete documentation index, see llms.txt. This page is also available as Markdown.

Generating real data with a known causal structure

How to use the Chambers to generate data from a physical system with a known causal structure.

The Light Tunnel Mk2 and the Wind Tunnel Mk2 come with a causal ground truth, built from background knowledge and empirically validated using randomized experiments. We express this ground truth as a map of effects, a directed graph showing how the chamber variables affect each other.

Right click to download the image (available under a CC BY-NC 4.0 non-commercial license).

For an exhaustive description of each edge and a visualization of the corresponding effect, see the documentation for this chamber.

Using the ground truth graph

The graphs above can be interpreted as a causal ground truth, as formalized in Gamella et al. (2025, Appendix V), i.e., an edge X \to Y signifies that—for some value of the other chamber inputs—an intervention on X will change the distribution of subsequent measurements of Y. This is the statement that we empirically validate, giving us a common framework for both instantaneous and time-lagged effects. There are two things you should consider:

  1. The graph should not be taken as a graphical model of statistical dependencies, as external influences on the system may create additional dependencies between variables. These are documented here and here.

  2. We empirically validate each edge in the graph using a randomized controlled trial (RCT) with large sample sizes. This allows us to verify that an edge exists, i.e., there is a significant effect between two variables. On the other hand, the absence of an edge between two variables does not preclude the existence of a causal effect between them; it simply means we could not find a significant effect.

Ultimately, such issues arise with any real, non-simulated system. They are a symptom of evaluating learned causal models by comparing them with a "gold-standard model", i.e., validating a model with another model. This only makes sense in a computer simulation, where the data-generating model is the truth. A more robust (and natural) way to evaluate a learned causal model is to verify its interventional predictions directly—see the section on generating interventional data.

Generating data: an example

So, how do we generate data from these causal structures? Here is a general recipe to sample from the complete graph or any subgraph of your choice. As the running example for the rest of this guide, we will focus on the following subgraph from the Light Tunnel Mk2.

The running example for this section.

Here's how it works.

1

Select your variables

Choose the subset of variables that should be part of the dataset. If you are sampling from one of the standard hardware configurations (i.e., exogenous inputs & sensor parameters), then the map of effects directly becomes the induced subgraph of these variables. See Adding causal effects to sample from more complex causal structures.

Example: in the graph above, our variables are green, pol_1, ir_1 and ir_3

2

Define a distribution over your inputs

Now, define the distribution or stochastic process from which you will sample your inputs. You can sample them independently or from an SCM, introducing additional dependencies. Check the variable table of the corresponding hardware configuration for the valid values.

Example: after checking the variables table, we will sample our inputs green and pol_1 independently and uniformly at random from {0,1,...,128} and [0, 90], respectively.

3

Set your inputs and take measurements

Now, for each draw of your inputs, use the SET instruction to set them in the hardware, and the MEASURE instruction to take a measurement. If the chamber has lagged effects (e.g., load_in \to rpm_in in the Wind Tunnel) and you want to measure after the system reaches equilibrium, you can add a WAIT instruction.

Putting it together

Here is the complete code for the example above, using the experiment queue to collect the data.

You can then monitor the experiment and download the data once it's finished. To simplify the above syntax, you can also define the experiment from a pandas dataframe.

Let's visualize the results!

Visualization of the resulting data, with the inputs green and pol_1 on the x-axis, and the sensor measurements ir_1 and ir_3 on the y-axis. As expected from the ground-truth graph, green has an effect on both measurements, whereas pol_1 affects only ir_3. Following Malus' law, as pol_1 approaches 90 degrees, the polarizer chain blocks most of the light reaching the third sensor, reducing the effect of green on ir_3. See the Chamber diagram for the placement of the different components.

Modifying causal effects

The Chambers are designed so that every effect between two variables can be modified by means of a third variable (i.e., a mechanism change). For example, we can manipulate the parameters of all sensors (marked with a P in the graphs), controlling their behavior and the resulting measurements. For example, we can use diode_ir_1 to change the photodiode used to produce the measurement ir_1, altering the incoming effects.

To see this, let's repeat the previous experiment, but set diode_ir_1=1 to use a smaller photodiode instead.

By using a smaller photodiode, we have reduced the sensor's sensitivity. If we plot the data, we can see that the effect between green and ir_1 is now "weaker" when compared to the original experiment (shown in gray).

Repeating the experiment in Figure 1 (data shown in gray) but using a smaller photodiode (diode_ir_1=1) to produce the measurement ir_1. The effect of green on ir_1 is weaker (smaller slope), while the other variables remain unaffected.

Removing causal effects

In some cases, we can also completely remove a causal effect. For example, in the Light Tunnel Mk2

  • we can disable the polarizer motors by setting the variables mot_1_enabled and mot_2_enabled to zero; changes in pol_1 (pol_2) will no longer affect the actual polarizer position, eliminating the outgoing edges from these variables.

  • we can use the variables res_* and offset_* to saturate the analog sensors that produce the measurements current_* and angle_* , removing the edges coming into these variables.

As an example, let's repeat the experiment from Figure 1, but disable the polarizer motor to cancel the effect of pol_1 on ir_3.

To do this, we need to set mot_1_enabled = 0 before starting the experiment:

Let's visualize the result. As expected, pol_1 no longer has an effect on ir_3. All other variables remain the same.

Repeating the experiment in Figure 1 (data shown in gray) but disabling the motor of the first polarizer by setting mot_1_enabled=0. As a result, pol_1 no longer has an effect on the polarizer position, removing its effect on ir_3.

Adding causal effects

In the standard hardware configurations, all the inputs and sensor parameters (marked with I and P in the graphs) must be set by the user. As a result, these are exogenous variables, and the ground truth is a bipartite graph, with all edges directed from an input or sensor parameter to a sensor measurement (marked with M in the graph).

In addition to sampling the inputs and parameters from an SCM, we can introduce additional effects by setting inputs or sensor parameters as functions of sensor measurements. This is done automatically by the chamber in some hardware configurations; for example, in the linked_leds and linked_leds_sigmoid configurations, the chamber sets led_2_uv and led_3_uv as a function of ir_1 and ir_2, respectively.

By operating the chamber in interactive (real-time) mode, you can also perform these operations on your end, allowing you to add arbitrary effects between variables. As an illustration, let's use this technique to add an additional effect from ir_1 to led_3_uv in the running example of this section:

The code is similar to the previous experiments, but we connect to a chamber in real-time and split our measurement step into two parts: first, we measure ir_1, and then we set led_3_uv and measure ir_3.

Let's visualize the results. As before, we plot the original experiment from Figure 1 in gray. To visualize the new causal effect from ir_1 to ir_3—resulting from the new edge ir_1 \to led_3_uv and the existing effect led_3_uv \toir_3—we color the new datapoints as follows: the fill color corresponds to the value of pol_1, and the edge color to the value of ir_1.

Repeating the experiment in Figure 1 (data shown in gray) with an additional edge from ir_1 to led_3_uv. This creates a causal relationship between ir_1 and ir_3. To visualize this new dependency, we color the edge of each datapoint according to the value of ir_1.

A note of caution

When adding new causal effects, you need to be careful not to create dependencies between successive measurements. For example, let's try to add the edge ir_1 \to red to our running example:

If we naively implement this into our two-step procedure above, we will create a dependency between successive measurements, breaking the i.i.d. assumption. Because red also affects ir_1 , the value of ir_1 in each measurement step will depend on the value of red in the previous step.

As a simple workaround, you can set the relevant inputs (red in this case) to a constant value at the beginning of each measurement cycle, breaking the dependency.

Generating interventional data

There are several ways to generate interventional data, depending on which variable receives the intervention.

  • If they are exogenous, you can intervene on inputs and sensor parameters (marked by I and P in the graphs) by changing the distribution or the process from which you sample them.

  • To intervene on sensor measurements (marked by M in the graphs), you can modify an underlying sensor parameter—or another third variable—while excluding it from the dataset. You can also intervene on hidden confounders (see Introducing confounders below).

  • You can also perform mechanism changes by modifying or removing causal effects.

An example

Let's see how this works for the running example of this section. We will perform interventions on the input green and the sensor measurement ir_1.

In this case, the intervention consists of changing the distribution from which we sample the input green, e.g., from a uniform to a truncated normal.

Let's visualize the results!

Repeating the experiment in Figure 1 (data shown in gray) with an intervention on green and an intervention on ir_1 (through the variable led_1_uv).

Introducing confounders

To introduce a hidden confounder or a latent variable, you can sample an input from a distribution or process of your choice, but exclude it from the dataset. This will introduce confounding between the sensor measurements affected by this input.

In our running example, we can sample the input red to create a confounder between the measurements ir_1 and ir_3.

Let's run the experiment and visualize the results.

Repeating the experiment in Figure 1 (data shown in gray) with the additional input red acting as a latent common cause between ir_1 and ir_3.

Citation

If you use this documentation, our open-source datasets, or the Remote Lab in your scientific work, please consider citing:

To directly reference this blog post, you can cite

References

[Gamella 2025] [PDF] Gamella, Juan L., Peters, Jonas & Bühlmann, Peter. Causal chambers as a real-world physical testbed for AI methodology. Nat Mach Intell 7, 107–118 (2025).

Last updated