TCLab Historian

Basic logging

The tclab.Historian class provides data logging. Given an instance of a TCLab object, an historian is created with the commands

import tclab
lab = tclab.TCLab()
h = tclab.Historian(lab.sources)

The historian initializes a data log. The sources for the data log are specified in the argument to tclab.Historian. A default set of sources for an instance lab is given by lab.sources. The specification for sources is described in a later section.

The data log is updated by issuing a command

h.update(t)

Where t is the current clock time. If t is omitted the historian will calculate its own time.

[1]:
import tclab

TCLab = tclab.setup(connected=False, speedup=10)

with TCLab() as lab:
    h = tclab.Historian(lab.sources)
    for t in tclab.clock(20):
        lab.Q1(100 if t <= 10 else 0)
        print("Time:", t, 'seconds')
        h.update(t)
TCLab version 0.4.7dev
Simulated TCLab
Time: 0 seconds
Time: 1.1 seconds
Time: 2.0 seconds
Time: 3.1 seconds
Time: 4.1 seconds
Time: 5.0 seconds
Time: 6.1 seconds
Time: 7.0 seconds
Time: 8.2 seconds
Time: 9.0 seconds
Time: 10.0 seconds
Time: 11.0 seconds
Time: 12.0 seconds
Time: 13.1 seconds
Time: 14.0 seconds
Time: 15.1 seconds
Time: 16.1 seconds
Time: 17.2 seconds
Time: 18.0 seconds
Time: 19.2 seconds
Time: 20.0 seconds
TCLab Model disconnected successfully.

Accessing the Data Log from the Historian

Historian maintains a data log that is updated on each encounter of the .update() function. The list of variables logged by an Historian is given by

[2]:
h.columns
[2]:
['Time', 'T1', 'T2', 'Q1', 'Q2']

Individual time series are available as elements of Historian.fields. For the default set of sources, the time series can be obtained as

t, T1, T2, Q1, Q2 = h.fields

For example, here’s how to plot the history of temperature T1 versus time from the example above.

[3]:
%matplotlib notebook
import matplotlib.pyplot as plt

t, T1, T2, Q1, Q2 = h.fields
plt.plot(t, T1)
plt.xlabel('Time / seconds')
plt.ylabel('Temperature / °C')
plt.grid()

Accessing fields by name

To enable easy access of a particular field by name, Historian allows individual field access via the logdict dictionary. It also has a t property for quick access of the time field, so the above code could also have been written this way:

[4]:
%matplotlib notebook
import matplotlib.pyplot as plt

column = 'T1'
plt.plot(h.t, h.logdict['T1'])
plt.xlabel('Time / seconds')
plt.ylabel('Temperature / °C')
plt.grid()

Here is a simple code using these access methods to plot all the fields an Historian is logging:

[5]:
def plotlog(historian):
    t = historian.t
    fig, axes = plt.subplots(nrows=len(historian.fields) - 1,
                        ncols=1, sharex='col')
    for axis, column in zip(axes, historian.columns[1:]):
        axis.step(t, historian.logdict[column], where='post')
        axis.grid()
        axis.set_ylabel(column)
    plt.xlabel('Time / Seconds')

plotlog(h)

Tabular access

The entire data history is available from the historian as the attribute .log. Here we show the first three rows from the log. The columns are returned in the same order as .columns.

[6]:
h.columns
[6]:
['Time', 'T1', 'T2', 'Q1', 'Q2']
[7]:
h.log[:3]
[7]:
[(0, 20.949499999999997, 20.6272, 100, 0),
 (1.1, 20.949499999999997, 20.949499999999997, 100, 0),
 (2.0, 20.949499999999997, 20.949499999999997, 100, 0)]

Saving to a file

The log can be saved to a CSV file using .to_csv(). This is useful for spreadsheet access.

[8]:
h.to_csv('saved_data.csv')

Accessing log data using Pandas

Pandas is a widely use Python library for manipulation and analysis of data sets. Here we show how to access the tclab.Historian log using Pandas.

The log can be converted to a Pandas dataframe.

[9]:
import pandas as pd

