Coverage for tasks/nart.py: 47%

95 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-11-08 23:14 +0000

1#!/usr/bin/env python 

2 

3""" 

4camcops_server/tasks/nart.py 

5 

6=============================================================================== 

7 

8 Copyright (C) 2012, University of Cambridge, Department of Psychiatry. 

9 Created by Rudolf Cardinal (rnc1001@cam.ac.uk). 

10 

11 This file is part of CamCOPS. 

12 

13 CamCOPS is free software: you can redistribute it and/or modify 

14 it under the terms of the GNU General Public License as published by 

15 the Free Software Foundation, either version 3 of the License, or 

16 (at your option) any later version. 

17 

18 CamCOPS is distributed in the hope that it will be useful, 

19 but WITHOUT ANY WARRANTY; without even the implied warranty of 

20 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

21 GNU General Public License for more details. 

22 

23 You should have received a copy of the GNU General Public License 

24 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>. 

25 

26=============================================================================== 

27 

28""" 

29 

30import math 

31from typing import Any, Dict, List, Optional, Tuple, Type 

32 

33from sqlalchemy.ext.declarative import DeclarativeMeta 

34from sqlalchemy.sql.sqltypes import Boolean, Float 

35 

36from camcops_server.cc_modules.cc_constants import CssClass 

37from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo 

38from camcops_server.cc_modules.cc_html import answer, td, tr_qa 

39from camcops_server.cc_modules.cc_request import CamcopsRequest 

40from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup 

41from camcops_server.cc_modules.cc_sqla_coltypes import ( 

42 BIT_CHECKER, 

43 CamcopsColumn, 

44) 

45from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

46from camcops_server.cc_modules.cc_task import ( 

47 Task, 

48 TaskHasClinicianMixin, 

49 TaskHasPatientMixin, 

50) 

51 

52 

53WORDLIST = [ # Value is true/1 for CORRECT, false/0 for INCORRECT 

54 "chord", 

55 "ache", 

56 "depot", 

57 "aisle", 

58 "bouquet", 

59 "psalm", 

60 "capon", 

61 "deny", # NB reserved word in SQL (auto-handled) 

62 "nausea", 

63 "debt", 

64 "courteous", 

65 "rarefy", 

66 "equivocal", 

67 "naive", # accent required 

68 "catacomb", 

69 "gaoled", 

70 "thyme", 

71 "heir", 

72 "radix", 

73 "assignate", 

74 "hiatus", 

75 "subtle", 

76 "procreate", 

77 "gist", 

78 "gouge", 

79 "superfluous", 

80 "simile", 

81 "banal", 

82 "quadruped", 

83 "cellist", 

84 "facade", # accent required 

85 "zealot", 

86 "drachm", 

87 "aeon", 

88 "placebo", 

89 "abstemious", 

90 "detente", # accent required 

91 "idyll", 

92 "puerperal", 

93 "aver", 

94 "gauche", 

95 "topiary", 

96 "leviathan", 

97 "beatify", 

98 "prelate", 

99 "sidereal", 

100 "demesne", 

101 "syncope", 

102 "labile", 

103 "campanile", 

104] 

105ACCENTED_WORDLIST = list(WORDLIST) 

106# noinspection PyUnresolvedReferences 

107ACCENTED_WORDLIST[ACCENTED_WORDLIST.index("naive")] = "naïve" 

108ACCENTED_WORDLIST[ACCENTED_WORDLIST.index("facade")] = "façade" 

109ACCENTED_WORDLIST[ACCENTED_WORDLIST.index("detente")] = "détente" 

110 

111 

112# ============================================================================= 

113# NART 

114# ============================================================================= 

115 

116 

117class NartMetaclass(DeclarativeMeta): 

118 # noinspection PyInitNewSignature 

119 def __init__( 

120 cls: Type["Nart"], 

121 name: str, 

122 bases: Tuple[Type, ...], 

123 classdict: Dict[str, Any], 

124 ) -> None: 

125 for w in WORDLIST: 

126 setattr( 

127 cls, 

128 w, 

129 CamcopsColumn( 

130 w, 

131 Boolean, 

132 permitted_value_checker=BIT_CHECKER, 

133 comment=f"Pronounced {w} correctly (0 no, 1 yes)", 

134 ), 

135 ) 

136 super().__init__(name, bases, classdict) 

137 

