Coverage for tasks/nart.py: 46%

92 statements  

« prev     ^ index     » next       coverage.py v7.9.2, created at 2025-07-15 14:23 +0100

1""" 

2camcops_server/tasks/nart.py 

3 

4=============================================================================== 

5 

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

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

8 

9 This file is part of CamCOPS. 

10 

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

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

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

14 (at your option) any later version. 

15 

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

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

18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

19 GNU General Public License for more details. 

20 

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

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

23 

24=============================================================================== 

25 

26""" 

27 

28import math 

29from typing import Any, List, Optional, Type 

30 

31from sqlalchemy.sql.sqltypes import Boolean, Float 

32 

33from camcops_server.cc_modules.cc_constants import CssClass 

34from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo 

35from camcops_server.cc_modules.cc_html import answer, pmid, td, tr_qa 

36from camcops_server.cc_modules.cc_request import CamcopsRequest 

37from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup 

38from camcops_server.cc_modules.cc_sqla_coltypes import ( 

39 BIT_CHECKER, 

40 camcops_column, 

41) 

42from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

43from camcops_server.cc_modules.cc_task import ( 

44 Task, 

45 TaskHasClinicianMixin, 

46 TaskHasPatientMixin, 

47) 

48 

49 

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

51 "chord", 

52 "ache", 

53 "depot", 

54 "aisle", 

55 "bouquet", 

56 "psalm", 

57 "capon", 

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

59 "nausea", 

60 "debt", 

61 "courteous", 

62 "rarefy", 

63 "equivocal", 

64 "naive", # accent required 

65 "catacomb", 

66 "gaoled", 

67 "thyme", 

68 "heir", 

69 "radix", 

70 "assignate", 

71 "hiatus", 

72 "subtle", 

73 "procreate", 

74 "gist", 

75 "gouge", 

76 "superfluous", 

77 "simile", 

78 "banal", 

79 "quadruped", 

80 "cellist", 

81 "facade", # accent required 

82 "zealot", 

83 "drachm", 

84 "aeon", 

85 "placebo", 

86 "abstemious", 

87 "detente", # accent required 

88 "idyll", 

89 "puerperal", 

90 "aver", 

91 "gauche", 

92 "topiary", 

93 "leviathan", 

94 "beatify", 

95 "prelate", 

96 "sidereal", 

97 "demesne", 

98 "syncope", 

99 "labile", 

100 "campanile", 

101] 

102ACCENTED_WORDLIST = list(WORDLIST) 

103# noinspection PyUnresolvedReferences 

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

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

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

107 

108 

109# ============================================================================= 

110# NART 

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

112 

113 

114class Nart( # type: ignore[misc] 

115 TaskHasPatientMixin, 

116 TaskHasClinicianMixin, 

117 Task, 

118): 

119 """ 

120 Server implementation of the NART task. 

121 """ 

122 

123 __tablename__ = "nart" 

124 shortname = "NART" 

125 

126 @classmethod 

127 def extend_columns(cls: Type["Nart"], **kwargs: Any) -> None: 

128 for w in WORDLIST: 

129 setattr( 

130 cls, 

131 w, 

132 camcops_column( 

133 w, 

134 Boolean, 

135 permitted_value_checker=BIT_CHECKER, 

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

137 ), 

138 ) 

139 

140 @staticmethod 

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

142 _ = req.gettext 

143 return _("National Adult Reading Test") 

144 

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

146 if not self.is_complete(): 

147 return CTV_INCOMPLETE 

148 return [ 

149 CtvInfo( 

150 content=( 

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

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

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

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

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

156 n_fsiq=self.nelson_full_scale_iq(), 

157 n_viq=self.nelson_verbal_iq(), 

158 n_piq=self.nelson_performance_iq(), 

159 nw_fsiq=self.nelson_willison_full_scale_iq(), 

160 b_fsiq=self.bright_full_scale_iq(), 

161 b_gai=self.bright_general_ability(), 

162 b_vci=self.bright_verbal_comprehension(), 

163 b_pri=self.bright_perceptual_reasoning(), 

164 b_wmi=self.bright_working_memory(), 

165 b_psi=self.bright_perceptual_speed(), 

166 ) 

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 ), 

179 SummaryElement( 

180 name="nelson_verbal_iq", 

181 coltype=Float(), 

182 value=self.nelson_verbal_iq(), 

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

184 ), 

185 SummaryElement( 

186 name="nelson_performance_iq", 

187 coltype=Float(), 

188 value=self.nelson_performance_iq(), 

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

190 ), 

191 SummaryElement( 

192 name="nelson_willison_full_scale_iq", 

193 coltype=Float(), 

194 value=self.nelson_willison_full_scale_iq(), 

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

196 "(Nelson & Willison 1991)", 

197 ), 

198 SummaryElement( 

199 name="bright_full_scale_iq", 

200 coltype=Float(), 

201 value=self.bright_full_scale_iq(), 

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

203 ), 

204 SummaryElement( 

205 name="bright_general_ability", 

206 coltype=Float(), 

207 value=self.bright_general_ability(), 

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

209 "(Bright 2016)", 

210 ), 

211 SummaryElement( 

212 name="bright_verbal_comprehension", 

213 coltype=Float(), 

214 value=self.bright_verbal_comprehension(), 

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

216 "(Bright 2016)", 

217 ), 

218 SummaryElement( 

219 name="bright_perceptual_reasoning", 

220 coltype=Float(), 

221 value=self.bright_perceptual_reasoning(), 

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

223 "(Bright 2016)", 

224 ), 

225 SummaryElement( 

226 name="bright_working_memory", 

227 coltype=Float(), 

228 value=self.bright_working_memory(), 

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

230 ), 

231 SummaryElement( 

232 name="bright_perceptual_speed", 

233 coltype=Float(), 

234 value=self.bright_perceptual_speed(), 

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

236 "(Bright 2016)", 

237 ), 

238 ] 

