Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1#!/usr/bin/env python 

2 

3""" 

4camcops_server/tasks/nart.py 

5 

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

7 

8 Copyright (C) 2012-2020 Rudolf Cardinal (rudolf@pobox.com). 

9 

10 This file is part of CamCOPS. 

11 

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

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

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

15 (at your option) any later version. 

16 

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

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

19 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

20 GNU General Public License for more details. 

21 

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

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

24 

25=============================================================================== 

26 

27""" 

28 

29import math 

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

31 

32from sqlalchemy.ext.declarative import DeclarativeMeta 

33from sqlalchemy.sql.sqltypes import Boolean, Float 

34 

35from camcops_server.cc_modules.cc_constants import CssClass 

36from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo 

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

38from camcops_server.cc_modules.cc_request import CamcopsRequest 

39from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup 

40from camcops_server.cc_modules.cc_sqla_coltypes import ( 

41 BIT_CHECKER, 

42 CamcopsColumn, 

43) 

44from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

45from camcops_server.cc_modules.cc_task import ( 

46 Task, 

47 TaskHasClinicianMixin, 

48 TaskHasPatientMixin, 

49) 

50 

51 

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

53 "chord", 

54 "ache", 

55 "depot", 

56 "aisle", 

57 "bouquet", 

58 "psalm", 

59 "capon", 

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

61 "nausea", 

62 "debt", 

63 "courteous", 

64 "rarefy", 

65 "equivocal", 

66 "naive", # accent required 

67 "catacomb", 

68 "gaoled", 

69 "thyme", 

70 "heir", 

71 "radix", 

72 "assignate", 

73 "hiatus", 

74 "subtle", 

75 "procreate", 

76 "gist", 

77 "gouge", 

78 "superfluous", 

79 "simile", 

80 "banal", 

81 "quadruped", 

82 "cellist", 

83 "facade", # accent required 

84 "zealot", 

85 "drachm", 

86 "aeon", 

87 "placebo", 

88 "abstemious", 

89 "detente", # accent required 

90 "idyll", 

91 "puerperal", 

92 "aver", 

93 "gauche", 

94 "topiary", 

95 "leviathan", 

96 "beatify", 

97 "prelate", 

98 "sidereal", 

99 "demesne", 

100 "syncope", 

101 "labile", 

102 "campanile" 

103] 

104ACCENTED_WORDLIST = list(WORDLIST) 

105# noinspection PyUnresolvedReferences 

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

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

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

109 

110 

111# ============================================================================= 

112# NART 

113# ============================================================================= 

114 

115class NartMetaclass(DeclarativeMeta): 

116 # noinspection PyInitNewSignature 

117 def __init__(cls: Type['Nart'], 

118 name: str, 

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

120 classdict: Dict[str, Any]) -> None: 

121 for w in WORDLIST: 

122 setattr( 

123 cls, 

124 w, 

125 CamcopsColumn( 

126 w, Boolean, 

127 permitted_value_checker=BIT_CHECKER, 

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

129 ) 

130 ) 

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

132 

133 

134class Nart(TaskHasPatientMixin, TaskHasClinicianMixin, Task, 

135 metaclass=NartMetaclass): 

136 """ 

137 Server implementation of the NART task. 

138 """ 

139 __tablename__ = "nart" 

140 shortname = "NART" 

141 

142 @staticmethod 

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

144 _ = req.gettext 

145 return _("National Adult Reading Test") 

146 

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

148 if not self.is_complete(): 

149 return CTV_INCOMPLETE 

150 return [CtvInfo( 

151 content=( 

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

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

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

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

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

157 n_fsiq=self.nelson_full_scale_iq(), 

158 n_viq=self.nelson_verbal_iq(), 

159 n_piq=self.nelson_performance_iq(), 

160 nw_fsiq=self.nelson_willison_full_scale_iq(), 

161 b_fsiq=self.bright_full_scale_iq(), 

162 b_gai=self.bright_general_ability(), 

163 b_vci=self.bright_verbal_comprehension(), 

164 b_pri=self.bright_perceptual_reasoning(), 

165 b_wmi=self.bright_working_memory(), 

166 b_psi=self.bright_perceptual_speed(), 

167 ) 

168 ) 

169 )] 

170 

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