138 

139class Nart( 

140 TaskHasPatientMixin, TaskHasClinicianMixin, Task, metaclass=NartMetaclass 

141): 

142 """ 

143 Server implementation of the NART task. 

144 """ 

145 

146 __tablename__ = "nart" 

147 shortname = "NART" 

148 

149 @staticmethod 

150 def longname(req: "CamcopsRequest") -> str: 

151 _ = req.gettext 

152 return _("National Adult Reading Test") 

153 

154 def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]: 

155 if not self.is_complete(): 

156 return CTV_INCOMPLETE 

157 return [ 

158 CtvInfo( 

159 content=( 

160 "NART predicted WAIS FSIQ {n_fsiq}, WAIS VIQ {n_viq}, " 

161 "WAIS PIQ {n_piq}, WAIS-R FSIQ {nw_fsiq}, " 

162 "WAIS-IV FSIQ {b_fsiq}, WAIS-IV GAI {b_gai}, " 

163 "WAIS-IV VCI {b_vci}, WAIS-IV PRI {b_pri}, " 

164 "WAIS_IV WMI {b_wmi}, WAIS-IV PSI {b_psi}".format( 

165 n_fsiq=self.nelson_full_scale_iq(), 

166 n_viq=self.nelson_verbal_iq(), 

167 n_piq=self.nelson_performance_iq(), 

168 nw_fsiq=self.nelson_willison_full_scale_iq(), 

169 b_fsiq=self.bright_full_scale_iq(), 

170 b_gai=self.bright_general_ability(), 

171 b_vci=self.bright_verbal_comprehension(), 

172 b_pri=self.bright_perceptual_reasoning(), 

173 b_wmi=self.bright_working_memory(), 

174 b_psi=self.bright_perceptual_speed(), 

175 ) 

176 ) 

177 ) 

178 ] 

179 

180 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]: 

181 return self.standard_task_summary_fields() + [ 

182 SummaryElement( 

183 name="nelson_full_scale_iq", 

184 coltype=Float(), 

185 value=self.nelson_full_scale_iq(), 

186 comment="Predicted WAIS full-scale IQ (Nelson 1982)", 

187 ), 

188 SummaryElement( 

189 name="nelson_verbal_iq", 

190 coltype=Float(), 

191 value=self.nelson_verbal_iq(), 

192 comment="Predicted WAIS verbal IQ (Nelson 1982)", 

193 ), 

194 SummaryElement( 

195 name="nelson_performance_iq", 

196 coltype=Float(), 

197 value=self.nelson_performance_iq(), 

198 comment="Predicted WAIS performance IQ (Nelson 1982", 

199 ), 

200 SummaryElement( 

201 name="nelson_willison_full_scale_iq", 

202 coltype=Float(), 

203 value=self.nelson_willison_full_scale_iq(), 

204 comment="Predicted WAIS-R full-scale IQ " 

205 "(Nelson & Willison 1991)", 

206 ), 

207 SummaryElement( 

208 name="bright_full_scale_iq", 

209 coltype=Float(), 

210 value=self.bright_full_scale_iq(), 

211 comment="Predicted WAIS-IV full-scale IQ (Bright 2016)", 

212 ), 

213 SummaryElement( 

214 name="bright_general_ability", 

215 coltype=Float(), 

216 value=self.bright_general_ability(), 

217 comment="Predicted WAIS-IV General Ability Index " 

218 "(Bright 2016)", 

219 ), 

220 SummaryElement( 

221 name="bright_verbal_comprehension", 

222 coltype=Float(), 

223 value=self.bright_verbal_comprehension(), 

224 comment="Predicted WAIS-IV Verbal Comprehension Index " 

225 "(Bright 2016)", 

226 ), 

227 SummaryElement( 

228 name="bright_perceptual_reasoning", 

229 coltype=Float(), 

230 value=self.bright_perceptual_reasoning(), 

231 comment="Predicted WAIS-IV Perceptual Reasoning Index " 

232 "(Bright 2016)", 

233 ), 

234 SummaryElement( 

235 name="bright_working_memory", 

236 coltype=Float(), 

237 value=self.bright_working_memory(), 

238 comment="Predicted WAIS-IV Working Memory Index (Bright 2016)", 

239 ), 

240 SummaryElement( 

241 name="bright_perceptual_speed", 

242 coltype=Float(), 

243 value=self.bright_perceptual_speed(), 

244 comment="Predicted WAIS-IV Perceptual Speed Index " 

245 "(Bright 2016)", 

246 ), 

247 ] 

