Generating a Room Temperature Heatmap

This guide assumes you are familiar with the DT ecosystem and have access to temperature sensors and Cloud Connectors. If not, take a look at our Getting Started guide and consider ordering a sensor kit from our Webshop

 

Introduction

Continuously logging temperature data in an office- or industry environment can provide a range of benefits. Be it optimizing energy usage by minimizing heat loss or tracking individual equipment temperatures, this information could be useful to gain better insight. Due to their small size, robustness, and long battery life, Disruptive Technologies (DT) Wireless Temperature Sensors can easily be employed to collect temperature data streams for almost any environment.

In this application note, a method of combining several DT temperature- and door-proximity sensors to generate a temperature heatmap is proposed. By using room layout features, the heatmap gradients are calculated by inverse distance weighting (IDW) [1]. The distances are found by euclidean path-finding [2], where traversal can not occur through walls, only around corners and through doors. The result is a heatmap where rooms are separated by the walls between them but allows for temperature gradients to expand through doors and open spaces like the wave of a droplet.

000002260.png

Figure 1: Temperature heatmap on a particularly hot day. Here, temperature sensors are represented by an 'x'. Doors can be in an open (green) or closed (red) state. An animation showing the temperature change during the entire day can be found at the end of this application note.

 

DT Studio Project Configuration

The implementation is built around using the developer API to interact with a single DT Studio project containing all sensors for which the heatmap is generated. If not already done, a project needs to be created and configured to enable API functionalities.

Project Authentication

For authenticating the developer API against the DT Studio project, three separate authentication details have to be located and generated, later to be used in the example code. A step-by-step setup guide can be found by following this link.

Adding sensors to the project

Any number and type of sensors can be included in the project. Only those included in the room layout file, which will be discussed later, are fetched by the API. The use of labels is not necessary for this implementation. The option to move sensors from one project to another can be found when viewing a particular sensor in DT Studio.

studio.png

Figure 2: DT Studio sensor overview menu where, among other things, sensors can be moved to different projects.

 

Sensor Placement

For the heatmap to produce an output consistent with the ambient temperature in the room independent of external sources, some thought should be put into sensor placement. During testing, it was found that a relatively large temperature difference could be seen depending on the height at which the sensor was placed, which were expected as warm air rises. The following recommendations should, therefore, be considered when placing the sensors.

  • Avoid placing sensors in direct sunlight.
  • Avoid placing sensors near air-conditioning, radiators, or other heat sources. This includes people, which are comparable to a 100-watt heat-source.
  • Placement height should be somewhat consistent between sensors. Approximately chest-level height was found to provide the most representative results.

IMG_1108.jpeg

Figure 3: Disruptive Technologies Temperature Sensors mounted on a wall.

 

Example Code

An example code repository is provided in this application note. It illustrates one way of building a heatmap from multiple sensor streams and could serve as a precursor for further development and implementation. It uses the Developer API to interface with the DT Studio project and constructs a virtual representation of the environment for which details are provided by the user in a room layout JSON input argument.

Source Access 

The example code source is publicly hosted on the official Disruptive Technologies GitHub account under the MIT license. It can be found by following this link.

Environment Setup

All code has been written in and tested for Python 3. While not required, it is recommended to use a virtual environment to avoid conflicts. Additional dependencies can be installed using pip and the provided requirements text file.

pip3 install -r requirements.txt 

Using the details found during the project authentication section, edit the following lines in sensor_stream.py to authenticate the API with your DT Studio project.

USERNAME   = "SERVICE_ACCOUNT_KEY"    # this is the key
PASSWORD   = "SERVICE_ACCOUNT_SECRET" # this is the secret
PROJECT_ID = "PROJECT_ID"             # this is the project id 

Defining the Rooms Layout

In order to provide the layout of your environment to the example code project, the JSON file format is used and should be provided as an input argument. If no layout is provided, the project will default to a sample layout located in ./config/sample_layout.json. Please see "Layout Construction" under the Implementation section for details about setting up your environment layout. It is recommended that the sample layout structure is understood before designing your own.

Usage

If the example code is correctly authenticated to the DT Studio project as described above, running the script sensor_stream.py will generate and show the sample layout. 

python3 sensor_stream.py 

If a custom layout file is provided, the script will start streaming data from all initialized sensors in the layout for which a heatmap is continuously updated as new data arrive. For more advanced usage, such as setting a historical time frame to pull data from, provide any of the following extra arguments and values.

usage: sensor_stream.py [-h] [--layout] [--starttime] [--endtime] [--timestep]
                        [--plot] [--debug] [--read]

Heatmap generation on Stream and Event History.

