This repository has been archived on 2023-02-21. You can view files and clone it, but cannot push or open issues or pull requests.
biomon/src/biomon/medibot/watcher.py

184 lines
7.9 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 . 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.8, default_episode_duration=20,
reception_period=2, 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, profiles):
patients = biomon_models.Patient.objects.filter(enabled=True).\
exclude(alert_profile__isnull=True).\
exclude(alert_profile__exact='')
if not profiles:
return {patient.id: patient.get_alert_profile()
for patient in patients}
res = dict()
for patient in patients:
if patient.id not in profiles or \
(patient.id in profiles and \
patient.alert_profile != json.dumps(profiles[patient.id])):
res[patient.id] = patient.get_alert_profile()
self.close_opened_episodes(patient)
else:
res[patient.id] = profiles[patient.id]
return res
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:
duration = self.default_episode_duration
data_needed_count = self.data_needed_count
if len(definition) > 4:
duration = int(definition[4])
data_needed_count = \
(duration / self.reception_period) * self.data_needed_rate
if data_needed_count < 2:
data_needed_count = 2
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_episode_watcher(self):
"""
Run in infinite loop
Watch for elementary episodes.
"""
self.close_opened_episodes()
profiles = None
while(1):
start = datetime.now()
profiles = self.get_profiles(profiles)
for patient_id, profile in profiles.items():
definitions = \
definition_utils.get_unique_elementary_definitions(profile)
self.check_definitions(patient_id, definitions)
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)