Skip to content

How to define abstraction models

T. Teijeiro edited this page Apr 8, 2019 · 9 revisions

This tutorial describes how new abstraction models can be defined so that the Construe algorithm can be used for the interpretation of different time series, beyond the electrocardiogram (ECG). As an example, the T wave abstraction pattern is used.

Prerequisites

You should understand the terminology and formalism supporting the framework. It might be painful, but you really should read the main paper [1] to understand the intuition and ideas behind each concept.

Besides, currently the framework only supports abstraction models implemented in Python, so a proper knowledge of the language is required.

Describing representation entities: Observables

According to the definition [1], an abstraction model of a domain consists of a set of Abstraction Grammars defining an abstraction relation between a set of Observables. The first step is then to define a set of Observables for our domain.

The framework provides two base classes to define Observables: construe.model.observable.Observable and construe.model.observable.EventObservable. The first one represents observables whose observations have a certain duration, and EventObservable represents instantaneous observables. They define three attributes, all of them of the Variable type. A Variable may take a value that is an Interval, so we can represent uncertainty in the value of a given attribute. All three attributes are related to the temporal location of the observable:

  • start: Represents the beginning of the observable.
  • time: Represents the reference time for the observable. It is usually equal to the start attribute.
  • end: Represents the end of the observable.

For EventObservable instances, all three attributes take always the same value.

The first step is then to define the set of observables for our problem, by subclassing the appropriate Observable base class. For example, to conjecture a T wave we require the observation of a leading environment QRS complex and a signal Deflection that is abstracted in the T wave hypothesis. These entities are defined as follows:

from construe.model import Observable

class TWave(Observable):
    """
    Observable that represents a T Wave

    Attributes
    ----------
    amplitude:
        Dictionary with the amplitude in mV of the T wave in each lead, indexed
        by lead.
    """
    def __init__(self):
        super(TWave, self).__init__()
        #The single reference will correspond to the start time
        self.time = self.start
        self.amplitude = {}


class QRS(Observable):
    """
    Observable that represents a QRS complex

    Attributes
    ----------
    shape:
        Dictionary with the shape of the QRS complex in each lead, indexed
        by lead.
    """
    def __init__(self):
        super(QRS, self).__init__()
        self.shape = {}


class Deflection(Observable):
    """
    This class represents a signal deviation consistent with the electrical
    activity of the cardiac muscle fibers. It is associated with a certain
    energy level derived from the wavelet decomposition/reconstruction of the
    signal.
    """
    def __init__(self):
        """Creates a new Deflection instance, at level 0"""
        super(Deflection, self).__init__()
        #The single reference will correspond to the start variable
        self.time = self.start
        self.level = {}

As you can see, an Observable may define new attributes to represent specific information.

Describing knowledge: Abstraction Patterns

The most important element in an abstraction model is the set of abstraction grammars generating the abstraction patterns that will support the hypothesize-and-test cycle implemented by the Construe algorithm. As defined in [1], an abstraction grammar is a right-linear formal attributed grammar, describing a sort of regular expression in the observables alphabet and allowing the description of temporal and value constraints between them.

To make it simpler the definition of grammars, they are described as the equivalent finite automaton, where non-terminals are replaced by states, and rules are described as transitions between states. For example, the grammar to describe the T wave abstraction pattern has the following form:

H = qTw → qQRS [c1] D
D → qDeflection [c2]

where c1 describes the constraints involving the QRS observation, and c2 describes the constraints involving the Deflection observation. The equivalent automaton has the following form:

T wave pattern automata

where # means the observable is in the environment evidence EP and @ means the observable is in the abstracted evidence AP.

For the definition of abstraction grammars through their equivalent automata, the framework provides the construe.model.automata.PatternAutomata class. As a public API, this class provides the following add_transition() method:

