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

# Source Code for Module Gnumed.business.gmClinicalCalculator

      1  # -*- coding: utf-8 -*-
      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  import datetime as pydt
     15
     16
     17  if __name__ == '__main__':
     18          sys.path.insert(0, '../../')
     19
     20  from Gnumed.pycommon import gmDateTime
     21  from Gnumed.pycommon import gmI18N
     22  from Gnumed.pycommon import gmLog2
     23
     24  if __name__ == '__main__':
     25          gmI18N.activate_locale()
     26          gmI18N.install_domain()
     27          gmDateTime.init()
     28
     29  from Gnumed.pycommon import gmTools
     30  from Gnumed.pycommon import gmBorg
     31  from Gnumed.business import gmLOINC
     32
     33
     34  _log = logging.getLogger('gm.calc')
     35
     36  #============================================================
     37 -class cClinicalResult(object):
     38
     39 -        def __init__(self, message=None):
     40                  self.message = message
     41                  self.numeric_value = None
     42                  self.unit = None
     43                  self.date_valid = None
     44                  self.formula_name = None
     45                  self.formula_source = None
     46                  self.variables = {}
     47                  self.sub_results = []
     48                  self.warnings = [_('THIS IS NOT A VERIFIED MEASUREMENT. DO NOT USE FOR ACTUAL CARE.')]
     49                  self.hints = []
     50
     51          #--------------------------------------------------------
     52 -        def __str__(self):
     53                  txt = '[cClinicalResult]: %s %s (%s)\n\n%s' % (
     54                          self.numeric_value,
     55                          self.unit,
     56                          self.date_valid,
     57                          self.format (
     58                                  left_margin = 0,
     59                                  width = 80,
     60                                  eol = '\n',
     61                                  with_formula = True,
     62                                  with_warnings = True,
     63                                  with_variables = True,
     64                                  with_sub_results = True,
     65                                  return_list = False
     66                          )
     67                  )
     68                  return txt
     69
     70          #--------------------------------------------------------
     71 -        def format(self, left_margin=0, eol='\n', width=None, with_formula=False, with_warnings=True, with_variables=False, with_sub_results=False, with_hints=True, return_list=False):
     72                  lines = []
     73                  lines.append(self.message)
     74
     75                  if with_formula:
     76                          txt = gmTools.wrap (
     77                                  text = '%s %s' % (
     78                                          _('Algorithm:'),
     79                                          self.formula_name
     80                                  ),
     81                                  width = width,
     82                                  initial_indent = ' ',
     83                                  subsequent_indent = ' ' * 2,
     84                                  eol = eol
     85                          )
     86                          lines.append(txt)
     87                          txt = gmTools.wrap (
     88                                  text = '%s %s' % (
     89                                          _('Source:'),
     90                                          self.formula_source
     91                                  ),
     92                                  width = width,
     93                                  initial_indent = ' ',
     94                                  subsequent_indent = ' ' * 2,
     95                                  eol = eol
     96                          )
     97                          lines.append(txt)
     98
     99                  if with_warnings:
    100                          if len(self.warnings) > 0:
    101                                  lines.append(' Caveat:')
    102                          for w in self.warnings:
    103                                  txt = gmTools.wrap(text = w, width = width, initial_indent = '  %s ' % gmTools.u_arrow2right, subsequent_indent = '    ', eol = eol)
    104                                  lines.append(txt)
    105
    106                  if with_hints:
    107                          if len(self.hints) > 0:
    108                                  lines.append(' Hints:')
    109                          for h in self.hints:
    110                                  txt = gmTools.wrap(text = h, width = width, initial_indent = '  %s ' % gmTools.u_arrow2right, subsequent_indent = '    ', eol = eol)
    111                                  lines.append(txt)
    112
    113                  if with_variables:
    114                          if len(self.variables) > 0:
    115                                  lines.append(' %s' % _('Variables:'))
    116                          for key in self.variables.keys():
    117                                  txt = '  %s %s: %s' % (
    118                                          gmTools.u_arrow2right,
    119                                          key,
    120                                          self.variables[key]
    121                                  )
    122                                  lines.append(txt)
    123
    124                  if with_sub_results:
    125                          if len(self.sub_results) > 0:
    126                                  lines.append(' %s' % _('Intermediate results:'))
    127                          for r in self.sub_results:
    128                                  lines.extend(r.format (
    129                                          left_margin = left_margin + 1,
    130                                          width = width,
    131                                          eol = eol,
    132                                          with_formula = with_formula,
    133                                          with_warnings = with_warnings,
    134                                          with_variables = with_variables,
    135                                          with_sub_results = False,                       # break cycles
    136                                          return_list = True
    137                                  ))
    138
    139                  lines = gmTools.strip_trailing_empty_lines(lines = lines, eol = eol)
    140                  if return_list:
    141                          return lines
    142
    143                  left_margin = ' ' * left_margin
    144                  return left_margin + (eol + left_margin).join(lines) + eol
    145
    146  #============================================================
    147 -class cClinicalCalculator(object):
    148
    149 -        def __init__(self, patient=None):
    150                  self.__cache = {}
    151                  self.__patient = patient
    152
    153          #--------------------------------------------------------
    154 -        def _get_patient(self):
    155                  return self.__patient
    156
    157 -        def _set_patient(self, patient):
    158                  if patient == self.__patient:
    159                          return
    160                  self.__patient = patient
    161                  self.remove_from_cache()                        # uncache all values
    162
    163          patient = property(lambda x:x, _set_patient)
    164
    165          #--------------------------------------------------------
    166 -        def remove_from_cache(self, key=None):
    167                  if key is None:
    168                          self.__cache = {}
    169                          return True
    170                  try:
    171                          del self.__cache[key]
    172                          return True
    173                  except KeyError:
    174                          _log.error('key [%s] does not exist in cache', key)
    175                          return False
    176
    177          #--------------------------------------------------------
    178          # formulae
    179          #--------------------------------------------------------
    180 -        def get_EDC(self, lmp=None, nullipara=True):
    181
    182                  result = cClinicalResult(_('unknown EDC'))
    183                  result.formula_name = 'EDC (Mittendorf 1990)'
    184                  result.formula_source = 'Mittendorf, R. et al., "The length of uncomplicated human gestation," OB/GYN, Vol. 75, No., 6 June, 1990, pp. 907-932.'
    185
    186                  if lmp is None:
    187                          result.message = _('EDC: unknown LMP')
    188                          return result
    189
    190                  result.variables['LMP'] = lmp
    191                  result.variables['nullipara'] = nullipara
    192                  if nullipara:
    193                          result.variables['parity_offset'] = 15          # days
    194                  else:
    195                          result.variables['parity_offset'] = 10          # days
    196
    197                  now = gmDateTime.pydt_now_here()
    198                  if lmp > now:
    199                          result.warnings.append(_('LMP in the future'))
    200
    201                  if self.__patient is None:
    202                          result.warnings.append(_('cannot run sanity checks, no patient'))
    203                  else:
    204                          if self.__patient['dob'] is None:
    205                                  result.warnings.append(_('cannot run sanity checks, no DOB'))
    206                          else:
    207                                  years, months, days, hours, minutes, seconds = gmDateTime.calculate_apparent_age(start = self.__patient['dob'])
    208                                  # 5 years -- Myth ?
    209                                  # http://www.mirror.co.uk/news/uk-news/top-10-crazy-amazing-and-world-789842
    210                                  if years < 10:
    211                                          result.warnings.append(_('patient less than 10 years old'))
    212                          if self.__patient['gender'] in [None, 'm']:
    213                                  result.warnings.append(_('atypical gender for pregnancy: %s') % self.__patient.gender_string)
    214                          if self.__patient['deceased'] is not None:
    215                                  result.warnings.append(_('patient already passed away'))
    216
    217                  if lmp.month > 3:
    218                          edc_month = lmp.month - 3
    219                          edc_year = lmp.year + 1
    220                  else:
    221                          edc_month = lmp.month + 9
    222                          edc_year = lmp.year
    223
    224                  result.numeric_value = gmDateTime.pydt_replace(dt = lmp, year = edc_year, month = edc_month, strict = False) + pydt.timedelta(days = result.variables['parity_offset'])
    225
    226                  result.message = _('EDC: %s') % gmDateTime.pydt_strftime (
    227                          result.numeric_value,
    228                          format = '%Y %b %d'
    229                  )
    230                  result.date_valid = now
    231
    232                  _log.debug('%s' % result)
    233
    234                  return result
    235
    236          #--------------------------------------------------------
    237 -        def _get_egfrs(self):
    238                  egfrs = [
    239                          self.eGFR_MDRD_short,
    240                          self.eGFR_Cockcroft_Gault,
    241                          self.eGFR_CKD_EPI,
    242                          self.eGFR_Schwartz
    243                  ]
    244                  return egfrs
    245
    246          eGFRs = property(_get_egfrs, lambda x:x)
    247
    248          #--------------------------------------------------------
    249 -        def _get_egfr(self):
    250
    251                  # < 18 ?
    252                  Schwartz = self.eGFR_Schwartz
    253                  if Schwartz.numeric_value is not None:
    254                          return Schwartz
    255
    256                  # this logic is based on "KVH aktuell 2/2014 Seite 10-15"
    257                  # expect normal GFR
    258                  CKD = self.eGFR_CKD_EPI
    259                  if CKD.numeric_value is not None:
    260                          if CKD.numeric_value > self.d(60):
    261                                  return CKD
    262
    263                  # CKD at or below 60
    264                  if self.__patient['dob'] is None:
    265                          return CKD                      # no method will work, so return CKD anyway
    266
    267                  CG = self.eGFR_Cockcroft_Gault
    268                  MDRD = self.eGFR_MDRD_short
    269                  age = None
    270                  if age is None:
    271                          try:
    272                                  age = CKD.variables['age@crea']
    273                          except KeyError:
    274                                  _log.warning('CKD-EPI: no age@crea')
    275                  if age is None:
    276                          try:
    277                                  age = CG.variables['age@crea']
    278                          except KeyError:
    279                                  _log.warning('CG: no age@crea')
    280                  if age is None:
    281                          try:
    282                                  age = MDRD.variables['age@crea']
    283                          except KeyError:
    284                                  _log.warning('MDRD: no age@crea')
    285                  if age is None:
    286                          age = gmDateTime.calculate_apparent_age(start = self.__patient['dob'])[0]
    287
    288                  # geriatric ?
    289                  if age > self.d(65):
    290                          if CG.numeric_value is not None:
    291                                  return CG
    292
    293                  # non-geriatric or CG not computable
    294                  if MDRD.numeric_value is None:
    295                          if (CKD.numeric_value is not None) or (CG.numeric_value is None):
    296                                  return CKD
    297                          return CG
    298
    299                  if MDRD.numeric_value > self.d(60):
    300                          if CKD.numeric_value is not None:
    301                                  # probably normal after all (>60) -> use CKD-EPI
    302                                  return CKD
    303
    304                  return MDRD
    305
    306          eGFR = property(_get_egfr, lambda x:x)
    307
    308          #--------------------------------------------------------
    309 -        def _get_gfr_mdrd_short(self):
    310
    311                  try:
    312                          return self.__cache['MDRD_short']
    313                  except KeyError:
    314                          pass
    315
    316                  result = cClinicalResult(_('unknown MDRD (4 vars/IDMS)'))
    317                  result.formula_name = 'eGFR from 4-variables IDMS-MDRD'
    318                  result.formula_source = '1/2013: http://en.wikipedia.org/Renal_function / http://www.ganfyd.org/index.php?title=Estimated_glomerular_filtration_rate (NHS)'
    319                  result.hints.append(_('best @ 30 < GFR < 60 ml/min'))
    320
    321                  if self.__patient is None:
    322                          result.message = _('MDRD (4 vars/IDMS): no patient')
    323                          return result
    324
    325                  if self.__patient['dob'] is None:
    326                          result.message = _('MDRD (4 vars/IDMS): no DOB (no age)')
    327                          return result
    328
    329                  # 1) gender
    330                  from Gnumed.business.gmPerson import map_gender2mf
    331                  result.variables['gender'] = self.__patient['gender']
    332                  result.variables['gender_mf'] = map_gender2mf[self.__patient['gender']]
    333                  if result.variables['gender_mf'] == 'm':
    334                          result.variables['gender_multiplier'] = self.d(1)
    335                  elif result.variables['gender_mf'] == 'f':
    336                          result.variables['gender_multiplier'] = self.d('0.742')
    337                  else:
    338                          result.message = _('MDRD (4 vars/IDMS): neither male nor female')
    339                          return result
    340
    341                  # 2) creatinine
    342                  result.variables['serum_crea'] = self.__patient.emr.get_most_recent_results_in_loinc_group(loincs = gmLOINC.LOINC_creatinine_quantity, no_of_results = 1)
    343                  if result.variables['serum_crea'] is None:
    344                          result.message = _('MDRD (4 vars/IDMS): serum creatinine value not found (LOINC: %s)') % gmLOINC.LOINC_creatinine_quantity
    345                          return result
    346                  if result.variables['serum_crea']['val_num'] is None:
    347                          result.message = _('MDRD (4 vars/IDMS): creatinine value not numeric')
    348                          return result
    349                  result.variables['serum_crea_val'] = self.d(result.variables['serum_crea']['val_num'])
    350                  if result.variables['serum_crea']['val_unit'] in ['mg/dl', 'mg/dL']:
    351                          result.variables['unit_multiplier'] = self.d(175)                                               # older: 186
    352                  elif result.variables['serum_crea']['val_unit'] in ['µmol/L', 'µmol/l']:
    353                          result.variables['unit_multiplier'] = self.d(30849)                                     # older: 32788
    354                  else:
    355                          result.message = _('MDRD (4 vars/IDMS): unknown serum creatinine unit (%s)') % result.variables['serum_crea']['val_unit']
    356                          return result
    357
    358                  # 3) age (at creatinine evaluation)
    359                  result.variables['dob'] = self.__patient['dob']
    360                  result.variables['age@crea'] = self.d (
    361                          gmDateTime.calculate_apparent_age (
    362                                  start = result.variables['dob'],
    363                                  end = result.variables['serum_crea']['clin_when']
    364                          )[0]
    365                  )
    366                  if (result.variables['age@crea'] > 84) or (result.variables['age@crea'] < 18):
    367                          result.message = _('MDRD (4 vars/IDMS): formula does not apply at age [%s] (17 < age < 85)') % result.variables['age@crea']
    368                          return result
    369
    370                  # 4) ethnicity
    371                  result.variables['ethnicity_multiplier'] = self.d(1)                    # non-black
    372                  result.warnings.append(_('ethnicity: GNUmed does not know patient ethnicity, ignoring correction factor'))
    373
    374                  # calculate
    375                  result.numeric_value =  result.variables['unit_multiplier'] * \
    376                          pow(result.variables['serum_crea_val'], self.d('-1.154')) * \
    377                          pow(result.variables['age@crea'], self.d('-0.203')) * \
    378                          result.variables['ethnicity_multiplier'] * \
    379                          result.variables['gender_multiplier']
    380                  result.unit = 'ml/min/1.73m²'
    381
    382                  BSA = self.body_surface_area
    383                  result.sub_results.append(BSA)
    384                  if BSA.numeric_value is None:
    385                          result.warnings.append(_('NOT corrected for non-average body surface (average = 1.73m²)'))
    386                  else:
    387                          result.variables['BSA'] = BSA.numeric_value
    388                          result_numeric_value = result.numeric_value / BSA.numeric_value
    389
    390                  result.message = _('eGFR(MDRD): %.1f %s (%s) [4-vars, IDMS]') % (
    391                          result.numeric_value,
    392                          result.unit,
    393                          gmDateTime.pydt_strftime (
    394                                  result.variables['serum_crea']['clin_when'],
    395                                  format = '%Y %b %d'
    396                          )
    397                  )
    398                  result.date_valid = result.variables['serum_crea']['clin_when']
    399
    400                  self.__cache['MDRD_short'] = result
    401                  _log.debug('%s' % result)
    402
    403                  return result
    404
    405          eGFR_MDRD_short = property(_get_gfr_mdrd_short, lambda x:x)
    406
    407          #--------------------------------------------------------
    408 -        def _get_gfr_ckd_epi(self):
    409
    410                  try:
    411                          return self.__cache['CKD-EPI']
    412                  except KeyError:
    413                          pass
    414
    415                  result = cClinicalResult(_('unknown CKD-EPI'))
    416                  result.formula_name = 'eGFR from CKD-EPI'
    417                  result.formula_source = '8/2014: http://en.wikipedia.org/Renal_function'
    418                  result.hints.append(_('best @ GFR > 60 ml/min'))
    419
    420                  if self.__patient is None:
    421                          result.message = _('CKD-EPI: no patient')
    422                          return result
    423
    424                  if self.__patient['dob'] is None:
    425                          result.message = _('CKD-EPI: no DOB (no age)')
    426                          return result
    427
    428                  # 1) gender
    429                  from Gnumed.business.gmPerson import map_gender2mf
    430                  result.variables['gender'] = self.__patient['gender']
    431                  result.variables['gender_mf'] = map_gender2mf[self.__patient['gender']]
    432                  if result.variables['gender_mf'] == 'm':
    433                          result.variables['gender_multiplier'] = self.d(1)
    434                          result.variables['k:gender_divisor'] = self.d('0.9')
    435                          result.variables['a:gender_power'] = self.d('-0.411')
    436                  elif result.variables['gender_mf'] == 'f':
    437                          result.variables['gender_multiplier'] = self.d('1.018')
    438                          result.variables['k:gender_divisor'] = self.d('0.7')
    439                          result.variables['a:gender_power'] = self.d('-0.329')
    440                  else:
    441                          result.message = _('CKD-EPI: neither male nor female')
    442                          return result
    443
    444                  # 2) creatinine
    445                  result.variables['serum_crea'] = self.__patient.emr.get_most_recent_results_in_loinc_group(loincs = gmLOINC.LOINC_creatinine_quantity, no_of_results = 1)
    446                  if result.variables['serum_crea'] is None:
    447                          result.message = _('CKD-EPI: serum creatinine value not found (LOINC: %s)') % gmLOINC.LOINC_creatinine_quantity
    448                          return result
    449                  if result.variables['serum_crea']['val_num'] is None:
    450                          result.message = _('CKD-EPI: creatinine value not numeric')
    451                          return result
    452                  result.variables['serum_crea_val'] = self.d(result.variables['serum_crea']['val_num'])
    453                  if result.variables['serum_crea']['val_unit'] in ['mg/dl', 'mg/dL']:
    454                          result.variables['serum_crea_val'] = self.d(result.variables['serum_crea']['val_num'])
    455                  elif result.variables['serum_crea']['val_unit'] in ['µmol/L', 'µmol/l']:
    456                          result.variables['serum_crea_val'] = self.d(result.variables['serum_crea']['val_num']) / self.d('88.4')
    457                  else:
    458                          result.message = _('CKD-EPI: unknown serum creatinine unit (%s)') % result.variables['serum_crea']['val_unit']
    459                          return result
    460
    461                  # 3) age (at creatinine evaluation)
    462                  result.variables['dob'] = self.__patient['dob']
    463                  result.variables['age@crea'] = self.d (
    464                          gmDateTime.calculate_apparent_age (
    465                                  start = result.variables['dob'],
    466                                  end = result.variables['serum_crea']['clin_when']
    467                          )[0]
    468                  )
    469  #               if (result.variables['age@crea'] > 84) or (result.variables['age@crea'] < 18):
    470  #                       result.message = _('CKD-EPI: formula does not apply at age [%s] (17 < age < 85)') % result.variables['age@crea']
    471  #                       return result
    472
    473                  # 4) ethnicity
    474                  result.variables['ethnicity_multiplier'] = self.d(1)                    # non-black
    475                  result.warnings.append(_('ethnicity: GNUmed does not know patient ethnicity, ignoring correction factor of 1.519 for "black"'))
    476
    477                  # calculate
    478                  result.numeric_value = (
    479                          self.d(141) * \
    480                          pow(min((result.variables['serum_crea_val'] / result.variables['k:gender_divisor']), self.d(1)), result.variables['a:gender_power']) * \
    481                          pow(max((result.variables['serum_crea_val'] / result.variables['k:gender_divisor']), self.d(1)), self.d('-1.209')) * \
    482                          pow(self.d('0.993'), result.variables['age@crea']) * \
    483                          result.variables['gender_multiplier'] * \
    484                          result.variables['ethnicity_multiplier']
    485                  )
    486                  result.unit = 'ml/min/1.73m²'
    487
    488                  result.message = _('eGFR(CKD-EPI): %.1f %s (%s)') % (
    489                          result.numeric_value,
    490                          result.unit,
    491                          gmDateTime.pydt_strftime (
    492                                  result.variables['serum_crea']['clin_when'],
    493                                  format = '%Y %b %d'
    494                          )
    495                  )
    496                  result.date_valid = result.variables['serum_crea']['clin_when']
    497
    498                  self.__cache['CKD-EPI'] = result
    499                  _log.debug('%s' % result)
    500
    501                  return result
    502
    503          eGFR_CKD_EPI = property(_get_gfr_ckd_epi, lambda x:x)
    504
    505          #--------------------------------------------------------
    506 -        def _get_gfr_cockcroft_gault(self):
    507
    508                  try:
    509                          return self.__cache['cockcroft_gault']
    510                  except KeyError:
    511                          pass
    512
    513                  result = cClinicalResult(_('unknown Cockcroft-Gault'))
    514                  result.formula_name = 'eGFR from Cockcroft-Gault'
    515                  result.formula_source = '8/2014: http://en.wikipedia.org/Renal_function'
    516                  result.hints.append(_('best @ age >65'))
    517
    518                  if self.__patient is None:
    519                          result.message = _('Cockcroft-Gault: no patient')
    520                          return result
    521
    522                  if self.__patient['dob'] is None:
    523                          result.message = _('Cockcroft-Gault: no DOB (no age)')
    524                          return result
    525
    526                  # 1) gender
    527                  from Gnumed.business.gmPerson import map_gender2mf
    528                  result.variables['gender'] = self.__patient['gender']
    529                  result.variables['gender_mf'] = map_gender2mf[self.__patient['gender']]
    530                  if result.variables['gender_mf'] not in ['m', 'f']:
    531                          result.message = _('Cockcroft-Gault: neither male nor female')
    532                          return result
    533
    534                  # 2) creatinine
    535                  result.variables['serum_crea'] = self.__patient.emr.get_most_recent_results_in_loinc_group(loincs = gmLOINC.LOINC_creatinine_quantity, no_of_results = 1)
    536                  if result.variables['serum_crea'] is None:
    537                          result.message = _('Cockcroft-Gault: serum creatinine value not found (LOINC: %s)') % gmLOINC.LOINC_creatinine_quantity
    538                          return result
    539                  if result.variables['serum_crea']['val_num'] is None:
    540                          result.message = _('Cockcroft-Gault: creatinine value not numeric')
    541                          return result
    542                  result.variables['serum_crea_val'] = self.d(result.variables['serum_crea']['val_num'])
    543                  if result.variables['serum_crea']['val_unit'] in ['mg/dl', 'mg/dL']:
    544                          result.variables['unit_multiplier'] = self.d(72)
    545                          if result.variables['gender_mf'] == 'm':
    546                                  result.variables['gender_multiplier'] = self.d('1')
    547                          else: #result.variables['gender_mf'] == 'f'
    548                                  result.variables['gender_multiplier'] = self.d('0.85')
    549                  elif result.variables['serum_crea']['val_unit'] in ['µmol/L', 'µmol/l']:
    550                          result.variables['unit_multiplier'] = self.d(1)
    551                          if result.variables['gender_mf'] == 'm':
    552                                  result.variables['gender_multiplier'] = self.d('1.23')
    553                          else: #result.variables['gender_mf'] == 'f'
    554                                  result.variables['gender_multiplier'] = self.d('1.04')
    555                  else:
    556                          result.message = _('Cockcroft-Gault: unknown serum creatinine unit (%s)') % result.variables['serum_crea']['val_unit']
    557                          return result
    558
    559                  # 3) age (at creatinine evaluation)
    560                  result.variables['dob'] = self.__patient['dob']
    561                  result.variables['age@crea'] = self.d (
    562                          gmDateTime.calculate_apparent_age (
    563                                  start = result.variables['dob'],
    564                                  end = result.variables['serum_crea']['clin_when']
    565                          )[0]
    566                  )
    567                  if (result.variables['age@crea'] < 18):
    568                          result.message = _('Cockcroft-Gault: formula does not apply at age [%s] (17 < age)') % result.variables['age@crea']
    569                          return result
    570
    571                  result.variables['weight'] = self.__patient.emr.get_most_recent_results_in_loinc_group(loincs = gmLOINC.LOINC_weight, no_of_results = 1)
    572                  if result.variables['weight'] is None:
    573                          result.message = _('Cockcroft-Gault: weight not found')
    574                          return result
    575                  if result.variables['weight']['val_num'] is None:
    576                          result.message = _('Cockcroft-Gault: weight not numeric')
    577                          return result
    578                  if result.variables['weight']['val_unit'] == 'kg':
    579                          result.variables['weight_kg'] = self.d(result.variables['weight']['val_num'])
    580                  elif result.variables['weight']['val_unit'] == 'g':
    581                          result.variables['weight_kg'] = self.d(result.variables['weight']['val_num'] / self.d(1000))
    582                  else:
    583                          result.message = _('Cockcroft-Gault: weight not in kg or g')
    584                          return result
    585
    586                  # calculate
    587                  result.numeric_value = ((
    588                          (140 - result.variables['age@crea']) * result.variables['weight_kg'] * result.variables['gender_multiplier']) \
    589                                          /                                                               \
    590                          (result.variables['unit_multiplier'] * result.variables['serum_crea_val'])
    591                  )
    592                  result.unit = 'ml/min'          #/1.73m²
    593
    594                  result.message = _('eGFR(CG): %.1f %s (%s)') % (
    595                          result.numeric_value,
    596                          result.unit,
    597                          gmDateTime.pydt_strftime (
    598                                  result.variables['serum_crea']['clin_when'],
    599                                  format = '%Y %b %d'
    600                          )
    601                  )
    602                  result.date_valid = result.variables['serum_crea']['clin_when']
    603
    604                  self.__cache['cockroft_gault'] = result
    605                  _log.debug('%s' % result)
    606
    607                  return result
    608
    609          eGFR_Cockcroft_Gault = property(_get_gfr_cockcroft_gault, lambda x:x)
    610
    611          #--------------------------------------------------------
    612 -        def _get_gfr_schwartz(self):
    613
    614                  try:
    615                          return self.__cache['gfr_schwartz']
    616                  except KeyError:
    617                          pass
    618
    619                  result = cClinicalResult(_('unknown eGFR (Schwartz)'))
    620                  result.formula_name = 'eGFR from updated Schwartz "bedside" formula (age < 19yrs)'
    621                  result.formula_source = '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'
    622                  result.hints.append(_('only applies @ age <18'))
    623
    624                  if self.__patient is None:
    625                          result.message = _('eGFR (Schwartz): no patient')
    626                          return result
    627
    628                  if self.__patient['dob'] is None:
    629                          result.message = _('eGFR (Schwartz): DOB needed for age')
    630                          return result
    631
    632                  result.variables['dob'] = self.__patient['dob']
    633
    634                  # creatinine
    635                  result.variables['serum_crea'] = self.__patient.emr.get_most_recent_results_in_loinc_group(loincs = gmLOINC.LOINC_creatinine_quantity, no_of_results = 1)
    636                  if result.variables['serum_crea'] is None:
    637                          result.message = _('eGFR (Schwartz): serum creatinine value not found (LOINC: %s') % gmLOINC.LOINC_creatinine_quantity
    638                          return result
    639                  if result.variables['serum_crea']['val_num'] is None:
    640                          result.message = _('eGFR (Schwartz): creatinine value not numeric')
    641                          return result
    642                  result.variables['serum_crea_val'] = self.d(result.variables['serum_crea']['val_num'])
    643                  if result.variables['serum_crea']['val_unit'] in ['mg/dl', 'mg/dL']:
    644                          result.variables['unit_multiplier'] = self.d(1)
    645                  elif result.variables['serum_crea']['val_unit'] in ['µmol/L', 'µmol/l']:
    646                          result.variables['unit_multiplier'] = self.d('0.00113')
    647                  else:
    648                          result.message = _('eGFR (Schwartz): unknown serum creatinine unit (%s)') % result.variables['serum_crea']['val_unit']
    649                          return result
    650
    651                  # age
    652                  result.variables['age@crea'] = self.d (
    653                          gmDateTime.calculate_apparent_age (
    654                                  start = result.variables['dob'],
    655                                  end = result.variables['serum_crea']['clin_when']
    656                          )[0]
    657                  )
    658                  if result.variables['age@crea'] > 17:
    659                          result.message = _('eGFR (Schwartz): formula does not apply at age [%s] (age must be <18)') % result.variables['age@crea']
    660                          return result
    661
    662                  # age-dependant constant
    663                  if result.variables['age@crea'] < 1:
    664                          # first year pre-term: k = 0.33
    665                          # first year full-term: k = (0.45)              0.41 (updated)
    666                          result.variables['constant_for_age'] = self.d('0.41')
    667                          result.warnings.append(_('eGFR (Schwartz): not known whether pre-term birth, applying full-term formula'))
    668                  else:
    669                          result.variables['constant_for_age'] = self.d('0.41')
    670
    671                  # height
    672                  result.variables['height'] = self.__patient.emr.get_result_at_timestamp (
    673                          timestamp = result.variables['serum_crea']['clin_when'],
    674                          loinc = gmLOINC.LOINC_height,
    675                          tolerance_interval = '7 days'
    676                  )
    677                  if result.variables['height'] is None:
    678                          result.message = _('eGFR (Schwartz): height not found')
    679                          return result
    680                  if result.variables['height']['val_num'] is None:
    681                          result.message = _('eGFR (Schwartz): height not numeric')
    682                          return result
    683                  if result.variables['height']['val_unit'] == 'cm':
    684                          result.variables['height_cm'] = self.d(result.variables['height']['val_num'])
    685                  elif result.variables['height']['val_unit'] == 'mm':
    686                          result.variables['height_cm'] = self.d(result.variables['height']['val_num'] / self.d(10))
    687                  elif result.variables['height']['val_unit'] == 'm':
    688                          result.variables['height_cm'] = self.d(result.variables['height']['val_num'] * 100)
    689                  else:
    690                          result.message = _('eGFR (Schwartz): height not in m, cm, or mm')
    691                          return result
    692
    693                  # calculate
    694                  result.numeric_value = (
    695                          result.variables['constant_for_age'] * result.variables['height_cm']
    696                  ) / (
    697                          result.variables['unit_multiplier'] * result.variables['serum_crea_val']
    698                  )
    699                  result.unit = 'ml/min/1.73m²'
    700
    701                  result.message = _('eGFR (Schwartz): %.1f %s (%s)') % (
    702                          result.numeric_value,
    703                          result.unit,
    704                          gmDateTime.pydt_strftime (
    705                                  result.variables['serum_crea']['clin_when'],
    706                                  format = '%Y %b %d'
    707                          )
    708                  )
    709                  result.date_valid = result.variables['serum_crea']['clin_when']
    710
    711                  self.__cache['gfr_schwartz'] = result
    712                  _log.debug('%s' % result)
    713
    714                  return result
    715
    716          eGFR_Schwartz = property(_get_gfr_schwartz, lambda x:x)
    717
    718          #--------------------------------------------------------
    719 -        def _get_body_surface_area(self):
    720
    721                  try:
    722                          return self.__cache['body_surface_area']
    723                  except KeyError:
    724                          pass
    725
    726                  result = cClinicalResult(_('unknown body surface area'))
    727                  result.formula_name = 'Du Bois Body Surface Area'
    728                  result.formula_source = '12/2012: http://en.wikipedia.org/wiki/Body_surface_area'
    729
    730                  if self.__patient is None:
    731                          result.message = _('Body Surface Area: no patient')
    732                          return result
    733
    734                  result.variables['height'] = self.__patient.emr.get_most_recent_results_in_loinc_group(loincs = gmLOINC.LOINC_height, no_of_results = 1)
    735                  if result.variables['height'] is None:
    736                          result.message = _('Body Surface Area: height not found')
    737                          return result
    738                  if result.variables['height']['val_num'] is None:
    739                          result.message = _('Body Surface Area: height not numeric')
    740                          return result
    741                  if result.variables['height']['val_unit'] == 'cm':
    742                          result.variables['height_cm'] = self.d(result.variables['height']['val_num'])
    743                  elif result.variables['height']['val_unit'] == 'mm':
    744                          result.variables['height_cm'] = self.d(result.variables['height']['val_num'] / self.d(10))
    745                  elif result.variables['height']['val_unit'] == 'm':
    746                          result.variables['height_cm'] = self.d(result.variables['height']['val_num'] * 100)
    747                  else:
    748                          result.message = _('Body Surface Area: height not in m, cm, or mm')
    749                          return result
    750
    751                  result.variables['weight'] = self.__patient.emr.get_result_at_timestamp (
    752                          timestamp = result.variables['height']['clin_when'],
    753                          loinc = gmLOINC.LOINC_weight,
    754                          tolerance_interval = '10 days'
    755                  )
    756                  if result.variables['weight'] is None:
    757                          result.message = _('Body Surface Area: weight not found')
    758                          return result
    759                  if result.variables['weight']['val_num'] is None:
    760                          result.message = _('Body Surface Area: weight not numeric')
    761                          return result
    762                  if result.variables['weight']['val_unit'] == 'kg':
    763                          result.variables['weight_kg'] = self.d(result.variables['weight']['val_num'])
    764                  elif result.variables['weight']['val_unit'] == 'g':
    765                          result.variables['weight_kg'] = self.d(result.variables['weight']['val_num'] / self.d(1000))
    766                  else:
    767                          result.message = _('Body Surface Area: weight not in kg or g')
    768                          return result
    769
    770                  result.numeric_value = self.d('0.007184') * \
    771                          pow(result.variables['weight_kg'], self.d('0.425')) * \
    772                          pow(result.variables['height_cm'], self.d('0.725'))
    773                  result.unit = 'm²'
    774
    775                  result.message = _('BSA (DuBois): %.2f %s') % (
    776                          result.numeric_value,
    777                          result.unit
    778                  )
    779                  result.date_valid = gmDateTime.pydt_now_here()
    780
    781                  self.__cache['body_surface_area'] = result
    782                  _log.debug('%s' % result)
    783
    784                  return result
    785
    786          body_surface_area = property(_get_body_surface_area, lambda x:x)
    787
    788          #--------------------------------------------------------
    789 -        def _get_body_mass_index(self):
    790
    791                  try:
    792                          return self.__cache['body_mass_index']
    793                  except KeyError:
    794                          pass
    795
    796                  result = cClinicalResult(_('unknown BMI'))
    797                  result.formula_name = 'BMI/Quetelet Index'
    798                  result.formula_source = '08/2014: https://en.wikipedia.org/wiki/Body_mass_index'
    799
    800                  if self.__patient is None:
    801                          result.message = _('BMI: no patient')
    802                          return result
    803
    804                  result.variables['height'] = self.__patient.emr.get_most_recent_results_in_loinc_group(loincs = gmLOINC.LOINC_height, no_of_results = 1)
    805                  if result.variables['height'] is None:
    806                          result.message = _('BMI: height not found')
    807                          return result
    808                  if result.variables['height']['val_num'] is None:
    809                          result.message = _('BMI: height not numeric')
    810                          return result
    811                  if result.variables['height']['val_unit'] == 'cm':
    812                          result.variables['height_m'] = self.d(result.variables['height']['val_num'] / self.d(100))
    813                  elif result.variables['height']['val_unit'] == 'mm':
    814                          result.variables['height_m'] = self.d(result.variables['height']['val_num'] / self.d(1000))
    815                  elif result.variables['height']['val_unit'] == 'm':
    816                          result.variables['height_m'] = self.d(result.variables['height']['val_num'])
    817                  else:
    818                          result.message = _('BMI: height not in m, cm, or mm')
    819                          return result
    820
    821                  result.variables['weight'] = self.__patient.emr.get_result_at_timestamp (
    822                          timestamp = result.variables['height']['clin_when'],
    823                          loinc = gmLOINC.LOINC_weight,
    824                          tolerance_interval = '10 days'
    825                  )
    826                  if result.variables['weight'] is None:
    827                          result.message = _('BMI: weight not found')
    828                          return result
    829                  if result.variables['weight']['val_num'] is None:
    830                          result.message = _('BMI: weight not numeric')
    831                          return result
    832                  if result.variables['weight']['val_unit'] == 'kg':
    833                          result.variables['weight_kg'] = self.d(result.variables['weight']['val_num'])
    834                  elif result.variables['weight']['val_unit'] == 'g':
    835                          result.variables['weight_kg'] = self.d(result.variables['weight']['val_num'] / self.d(1000))
    836                  else:
    837                          result.message = _('BMI: weight not in kg or g')
    838                          return result
    839
    840                  result.variables['dob'] = self.__patient['dob']
    841                  start = result.variables['dob']
    842                  end = result.variables['height']['clin_when']
    843                  multiplier = 1
    844                  if end < start:
    845                          start = result.variables['height']['clin_when']
    846                          end = result.variables['dob']
    847                          multiplier = -1
    848                  result.variables['age@height'] = multiplier * self.d(gmDateTime.calculate_apparent_age(start, end)[0])
    849                  if (result.variables['age@height'] < 18):
    850                          result.message = _('BMI (Quetelet): formula does not apply at age [%s] (0 < age < 18)') % result.variables['age@height']
    851                          return result
    852
    853                  # bmi = mass kg / height m2
    854                  result.numeric_value = result.variables['weight_kg'] / (result.variables['height_m'] * result.variables['height_m'])
    855                  result.unit = 'kg/m²'
    856
    857                  result.message = _('BMI (Quetelet): %.2f %s') % (
    858                          result.numeric_value,
    859                          result.unit
    860                  )
    861                  result.date_valid = gmDateTime.pydt_now_here()
    862
    863                  self.__cache['body_mass_index'] = result
    864                  _log.debug('%s' % result)
    865
    866                  return result
    867
    868          body_mass_index = property(_get_body_mass_index, lambda x:x)
    869          bmi = property(_get_body_mass_index, lambda x:x)
    870
    871          #--------------------------------------------------------
    872          # helper functions
    873          #--------------------------------------------------------
    874 -        def d(self, initial):
    875                  if isinstance(initial, decimal.Decimal):
    876                          return initial
    877
    878                  val = initial
    879
    880                  # float ? -> to string first
    881                  if type(val) == type(float(1.4)):
    882                          val = str(val)
    883
    884                  # string ? -> "," to "."
    885                  if isinstance(val, str):
    886                          val = val.replace(',', '.', 1)
    887                          val = val.strip()
    888
    889                  try:
    890                          d = decimal.Decimal(val)
    891                          return d
    892                  except (TypeError, decimal.InvalidOperation):
    893                          return None
    894
    895  #============================================================
    896  # main
    897  #------------------------------------------------------------
    898  if __name__ == "__main__":
    899
    900          if len(sys.argv) == 1:
    901                  sys.exit()
    902
    903          if sys.argv[1] != 'test':
    904                  sys.exit()
    905
    906          from Gnumed.pycommon import gmLog2
    907          #-----------------------------------------
    908 -        def test_clin_calc():
    909                  from Gnumed.business.gmPraxis import gmCurrentPraxisBranch
    910                  praxis = gmCurrentPraxisBranch()
    911                  from Gnumed.business.gmPerson import cPatient
    912                  pat = cPatient(aPK_obj = 12)
    913                  calc = cClinicalCalculator(patient = pat)
    914                  #result = calc.eGFR_MDRD_short
    915                  #result = calc.eGFR_Schwartz
    916                  result = calc.eGFR
    917                  #result = calc.body_surface_area
    918                  #result = calc.get_EDC(lmp = gmDateTime.pydt_now_here())
    919                  #result = calc.body_mass_index
    920                  #result = calc.eGFRs
    921                  print('%s' % result.format(with_formula = True, with_warnings = True, with_variables = True, with_sub_results = True))
    922          #-----------------------------------------
    923          test_clin_calc()
    924