172 return self.standard_task_summary_fields() + [ 

173 SummaryElement( 

174 name="nelson_full_scale_iq", 

175 coltype=Float(), 

176 value=self.nelson_full_scale_iq(), 

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

178 SummaryElement( 

179 name="nelson_verbal_iq", 

180 coltype=Float(), 

181 value=self.nelson_verbal_iq(), 

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

183 SummaryElement( 

184 name="nelson_performance_iq", 

185 coltype=Float(), 

186 value=self.nelson_performance_iq(), 

187 comment="Predicted WAIS performance IQ (Nelson 1982"), 

188 SummaryElement( 

189 name="nelson_willison_full_scale_iq", 

190 coltype=Float(), 

191 value=self.nelson_willison_full_scale_iq(), 

192 comment="Predicted WAIS-R full-scale IQ (Nelson & Willison 1991"), # noqa 

193 SummaryElement( 

194 name="bright_full_scale_iq", 

195 coltype=Float(), 

196 value=self.bright_full_scale_iq(), 

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

198 SummaryElement( 

199 name="bright_general_ability", 

200 coltype=Float(), 

201 value=self.bright_general_ability(), 

202 comment="Predicted WAIS-IV General Ability Index (Bright 2016)"), # noqa 

203 SummaryElement( 

204 name="bright_verbal_comprehension", 

205 coltype=Float(), 

206 value=self.bright_verbal_comprehension(), 

207 comment="Predicted WAIS-IV Verbal Comprehension Index (Bright 2016)"), # noqa 

208 SummaryElement( 

209 name="bright_perceptual_reasoning", 

210 coltype=Float(), 

211 value=self.bright_perceptual_reasoning(), 

212 comment="Predicted WAIS-IV Perceptual Reasoning Index (Bright 2016)"), # noqa 

213 SummaryElement( 

214 name="bright_working_memory", 

215 coltype=Float(), 

216 value=self.bright_working_memory(), 

217 comment="Predicted WAIS-IV Working Memory Index (Bright 2016)"), # noqa 

218 SummaryElement( 

219 name="bright_perceptual_speed", 

220 coltype=Float(), 

221 value=self.bright_perceptual_speed(), 

222 comment="Predicted WAIS-IV Perceptual Speed Index (Bright 2016)"), # noqa 

223 ] 

224 

225 def is_complete(self) -> bool: 

226 return ( 

227 self.all_fields_not_none(WORDLIST) and 

228 self.field_contents_valid() 

229 ) 

230 

231 def n_errors(self) -> int: 

232 e = 0 

233 for w in WORDLIST: 

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

235 e += 1 

236 return e 

237 

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

239 # Table rows for individual words 

240 q_a = "" 

241 nwords = len(WORDLIST) 

242 ncolumns = 3 

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

244 column = 0 

245 row = 0 

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

247 for unused_loopvar in range(nwords): 

248 x = (column * nrows) + row 

249 if column == 0: # first column 

250 q_a += "<tr>" 

251 q_a += td(ACCENTED_WORDLIST[x]) 

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

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

254 q_a += "</tr>" 

255 row += 1 

256 column = (column + 1) % ncolumns 

257 

258 # Annotations 

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

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

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

262 

263 # HTML 

264 h = """ 

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

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

267 {tr_is_complete} 

268 {tr_total_errors} 

269 

270 {nelson_full_scale_iq} 

271 {nelson_verbal_iq} 

272 {nelson_performance_iq} 

273 {nelson_willison_full_scale_iq} 

274 

275 {bright_full_scale_iq} 

276 {bright_general_ability} 

277 {bright_verbal_comprehension} 

278 {bright_perceptual_reasoning} 

279 {bright_working_memory} 

280 {bright_perceptual_speed} 

281 </table> 

282 </div> 

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

284 Estimates premorbid IQ by pronunciation of irregular words. 

285 </div> 

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

287 <tr> 

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

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

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

291 </tr> 

292 {q_a} 

293 </table> 

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

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

296 For the Assessment of Premorbid Intelligence in Patients 

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

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

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

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

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

302 restandardisation against the Wechsler Adult Intelligence 

303 Scale—Fourth edition. 

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

305 27624393</a>. 

306 </div> 

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

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

309 </div> 

310 """.format( 

311 CssClass=CssClass, 

312 tr_is_complete=self.get_is_complete_tr(req), 

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

314 nelson_full_scale_iq=tr_qa( 

315 "Predicted WAIS full-scale IQ = 127.7 – 0.826 × errors" + nelson, # noqa 

316 self.nelson_full_scale_iq() 

317 ), 

318 nelson_verbal_iq=tr_qa( 

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

320 self.nelson_verbal_iq() 

321 ), 

322 nelson_performance_iq=tr_qa( 

323 "Predicted WAIS performance IQ = 123.5 – 0.645 × errors" + 

324 nelson, 

325 self.nelson_performance_iq() 

326 ), 

327 nelson_willison_full_scale_iq=tr_qa( 

328 "Predicted WAIS-R full-scale IQ " 

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

330 self.nelson_willison_full_scale_iq() 

331 ), 

332 bright_full_scale_iq=tr_qa( 

333 "Predicted WAIS-IV full-scale IQ " 

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

335 self.bright_full_scale_iq() 

336 ), 

337 bright_general_ability=tr_qa( 

338 "Predicted WAIS-IV General Ability Index " 

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

340 self.bright_general_ability() 

341 ), 

342 bright_verbal_comprehension=tr_qa( 

343 "Predicted WAIS-IV Verbal Comprehension Index " 

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

345 self.bright_verbal_comprehension() 

346 ), 

347 bright_perceptual_reasoning=tr_qa( 

348 "Predicted WAIS-IV Perceptual Reasoning Index " 

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

350 self.bright_perceptual_reasoning() 

351 ), 

352 bright_working_memory=tr_qa( 

353 "Predicted WAIS-IV Working Memory Index " 

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

355 self.bright_working_memory() 

356 ), 

357 bright_perceptual_speed=tr_qa( 

358 "Predicted WAIS-IV Perceptual Speed Index " 

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

360 self.bright_perceptual_speed() 

361 ), 

362 q_a=q_a, 

363 ) 

364 return h 

365 

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

367 if not self.is_complete(): 

368 return None 

369 return intercept + slope * self.n_errors() 

370 

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

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

373 

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

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

376 

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

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

379 

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

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

382 

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

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

385 

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

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

388 

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

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

391 

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

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

394 

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

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

397 

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

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

400 

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

402 codes = [SnomedExpression(req.snomed(SnomedLookup.NART_PROCEDURE_ASSESSMENT))] # noqa 

403 if self.is_complete(): 

404 codes.append(SnomedExpression( 

405 req.snomed(SnomedLookup.NART_SCALE), 

406 { 

407 # Best value debatable: 

408 req.snomed(SnomedLookup.NART_SCORE): self.nelson_full_scale_iq(), # noqa 

409 } 

410 )) 

411 return codes