df = pd.DataFrame.from_records(h.log, columns=h.columns, index='Time')
df.head()
[9]:
T1 T2 Q1 Q2
Time
0.0 20.9495 20.6272 100 0
1.1 20.9495 20.9495 100 0
2.0 20.9495 20.9495 100 0
3.1 20.9495 20.9495 100 0
4.1 20.9495 20.9495 100 0

The following cells provide examples of plots that can be constructed once the data log has been converted to a pandas dataframe.

[10]:
df.plot()
[10]:
<matplotlib.axes._subplots.AxesSubplot at 0x10a5e2cf8>
[11]:
df[['T1','T2']].plot(grid=True)
[11]:
<matplotlib.axes._subplots.AxesSubplot at 0x10aa7e8d0>

Specifying Sources for tclab.Historian

To create an instance of tclab.Historian, a set of sources needs to be specified. For many cases the default sources created for an instance of TCLab is sufficient. However, it is possible to specify additional sources which can be useful when implementing more complex algorithms for process control.

sources is specified as a list of tuples. Each tuple as two elements. The first element is a label for the source. The second element is a function that returns a value.

The following cell shows how to create a source with the label Power with a value equal to the estimated heater power measured in watts. This is created on the assumption that 100% of a maximum power of 200 corresponds to 4.2 watts.

[12]:
import tclab

TCLab = tclab.setup(connected=False, speedup=10)

with TCLab() as lab:
    sources = [
        ('T1', lambda: lab.T1),
        ('Power', lambda: lab.P1*lab.U1*4.2/(200*100))
    ]
    h = tclab.Historian(sources)
    for t in tclab.clock(20):
        lab.Q1(100 if t <= 10 else 0)
        print("Time:", t, 'seconds')
        h.update(t)
TCLab version 0.4.7dev
Simulated TCLab
Time: 0 seconds
Time: 1.0 seconds
Time: 2.0 seconds
Time: 3.0 seconds
Time: 4.1 seconds
Time: 5.1 seconds
Time: 6.1 seconds
Time: 7.0 seconds
Time: 8.0 seconds
Time: 9.0 seconds
Time: 10.1 seconds
Time: 11.2 seconds
Time: 12.1 seconds
Time: 13.2 seconds
Time: 14.2 seconds
Time: 15.0 seconds
Time: 16.1 seconds
Time: 17.0 seconds
Time: 18.0 seconds
Time: 19.0 seconds
Time: 20.1 seconds
TCLab Model disconnected successfully.
[13]:
import pandas as pd

df = pd.DataFrame.from_records(h.log, columns=h.columns, index='Time')
df.head()
[13]:
T1 Power
Time
0.0 20.9495 4.2
1.0 20.9495 4.2
2.0 20.9495 4.2
3.0 20.9495 4.2
4.1 20.9495 4.2

Functions with multiple returns

In some cases it is easier to calculate a number of different variables to be logged in one function, especially if intermediate results are used in following calculations. This can be accommodated by Historian by passing None as the function for subsequent values if a previous value returned a list of values.

[14]:
import tclab

TCLab = tclab.setup(connected=False, speedup=10)

def log_values():
    T1 = lab.T1
    T1Kelvin = T1 + 273.15
    power = lab.P1*lab.U1*4.2/(200*100)
    return T1, T1Kelvin, power

with TCLab() as lab:
    h = tclab.Historian([('T1', log_values),
                         ('T1Kelvin', None),
                         ('Power', None)])
    for t in tclab.clock(20):
        lab.Q1(100 if t <= 10 else 0)
        print("Time:", t, 'seconds')
        h.update(t)
TCLab version 0.4.7dev
Simulated TCLab
Time: 0 seconds
Time: 1.0 seconds
Time: 2.0 seconds
Time: 3.0 seconds
Time: 4.0 seconds
Time: 5.0 seconds
Time: 6.0 seconds
Time: 7.2 seconds
Time: 8.0 seconds
Time: 9.2 seconds
Time: 10.1 seconds
Time: 11.0 seconds
Time: 12.2 seconds
Time: 13.0 seconds
Time: 14.0 seconds
Time: 15.0 seconds
Time: 16.1 seconds
Time: 17.0 seconds
Time: 18.1 seconds
Time: 19.1 seconds
Time: 20.0 seconds
TCLab Model disconnected successfully.
[15]:
import pandas as pd

