205 lines
9.2 KiB
Python
205 lines
9.2 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
'''
|
|
biomon - Signs monitoring and patient management application
|
|
|
|
Copyright (C) 2015 Entr'ouvert
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as
|
|
published by the Free Software Foundation, either version 3 of the
|
|
License, or (at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
'''
|
|
|
|
|
|
import json
|
|
import math
|
|
from time import sleep
|
|
from datetime import datetime
|
|
|
|
from django.conf import settings
|
|
from django.db.models import ObjectDoesNotExist
|
|
from django.db.models import Q
|
|
|
|
from . import definitions as definition_utils
|
|
from .. import whisper_backend
|
|
from . import models
|
|
from .. import models as biomon_models
|
|
|
|
|
|
class Watcher:
|
|
def __init__(self, acceptance_rate=0.9, default_episode_duration=24,
|
|
reception_period=8, data_needed_rate=0.5):
|
|
# Acceptance rate is the rate of data samples over the threshold on
|
|
# the minimal episode duration expected.
|
|
self.acceptance_rate = acceptance_rate
|
|
# the minimal episode duration expected to consider an episode
|
|
# synonymous of the data frame size analyzed per round
|
|
self.default_episode_duration = default_episode_duration
|
|
# normal periode between the reception of two data samples
|
|
self.reception_period = reception_period
|
|
# There could be missing data samples, so this is an accepted rate
|
|
# of the samples received to take the decision to accept the episode
|
|
# as existing
|
|
self.data_needed_rate = data_needed_rate
|
|
# This is the number of samples expected on the data frame to
|
|
# take a decision.
|
|
self.data_needed_count = \
|
|
(default_episode_duration / reception_period) * data_needed_rate
|
|
if self.data_needed_count < 2:
|
|
self.data_needed_count = 2
|
|
# The data must be analyzed at least at the ferequency that samples
|
|
# are received. It determines also the refresh frequency of the end
|
|
# time of current events.
|
|
self.idle_time = reception_period / 2
|
|
|
|
def get_profiles(self):
|
|
not_ap = Q(alert_profile__isnull=True) | Q(alert_profile__exact='')
|
|
not_sap = Q(simple_alert_profile__isnull=True) | Q(simple_alert_profile__exact='')
|
|
enabled = Q(enabled=True)
|
|
patients = biomon_models.Patient.objects.filter(enabled, ~not_ap | ~not_sap)
|
|
if not patients:
|
|
return None
|
|
return {patient.id: (patient.get_alert_profile(),
|
|
patient.get_simple_alert_profile())
|
|
for patient in patients}
|
|
|
|
def episode_exists(self, definition, data, data_needed_count, duration):
|
|
match_data = [(date, val) for date, val in data \
|
|
if definition_utils.ops[definition[2]](val, definition[3])]
|
|
'''
|
|
match_data are samples of the time frame over the threshold.
|
|
|
|
The number of samples of the time frame must be high enough
|
|
according to the normal number of samples of a time frame. It
|
|
means that detection work even if some samples are missing but the
|
|
data_needed_count fix the limit.
|
|
|
|
The number of matching samples to consider the event must be higher
|
|
than the total len of samples of the time frame times the
|
|
acceptance ratio. E.g. the acceptance ratio of 0.5 means that the
|
|
detection can happen even if one sample on two is over the
|
|
threshold.
|
|
'''
|
|
if match_data and len(data) >= data_needed_count and \
|
|
float(len(match_data))/len(data) > self.acceptance_rate:
|
|
return (match_data[0][0], match_data[-1][0])
|
|
return None, None
|
|
|
|
def check_definitions(self, patient_id, definitions):
|
|
for definition in definitions:
|
|
if definition[1] in settings.SENSOR_MAPPING:
|
|
duration = self.default_episode_duration
|
|
data_needed_count = self.data_needed_count
|
|
if len(definition) > 4:
|
|
try:
|
|
duration = int(definition[4])
|
|
data_needed_count = \
|
|
(duration / self.reception_period) * self.data_needed_rate
|
|
if data_needed_count < 2:
|
|
data_needed_count = 2
|
|
except:
|
|
pass
|
|
backend = whisper_backend.WhisperBackend(str(patient_id),
|
|
settings.SENSOR_MAPPING[definition[1]])
|
|
data = backend.get_timestamped_data(duration + self.reception_period, None)
|
|
start, end = self.episode_exists(definition, data,
|
|
data_needed_count, duration)
|
|
if start:
|
|
try:
|
|
episode = models.Episode.objects.get(\
|
|
patient_id=patient_id,
|
|
definition=json.dumps(definition),
|
|
opened=True)
|
|
episode.end = end
|
|
episode.save()
|
|
except ObjectDoesNotExist:
|
|
'''
|
|
We deal with minimal episode duration only at creation
|
|
and not in episode detection because an update could be
|
|
with a duration less than the miniaml required for
|
|
creation.
|
|
'''
|
|
if (end - start).total_seconds() >= duration:
|
|
episode = models.Episode(start=start, end=end,
|
|
patient_id=patient_id,
|
|
definition=json.dumps(definition),
|
|
level=definition[0],
|
|
metric=definition[1],
|
|
opened=True)
|
|
episode.save()
|
|
else:
|
|
try:
|
|
episode = models.Episode.objects.get(\
|
|
patient_id=patient_id,
|
|
definition=json.dumps(definition),
|
|
opened=True)
|
|
episode.opened = False
|
|
episode.save()
|
|
except ObjectDoesNotExist:
|
|
pass
|
|
|
|
def close_opened_episodes(self, patient=None):
|
|
opened_episodes = models.Episode.objects.filter(opened=True)
|
|
if patient:
|
|
opened_episodes = opened_episodes.filter(patient=patient)
|
|
for opened_episode in opened_episodes:
|
|
opened_episode.opened = False
|
|
opened_episode.save()
|
|
|
|
def simple_alert_profile_to_langage(self, sap):
|
|
l = list()
|
|
if sap:
|
|
for k in settings.SENSOR_MAPPING:
|
|
value = sap.get(k.lower() + '_max_critical', None)
|
|
duration = sap.get('duration_' + k.lower() + '_max_critical', None)
|
|
if value:
|
|
l.append(('CRITICAL', k, '>', value, duration))
|
|
value = sap.get(k.lower() + '_min_critical', None)
|
|
duration = sap.get('duration_' + k.lower() + '_min_critical', None)
|
|
if value:
|
|
l.append(('CRITICAL', k, '<', value, duration))
|
|
value = sap.get(k.lower() + '_max_dangerous', None)
|
|
duration = sap.get('duration_' + k.lower() + '_max_dangerous', None)
|
|
if value:
|
|
l.append(('DANGEROUS', k, '>', value, duration))
|
|
value = sap.get(k.lower() + '_min_dangerous', None)
|
|
duration = sap.get('duration_' + k.lower() + '_min_dangerous', None)
|
|
if value:
|
|
l.append(('DANGEROUS', k, '<', value, duration))
|
|
return l
|
|
|
|
def simple_episode_watcher(self):
|
|
"""
|
|
Run in infinite loop
|
|
|
|
Watch for elementary episodes.
|
|
"""
|
|
self.close_opened_episodes()
|
|
while(1):
|
|
start = datetime.now()
|
|
profiles = self.get_profiles()
|
|
if profiles:
|
|
for patient_id, profiles in profiles.items():
|
|
ap, sap = profiles
|
|
definitions = \
|
|
definition_utils.get_unique_elementary_definitions(ap)
|
|
self.check_definitions(patient_id, definitions + self.simple_alert_profile_to_langage(sap))
|
|
end = datetime.now()
|
|
# Try to keep a constant frequency of checking
|
|
# As precision is at the millisecond generally for sleeping ,
|
|
# be sure to wait less than more...
|
|
# Derivation will increase the frequency
|
|
wait = self.idle_time - \
|
|
((end - start).total_seconds() + \
|
|
(end - start).microseconds/math.pow(10, 6)) - 0.001
|
|
sleep(wait)
|