Coverage for tasks/nart.py: 46%

1"""

2 camcops_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

28 import math

29 from typing import Any, List, Optional, Type

30

31 from sqlalchemy.sql.sqltypes import Boolean, Float

32

33 from camcops_server.cc_modules.cc_constants import CssClass

34 from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo

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

36 from camcops_server.cc_modules.cc_request import CamcopsRequest

37 from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup

38 from camcops_server.cc_modules.cc_sqla_coltypes import (

39 BIT_CHECKER,

40 camcops_column,

41)

42 from camcops_server.cc_modules.cc_summaryelement import SummaryElement

43 from camcops_server.cc_modules.cc_task import (

44 Task,

45 TaskHasClinicianMixin,

46 TaskHasPatientMixin,

47)

48

49

50 WORDLIST = [ # 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]

102 ACCENTED_WORDLIST = list(WORDLIST)

103# noinspection PyUnresolvedReferences

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

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

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

107

108

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

110# NART

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

112

113

114 class 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