239 

240 def is_complete(self) -> bool: 

241 return ( 

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

243 ) 

244 

245 def n_errors(self) -> int: 

246 e = 0 

247 for w in WORDLIST: 

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

249 e += 1 

250 return e 

251 

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

253 # Table rows for individual words 

254 q_a = "" 

255 nwords = len(WORDLIST) 

256 ncolumns = 3 

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

258 column = 0 

259 row = 0 

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

261 for unused_loopvar in range(nwords): 

262 x = (column * nrows) + row 

263 if column == 0: # first column 

264 q_a += "<tr>" 

265 q_a += td(ACCENTED_WORDLIST[x]) 

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

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

268 q_a += "</tr>" 

269 row += 1 

270 column = (column + 1) % ncolumns 

271 

272 # Annotations 

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

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

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

276 

277 # HTML 

278 h = """ 

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

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

281 {tr_is_complete} 

282 {tr_total_errors} 

283 

284 {nelson_full_scale_iq} 

285 {nelson_verbal_iq} 

286 {nelson_performance_iq} 

287 {nelson_willison_full_scale_iq} 

288 

289 {bright_full_scale_iq} 

290 {bright_general_ability} 

291 {bright_verbal_comprehension} 

292 {bright_perceptual_reasoning} 

293 {bright_working_memory} 

294 {bright_perceptual_speed} 

295 </table> 

296 </div> 

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

298 Estimates premorbid IQ by pronunciation of irregular words. 

299 </div> 

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

301 <tr> 

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

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

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

305 </tr> 

306 {q_a} 

307 </table> 

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

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

310 For the Assessment of Premorbid Intelligence in Patients 

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

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

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

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

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

316 restandardisation against the Wechsler Adult Intelligence 

317 Scale—Fourth edition. {pmid}. 

318 </div> 

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

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

321 </div> 

322 """.format( 

323 CssClass=CssClass, 

324 tr_is_complete=self.get_is_complete_tr(req), 

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

326 nelson_full_scale_iq=tr_qa( 

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

328 + nelson, 

329 self.nelson_full_scale_iq(), 

330 ), 

331 nelson_verbal_iq=tr_qa( 

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

333 self.nelson_verbal_iq(), 

334 ), 

335 nelson_performance_iq=tr_qa( 

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

337 + nelson, 

338 self.nelson_performance_iq(), 

339 ), 

340 nelson_willison_full_scale_iq=tr_qa( 

341 "Predicted WAIS-R full-scale IQ " 

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

343 self.nelson_willison_full_scale_iq(), 

344 ), 

345 bright_full_scale_iq=tr_qa( 

346 "Predicted WAIS-IV full-scale IQ " 

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

348 self.bright_full_scale_iq(), 

349 ), 

350 bright_general_ability=tr_qa( 

351 "Predicted WAIS-IV General Ability Index " 

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

353 self.bright_general_ability(), 

354 ), 

355 bright_verbal_comprehension=tr_qa( 

356 "Predicted WAIS-IV Verbal Comprehension Index " 

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

358 self.bright_verbal_comprehension(), 

359 ), 

360 bright_perceptual_reasoning=tr_qa( 

361 "Predicted WAIS-IV Perceptual Reasoning Index " 

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

363 self.bright_perceptual_reasoning(), 

364 ), 

365 bright_working_memory=tr_qa( 

366 "Predicted WAIS-IV Working Memory Index " 

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

368 self.bright_working_memory(), 

369 ), 

370 bright_perceptual_speed=tr_qa( 

371 "Predicted WAIS-IV Perceptual Speed Index " 

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

373 self.bright_perceptual_speed(), 

374 ), 

375 q_a=q_a, 

376 pmid=pmid(27624393), 

377 ) 

378 return h 

379 

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

381 if not self.is_complete(): 

382 return None 

383 return intercept + slope * self.n_errors() 

384 

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

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

387 

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

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

390 

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

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

393 

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

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

396 

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

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

399 

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

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

402 

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

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

405 

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

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

408 

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

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

411 

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

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

414 

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

416 codes = [ 

417 SnomedExpression( 

418 req.snomed(SnomedLookup.NART_PROCEDURE_ASSESSMENT) 

419 ) 

420 ] 

421 if self.is_complete(): 

422 codes.append( 

423 SnomedExpression( 

424 req.snomed(SnomedLookup.NART_SCALE), 

425 { 

426 # Best value debatable: 

427 req.snomed( 

428 SnomedLookup.NART_SCORE 

429 ): self.nelson_full_scale_iq() 

430 }, 

431 ) 

432 ) 

433 return codes