def add_transition(self, istate = None, fstate = None, observable = None,
                            abstracted = ABSTRACTED, tconst = NULL_CONST,
                                                          gconst = NULL_CONST):
        """
        Adds a new *Transition* to this automata.

        Parameters
        ----------
        istate:
            Initial state of the transition.
        fstate:
            Final state of the transition.
        observable:
            Observable type that allows the transition.
        abstracted:
            Flag that indicates if the observation that allows the transition
            should be considered as an Abstracted observation or an Environment
            observation.
        tconst:
            Function that receives as a parameter the AbstractionPattern object,
            and adds the temporal constraints introduced by this transition
            with the rest of the variables of the pattern, including the
            hypothesis. These constraints are added before the matching of the
            predicted finding with an actual observation.
        gconst:
            Function that receives as a parameter the AbstractionPattern object,
            and checks any additional constraint in the value of the
            observations. These constraints are checked after the matching of
            the finding with an actual observation.
        """

This function allows to define the transitions in the pattern automaton (equivalent to the rules in an abstraction grammar), describing the evidence observable, its membership to AP or EP, and the temporal and value constraints. Constraints are defined as functions receiving an AbstractionPattern instance and the Observable observed in the transition, and they are expected to raise an InconsistencyError exception if constraints are not satisfied, or finish silently if they do. As a result, they may assign values to the attributes of the hypothesized observable, so they also define the observation procedure of the pattern. Let's see an example to make things more clear:

import construe.knowledge.observables as o

###########################
### Automata definition ###
###########################

TWAVE_PATTERN = PatternAutomata()
TWAVE_PATTERN.name = 'T Wave'
TWAVE_PATTERN.Hypothesis = o.TWave
TWAVE_PATTERN.add_transition(0, 1, o.QRS, ENVIRONMENT, _t_qrs_tconst)
TWAVE_PATTERN.add_transition(1, 2, o.Deflection, ABSTRACTED, _t_defl_tconst,
                                                                     _t_gconst)
TWAVE_PATTERN.final_states.add(2)

In these lines, we define a pattern automaton to conjecture a Twave from the observation of an environment QRS complex and an abstracted Deflection. Temporal and value constraints between these observables are defined in the _t_qrs_tconst, _t_eint_tconst and _t_gconst functions. For example, the temporal constraints between the environment QRS complex and the abstracted Twave are the following:

from construe.model import Interval as Iv
from construe.utils.units_helper import msec2samples as m2s

################################
### T Wave related constants ###
################################
TW_DURATION = Iv(m2s(80), m2s(450)) #T Wave duration limits
ST_INTERVAL = Iv(m2s(0), m2s(250))  #ST segment duration limits
QT_INTERVAL = Iv(m2s(250), m2s(900))#QT maximum limits (not normal)
#Maximum limits from the end of the QRS to the end of the T Wave (not normal)
SQT_INTERVAL = Iv(m2s(150), m2s(750))

def _t_qrs_tconst(pattern, qrs):
    """
    Temporal constraints wrt the leading QRS complex.
    """
    BASIC_TCONST(pattern, qrs)
    twave = pattern.hypothesis
    tc = pattern.tnet
    tc.add_constraint(qrs.end, twave.start, ST_INTERVAL)
    tc.add_constraint(qrs.start, twave.end, QT_INTERVAL)
    tc.add_constraint(qrs.end, twave.end, SQT_INTERVAL)
    tc.add_constraint(twave.start, twave.end, TW_DURATION)

As we can see, the framework provides some utility functions to define temporal constraints following the Simple Temporal Problem (STP) formalism [2]. It is possible to define an interval constraining the minimum and maximum temporal distance between two temporal variables, and these constraints are automatically enforced during the interpretation. The pattern parameter corresponds to an AbstractionPattern instance, and qrs is an instance of the o.QRS observable for which the matching relation is set. The BASIC_TCONST function simply sets the constraint that the beginning of the qrs instance must be before its end, and it should be added to any temporal constraints description function. Then, specific constraints are added to the pattern temporal network, aka pattern.tnet. For example, the first constraint requires the distance between the end of the QRS complex and the beginning of the T wave to be between 0 and 250 milliseconds.

