def test_set_before(self): v0, v1 = [Interval(-1, x) for x in range(2)] nw = ConstraintNetwork() nw.set_before(v0, v1) nw.minimize_network() assert v0 < v1 assert v0.start == v1.start assert v0.start == -1 nw.set_before(v1, v0) nw.minimize_network() assert v0 == v1 assert v0 == v1 assert v0.start == -1
def _reg_nae_tconst(pattern, qrs): """ Temporal constraints for regular beats not coming after ectopic beats. """ beats = pattern.evidence[o.QRS] idx = beats.index(qrs) assert not _is_ectopic(idx) hyp = pattern.hypothesis tnet = pattern.last_tnet prev = beats[idx - 1] if idx > 3: #We create a new temporal network for the new trigeminy cycle. tnet.remove_constraint(hyp.end, prev.time) tnet = ConstraintNetwork() pattern.temporal_constraints.append(tnet) rrev = beats[idx - 3].time.start - beats[idx - 4].time.start ##RR evolution constraint. else: rrev = pattern.evidence[o.Cardiac_Rhythm][0].meas.rr[0] tnet.add_constraint(prev.time, qrs.time, Iv(rrev - C.RR_MAX_DIFF, rrev + C.RR_MAX_DIFF)) BASIC_TCONST(pattern, qrs) tnet.add_constraint(qrs.start, qrs.end, C.NQRS_DUR) tnet.set_before(qrs.time, hyp.end) #Constraints with the precedent T Wave _qrs_after_twave(pattern, qrs) #Morphology should be similar to the previous QRS, since both are normal qrs.shape = prev.shape qrs.paced = prev.paced
def test_equal(self): v0 = Interval(0, 10) v1 = Interval(7, 15) nw = ConstraintNetwork() nw.set_equal(v0, v1) nw.minimize_network() assert v0 == v1
def _deflection_tconst(pattern, defl): """Temporal constraints of the posterior deflections""" defls = pattern.evidence[o.Deflection] idx = defls.index(defl) hyp = pattern.hypothesis tnet = pattern.last_tnet prev = defls[idx - 1] tnet.remove_constraint(hyp.end, prev.time) #We create a new temporal network for the cyclic observations tnet = ConstraintNetwork() tnet.add_constraint(prev.time, defl.time, C.VFLUT_WW_INTERVAL) pattern.temporal_constraints.append(tnet) BASIC_TCONST(pattern, defl) tnet.add_constraint(defl.start, defl.end, Iv(0, C.VFLUT_WW_INTERVAL.end)) tnet.set_before(defl.time, hyp.end)
def test_between(self): v0 = Interval(0, 10) v1 = Interval(7, 15) v2 = Interval(4, 10) nw = ConstraintNetwork() nw.set_between(v0, v1, v2) nw.minimize_network() assert v0 <= v1 <= v2 v0 = Interval(0, 10) v1 = Interval(7, 15) v2 = Interval(4, 10) nw = ConstraintNetwork() nw.set_between(v2, v1, v0) nw.minimize_network() assert v2 <= v1 <= v0
def test_add_constraint(self): # Known example assertion (Detcher STP example in TCN paper) v0, v1, v2, v3, v4 = [Interval(-np.inf, np.inf) for _ in range(5)] v0.set(0, 0) nw = ConstraintNetwork() nw.add_constraint(v0, v1, Interval(10, 20)) nw.add_constraint(v1, v2, Interval(30, 40)) nw.add_constraint(v3, v2, Interval(10, 20)) nw.add_constraint(v3, v4, Interval(40, 50)) nw.add_constraint(v0, v4, Interval(60, 70)) nw.minimize_network() assert v0 == Interval(0, 0) assert v1 == Interval(10, 20) assert v2 == Interval(40, 50) assert v3 == Interval(20, 30) assert v4 == Interval(60, 70) assert tuple(v0) == (0, 0) assert tuple(v1) == (10, 20) assert tuple(v2) == (40, 50) assert tuple(v3) == (20, 30) assert tuple(v4) == (60, 70) # Testing if a stricker constraint is applied v0, v1, v2, v3, v4 = [Interval(-np.inf, np.inf) for _ in range(5)] v0.set(0, 0) nw = ConstraintNetwork() nw.add_constraint(v0, v1, Interval(10, 20)) nw.add_constraint(v1, v2, Interval(30, 40)) nw.add_constraint(v3, v2, Interval(10, 20)) nw.add_constraint(v3, v4, Interval(40, 50)) nw.add_constraint(v0, v4, Interval(60, 70)) nw.add_constraint(v0, v1, Interval(10, 15)) nw.add_constraint(v1, v2, Interval(30, 35)) nw.add_constraint(v3, v2, Interval(10, 15)) nw.add_constraint(v3, v4, Interval(40, 45)) nw.add_constraint(v0, v4, Interval(60, 65)) nw.minimize_network() assert v0 == Interval(0, 0) assert v1 == Interval(10, 10) assert v2 == Interval(40, 40) assert v3 == Interval(25, 25) assert v4 == Interval(65, 65)
def test_add_constraint(self): # Known example assertion (Detcher STP example in TCN paper) v0, v1, v2, v3, v4 = [Variable() for _ in range(5)] v0.value = Interval(0, 0) nw = ConstraintNetwork() nw.add_constraint(v0, v1, Interval(10, 20)) nw.add_constraint(v1, v2, Interval(30, 40)) nw.add_constraint(v3, v2, Interval(10, 20)) nw.add_constraint(v3, v4, Interval(40, 50)) nw.add_constraint(v0, v4, Interval(60, 70)) nw.minimize_network() assert v0.value == Interval(0, 0) assert v1.value == Interval(10, 20) assert v2.value == Interval(40, 50) assert v3.value == Interval(20, 30) assert v4.value == Interval(60, 70) # Testing if a stricker constraint is applied v0, v1, v2, v3, v4 = [Variable() for _ in range(5)] v0.value = Interval(0, 0) nw = ConstraintNetwork() nw.add_constraint(v0, v1, Interval(10, 20)) nw.add_constraint(v1, v2, Interval(30, 40)) nw.add_constraint(v3, v2, Interval(10, 20)) nw.add_constraint(v3, v4, Interval(40, 50)) nw.add_constraint(v0, v4, Interval(60, 70)) nw.add_constraint(v0, v1, Interval(10, 15)) nw.add_constraint(v1, v2, Interval(30, 35)) nw.add_constraint(v3, v2, Interval(10, 15)) nw.add_constraint(v3, v4, Interval(40, 45)) nw.add_constraint(v0, v4, Interval(60, 65)) nw.minimize_network() assert v0.value == Interval(0, 0) assert v1.value == Interval(10, 10) assert v2.value == Interval(40, 40) assert v3.value == Interval(25, 25) assert v4.value == Interval(65, 65)
def _ect_qrs_tconst(pattern, qrs): """ Temporal constraints for ectopic beats, which appear after every regular beat. """ beats = pattern.evidence[o.QRS] idx = beats.index(qrs) tnet = pattern.last_tnet hyp = pattern.hypothesis if idx > 0: prev = beats[idx - 1] #After the second couplet, every ectopic beat introduces a new temporal #network in the pattern to make it easier the minimization. if idx > 3: tnet.remove_constraint(hyp.end, prev.time) #We create a new temporal network for the cyclic observations tnet = ConstraintNetwork() pattern.temporal_constraints.append(tnet) #The duration of each couplet should not have high instantaneous #variations. refrr = beats[idx - 2].time.end - beats[idx - 3].time.start tnet.add_constraint( prev.time, qrs.time, Iv(refrr - C.RR_MAX_DIFF, refrr + C.RR_MAX_DIFF)) #We guide the morphology search to be similar to the previous #ectopic QRS complex. qrs.shape = beats[idx - 2].shape #The reference RR varies from an upper limit to the last measurement, #through the contextual previous rhythm. refrr = C.BRADY_RR.end stdrr = 0.1 * refrr if pattern.evidence[o.Cardiac_Rhythm] and idx == 1: mrr, srr = pattern.evidence[o.Cardiac_Rhythm][0].meas.rr if mrr > 0: refrr, stdrr = mrr, srr elif idx > 1: refrr, stdrr = hyp.meas.rr #Ectopic beats must be advanced wrt the reference RR tnet.add_constraint( prev.time, qrs.time, Iv(C.TACHY_RR.start, max(C.TACHY_RR.start, refrr - stdrr))) #Beats cannot overlap tnet.add_constraint(prev.end, qrs.start, Iv(C.TQ_INTERVAL_MIN, np.Inf)) BASIC_TCONST(pattern, qrs) tnet.add_constraint(qrs.start, qrs.end, C.QRS_DUR) tnet.set_before(qrs.time, hyp.end) #Constraints with the precedent T Wave _qrs_after_twave(pattern, qrs)
def _qrsn_tconst(pattern, qrs): """ Temporal constraints for the QRS complexes. """ beats = pattern.evidence[o.QRS] idx = beats.index(qrs) hyp = pattern.hypothesis tnet = pattern.last_tnet obseq = pattern.obs_seq oidx = pattern.get_step(qrs) prev = beats[idx-1] #In cyclic observations, we have to introduce more networks to simplify #the minimization operation. tnet.remove_constraint(hyp.end, prev.time) tnet = ConstraintNetwork() pattern.temporal_constraints.append(tnet) meanrr, stdrr = pattern.hypothesis.meas.rr rr_bounds = Iv(min(C.ASYSTOLE_RR.start, meanrr-stdrr+C.RR_MAX_DIFF), C.ASYSTOLE_RR.start) tnet.add_constraint(prev.time, qrs.time, rr_bounds) tnet.add_constraint(prev.start, qrs.start, rr_bounds) tnet.add_constraint(prev.end, qrs.end, rr_bounds) tnet.set_before(prev.end, qrs.start) #If there is a prior T Wave, it must finish before the start #of the QRS complex. if isinstance(obseq[oidx-1], o.TWave): prevt = obseq[oidx-1] tnet.set_before(prevt.end, qrs.start) BASIC_TCONST(pattern, qrs) tnet.add_constraint(qrs.start, qrs.end, C.QRS_DUR) tnet.set_before(qrs.time, hyp.end) #We can introduce constraints on the morphology of the new QRS complex. if hyp.morph and not qrs.frozen: qrs.shape = hyp.morph
def set_knowledge_base(knowledge): """ Sets the knowledge base to be used in the interpretation, and updates the necessary global variables. """ global KNOWLEDGE, _OBSERVABLES, _ABDUCIBLES, _LMAP, _EXCLUSION KNOWLEDGE = knowledge #First, we check the consistency of every single abstraction pattern for p in KNOWLEDGE: #In the Environment and Abstracted sets no repeated types can happen. for qset in (p.abstracted, p.environment): for q in qset: #The only coincidence must be q assert len(set(q.mro()) & qset) == 1 #There should be no subclass relations between hyp. and abstractions for q in p.abstracted: assert not p.Hypothesis in q.mro() assert not set(p.Hypothesis.mro()) & p.abstracted #The abstraction transitions must be properly set. for q in p.abstractions: assert q in p.abstracted for tr in p.abstractions[q]: assert tr.observable is q assert tr.abstracted is ABSTRACTED assert tr in p.transitions #Organization of all the observables in abstraction levels. _OBSERVABLES = set.union(*(({p.Hypothesis} | p.abstracted | p.environment) for p in KNOWLEDGE)) _ABDUCIBLES = tuple(set.union(*(p.abstracted for p in KNOWLEDGE))) #To perform the level assignment, we use a constraint network. _CNET = ConstraintNetwork() #Mapping from observable types to abstraction levels. _LMAP = {} for q in _OBSERVABLES: _LMAP[q] = Variable(value=Interval(0, numpy.inf)) #We set the restrictions, and minimize the network #All subclasses must have the same level than the superclasses for q in _OBSERVABLES: for sup in (set(q.mro()) & _OBSERVABLES) - {q}: _CNET.set_equal(_LMAP[q], _LMAP[sup]) #Abstractions force a level increasing for p in KNOWLEDGE: for qabs in p.abstracted: _CNET.add_constraint(_LMAP[qabs], _LMAP[p.Hypothesis], Interval(1, numpy.inf)) #Network minimization _CNET.minimize_network() #Now we assign to each observable the minimum of the solutions interval. for q in _OBSERVABLES: _LMAP[q] = int(_LMAP[q].start) #Manual definition of the exclusion relation between observables. _EXCLUSION = { Deflection: (Deflection, ), QRS: (QRS, ), TWave: (TWave, ), PWave: (PWave, ), CardiacCycle: (CardiacCycle, ), Cardiac_Rhythm: (Cardiac_Rhythm, ) } #Automatic expansion of the exclusion relation. for q, qexc in _EXCLUSION.iteritems(): _EXCLUSION[q] = tuple( (q2 for q2 in _OBSERVABLES if issubclass(q2, qexc))) for q in sorted(_OBSERVABLES, key=_LMAP.get, reverse=True): _EXCLUSION[q] = tuple( set.union(*(set(_EXCLUSION[q2]) for q2 in _EXCLUSION if issubclass(q, q2))))
def _qrs_tconst(pattern, qrs): """ Temporal constraints to observe a new QRS complex. """ beats = pattern.evidence[o.QRS] idx = beats.index(qrs) hyp = pattern.hypothesis tnet = pattern.last_tnet obseq = pattern.obs_seq oidx = pattern.get_step(qrs) #The environment complex sets the start of the rhythm observation. if pattern.get_evidence_type(qrs)[1] is ENVIRONMENT: tnet.set_equal(hyp.start, qrs.time) else: if idx > 0: prev = beats[idx - 1] tnet.remove_constraint(hyp.end, prev.time) #We create a new temporal network for the cyclic observations tnet = ConstraintNetwork() tnet.add_constraint(prev.time, qrs.time, rr_bounds) if rr_bounds is not C.TACHY_RR: #Also bounding on begin and end, but with relaxed variation #margin. rlx_rrb = Iv(rr_bounds.start - C.TMARGIN, rr_bounds.end + C.TMARGIN) tnet.add_constraint(prev.start, qrs.start, rlx_rrb) tnet.add_constraint(prev.end, qrs.end, rlx_rrb) tnet.set_before(prev.end, qrs.start) #If there is a prior T Wave, it must finish before the start #of the QRS complex. if isinstance(obseq[oidx - 1], o.TWave): prevt = obseq[oidx - 1] tnet.set_before(prevt.end, qrs.start) ##RR evolution constraint. We combine the statistical limits #with a dynamic evolution. if idx > 1: prev2 = beats[idx - 2] rrev = prev.time.start - prev2.time.start if hyp.meas.rr[0] > 0: meanrr, stdrr = hyp.meas.rr const = Iv( min(0.8 * rrev, rrev - C.RR_MAX_DIFF, meanrr - 2 * stdrr), max(1.2 * rrev, rrev + C.RR_MAX_DIFF, meanrr + 2 * stdrr)) else: const = Iv(min(0.8 * rrev, rrev - C.RR_MAX_DIFF), max(1.2 * rrev, rrev + C.RR_MAX_DIFF)) tnet.add_constraint(prev.time, qrs.time, const) pattern.temporal_constraints.append(tnet) #TODO improve if not qrs.frozen and hyp.morph: nullsh = o.QRSShape() refbeat = next((b for b in reversed(beats[:idx]) if not b.clustered and all( b.shape.get(lead, nullsh).tag == hyp.morph[lead].tag for lead in hyp.morph)), None) if refbeat is not None: qrs.shape = refbeat.shape qrs.paced = refbeat.paced BASIC_TCONST(pattern, qrs) tnet.add_constraint(qrs.start, qrs.end, C.QRS_DUR) tnet.set_before(qrs.time, hyp.end)