248 

249 def is_complete(self) -> bool: 

250 return ( 

251 self.all_fields_not_none(WORDLIST) and self.field_contents_valid() 

252 ) 

253 

254 def n_errors(self) -> int: 

255 e = 0 

256 for w in WORDLIST: 

257 if getattr(self, w) is not None and not getattr(self, w): 

258 e += 1 

259 return e 

260 

261 def get_task_html(self, req: CamcopsRequest) -> str: 

262 # Table rows for individual words 

263 q_a = "" 

264 nwords = len(WORDLIST) 

265 ncolumns = 3 

266 nrows = int(math.ceil(float(nwords) / float(ncolumns))) 

267 column = 0 

268 row = 0 

269 # x: word index (shown in top-to-bottom then left-to-right sequence) 

270 for unused_loopvar in range(nwords): 

271 x = (column * nrows) + row 

272 if column == 0: # first column 

273 q_a += "<tr>" 

274 q_a += td(ACCENTED_WORDLIST[x]) 

275 q_a += td(answer(getattr(self, WORDLIST[x]))) 

276 if column == (ncolumns - 1): # last column 

277 q_a += "</tr>" 

278 row += 1 

279 column = (column + 1) % ncolumns 

280 

281 # Annotations 

282 nelson = "; Nelson 1982 <sup>[1]</sup>" 

283 nelson_willison = "; Nelson &amp; Willison 1991 <sup>[2]</sup>" 

284 bright = "; Bright 2016 <sup>[3]</sup>" 

285 

286 # HTML 

287 h = """ 

288 <div class="{CssClass.SUMMARY}"> 

289 <table class="{CssClass.SUMMARY}"> 

290 {tr_is_complete} 

291 {tr_total_errors} 

292 

293 {nelson_full_scale_iq} 

294 {nelson_verbal_iq} 

295 {nelson_performance_iq} 

296 {nelson_willison_full_scale_iq} 

297 

298 {bright_full_scale_iq} 

299 {bright_general_ability} 

300 {bright_verbal_comprehension} 

301 {bright_perceptual_reasoning} 

302 {bright_working_memory} 

303 {bright_perceptual_speed} 

304 </table> 

305 </div> 

306 <div class="{CssClass.EXPLANATION}"> 

307 Estimates premorbid IQ by pronunciation of irregular words. 

308 </div> 

309 <table class="{CssClass.TASKDETAIL}"> 

310 <tr> 

311 <th width="16%">Word</th><th width="16%">Correct?</th> 

312 <th width="16%">Word</th><th width="16%">Correct?</th> 

313 <th width="16%">Word</th><th width="16%">Correct?</th> 

314 </tr> 

315 {q_a} 

316 </table> 

317 <div class="{CssClass.FOOTNOTES}"> 

318 [1] Nelson HE (1982), <i>National Adult Reading Test (NART): 

319 For the Assessment of Premorbid Intelligence in Patients 

320 with Dementia: Test Manual</i>, NFER-Nelson, Windsor, UK. 

321 [2] Nelson HE, Wilson J (1991) 

322 <i>National Adult Reading Test (NART)</i>, 

323 NFER-Nelson, Windsor, UK; see [3]. 

324 [3] Bright P et al (2016). The National Adult Reading Test: 

325 restandardisation against the Wechsler Adult Intelligence 

326 Scale—Fourth edition. 

327 <a href="https://www.ncbi.nlm.nih.gov/pubmed/27624393">PMID 

328 27624393</a>. 

329 </div> 

330 <div class="{CssClass.COPYRIGHT}"> 

331 NART: Copyright © Hazel E. Nelson. Used with permission. 

332 </div> 

333 """.format( 

334 CssClass=CssClass, 

335 tr_is_complete=self.get_is_complete_tr(req), 

336 tr_total_errors=tr_qa("Total errors", self.n_errors()), 

337 nelson_full_scale_iq=tr_qa( 

338 "Predicted WAIS full-scale IQ = 127.7 – 0.826 × errors" 

339 + nelson, # noqa 

340 self.nelson_full_scale_iq(), 

341 ), 

342 nelson_verbal_iq=tr_qa( 

343 "Predicted WAIS verbal IQ = 129.0 – 0.919 × errors" + nelson, 

344 self.nelson_verbal_iq(), 

345 ), 

346 nelson_performance_iq=tr_qa( 

347 "Predicted WAIS performance IQ = 123.5 – 0.645 × errors" 

348 + nelson, 

349 self.nelson_performance_iq(), 

350 ), 

351 nelson_willison_full_scale_iq=tr_qa( 

352 "Predicted WAIS-R full-scale IQ " 

353 "= 130.6 – 1.24 × errors" + nelson_willison, 

354 self.nelson_willison_full_scale_iq(), 

355 ), 

356 bright_full_scale_iq=tr_qa( 

357 "Predicted WAIS-IV full-scale IQ " 

358 "= 126.41 – 0.9775 × errors" + bright, 

359 self.bright_full_scale_iq(), 

360 ), 

361 bright_general_ability=tr_qa( 

362 "Predicted WAIS-IV General Ability Index " 

363 "= 126.5 – 0.9656 × errors" + bright, 

364 self.bright_general_ability(), 

365 ), 

366 bright_verbal_comprehension=tr_qa( 

367 "Predicted WAIS-IV Verbal Comprehension Index " 

368 "= 126.81 – 1.0745 × errors" + bright, 

369 self.bright_verbal_comprehension(), 

370 ), 

371 bright_perceptual_reasoning=tr_qa( 

372 "Predicted WAIS-IV Perceptual Reasoning Index " 

373 "= 120.18 – 0.6242 × errors" + bright, 

374 self.bright_perceptual_reasoning(), 

375 ), 

376 bright_working_memory=tr_qa( 

377 "Predicted WAIS-IV Working Memory Index " 

378 "= 120.53 – 0.7901 × errors" + bright, 

379 self.bright_working_memory(), 

380 ), 

381 bright_perceptual_speed=tr_qa( 

382 "Predicted WAIS-IV Perceptual Speed Index " 

383 "= 114.53 – 0.5285 × errors" + bright, 

384 self.bright_perceptual_speed(), 

385 ), 

386 q_a=q_a, 

387 ) 