df = pd.DataFrame.from_records(h.log, columns=h.columns, index='Time')
df.head()
[15]:
T1 T1Kelvin Power
Time
0.0 20.9495 294.0995 4.2
1.0 20.9495 294.0995 4.2
2.0 20.9495 294.0995 4.2
3.0 20.9495 294.0995 4.2
4.0 20.9495 294.0995 4.2
[16]:
h.log
[16]:
[(0, 20.949499999999997, 294.0995, 4.2),
 (1.0, 20.949499999999997, 294.0995, 4.2),
 (2.0, 20.949499999999997, 294.0995, 4.2),
 (3.0, 20.949499999999997, 294.0995, 4.2),
 (4.0, 20.949499999999997, 294.0995, 4.2),
 (5.0, 21.2718, 294.42179999999996, 4.2),
 (6.0, 21.2718, 294.42179999999996, 4.2),
 (7.2, 21.594099999999997, 294.7441, 4.2),
 (8.0, 21.594099999999997, 294.7441, 4.2),
 (9.2, 21.594099999999997, 294.7441, 4.2),
 (10.1, 21.9164, 295.0664, 0.0),
 (11.0, 21.9164, 295.0664, 0.0),
 (12.2, 22.238699999999998, 295.3887, 0.0),
 (13.0, 22.238699999999998, 295.3887, 0.0),
 (14.0, 22.561, 295.71099999999996, 0.0),
 (15.0, 22.561, 295.71099999999996, 0.0),
 (16.1, 22.8833, 296.0333, 0.0),
 (17.0, 22.8833, 296.0333, 0.0),
 (18.1, 22.8833, 296.0333, 0.0),
 (19.1, 23.205599999999997, 296.3556, 0.0),
 (20.0, 23.205599999999997, 296.3556, 0.0)]

Sessions

It is possible to run multiple experiments using the same Historian and page back to them after running them:

[17]:
import tclab

TCLab = tclab.setup(connected=False, speedup=10)

with TCLab() as lab:
    h = tclab.Historian(lab.sources)
    for t in tclab.clock(20):
        lab.Q1(100 if t <= 10 else 0)
        h.update(t)
    h.new_session()
    for t in tclab.clock(20):
        lab.Q1(0 if t <= 10 else 100)
        h.update(t)
TCLab version 0.4.7dev
Simulated TCLab
TCLab Model disconnected successfully.

To see the stored sessions, use get_sessions:

[18]:
h.get_sessions()
[18]:
[(1, '2018-03-18 08:10:02', 21), (2, '2018-03-18 08:10:03', 21)]

The historian log shows the data for the last session, where the heater started open

[19]:
h.log[:2]
[19]:
[(0, 23.205599999999997, 20.949499999999997, 0, 0),
 (1.1, 23.205599999999997, 20.949499999999997, 0, 0)]

To roll back to a different session, use load_session():

[20]:
h.load_session(1)
[21]:
h.log[:2]
[21]:
[(0, 20.949499999999997, 20.949499999999997, 100, 0),
 (1.0, 20.949499999999997, 20.949499999999997, 100, 0)]

Persistence

Historian stores results in a SQLite database. By default, it uses the special file :memory: which means the results are lost when you shut down or restart the kernel. To retain values across runs, you can specify a filename (by convention SQLite databases have the .db extension).

[22]:
import tclab

TCLab = tclab.setup(connected=False, speedup=10)

with TCLab() as lab:
    h = tclab.Historian(lab.sources, dbfile='test.db')
    for t in tclab.clock(20):
        lab.Q1(100 if t <= 10 else 0)
        h.update(t)
TCLab version 0.4.7dev
Simulated TCLab
TCLab Model disconnected successfully.

Every time you run this cell, new sessions will be added to the file. These sessions can be loaded as shown in the Sessions section above. There is currently no support for managing sessions. If you want to remove the old sessions, delete the database file.