Package Gnumed :: Package business :: Module gmClinicalCalculator |
[frames] | no frames]

# Source Code for Module Gnumed.business.gmClinicalCalculator

      1  # -*- coding: utf8 -*-
      2  """GNUmed clinical calculator(s)
      3
      4  THIS IS NOT A VERIFIED CALCULATOR. DO NOT USE FOR ACTUAL CARE.
      5  """
      6  #============================================================
      7  __author__ = "K.Hilbert <Karsten.Hilbert@gmx.net>"
      8  __license__ = "GPL v2 or later"
      9
     10  # standard libs
     11  import sys
     12  import logging
     13  import decimal
     14
     15
     16  if __name__ == '__main__':
     17          sys.path.insert(0, '../../')
     18
     19  from Gnumed.pycommon import gmDateTime
     20  from Gnumed.pycommon import gmI18N
     21  from Gnumed.pycommon import gmLog2
     22
     23  if __name__ == '__main__':
     24          gmI18N.activate_locale()
     25          gmI18N.install_domain()
     26          gmDateTime.init()
     27
     28  from Gnumed.pycommon import gmTools
     29  from Gnumed.business import gmLOINC
     30
     31
     32  _log = logging.getLogger('gm.calc')
     33
     34  #============================================================
     35 -class cClinicalResult(object):
     36
     37 -        def __init__(self, message=None):
     38                  self.message = message
     39                  self.numeric_value = None
     40                  self.unit = None
     41                  self.date_valid = None
     42                  self.formula_name = None
     43                  self.formula_source = None
     44                  self.variables = {}
     45                  self.sub_results = []
     46                  self.warnings = [_('THIS IS NOT A VERIFIED MEASUREMENT. DO NOT USE FOR ACTUAL CARE.')]
     47          #--------------------------------------------------------
     48 -        def __unicode__(self):
     49                  txt = u'[cClinicalResult]: %s %s (%s)\n\n%s' % (
     50                          self.numeric_value,
     51                          self.unit,
     52                          self.date_valid,
     53                          self.format (
     54                                  left_margin = 0,
     55                                  width = 80,
     56                                  eol = u'\n',
     57                                  with_formula = True,
     58                                  with_warnings = True,
     59                                  with_variables = True,
     60                                  with_sub_results = True,
     61                                  return_list = False
     62                          )
     63                  )
     64                  return txt
     65          #--------------------------------------------------------
     66 -        def format(self, left_margin=0, eol=u'\n', width=None, with_formula=False, with_warnings=True, with_variables=False, with_sub_results=False, return_list=False):
     67                  lines = []
     68                  lines.append(self.message)
     69
     70                  if with_formula:
     71                          txt = gmTools.wrap (
     72                                  text = u'%s %s' % (
     73                                          _('Algorithm:'),
     74                                          self.formula_name
     75                                  ),
     76                                  width = width,
     77                                  initial_indent = u' ',
     78                                  subsequent_indent = u' ' * 2,
     79                                  eol = eol
     80                          )
     81                          lines.append(txt)
     82                          txt = gmTools.wrap (
     83                                  text = u'%s %s' % (
     84                                          _('Source:'),
     85                                          self.formula_source
     86                                  ),
     87                                  width = width,
     88                                  initial_indent = u' ',
     89                                  subsequent_indent = u' ' * 2,
     90                                  eol = eol
     91                          )
     92                          lines.append(txt)
     93
     94                  if with_warnings:
     95                          if len(self.warnings) > 0:
     96                                  lines.append(u' Caveat:')
     97                          for w in self.warnings:
     98                                  txt = gmTools.wrap(text = w, width = width, initial_indent = u'  %s ' % gmTools.u_right_arrow, subsequent_indent = u'    ', eol = eol)
     99                                  lines.append(txt)
    100                          if len(self.warnings) > 0:
    101                                  lines.append(u'')
    102
    103                  if with_variables:
    104                          if len(self.variables) > 0:
    105                                  lines.append(u' %s' % _('Variables:'))
    106                          for key in self.variables.keys():
    107                                  txt = u'  %s %s: %s' % (
    108                                          gmTools.u_right_arrow,
    109                                          key,
    110                                          self.variables[key]
    111                                  )
    112                                  lines.append(txt)
    113
    114                  if with_sub_results:
    115                          if len(self.sub_results) > 0:
    116                                  lines.append(u' %s' % _('Intermediate results:'))
    117                          for r in self.sub_results:
    118                                  lines.extend(r.format (
    119                                          left_margin = left_margin + 1,
    120                                          width = width,
    121                                          eol = eol,
    122                                          with_formula = with_formula,
    123                                          with_warnings = with_warnings,
    124                                          with_variables = with_variables,
    125                                          with_sub_results = False,                       # break cycles
    126                                          return_list = True
    127                                  ))
    128
    129                  lines = gmTools.strip_trailing_empty_lines(lines = lines, eol = eol)
    130                  if return_list:
    131                          return lines
    132
    133                  left_margin = u' ' * left_margin
    134                  return left_margin + (eol + left_margin).join(lines) + eol
    135
    136  #============================================================
    137 -class cClinicalCalculator(object):
    138
    139 -        def __init__(self, patient=None):
    140                  self.__cache = {}
    141                  self.__patient = patient
    142          #--------------------------------------------------------
    143 -        def _get_patient(self):
    144                  return self.__patient
    145
    146 -        def _set_patient(self, patient):
    147                  if patient == self.__patient:
    148                          return
    149                  self.__patient = patient
    150                  self.remove_from_cache()                        # uncache all values
    151
    152          patient = property(lambda x:x, _set_patient)
    153          #--------------------------------------------------------
    154  #       def suggest_algorithm(self, pk_test_type):
    155  #               return None
    156          #--------------------------------------------------------
    157 -        def remove_from_cache(self, key=None):
    158                  if key is None:
    159                          self.__cache = {}
    160                          return True
    161                  try:
    162                          del self.__cache[key]
    163                          return True
    164                  except KeyError:
    165                          _log.error('key [%s] does not exist in cache', key)
    166                          return False
    167          #--------------------------------------------------------
    168          # formulae
    169          #--------------------------------------------------------
    170 -        def _get_egfr(self):
    171                  eGFR = self.eGFR_Schwartz
    172                  if eGFR.numeric_value is None:
    173                          eGFR = self.eGFR_MDRD_short
    174                  return eGFR
    175
    176          eGFR = property(_get_egfr, lambda x:x)
    177          #--------------------------------------------------------
    178 -        def _get_mdrd_short(self):
    179
    180                  try:
    181                          return self.__cache['MDRD_short']
    182                  except KeyError:
    183                          pass
    184
    185                  result = cClinicalResult(_('unknown MDRD (4 vars/IDMS)'))
    186                  result.formula_name = u'eGFR from 4-variables IDMS-MDRD'
    187                  result.formula_source = u'1/2013: http://en.wikipedia.org/Renal_function / http://www.ganfyd.org/index.php?title=Estimated_glomerular_filtration_rate (NHS)'
    188
    189                  if self.__patient is None:
    190                          result.message = _('MDRD (4 vars/IDMS): no patient')
    191                          return result
    192
    193                  if self.__patient['dob'] is None:
    194                          result.message = _('MDRD (4 vars/IDMS): no DOB (no age)')
    195                          return result
    196
    197                  # 1) gender
    198                  from Gnumed.business.gmPerson import map_gender2mf
    199                  result.variables['gender'] = self.__patient['gender']
    200                  result.variables['gender_mf'] = map_gender2mf[self.__patient['gender']]
    201                  if result.variables['gender_mf'] == 'm':
    202                          result.variables['gender_multiplier'] = self.d(1)
    203                  elif result.variables['gender_mf'] == 'f':
    204                          result.variables['gender_multiplier'] = self.d('0.742')
    205                  else:
    206                          result.message = _('MDRD (4 vars/IDMS): neither male nor female')
    207                          return result
    208
    209                  # 2) creatinine
    210                  result.variables['serum_crea'] = self.__patient.emr.get_most_recent_results(loinc = gmLOINC.LOINC_creatinine_quantity, no_of_results = 1)
    211                  if result.variables['serum_crea'] is None:
    212                          result.message = _('MDRD (4 vars/IDMS): serum creatinine value not found (LOINC: %s)') % gmLOINC.LOINC_creatinine_quantity
    213                          return result
    214                  if result.variables['serum_crea']['val_num'] is None:
    215                          result.message = _('MDRD (4 vars/IDMS): creatinine value not numeric')
    216                          return result
    217                  result.variables['serum_crea_val'] = self.d(result.variables['serum_crea']['val_num'])
    218                  if result.variables['serum_crea']['val_unit'] in [u'mg/dl', u'mg/dL']:
    219                          result.variables['unit_multiplier'] = self.d(175)                                               # older: 186
    220                  elif result.variables['serum_crea']['val_unit'] in [u'µmol/L', u'µmol/l']:
    221                          result.variables['unit_multiplier'] = self.d(30849)                                     # older: 32788
    222                  else:
    223                          result.message = _('MDRD (4 vars/IDMS): unknown serum creatinine unit (%s)') % result.variables['serum_crea']['val_unit']
    224                          return result
    225
    226                  # 3) age (at creatinine evaluation)
    227                  result.variables['dob'] = self.__patient['dob']
    228                  result.variables['age@crea'] = self.d (
    229                          gmDateTime.calculate_apparent_age (
    230                                  start = result.variables['dob'],
    231                                  end = result.variables['serum_crea']['clin_when']
    232                          )[0]
    233                  )
    234                  if (result.variables['age@crea'] > 84) or (result.variables['age@crea'] < 18):
    235                          result.message = _('MDRD (4 vars/IDMS): formula does not apply at age [%s] (17 < age < 85)') % result.variables['age@crea']
    236                          return result
    237
    238                  # 4) ethnicity
    239                  result.variables['ethnicity_multiplier'] = self.d(1)                    # non-black
    240                  result.warnings.append(_('ethnicity: GNUmed does not know patient ethnicity, ignoring correction factor'))
    241
    242                  # calculate
    243                  result.numeric_value =  result.variables['unit_multiplier'] * \
    244                          pow(result.variables['serum_crea_val'], self.d('-1.154')) * \
    245                          pow(result.variables['age@crea'], self.d('-0.203')) * \
    246                          result.variables['ethnicity_multiplier'] * \
    247                          result.variables['gender_multiplier']
    248                  result.unit = u'ml/min/1.73m²'
    249
    250                  BSA = self.body_surface_area
    251                  result.sub_results.append(BSA)
    252                  if BSA.numeric_value is None:
    253                          result.warnings.append(_(u'NOT corrected for non-average body surface (average = 1.73m²)'))
    254                  else:
    255                          result.variables['BSA'] = BSA.numeric_value
    256                          result_numeric_value = result.numeric_value / BSA.numeric_value
    257
    258                  result.message = _('eGFR(MDRD): %.1f %s (%s) [4-vars, IDMS]') % (
    259                          result.numeric_value,
    260                          result.unit,
    261                          gmDateTime.pydt_strftime (
    262                                  result.variables['serum_crea']['clin_when'],
    263                                  format = '%Y %b %d'
    264                          )
    265                  )
    266                  result.date_valid = result.variables['serum_crea']['clin_when']
    267
    268                  self.__cache['MDRD_short'] = result
    269                  _log.debug(u'%s' % result)
    270
    271                  return result
    272
    273          eGFR_MDRD_short = property(_get_mdrd_short, lambda x:x)
    274          #--------------------------------------------------------
    275 -        def _get_gfr_schwartz(self):
    276
    277                  try:
    278                          return self.__cache['gfr_schwartz']
    279                  except KeyError:
    280                          pass
    281
    282                  result = cClinicalResult(_('unknown eGFR (Schwartz)'))
    283                  result.formula_name = u'eGFR from updated Schwartz "bedside" formula (age < 19yrs)'
    284                  result.formula_source = u'1/2013: http://en.wikipedia.org/Renal_function / http://www.ganfyd.org/index.php?title=Estimated_glomerular_filtration_rate (NHS) / doi 10.1681/ASN.2008030287 / doi: 10.2215/CJN.01640309'
    285
    286                  if self.__patient is None:
    287                          result.message = _('eGFR (Schwartz): no patient')
    288                          return result
    289
    290                  if self.__patient['dob'] is None:
    291                          result.message = _('eGFR (Schwartz): DOB needed for age')
    292                          return result
    293
    294                  result.variables['dob'] = self.__patient['dob']
    295
    296                  # creatinine
    297                  result.variables['serum_crea'] = self.__patient.emr.get_most_recent_results(loinc = gmLOINC.LOINC_creatinine_quantity, no_of_results = 1)
    298                  if result.variables['serum_crea'] is None:
    299                          result.message = _('eGFR (Schwartz): serum creatinine value not found (LOINC: %s') % gmLOINC.LOINC_creatinine_quantity
    300                          return result
    301                  if result.variables['serum_crea']['val_num'] is None:
    302                          result.message = _('eGFR (Schwartz): creatinine value not numeric')
    303                          return result
    304                  result.variables['serum_crea_val'] = self.d(result.variables['serum_crea']['val_num'])
    305                  if result.variables['serum_crea']['val_unit'] in [u'mg/dl', u'mg/dL']:
    306                          result.variables['unit_multiplier'] = self.d(1)
    307                  elif result.variables['serum_crea']['val_unit'] in [u'µmol/L', u'µmol/l']:
    308                          result.variables['unit_multiplier'] = self.d('0.00113')
    309                  else:
    310                          result.message = _('eGFR (Schwartz): unknown serum creatinine unit (%s)') % result.variables['serum_crea']['val_unit']
    311                          return result
    312
    313                  # age
    314                  result.variables['age@crea'] = self.d (
    315                          gmDateTime.calculate_apparent_age (
    316                                  start = result.variables['dob'],
    317                                  end = result.variables['serum_crea']['clin_when']
    318                          )[0]
    319                  )
    320                  if result.variables['age@crea'] > 17:
    321                          result.message = _('eGFR (Schwartz): formula does not apply at age [%s] (age must be <18)') % result.variables['age@crea']
    322                          return result
    323
    324                  # age-dependant constant
    325                  if result.variables['age@crea'] < 1:
    326                          # first year pre-term: k = 0.33
    327                          # first year full-term: k = (0.45)              0.41 (updated)
    328                          result.variables['constant_for_age'] = self.d('0.41')
    329                          result.warnings.append(_('eGFR (Schwartz): not known whether pre-term birth, applying full-term formula'))
    330                  else:
    331                          result.variables['constant_for_age'] = self.d('0.41')
    332
    333                  # height
    334                  result.variables['height'] = self.__patient.emr.get_result_at_timestamp (
    335                          timestamp = result.variables['serum_crea']['clin_when'],
    336                          loinc = gmLOINC.LOINC_height,
    337                          tolerance_interval = '7 days'
    338                  )
    339                  if result.variables['height'] is None:
    340                          result.message = _('eGFR (Schwartz): height not found')
    341                          return result
    342                  if result.variables['height']['val_num'] is None:
    343                          result.message = _('eGFR (Schwartz): height not numeric')
    344                          return result
    345                  if result.variables['height']['val_unit'] == u'cm':
    346                          result.variables['height_cm'] = self.d(result.variables['height']['val_num'])
    347                  elif result.variables['height']['val_unit'] == u'mm':
    348                          result.variables['height_cm'] = self.d(result.variables['height']['val_num'] / self.d(10))
    349                  elif result.variables['height']['val_unit'] == u'm':
    350                          result.variables['height_cm'] = self.d(result.variables['height']['val_num'] * 100)
    351                  else:
    352                          result.message = _('eGFR (Schwartz): height not in m, cm, or mm')
    353                          return result
    354
    355                  # calculate
    356                  result.numeric_value = (
    357                          result.variables['constant_for_age'] * result.variables['height_cm']
    358                  ) / (
    359                          result.variables['unit_multiplier'] * result.variables['serum_crea_val']
    360                  )
    361                  result.unit = u'ml/min/1.73m²'
    362
    363                  result.message = _('eGFR (Schwartz): %.1f %s (%s)') % (
    364                          result.numeric_value,
    365                          result.unit,
    366                          gmDateTime.pydt_strftime (
    367                                  result.variables['serum_crea']['clin_when'],
    368                                  format = '%Y %b %d'
    369                          )
    370                  )
    371                  result.date_valid = result.variables['serum_crea']['clin_when']
    372
    373                  self.__cache['gfr_schwartz'] = result
    374                  _log.debug(u'%s' % result)
    375
    376                  return result
    377
    378          eGFR_Schwartz = property(_get_gfr_schwartz, lambda x:x)
    379          #--------------------------------------------------------
    380 -        def _get_body_surface_area(self):
    381
    382                  try:
    383                          return self.__cache['body_surface_area']
    384                  except KeyError:
    385                          pass
    386
    387                  result = cClinicalResult(_('unknown body surface area'))
    388                  result.formula_name = u'Du Bois Body Surface Area'
    389                  result.formula_source = u'12/2012: http://en.wikipedia.org/wiki/Body_surface_area'
    390
    391                  if self.__patient is None:
    392                          result.message = _('Body Surface Area: no patient')
    393                          return result
    394
    395                  result.variables['height'] = self.__patient.emr.get_most_recent_results(loinc = gmLOINC.LOINC_height, no_of_results = 1)
    396                  if result.variables['height'] is None:
    397                          result.message = _('Body Surface Area: height not found')
    398                          return result
    399                  if result.variables['height']['val_num'] is None:
    400                          result.message = _('Body Surface Area: height not numeric')
    401                          return result
    402                  if result.variables['height']['val_unit'] == u'cm':
    403                          result.variables['height_cm'] = self.d(result.variables['height']['val_num'])
    404                  elif result.variables['height']['val_unit'] == u'mm':
    405                          result.variables['height_cm'] = self.d(result.variables['height']['val_num'] / self.d(10))
    406                  elif result.variables['height']['val_unit'] == u'm':
    407                          result.variables['height_cm'] = self.d(result.variables['height']['val_num'] * 100)
    408                  else:
    409                          result.message = _('Body Surface Area: height not in m, cm, or mm')
    410                          return result
    411
    412                  result.variables['weight'] = self.__patient.emr.get_result_at_timestamp (
    413                          timestamp = result.variables['height']['clin_when'],
    414                          loinc = gmLOINC.LOINC_weight,
    415                          tolerance_interval = '10 days'
    416                  )
    417                  if result.variables['weight'] is None:
    418                          result.message = _('Body Surface Area: weight not found')
    419                          return result
    420                  if result.variables['weight']['val_num'] is None:
    421                          result.message = _('Body Surface Area: weight not numeric')
    422                          return result
    423                  if result.variables['weight']['val_unit'] == u'kg':
    424                          result.variables['weight_kg'] = self.d(result.variables['weight']['val_num'])
    425                  elif result.variables['weight']['val_unit'] == u'g':
    426                          result.variables['weight_kg'] = self.d(result.variables['weight']['val_num'] / self.d(1000))
    427                  else:
    428                          result.message = _('Body Surface Area: weight not in kg or g')
    429                          return result
    430
    431                  result.numeric_value = self.d('0.007184') * \
    432                          pow(result.variables['weight_kg'], self.d('0.425')) * \
    433                          pow(result.variables['height_cm'], self.d('0.725'))
    434                  result.unit = u'm²'
    435
    436                  result.message = _('BSA (DuBois): %.2f %s') % (
    437                          result.numeric_value,
    438                          result.unit
    439                  )
    440                  result.date_valid = gmDateTime.pydt_now_here()
    441
    442                  self.__cache['body_surface_area'] = result
    443                  _log.debug(u'%s' % result)
    444
    445                  return result
    446
    447          body_surface_area = property(_get_body_surface_area, lambda x:x)
    448          #--------------------------------------------------------
    449          # helper functions
    450          #--------------------------------------------------------
    451 -        def d(self, initial):
    452                  if isinstance(initial, decimal.Decimal):
    453                          return initial
    454
    455                  val = initial
    456
    457                  # float ? -> to string first
    458                  if type(val) == type(float(1.4)):
    459                          val = str(val)
    460
    461                  # string ? -> "," to "."
    462                  if isinstance(val, basestring):
    463                          val = val.replace(',', '.', 1)
    464                          val = val.strip()
    465
    466                  try:
    467                          d = decimal.Decimal(val)
    468                          return d
    469                  except (TypeError, decimal.InvalidOperation):
    470                          return None
    471
    472  #============================================================
    473  # main
    474  #------------------------------------------------------------
    475  if __name__ == "__main__":
    476
    477          if len(sys.argv) == 1:
    478                  sys.exit()
    479
    480          if sys.argv[1] != 'test':
    481                  sys.exit()
    482
    483          from Gnumed.pycommon import gmLog2
    484          #-----------------------------------------
    485 -        def test_clin_calc():
    486                  from Gnumed.business.gmPerson import cPatient
    487                  pat = cPatient(aPK_obj = 12)
    488                  calc = cClinicalCalculator(patient = pat)
    489                  result = calc.eGFR_MDRD_short
    490                  #result = calc.eGFR_Schwartz
    491                  #result = calc.eGFR
    492                  #result = calc.body_surface_area
    493                  print u'%s' % result
    494          #-----------------------------------------
    495          test_clin_calc()
    496