optional arguments:
  -h, --help    show this help message and exit
  --layout      Path to json layout file.
  --starttime   Event history UTC starttime [YYYY-MM-DDTHH:MM:SSZ].
  --endtime     Event history UTC endtime [YYYY-MM-DDTHH:MM:SSZ].
  --timestep    Heatmap update period.
  --plot        Plot the estimated desk occupancy.
  --debug       Disables multithreading for debug visualization.
  --read        Import cached distance maps. 

The arguments --starttime and --endtime should be of the format YYYY-MM-DDThh:mm:ssZ, where YYYY is the year, MM the month, and DD the day. Likewise, hh, mm, and ss are the hour, minutes, and seconds respectively. Notice the separator, T, and Z, which must be included. It should also be noted that the time is given in UTC. Local timezone corrections should, therefore, be made accordingly.

With a default value of 1 hour, the --timestep argument represents the number of seconds between each time the heatmap is updated when looking at historical data. During a stream, the heatmap will update every new event regardless.

 

Implementation

With the aim of generating a heatmap that continuously updates as new temperature data arrives in the stream, the implementation is written such that any processing intensive task is done during initialization. By pre-calculating a Euclidean distance map for each sensor covering the entire room layout, the inverse distance weighted (IDW) temperature value at any point can quickly be calculating using the maps as lookup tables. Therefore, to update the heatmap, only a single scan of the grid is required. This results in a total delay of only a few seconds from sensor sampling heartbeat to heatmap update. The maps are written to temporary files and can be loaded on successive runs. 

Layout Construction

To illustrate how custom room layouts are generated, the provided sample layout will be used as an example. It consists of 2 rooms, a door connecting them, and a few sensors with some initial temperature. The constructed sample layout is shown in figure 4, where sensors are represented by 'x', and objects of interest (OOF) by a circle. An OOF is simply a temperature value that is displayed without affecting the heatmap itself.

sample_layout.png

Figure 4: Sample layout provided with the example code. Here, an 'x' represents a sensor, a green filled line an open door, and a circle an OOF.

The following lines show the JSON format used to generate the room layout shown in figure 4. Each room in the rooms list is defined by a name, a list of corners, and the sensors that exist in the room. Observe how the corners are listed in a clockwise manner. Following this template, the layout can be expanded to an undefined number of rooms and sensors.

{
    "rooms": [
        {
            "name": "room0",
            "corners": [
                {"x": 0.0, "y": 0.0},
                {"x": 0.0, "y": 5.0},
                {"x":10.0, "y": 5.0},
                {"x":10.0, "y": 0.0}
            ],
            "sensors": [
                {"x": 0.2, "y": 2.5, "sensor_id": "xxxxx", "t0": 23},
                {"x": 5.0, "y": 4.8, "sensor_id": "xxxxx", "t0": 22},
                {"x": 8.0, "y": 0.2, "sensor_id": "xxxxx", "t0": 24}
            ]
        },
        {
            "name": "room1",
            "corners": [
                {"x":10.0, "y": 0.0},
                {"x":10.0, "y":8.0},
                {"x":15.0, "y":8.0},
                {"x":15.0, "y": 0.0}
            ],
            "sensors": [
                {"x":12.5, "y": 7.5, "sensor_id": "xxxxx", "t0": 22}
            ]
        }
    ],
    "doors": [
        {
            "p1": {"x":10.0, "y": 3.0}, 
            "p2": {"x":10.0, "y": 4.0}, 
            "room1": "room0", 
            "room2": "room1", 
            "sensor_id": "xxxxx",
            "closed": false
        }
    ],
    "oofs": [
        {"x": 17.5, "y": 8.3, "sensor_id": "xxxxx"}
    ]
}

Sensors can be initialized with a temperature t0. This value is only used if the script does not connect to the API, such as when using the sample layout. Then, the heatmap will be generated using said initial values instead. These can, however, be omitted and is meant for debugging purposes. The same goes for initializing doors with a closed boolean.

Considerations when designing your layout:

  • Sensors must not be placed exactly on walls. This will create a line-of-sight issue when calculating the euclidean distance map. Sensors should therefore be slightly offset towards the room center. Any positive non-zero value will suffice.
  • The corners of a room must be provided in chronological order, as two adjacent corners define a single wall. 
  • Two adjacent rooms should only share a single door. While a single room can have doors leading into multiple rooms, only a single door should be used between any one pair of rooms. This is to avoid generating an exponential amount of distance maps.

Euclidean Distance Map Population

In order to produce a heatmap in which each sensor's contribution is weighted by their distance, the distance from each sensor to every point in the grid needs to be known. This would be rather simple if walls were not to be considered. However, in order to create a more believable heatmap, the distance from a sensor to any point in any room can only be calculated as a path through doors, and not walls, as is the case with airflow.

For every sensor, a distance map representing the shortest Euclidean distance to every point in the grid is created where the euclidean distance is given by

