Gnumed.business.gmClinicalCalculator

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:
    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                  creas = self.__patient.emr.get_most_recent_results_in_loinc_group(loincs = gmLOINC.LOINC_creatinine_quantity, max_no_of_results = 1)
    343                  result.variables['serum_crea'] = creas[0] if len(creas) > 0 else None
    344                  if result.variables['serum_crea'] is None:
    345                          result.message = _('MDRD (4 vars/IDMS): serum creatinine value not found (LOINC: %s)') % gmLOINC.LOINC_creatinine_quantity
    346                          return result
    347                  if result.variables['serum_crea']['val_num'] is None:
    348                          result.message = _('MDRD (4 vars/IDMS): creatinine value not numeric')
    349                          return result
    350                  result.variables['serum_crea_val'] = self.d(result.variables['serum_crea']['val_num'])
    351                  if result.variables['serum_crea']['val_unit'] in ['mg/dl', 'mg/dL']:
    352                          result.variables['unit_multiplier'] = self.d(175)                                               # older: 186
    353                  elif result.variables['serum_crea']['val_unit'] in ['µmol/L', 'µmol/l']:
    354                          result.variables['unit_multiplier'] = self.d(30849)                                     # older: 32788
    355                  else:
    356                          result.message = _('MDRD (4 vars/IDMS): unknown serum creatinine unit (%s)') % result.variables['serum_crea']['val_unit']
    357                          return result
    358
    359                  # 3) age (at creatinine evaluation)
    360                  result.variables['dob'] = self.__patient['dob']
    361                  result.variables['age@crea'] = self.d (
    362                          gmDateTime.calculate_apparent_age (
    363                                  start = result.variables['dob'],
    364                                  end = result.variables['serum_crea']['clin_when']
    365                          )[0]
    366                  )
    367                  if (result.variables['age@crea'] > 84) or (result.variables['age@crea'] < 18):
    368                          result.message = _('MDRD (4 vars/IDMS): formula does not apply at age [%s] (17 < age < 85)') % result.variables['age@crea']
    369                          return result
    370
    371                  # 4) ethnicity
    372                  result.variables['ethnicity_multiplier'] = self.d(1)                    # non-black
    373                  result.warnings.append(_('ethnicity: GNUmed does not know patient ethnicity, ignoring correction factor'))
    374
    375                  # calculate
    376                  result.numeric_value =  result.variables['unit_multiplier'] * \
    377                          pow(result.variables['serum_crea_val'], self.d('-1.154')) * \
    378                          pow(result.variables['age@crea'], self.d('-0.203')) * \
    379                          result.variables['ethnicity_multiplier'] * \
    380                          result.variables['gender_multiplier']
    381                  result.unit = 'ml/min/1.73m²'
    382
    383                  BSA = self.body_surface_area
    384                  result.sub_results.append(BSA)
    385                  if BSA.numeric_value is None:
    386                          result.warnings.append(_('NOT corrected for non-average body surface (average = 1.73m²)'))
    387                  else:
    388                          result.variables['BSA'] = BSA.numeric_value
    389                          result_numeric_value = result.numeric_value / BSA.numeric_value
    390
    391                  result.message = _('eGFR(MDRD): %.1f %s (%s) [4-vars, IDMS]') % (
    392                          result.numeric_value,
    393                          result.unit,
    394                          gmDateTime.pydt_strftime (
    395                                  result.variables['serum_crea']['clin_when'],
    396                                  format = '%Y %b %d'
    397                          )
    398                  )
    399                  result.date_valid = result.variables['serum_crea']['clin_when']
    400
    401                  self.__cache['MDRD_short'] = result
    402                  _log.debug('%s' % result)
    403
    404                  return result
    405
    406          eGFR_MDRD_short = property(_get_gfr_mdrd_short, lambda x:x)
    407
    408          #--------------------------------------------------------
    409 -        def _get_gfr_ckd_epi(self):
    410
    411                  try:
    412                          return self.__cache['CKD-EPI']
    413                  except KeyError:
    414                          pass
    415
    416                  result = cClinicalResult(_('unknown CKD-EPI'))
    417                  result.formula_name = 'eGFR from CKD-EPI'
    418                  result.formula_source = '8/2014: http://en.wikipedia.org/Renal_function'
    419                  result.hints.append(_('best @ GFR > 60 ml/min'))
    420
    421                  if self.__patient is None:
    422                          result.message = _('CKD-EPI: no patient')
    423                          return result
    424
    425                  if self.__patient['dob'] is None:
    426                          result.message = _('CKD-EPI: no DOB (no age)')
    427                          return result
    428
    429                  # 1) gender
    430                  from Gnumed.business.gmPerson import map_gender2mf
    431                  result.variables['gender'] = self.__patient['gender']
    432                  result.variables['gender_mf'] = map_gender2mf[self.__patient['gender']]
    433                  if result.variables['gender_mf'] == 'm':
    434                          result.variables['gender_multiplier'] = self.d(1)
    435                          result.variables['k:gender_divisor'] = self.d('0.9')
    436                          result.variables['a:gender_power'] = self.d('-0.411')
    437                  elif result.variables['gender_mf'] == 'f':
    438                          result.variables['gender_multiplier'] = self.d('1.018')
    439                          result.variables['k:gender_divisor'] = self.d('0.7')
    440                          result.variables['a:gender_power'] = self.d('-0.329')
    441                  else:
    442                          result.message = _('CKD-EPI: neither male nor female')
    443                          return result
    444
    445                  # 2) creatinine
    446                  creas = self.__patient.emr.get_most_recent_results_in_loinc_group(loincs = gmLOINC.LOINC_creatinine_quantity, max_no_of_results = 1)
    447                  result.variables['serum_crea'] = creas[0] if len(creas) > 0 else None
    448                  if result.variables['serum_crea'] is None:
    449                          result.message = _('CKD-EPI: serum creatinine value not found (LOINC: %s)') % gmLOINC.LOINC_creatinine_quantity
    450                          return result
    451                  if result.variables['serum_crea']['val_num'] is None:
    452                          result.message = _('CKD-EPI: creatinine value not numeric')
    453                          return result
    454                  result.variables['serum_crea_val'] = self.d(result.variables['serum_crea']['val_num'])
    455                  if result.variables['serum_crea']['val_unit'] in ['mg/dl', 'mg/dL']:
    456                          result.variables['serum_crea_val'] = self.d(result.variables['serum_crea']['val_num'])
    457                  elif result.variables['serum_crea']['val_unit'] in ['µmol/L', 'µmol/l']:
    458                          result.variables['serum_crea_val'] = self.d(result.variables['serum_crea']['val_num']) / self.d('88.4')
    459                  else:
    460                          result.message = _('CKD-EPI: unknown serum creatinine unit (%s)') % result.variables['serum_crea']['val_unit']
    461                          return result
    462
    463                  # 3) age (at creatinine evaluation)
    464                  result.variables['dob'] = self.__patient['dob']
    465                  result.variables['age@crea'] = self.d (
    466                          gmDateTime.calculate_apparent_age (
    467                                  start = result.variables['dob'],
    468                                  end = result.variables['serum_crea']['clin_when']
    469                          )[0]
    470                  )
    471  #               if (result.variables['age@crea'] > 84) or (result.variables['age@crea'] < 18):
    472  #                       result.message = _('CKD-EPI: formula does not apply at age [%s] (17 < age < 85)') % result.variables['age@crea']
    473  #                       return result
    474
    475                  # 4) ethnicity
    476                  result.variables['ethnicity_multiplier'] = self.d(1)                    # non-black
    477                  result.warnings.append(_('ethnicity: GNUmed does not know patient ethnicity, ignoring correction factor of 1.519 for "black"'))
    478
    479                  # calculate
    480                  result.numeric_value = (
    481                          self.d(141) * \
    482                          pow(min((result.variables['serum_crea_val'] / result.variables['k:gender_divisor']), self.d(1)), result.variables['a:gender_power']) * \
    483                          pow(max((result.variables['serum_crea_val'] / result.variables['k:gender_divisor']), self.d(1)), self.d('-1.209')) * \
    484                          pow(self.d('0.993'), result.variables['age@crea']) * \
    485                          result.variables['gender_multiplier'] * \
    486                          result.variables['ethnicity_multiplier']
    487                  )
    488                  result.unit = 'ml/min/1.73m²'
    489
    490                  result.message = _('eGFR(CKD-EPI): %.1f %s (%s)') % (
    491                          result.numeric_value,
    492                          result.unit,
    493                          gmDateTime.pydt_strftime (
    494                                  result.variables['serum_crea']['clin_when'],
    495                                  format = '%Y %b %d'
    496                          )
    497                  )
    498                  result.date_valid = result.variables['serum_crea']['clin_when']
    499
    500                  self.__cache['CKD-EPI'] = result
    501                  _log.debug('%s' % result)
    502
    503                  return result
    504
    505          eGFR_CKD_EPI = property(_get_gfr_ckd_epi, lambda x:x)
    506
    507          #--------------------------------------------------------
    508 -        def _get_gfr_cockcroft_gault(self):
    509
    510                  try:
    511                          return self.__cache['cockcroft_gault']
    512                  except KeyError:
    513                          pass
    514
    515                  result = cClinicalResult(_('unknown Cockcroft-Gault'))
    516                  result.formula_name = 'eGFR from Cockcroft-Gault'
    517                  result.formula_source = '8/2014: http://en.wikipedia.org/Renal_function'
    518                  result.hints.append(_('best @ age >65'))
    519
    520                  if self.__patient is None:
    521                          result.message = _('Cockcroft-Gault: no patient')
    522                          return result
    523
    524                  if self.__patient['dob'] is None:
    525                          result.message = _('Cockcroft-Gault: no DOB (no age)')
    526                          return result
    527
    528                  # 1) gender
    529                  from Gnumed.business.gmPerson import map_gender2mf
    530                  result.variables['gender'] = self.__patient['gender']
    531                  result.variables['gender_mf'] = map_gender2mf[self.__patient['gender']]
    532                  if result.variables['gender_mf'] not in ['m', 'f']:
    533                          result.message = _('Cockcroft-Gault: neither male nor female')
    534                          return result
    535
    536                  # 2) creatinine
    537                  creas = self.__patient.emr.get_most_recent_results_in_loinc_group(loincs = gmLOINC.LOINC_creatinine_quantity, max_no_of_results = 1)
    538                  result.variables['serum_crea'] = creas[0] if len(creas) > 0 else None
    539                  if result.variables['serum_crea'] is None:
    540                          result.message = _('Cockcroft-Gault: serum creatinine value not found (LOINC: %s)') % gmLOINC.LOINC_creatinine_quantity
    541                          return result
    542                  if result.variables['serum_crea']['val_num'] is None:
    543                          result.message = _('Cockcroft-Gault: creatinine value not numeric')
    544                          return result
    545                  result.variables['serum_crea_val'] = self.d(result.variables['serum_crea']['val_num'])
    546                  if result.variables['serum_crea']['val_unit'] in ['mg/dl', 'mg/dL']:
    547                          result.variables['unit_multiplier'] = self.d(72)
    548                          if result.variables['gender_mf'] == 'm':
    549                                  result.variables['gender_multiplier'] = self.d('1')
    550                          else: #result.variables['gender_mf'] == 'f'
    551                                  result.variables['gender_multiplier'] = self.d('0.85')
    552                  elif result.variables['serum_crea']['val_unit'] in ['µmol/L', 'µmol/l']:
    553                          result.variables['unit_multiplier'] = self.d(1)
    554                          if result.variables['gender_mf'] == 'm':
    555                                  result.variables['gender_multiplier'] = self.d('1.23')
    556                          else: #result.variables['gender_mf'] == 'f'
    557                                  result.variables['gender_multiplier'] = self.d('1.04')
    558                  else:
    559                          result.message = _('Cockcroft-Gault: unknown serum creatinine unit (%s)') % result.variables['serum_crea']['val_unit']
    560                          return result
    561
    562                  # 3) age (at creatinine evaluation)
    563                  result.variables['dob'] = self.__patient['dob']
    564                  result.variables['age@crea'] = self.d (
    565                          gmDateTime.calculate_apparent_age (
    566                                  start = result.variables['dob'],
    567                                  end = result.variables['serum_crea']['clin_when']
    568                          )[0]
    569                  )
    570                  if (result.variables['age@crea'] < 18):
    571                          result.message = _('Cockcroft-Gault: formula does not apply at age [%s] (17 < age)') % result.variables['age@crea']
    572                          return result
    573
    574                  weights = self.__patient.emr.get_most_recent_results_in_loinc_group(loincs = gmLOINC.LOINC_weight, max_no_of_results = 1)
    575                  result.variables['weight'] = weights[0] if len(weights) > 0 else None
    576                  if result.variables['weight'] is None:
    577                          result.message = _('Cockcroft-Gault: weight not found')
    578                          return result
    579                  if result.variables['weight']['val_num'] is None:
    580                          result.message = _('Cockcroft-Gault: weight not numeric')
    581                          return result
    582                  if result.variables['weight']['val_unit'] == 'kg':
    583                          result.variables['weight_kg'] = self.d(result.variables['weight']['val_num'])
    584                  elif result.variables['weight']['val_unit'] == 'g':
    585                          result.variables['weight_kg'] = self.d(result.variables['weight']['val_num'] / self.d(1000))
    586                  else:
    587                          result.message = _('Cockcroft-Gault: weight not in kg or g')
    588                          return result
    589
    590                  # calculate
    591                  result.numeric_value = ((
    592                          (140 - result.variables['age@crea']) * result.variables['weight_kg'] * result.variables['gender_multiplier']) \
    593                                          /                                                               \
    594                          (result.variables['unit_multiplier'] * result.variables['serum_crea_val'])
    595                  )
    596                  result.unit = 'ml/min'          #/1.73m²
    597
    598                  result.message = _('eGFR(CG): %.1f %s (%s)') % (
    599                          result.numeric_value,
    600                          result.unit,
    601                          gmDateTime.pydt_strftime (
    602                                  result.variables['serum_crea']['clin_when'],
    603                                  format = '%Y %b %d'
    604                          )
    605                  )
    606                  result.date_valid = result.variables['serum_crea']['clin_when']
    607
    608                  self.__cache['cockroft_gault'] = result
    609                  _log.debug('%s' % result)
    610
    611                  return result
    612
    613          eGFR_Cockcroft_Gault = property(_get_gfr_cockcroft_gault, lambda x:x)
    614
    615          #--------------------------------------------------------
    616 -        def _get_gfr_schwartz(self):
    617
    618                  try:
    619                          return self.__cache['gfr_schwartz']
    620                  except KeyError:
    621                          pass
    622
    623                  result = cClinicalResult(_('unknown eGFR (Schwartz)'))
    624                  result.formula_name = 'eGFR from updated Schwartz "bedside" formula (age < 19yrs)'
    625                  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'
    626                  result.hints.append(_('only applies @ age <18'))
    627
    628                  if self.__patient is None:
    629                          result.message = _('eGFR (Schwartz): no patient')
    630                          return result
    631
    632                  if self.__patient['dob'] is None:
    633                          result.message = _('eGFR (Schwartz): DOB needed for age')
    634                          return result
    635
    636                  result.variables['dob'] = self.__patient['dob']
    637
    638                  # creatinine
    639                  creas = self.__patient.emr.get_most_recent_results_in_loinc_group(loincs = gmLOINC.LOINC_creatinine_quantity, max_no_of_results = 1)
    640                  result.variables['serum_crea'] = creas[0] if len(creas) > 0 else None
    641                  if result.variables['serum_crea'] is None:
    642                          result.message = _('eGFR (Schwartz): serum creatinine value not found (LOINC: %s') % gmLOINC.LOINC_creatinine_quantity
    643                          return result
    644                  if result.variables['serum_crea']['val_num'] is None:
    645                          result.message = _('eGFR (Schwartz): creatinine value not numeric')
    646                          return result
    647                  result.variables['serum_crea_val'] = self.d(result.variables['serum_crea']['val_num'])
    648                  if result.variables['serum_crea']['val_unit'] in ['mg/dl', 'mg/dL']:
    649                          result.variables['unit_multiplier'] = self.d(1)
    650                  elif result.variables['serum_crea']['val_unit'] in ['µmol/L', 'µmol/l']:
    651                          result.variables['unit_multiplier'] = self.d('0.00113')
    652                  else:
    653                          result.message = _('eGFR (Schwartz): unknown serum creatinine unit (%s)') % result.variables['serum_crea']['val_unit']
    654                          return result
    655
    656                  # age
    657                  result.variables['age@crea'] = self.d (
    658                          gmDateTime.calculate_apparent_age (
    659                                  start = result.variables['dob'],
    660                                  end = result.variables['serum_crea']['clin_when']
    661                          )[0]
    662                  )
    663                  if result.variables['age@crea'] > 17:
    664                          result.message = _('eGFR (Schwartz): formula does not apply at age [%s] (age must be <18)') % result.variables['age@crea']
    665                          return result
    666
    667                  # age-dependant constant
    668                  if result.variables['age@crea'] < 1:
    669                          # first year pre-term: k = 0.33
    670                          # first year full-term: k = (0.45)              0.41 (updated)
    671                          result.variables['constant_for_age'] = self.d('0.41')
    672                          result.warnings.append(_('eGFR (Schwartz): not known whether pre-term birth, applying full-term formula'))
    673                  else:
    674                          result.variables['constant_for_age'] = self.d('0.41')
    675
    676                  # height
    677                  result.variables['height'] = self.__patient.emr.get_result_at_timestamp (
    678                          timestamp = result.variables['serum_crea']['clin_when'],
    679                          loinc = gmLOINC.LOINC_height,
    680                          tolerance_interval = '7 days'
    681                  )
    682                  if result.variables['height'] is None:
    683                          result.message = _('eGFR (Schwartz): height not found')
    684                          return result
    685                  if result.variables['height']['val_num'] is None:
    686                          result.message = _('eGFR (Schwartz): height not numeric')
    687                          return result
    688                  if result.variables['height']['val_unit'] == 'cm':
    689                          result.variables['height_cm'] = self.d(result.variables['height']['val_num'])
    690                  elif result.variables['height']['val_unit'] == 'mm':
    691                          result.variables['height_cm'] = self.d(result.variables['height']['val_num'] / self.d(10))
    692                  elif result.variables['height']['val_unit'] == 'm':
    693                          result.variables['height_cm'] = self.d(result.variables['height']['val_num'] * 100)
    694                  else:
    695                          result.message = _('eGFR (Schwartz): height not in m, cm, or mm')
    696                          return result
    697
    698                  # calculate
    699                  result.numeric_value = (
    700                          result.variables['constant_for_age'] * result.variables['height_cm']
    701                  ) / (
    702                          result.variables['unit_multiplier'] * result.variables['serum_crea_val']
    703                  )
    704                  result.unit = 'ml/min/1.73m²'
    705
    706                  result.message = _('eGFR (Schwartz): %.1f %s (%s)') % (
    707                          result.numeric_value,
    708                          result.unit,
    709                          gmDateTime.pydt_strftime (
    710                                  result.variables['serum_crea']['clin_when'],
    711                                  format = '%Y %b %d'
    712                          )
    713                  )
    714                  result.date_valid = result.variables['serum_crea']['clin_when']
    715
    716                  self.__cache['gfr_schwartz'] = result
    717                  _log.debug('%s' % result)
    718
    719                  return result
    720
    721          eGFR_Schwartz = property(_get_gfr_schwartz, lambda x:x)
    722
    723          #--------------------------------------------------------
    724 -        def _get_body_surface_area(self):
    725
    726                  try:
    727                          return self.__cache['body_surface_area']
    728                  except KeyError:
    729                          pass
    730
    731                  result = cClinicalResult(_('unknown body surface area'))
    732                  result.formula_name = 'Du Bois Body Surface Area'
    733                  result.formula_source = '12/2012: http://en.wikipedia.org/wiki/Body_surface_area'
    734
    735                  if self.__patient is None:
    736                          result.message = _('Body Surface Area: no patient')
    737                          return result
    738
    739                  heights = self.__patient.emr.get_most_recent_results_in_loinc_group(loincs = gmLOINC.LOINC_height, max_no_of_results = 1)
    740                  result.variables['height'] = heights[0] if len(heights) > 0 else None
    741                  if result.variables['height'] is None:
    742                          result.message = _('Body Surface Area: height not found')
    743                          return result
    744
    745                  if result.variables['height']['val_num'] is None:
    746                          result.message = _('Body Surface Area: height not numeric')
    747                          return result
    748
    749                  if result.variables['height']['val_unit'] == 'cm':
    750                          result.variables['height_cm'] = self.d(result.variables['height']['val_num'])
    751                  elif result.variables['height']['val_unit'] == 'mm':
    752                          result.variables['height_cm'] = self.d(result.variables['height']['val_num'] / self.d(10))
    753                  elif result.variables['height']['val_unit'] == 'm':
    754                          result.variables['height_cm'] = self.d(result.variables['height']['val_num'] * 100)
    755                  else:
    756                          result.message = _('Body Surface Area: height not in m, cm, or mm')
    757                          return result
    758
    759                  result.variables['weight'] = self.__patient.emr.get_result_at_timestamp (
    760                          timestamp = result.variables['height']['clin_when'],
    761                          loinc = gmLOINC.LOINC_weight,
    762                          tolerance_interval = '10 days'
    763                  )
    764                  if result.variables['weight'] is None:
    765                          result.message = _('Body Surface Area: weight not found')
    766                          return result
    767
    768                  if result.variables['weight']['val_num'] is None:
    769                          result.message = _('Body Surface Area: weight not numeric')
    770                          return result
    771
    772                  if result.variables['weight']['val_unit'] == 'kg':
    773                          result.variables['weight_kg'] = self.d(result.variables['weight']['val_num'])
    774                  elif result.variables['weight']['val_unit'] == 'g':
    775                          result.variables['weight_kg'] = self.d(result.variables['weight']['val_num'] / self.d(1000))
    776                  else:
    777                          result.message = _('Body Surface Area: weight not in kg or g')
    778                          return result
    779
    780                  result.numeric_value = self.d('0.007184') * \
    781                          pow(result.variables['weight_kg'], self.d('0.425')) * \
    782                          pow(result.variables['height_cm'], self.d('0.725'))
    783                  result.unit = 'm²'
    784                  result.message = _('BSA (DuBois): %.2f %s') % (
    785                          result.numeric_value,
    786                          result.unit
    787                  )
    788                  result.date_valid = gmDateTime.pydt_now_here()
    789                  self.__cache['body_surface_area'] = result
    790                  _log.debug('%s' % result)
    791                  return result
    792
    793          body_surface_area = property(_get_body_surface_area, lambda x:x)
    794
    795          #--------------------------------------------------------
    796 -        def _get_body_mass_index(self):
    797
    798                  try:
    799                          return self.__cache['body_mass_index']
    800                  except KeyError:
    801                          pass
    802
    803                  result = cClinicalResult(_('unknown BMI'))
    804                  result.formula_name = 'BMI/Quetelet Index'
    805                  result.formula_source = '08/2014: https://en.wikipedia.org/wiki/Body_mass_index'
    806
    807                  if self.__patient is None:
    808                          result.message = _('BMI: no patient')
    809                          return result
    810
    811                  heights = self.__patient.emr.get_most_recent_results_in_loinc_group(loincs = gmLOINC.LOINC_height, max_no_of_results = 1)
    812                  result.variables['height'] = heights[0] if len(heights) > 0 else None
    813                  if result.variables['height'] is None:
    814                          result.message = _('BMI: height not found')
    815                          return result
    816
    817                  if result.variables['height']['val_num'] is None:
    818                          result.message = _('BMI: height not numeric')
    819                          return result
    820
    821                  if result.variables['height']['val_unit'] == 'cm':
    822                          result.variables['height_m'] = self.d(result.variables['height']['val_num'] / self.d(100))
    823                  elif result.variables['height']['val_unit'] == 'mm':
    824                          result.variables['height_m'] = self.d(result.variables['height']['val_num'] / self.d(1000))
    825                  elif result.variables['height']['val_unit'] == 'm':
    826                          result.variables['height_m'] = self.d(result.variables['height']['val_num'])
    827                  else:
    828                          result.message = _('BMI: height not in m, cm, or mm')
    829                          return result
    830
    831                  result.variables['weight'] = self.__patient.emr.get_result_at_timestamp (
    832                          timestamp = result.variables['height']['clin_when'],
    833                          loinc = gmLOINC.LOINC_weight,
    834                          tolerance_interval = '10 days'
    835                  )
    836                  if result.variables['weight'] is None:
    837                          result.message = _('BMI: weight not found')
    838                          return result
    839                  if result.variables['weight']['val_num'] is None:
    840                          result.message = _('BMI: weight not numeric')
    841                          return result
    842                  if result.variables['weight']['val_unit'] == 'kg':
    843                          result.variables['weight_kg'] = self.d(result.variables['weight']['val_num'])
    844                  elif result.variables['weight']['val_unit'] == 'g':
    845                          result.variables['weight_kg'] = self.d(result.variables['weight']['val_num'] / self.d(1000))
    846                  else:
    847                          result.message = _('BMI: weight not in kg or g')
    848                          return result
    849
    850                  result.variables['dob'] = self.__patient['dob']
    851                  start = result.variables['dob']
    852                  end = result.variables['height']['clin_when']
    853                  multiplier = 1
    854                  if end < start:
    855                          start = result.variables['height']['clin_when']
    856                          end = result.variables['dob']
    857                          multiplier = -1
    858                  result.variables['age@height'] = multiplier * self.d(gmDateTime.calculate_apparent_age(start, end)[0])
    859                  if (result.variables['age@height'] < 18):
    860                          result.message = _('BMI (Quetelet): formula does not apply at age [%s] (0 < age < 18)') % result.variables['age@height']
    861                          return result
    862
    863                  # bmi = mass kg / height m2
    864                  result.numeric_value = result.variables['weight_kg'] / (result.variables['height_m'] * result.variables['height_m'])
    865                  result.unit = 'kg/m²'
    866
    867                  result.message = _('BMI (Quetelet): %.2f %s') % (
    868                          result.numeric_value,
    869                          result.unit
    870                  )
    871                  result.date_valid = gmDateTime.pydt_now_here()
    872
    873                  self.__cache['body_mass_index'] = result
    874                  _log.debug('%s' % result)
    875
    876                  return result
    877
    878          body_mass_index = property(_get_body_mass_index, lambda x:x)
    879          bmi = property(_get_body_mass_index, lambda x:x)
    880
    881          #--------------------------------------------------------
    882          # helper functions
    883          #--------------------------------------------------------
    884 -        def d(self, initial):
    885                  if isinstance(initial, decimal.Decimal):
    886                          return initial
    887
    888                  val = initial
    889
    890                  # float ? -> to string first
    891                  if type(val) == type(float(1.4)):
    892                          val = str(val)
    893
    894                  # string ? -> "," to "."
    895                  if isinstance(val, str):
    896                          val = val.replace(',', '.', 1)
    897                          val = val.strip()
    898
    899                  try:
    900                          d = decimal.Decimal(val)
    901                          return d
    902                  except (TypeError, decimal.InvalidOperation):
    903                          return None
    904
    905  #============================================================
    906  # main
    907  #------------------------------------------------------------
    908  if __name__ == "__main__":
    909
    910          if len(sys.argv) == 1:
    911                  sys.exit()
    912
    913          if sys.argv[1] != 'test':
    914                  sys.exit()
    915
    916          from Gnumed.pycommon import gmLog2
    917
    918          #-----------------------------------------
    919 -        def test_clin_calc():
    920                  from Gnumed.business import gmPraxis
    921                  branches = gmPraxis.get_praxis_branches()
    922                  praxis = gmPraxis.gmCurrentPraxisBranch(branches[0])
    923
    924                  from Gnumed.business.gmPerson import cPatient
    925                  pat = cPatient(aPK_obj = 201)
    926
    927                  calc = cClinicalCalculator(patient = pat)
    928                  #result = calc.eGFR_MDRD_short
    929                  #result = calc.eGFR_Schwartz
    930                  result = calc.eGFR
    931                  #result = calc.body_surface_area
    932                  #result = calc.get_EDC(lmp = gmDateTime.pydt_now_here())
    933                  #result = calc.body_mass_index
    934                  #result = calc.eGFRs
    935                  print('%s' % result.format(with_formula = True, with_warnings = True, with_variables = True, with_sub_results = True))
    936
    937          #-----------------------------------------
    938          test_clin_calc()
    939