Now, let's take a look to a more complex procedure, corresponding to the value constraints of the T wave pattern, also involving the interpretation procedure:

#Factor limiting the maximum slope of a T Wave wrt the corresponding QRS.
TQRS_MAX_DIFFR = 0.7
#Temporal margin for measurement discrepancies (1 mm in standard ECG scale)
TMARGIN = int(math.ceil(m2s(40)))

def _t_gconst(pattern, defl):
    """
    T Wave abstraction pattern general constraints, checked when all the
    evidence has been observed.
    """
    twave = pattern.hypothesis
    if defl.earlystart != defl.latestart or not pattern.evidence[o.QRS]:
        return
    qrs = pattern.evidence[o.QRS][0]
    #Wave limits
    beg = int(twave.earlystart)
    end = int(twave.lateend)
    ls_lim = int(twave.latestart - beg)
    ee_lim = int(twave.earlyend - beg)
    #Start and end estimation.
    endpoints = {}
    for lead in sorted(qrs.shape, key= lambda l: qrs.shape[l].amplitude,
                                                             reverse=True):
        baseline, _ = characterize_baseline(lead, beg, end)
        sig = sig_buf.get_signal_fragment(beg, end, lead=lead)[0]
        verify(len(sig) == end-beg+1)
        ep = _delimit_t(sig, baseline, ls_lim, ee_lim, qrs.shape[lead])
        if ep is not None:
            endpoints[lead] = ep
    verify(endpoints)
    limits = max(endpoints.iteritems(), key= lambda ep:ep[1][1])[1][0]
    #We verify that in all leads the maximum slope of the T wave fragment does
    #not exceed the threshold.
    for lead in endpoints:
        sig = sig_buf.get_signal_fragment(beg+limits.start, beg+limits.end,
                                                                  lead=lead)[0]
        verify(np.max(np.abs(np.diff(sig))) <=
                                   qrs.shape[lead].maxslope * TQRS_MAX_DIFFR)
        #Amplitude measure
        if lead in endpoints:
            twave.amplitude[lead] = np.ptp(sig)
    twave.start.value = Iv(beg+limits.start, beg+limits.start)
    twave.end.value = Iv(beg+limits.end, beg+limits.end)
    #The duration of the T Wave must be greater than the QRS
    #(with a security margin)
    verify(twave.earlyend-twave.latestart >
                                          qrs.earlyend-qrs.latestart-TMARGIN)
    #The overlapping between the energy interval and the T Wave must be at
    #least the half of the duration of the energy interval.
    verify(Iv(twave.earlystart, twave.lateend).intersection(
                    Iv(defl.earlystart, defl.lateend)).length >=
                                            (defl.lateend-defl.earlystart)/2.0)

The first line, gets the reference to the Twave instance, just for convenience. The second line checks that all the necessary evidence has been observed, and otherwise the function ends without performing any action. Then, the specific beginning and end of the hypothesis is estimated in each signal lead where the QRS complex has been also observed, and these limits are stored in the endpoints dictionary (for this, the _delimit_t() function is used, that might be any T wave delineation method). The success of this stage is enforced by using the verify() function, which raises an InconsistencyError exception if the checked condition is not satisfied.

After the T wave delineation ends, some additional constraints are checked, for example forcing the maximum slope of the signal during the T wave does not exceed 70% of the maximum slope during the QRS complex. If this condition is satisfied, the corresponding amplitude attribute in the hypothesized observation is set.

As can be seen, the definition of abstraction grammars offers great flexibility to include external methods for constraint enforcement or observation procedures.

References

[1]: T. Teijeiro and P. Félix (2018). On the adoption of abductive reasoning for time series interpretation. Artificial Intelligence, 262, 163–188. https://doi.org/10.1016/j.artint.2018.06.005

[2]: M. Dechter, J. Meiri, and J. Pearl, “Temporal constraint networks,” Artificial Intelligence, vol. 49, pp. 61–95, 1991.