\begin{equation}
d = \sqrt{\sum_{i=1}^n(q_i-p_i)^2},
\end{equation}
where \(q_i\) and \(p_i\) are the \(i\)-th coordinate values of two points in an n-dimensional space. The following code snippet represents the recursive function implemented to do a breadth-first search for the shortest path from a sensor to any door- and convex corner [3].

def find_shortest_paths(current, path, path_length):
    # append path with currently active point
    path.append(current)

    # stop if we've been here before on a shorter path
    if path_length > length of current.shortest_path:
        return path

    # copy path to current point to save it
    current.shortest_path = path

    # find candidate corners for path expansion
    candidates = find(doors and convex corners in line of sight of current)

    # recursively iterate candidates
    for c in candidates:
        # calculate distance from current to candidate
        new_distance = euclidean_distance(current.x, current.y, c.x, c.y)

        # next recursive step
        path = find_shortest_paths(c, path, length of path + new_distance)
            
        # remove current step from path
        path.pop()
            
    return path

Essentially, for each recursive step starting at the sensor position, all convex corners and doors in line of sight are defined as candidates for the next step. Here, the line of sight is defined as a point to which a direct line can be stretched without intersecting a wall. Concave corners are ignored as they will never represent the shortest path. As they are visited, each point subsequently saves the shortest path to it, as shown in figure 6, used in the next step to populate the euclidean distance map.

000000000.png

Figure 5: Shortest paths (red) from a sensor to all convex corners and doors in layout found through recursively locating all corners within line of sight at any step.

For each step in every recursive path found previously, the euclidean distance to all points in the grid within its line of sight is calculated. If the new distance plus the total length traveled by the path is shorter than the existing value, it is updated. The result is a grid in which each point represents the shortest possible distance from the initial sensor location, shown in figure 6, where an increase in brightness represents a longer distance traveled. Notice how the distance gradient does not increase through walls but instead enters a room only through a doorway as expected.

shortest_path.png

Figure 6: Euclidean distance grid population process where each point in the grid is filled with the shortest distance from the sensor, traveling through a path of convex corners and doors as necessary. Here, as distance increases, the color becomes lighter.

Updating the Heatmap

For every new temperature value that is received in the stream, the heatmap is recalculated. This is very quick as the distance from every sensor to every point is known from the previous steps. Therefore, the heatmap grid is scanned once, where for each grid point, the value is set to the inverse distance weighted (IDW) temperature of all sensors. The IDW is given by

\begin{equation}
d = \sum_{i=1}^n\frac{z_i}{d_i^p} / \sum_{i=1}^n\frac{1}{d_i^p},
\end{equation}
where \(n\) is the number of sensors, \(z_i\) the value of sensor \(i\), and \(d_i^p\) the distance to sensor \(i\) to the power of \(p\). It is a simple but flexible spatial interpolation method that produces a smooth gradient between sensors. Other methods, such as kriging statistical modeling or radial basis functions, could also be experimented with to produce the heatmap.

Door State

To produce a more natural heatmap, the temperature gradient in a room is only affected by sensors in other rooms - should the door between them be open. By using a Disruptive Technologies Proximity Sensors in a door frame, the heatmap can be automatically updated should doors open and/or close.

Due to calculating the euclidean distance map earlier, information regarding which doors the path pass through is known. Therefore, to determine whether or not a sensor should be involved in the IDW estimate for a given grid point during heatmap update, only a simple boolean check for the doors in each path has to be done with a negligible execution time impact. Figure 7 shows how the temperature gradient changes when a door between a cold and warm room opens (green) and closes (red).

output.gif

Figure 7: Difference in temperature gradient depending on whether or not the door between the two rooms are open (green) or closed (red).

 

Animated Result

The following animation represents the same day, as shown in figure 1, but includes the temperature variations for the whole day. A second in the animation represents 2 hours of real-time. Notice how doors open and close, and how the temperature spikes in the morning, before being brought back down to normal by the air-conditioning system kicking in.

output.gif

Figure 8: Animated heatmap for an entire day where the temperatures were particularly hot, showing how the air-conditioning system kicks in to suppress the heat.

 

Future Improvements

  • Currently, walls can only be defined as a straight line between two points. Spline interpolation could be implemented to solve this should it be necessary.
  • Doorways are currently implemented so that the temperature gradient only passes through the exact middle. This approximation reduces the recursive depth when finding the shortest path while having a negligible impact on the resulting heatmap. However, if a doorway is several meters wide, this would be noticeable and should be fixed.

 

References

  1. https://en.wikipedia.org/wiki/Inverse_distance_weighting
  2. https://en.wikipedia.org/wiki/Pathfinding
  3. https://en.wikipedia.org/wiki/Breadth-first_search