388 return h 

389 

390 def predict(self, intercept: float, slope: float) -> Optional[float]: 

391 if not self.is_complete(): 

392 return None 

393 return intercept + slope * self.n_errors() 

394 

395 def nelson_full_scale_iq(self) -> Optional[float]: 

396 return self.predict(intercept=127.7, slope=-0.826) 

397 

398 def nelson_verbal_iq(self) -> Optional[float]: 

399 return self.predict(intercept=129.0, slope=-0.919) 

400 

401 def nelson_performance_iq(self) -> Optional[float]: 

402 return self.predict(intercept=123.5, slope=-0.645) 

403 

404 def nelson_willison_full_scale_iq(self) -> Optional[float]: 

405 return self.predict(intercept=130.6, slope=-1.24) 

406 

407 def bright_full_scale_iq(self) -> Optional[float]: 

408 return self.predict(intercept=126.41, slope=-0.9775) 

409 

410 def bright_general_ability(self) -> Optional[float]: 

411 return self.predict(intercept=126.5, slope=-0.9656) 

412 

413 def bright_verbal_comprehension(self) -> Optional[float]: 

414 return self.predict(intercept=126.81, slope=-1.0745) 

415 

416 def bright_perceptual_reasoning(self) -> Optional[float]: 

417 return self.predict(intercept=120.18, slope=-0.6242) 

418 

419 def bright_working_memory(self) -> Optional[float]: 

420 return self.predict(intercept=120.53, slope=-0.7901) 

421 

422 def bright_perceptual_speed(self) -> Optional[float]: 

423 return self.predict(intercept=114.53, slope=-0.5285) 

424 

425 def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]: 

426 codes = [ 

427 SnomedExpression( 

428 req.snomed(SnomedLookup.NART_PROCEDURE_ASSESSMENT) 

429 ) 

430 ] 

431 if self.is_complete(): 

432 codes.append( 

433 SnomedExpression( 

434 req.snomed(SnomedLookup.NART_SCALE), 

435 { 

436 # Best value debatable: 

437 req.snomed( 

438 SnomedLookup.NART_SCORE 

439 ): self.nelson_full_scale_iq() # noqa 

440 }, 

441 ) 

442 ) 

443 return codes