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/ace3.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 

29from typing import Any, Dict, List, Optional, Tuple, Type, TYPE_CHECKING 

30 

31from cardinal_pythonlib.stringfunc import strseq 

32import cardinal_pythonlib.rnc_web as ws 

33import numpy 

34from sqlalchemy.ext.declarative import DeclarativeMeta 

35from sqlalchemy.sql.schema import Column 

36from sqlalchemy.sql.sqltypes import Integer, String, UnicodeText 

37 

38from camcops_server.cc_modules.cc_blob import ( 

39 blob_relationship, 

40 get_blob_img_html, 

41) 

42from camcops_server.cc_modules.cc_constants import ( 

43 CssClass, 

44 PlotDefaults, 

45 PV, 

46) 

47from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo 

48from camcops_server.cc_modules.cc_db import add_multiple_columns 

49from camcops_server.cc_modules.cc_html import ( 

50 answer, 

51 italic, 

52 subheading_spanning_two_columns, 

53 tr, 

54 tr_qa, 

55 tr_span_col, 

56) 

57from camcops_server.cc_modules.cc_request import CamcopsRequest 

58from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup 

59from camcops_server.cc_modules.cc_sqla_coltypes import ( 

60 BIT_CHECKER, 

61 CamcopsColumn, 

62 PermittedValueChecker, 

63) 

64from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

65from camcops_server.cc_modules.cc_task import ( 

66 Task, 

67 TaskHasClinicianMixin, 

68 TaskHasPatientMixin, 

69) 

70from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo 

71 

72if TYPE_CHECKING: 

73 from camcops_server.cc_modules.cc_blob import Blob 

74 

75 

76# ============================================================================= 

77# Constants 

78# ============================================================================= 

79 

80ADDRESS_PARTS = ["forename", "surname", "number", "street_1", 

81 "street_2", "town", "county"] 

82RECALL_WORDS = ["lemon", "key", "ball"] 

83PERCENT_DP = 1 

84TOTAL_MAX = 100 

85ATTN_MAX = 18 

86MEMORY_MAX = 26 

87FLUENCY_MAX = 14 

88LANG_MAX = 26 

89VSP_MAX = 16 

90 

91 

92# ============================================================================= 

93# Ancillary functions 

94# ============================================================================= 

95 

96def score_zero_for_absent(x: Optional[int]) -> int: 

97 """0 if x is None else x""" 

98 return 0 if x is None else x 

99 

100 

101# ============================================================================= 

102# ACE-III 

103# ============================================================================= 

104 

105class Ace3Metaclass(DeclarativeMeta): 

106 # noinspection PyInitNewSignature 

107 def __init__(cls: Type['Ace3'], 

108 name: str, 

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

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

111 add_multiple_columns( 

112 cls, "attn_time", 1, 5, pv=PV.BIT, 

113 comment_fmt="Attention, time, {n}/5, {s} (0 or 1)", 

114 comment_strings=["day", "date", "month", "year", "season"], 

115 ) 

116 add_multiple_columns( 

117 cls, "attn_place", 1, 5, pv=PV.BIT, 

118 comment_fmt="Attention, place, {n}/5, {s} (0 or 1)", 

119 comment_strings=["house number/floor", "street/hospital", 

120 "town", "county", "country"], 

121 ) 

122 add_multiple_columns( 

123 cls, "attn_repeat_word", 1, 3, pv=PV.BIT, 

124 comment_fmt="Attention, repeat word, {n}/3, {s} (0 or 1)", 

125 comment_strings=RECALL_WORDS, 

126 ) 

127 add_multiple_columns( 

128 cls, "attn_serial7_subtraction", 1, 5, pv=PV.BIT, 

129 comment_fmt="Attention, serial sevens, {n}/5 (0 or 1)", 

130 ) 

131 

132 add_multiple_columns( 

133 cls, "mem_recall_word", 1, 3, pv=PV.BIT, 

134 comment_fmt="Memory, recall word, {n}/3, {s} (0 or 1)", 

135 comment_strings=RECALL_WORDS, 

136 ) 

137 add_multiple_columns( 

138 cls, "mem_repeat_address_trial1_", 1, 7, pv=PV.BIT, 

139 comment_fmt="Memory, address registration trial 1/3 " 

140 "(not scored), {s} (0 or 1)", 

141 comment_strings=ADDRESS_PARTS, 

142 ) 

143 add_multiple_columns( 

144 cls, "mem_repeat_address_trial2_", 1, 7, pv=PV.BIT, 

145 comment_fmt="Memory, address registration trial 2/3 " 

146 "(not scored), {s} (0 or 1)", 

147 comment_strings=ADDRESS_PARTS, 

148 ) 

149 add_multiple_columns( 

150 cls, "mem_repeat_address_trial3_", 1, 7, pv=PV.BIT, 

151 comment_fmt="Memory, address registration trial 3/3 " 

152 "(scored), {s} (0 or 1)", 

153 comment_strings=ADDRESS_PARTS, 

154 ) 

155 add_multiple_columns( 

156 cls, "mem_famous", 1, 4, pv=PV.BIT, 

157 comment_fmt="Memory, famous people, {n}/4, {s} (0 or 1)", 

158 comment_strings=["current PM", "woman PM", "USA president", "JFK"], 

159 ) 

160 

161 add_multiple_columns( 

162 cls, "lang_follow_command", 1, 3, pv=PV.BIT, 

163 comment_fmt="Language, command {n}/3 (0 or 1)", 

164 ) 

165 add_multiple_columns( 

166 cls, "lang_write_sentences_point", 1, 2, pv=PV.BIT, 

167 comment_fmt="Language, write sentences, {n}/2, {s} (0 or 1)", 

168 comment_strings=["two sentences on same topic", 

169 "grammar/spelling"], 

170 ) 

171 add_multiple_columns( 

172 cls, "lang_repeat_word", 1, 4, pv=PV.BIT, 

173 comment_fmt="Language, repeat word, {n}/4, {s} (0 or 1)", 

174 comment_strings=["caterpillar", "eccentricity", 

175 "unintelligible", "statistician"], 

176 ) 

177 add_multiple_columns( 

178 cls, "lang_repeat_sentence", 1, 2, pv=PV.BIT, 

179 comment_fmt="Language, repeat sentence, {n}/2, {s} (0 or 1)", 

180 comment_strings=["glitters_gold", "stitch_time"], 

181 ) 

182 add_multiple_columns( 

183 cls, "lang_name_picture", 1, 12, pv=PV.BIT, 

184 comment_fmt="Language, name picture, {n}/12, {s} (0 or 1)", 

185 comment_strings=["spoon", "book", "kangaroo/wallaby", 

186 "penguin", "anchor", "camel/dromedary", 

187 "harp", "rhinoceros", "barrel/keg/tub", 

188 "crown", "alligator/crocodile", 

189 "accordion/piano accordion/squeeze box"], 

190 ) 

191 add_multiple_columns( 

192 cls, "lang_identify_concept", 1, 4, pv=PV.BIT, 

193 comment_fmt="Language, identify concept, {n}/4, {s} (0 or 1)", 

194 comment_strings=["monarchy", "marsupial", "Antarctic", "nautical"], 

195 ) 

196 

197 add_multiple_columns( 

198 cls, "vsp_count_dots", 1, 4, pv=PV.BIT, 

199 comment_fmt="Visuospatial, count dots {n}/4, {s} dots (0-1)", 

200 comment_strings=["8", "10", "7", "9"], 

201 ) 

202 add_multiple_columns( 

203 cls, "vsp_identify_letter", 1, 4, pv=PV.BIT, 

204 comment_fmt="Visuospatial, identify letter {n}/4, {s} (0-1)", 

205 comment_strings=["K", "M", "A", "T"], 

206 ) 

207 add_multiple_columns( 

208 cls, "mem_recall_address", 1, 7, pv=PV.BIT, 

209 comment_fmt="Memory, recall address {n}/7, {s} (0-1)", 

210 comment_strings=ADDRESS_PARTS, 

211 ) 

212 add_multiple_columns( 

213 cls, "mem_recognize_address", 1, 5, pv=PV.BIT, 

214 comment_fmt="Memory, recognize address {n}/5 (if " 

215 "applicable) ({s}) (0-1)", 

216 comment_strings=["name", "number", "street", "town", "county"], 

217 ) 

218 add_multiple_columns( # tablet version 2.0.0 onwards 

219 cls, "mem_recognize_address_choice", 1, 5, 

220 coltype=String(length=1), # was Text 

221 comment_fmt="Memory, recognize address {n}/5, CHOICE (if " 

222 "applicable) ({s}) (A/B/C)", 

223 comment_strings=["name", "number", "street", "town", "county"], 

224 ) 

225 

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

227 

228 

229class Ace3(TaskHasPatientMixin, TaskHasClinicianMixin, Task, 

230 metaclass=Ace3Metaclass): 

231 """ 

232 Server implementation of the ACE-III task. 

233 """ 

234 __tablename__ = "ace3" 

235 shortname = "ACE-III" 

236 provides_trackers = True 

237 

238 prohibits_commercial = True 

239 

240 age_at_leaving_full_time_education = Column( 

241 "age_at_leaving_full_time_education", Integer, 

242 comment="Age at leaving full time education" 

243 ) 

244 occupation = Column( 

245 "occupation", UnicodeText, 

246 comment="Occupation" 

247 ) 

248 handedness = CamcopsColumn( 

249 "handedness", String(length=1), # was Text 

250 comment="Handedness (L or R)", 

251 permitted_value_checker=PermittedValueChecker( 

252 permitted_values=["L", "R"]) 

253 ) 

254 attn_num_registration_trials = Column( 

255 "attn_num_registration_trials", Integer, 

256 comment="Attention, repetition, number of trials (not scored)" 

257 ) 

258 fluency_letters_score = CamcopsColumn( 

259 "fluency_letters_score", Integer, 

260 comment="Fluency, words beginning with P, score 0-7", 

261 permitted_value_checker=PermittedValueChecker(minimum=0, maximum=7) 

262 ) 

263 fluency_animals_score = CamcopsColumn( 

264 "fluency_animals_score", Integer, 

265 comment="Fluency, animals, score 0-7", 

266 permitted_value_checker=PermittedValueChecker(minimum=0, maximum=7) 

267 ) 

268 lang_follow_command_practice = CamcopsColumn( 

269 "lang_follow_command_practice", Integer, 

270 comment="Language, command, practice trial (not scored)", 

271 permitted_value_checker=BIT_CHECKER 

272 ) 

273 lang_read_words_aloud = CamcopsColumn( 

274 "lang_read_words_aloud", Integer, 

275 comment="Language, read five irregular words (0 or 1)", 

276 permitted_value_checker=BIT_CHECKER 

277 ) 

278 vsp_copy_infinity = CamcopsColumn( 

279 "vsp_copy_infinity", Integer, 

280 comment="Visuospatial, copy infinity (0-1)", 

281 permitted_value_checker=BIT_CHECKER 

282 ) 

283 vsp_copy_cube = CamcopsColumn( 

284 "vsp_copy_cube", Integer, 

285 comment="Visuospatial, copy cube (0-2)", 

286 permitted_value_checker=PermittedValueChecker(minimum=0, maximum=2) 

287 ) 

288 vsp_draw_clock = CamcopsColumn( 

289 "vsp_draw_clock", Integer, 

290 comment="Visuospatial, draw clock (0-5)", 

291 permitted_value_checker=PermittedValueChecker(minimum=0, maximum=5) 

292 ) 

293 picture1_blobid = CamcopsColumn( 

294 "picture1_blobid", Integer, 

295 comment="Photo 1/2 PNG BLOB ID", 

296 is_blob_id_field=True, blob_relationship_attr_name="picture1" 

297 ) 

298 picture1_rotation = Column( 

299 # DEFUNCT as of v2.0.0 

300 # IGNORED. REMOVE WHEN ALL PRE-2.0.0 TABLETS GONE 

301 "picture1_rotation", Integer, 

302 comment="Photo 1/2 rotation (degrees clockwise)" 

303 ) 

304 picture2_blobid = CamcopsColumn( 

305 "picture2_blobid", Integer, 

306 comment="Photo 2/2 PNG BLOB ID", 

307 is_blob_id_field=True, blob_relationship_attr_name="picture2" 

308 ) 

309 picture2_rotation = Column( 

310 # DEFUNCT as of v2.0.0 

311 # IGNORED. REMOVE WHEN ALL PRE-2.0.0 TABLETS GONE 

312 "picture2_rotation", Integer, 

313 comment="Photo 2/2 rotation (degrees clockwise)" 

314 ) 

315 comments = Column( 

316 "comments", UnicodeText, 

317 comment="Clinician's comments" 

318 ) 

319 

320 picture1 = blob_relationship("Ace3", "picture1_blobid") # type: Optional[Blob] # noqa 

321 picture2 = blob_relationship("Ace3", "picture2_blobid") # type: Optional[Blob] # noqa 

322 

323 ATTN_SCORE_FIELDS = (strseq("attn_time", 1, 5) + 

324 strseq("attn_place", 1, 5) + 

325 strseq("attn_repeat_word", 1, 3) + 

326 strseq("attn_serial7_subtraction", 1, 5)) 

327 MEM_NON_RECOG_SCORE_FIELDS = (strseq("mem_recall_word", 1, 3) + 

328 strseq("mem_repeat_address_trial3_", 1, 7) + 

329 strseq("mem_famous", 1, 4) + 

330 strseq("mem_recall_address", 1, 7)) 

331 LANG_SIMPLE_SCORE_FIELDS = (strseq("lang_write_sentences_point", 1, 2) + 

332 strseq("lang_repeat_sentence", 1, 2) + 

333 strseq("lang_name_picture", 1, 12) + 

334 strseq("lang_identify_concept", 1, 4)) 

335 LANG_FOLLOW_CMD_FIELDS = strseq("lang_follow_command", 1, 3) 

336 LANG_REPEAT_WORD_FIELDS = strseq("lang_repeat_word", 1, 4) 

337 VSP_SIMPLE_SCORE_FIELDS = (strseq("vsp_count_dots", 1, 4) + 

338 strseq("vsp_identify_letter", 1, 4)) 

339 BASIC_COMPLETENESS_FIELDS = ( 

340 ATTN_SCORE_FIELDS + 

341 MEM_NON_RECOG_SCORE_FIELDS + 

342 [ 

343 "fluency_letters_score", 

344 "fluency_animals_score" 

345 ] + 

346 [ 

347 "lang_follow_command_practice" 

348 ] + 

349 LANG_SIMPLE_SCORE_FIELDS + 

350 LANG_REPEAT_WORD_FIELDS + 

351 [ 

352 "lang_read_words_aloud", 

353 "vsp_copy_infinity", 

354 "vsp_copy_cube", 

355 "vsp_draw_clock" 

356 ] + 

357 VSP_SIMPLE_SCORE_FIELDS + 

358 strseq("mem_recall_address", 1, 7) 

359 ) 

360 

361 @staticmethod 

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

363 _ = req.gettext 

364 return _("Addenbrooke’s Cognitive Examination III") 

365 

366 def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]: 

367 return [TrackerInfo( 

368 value=self.total_score(), 

369 plot_label="ACE-III total score", 

370 axis_label="Total score (out of 100)", 

371 axis_min=-0.5, 

372 axis_max=100.5, 

373 horizontal_lines=[82.5, 88.5] 

374 )] 

375 

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

377 if not self.is_complete(): 

378 return CTV_INCOMPLETE 

379 a = self.attn_score() 

380 m = self.mem_score() 

381 f = self.fluency_score() 

382 lang = self.lang_score() 

383 v = self.vsp_score() 

384 t = a + m + f + lang + v 

385 text = ( 

386 f"ACE-III total: {t}/{TOTAL_MAX} " 

387 f"(attention {a}/{ATTN_MAX}, memory {m}/{MEMORY_MAX}, " 

388 f"fluency {f}/{FLUENCY_MAX}, language {lang}/{LANG_MAX}, " 

389 f"visuospatial {v}/{VSP_MAX})" 

390 ) 

391 return [CtvInfo(content=text)] 

392 

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

394 return self.standard_task_summary_fields() + [ 

395 SummaryElement(name="total", 

396 coltype=Integer(), 

397 value=self.total_score(), 

398 comment=f"Total score (/{TOTAL_MAX})"), 

399 SummaryElement(name="attn", 

400 coltype=Integer(), 

401 value=self.attn_score(), 

402 comment=f"Attention (/{ATTN_MAX})"), 

403 SummaryElement(name="mem", 

404 coltype=Integer(), 

405 value=self.mem_score(), 

406 comment=f"Memory (/{MEMORY_MAX})"), 

407 SummaryElement(name="fluency", 

408 coltype=Integer(), 

409 value=self.fluency_score(), 

410 comment=f"Fluency (/{FLUENCY_MAX})"), 

411 SummaryElement(name="lang", 

412 coltype=Integer(), 

413 value=self.lang_score(), 

414 comment=f"Language (/{LANG_MAX})"), 

415 SummaryElement(name="vsp", 

416 coltype=Integer(), 

417 value=self.vsp_score(), 

418 comment=f"Visuospatial (/{VSP_MAX})"), 

419 ] 

420 

421 def attn_score(self) -> int: 

422 return self.sum_fields(self.ATTN_SCORE_FIELDS) 

423 

424 @staticmethod 

425 def get_recog_score(recalled: Optional[int], 

426 recognized: Optional[int]) -> int: 

427 if recalled == 1: 

428 return 1 

429 return score_zero_for_absent(recognized) 

430 

431 @staticmethod 

432 def get_recog_text(recalled: Optional[int], 

433 recognized: Optional[int]) -> str: 

434 if recalled: 

435 return "<i>1 (already recalled)</i>" 

436 return answer(recognized) 

437 

438 # noinspection PyUnresolvedReferences 

439 def get_mem_recognition_score(self) -> int: 

440 score = 0 

441 score += self.get_recog_score( 

442 (self.mem_recall_address1 == 1 and self.mem_recall_address2 == 1), 

443 self.mem_recognize_address1) 

444 score += self.get_recog_score( 

445 (self.mem_recall_address3 == 1), 

446 self.mem_recognize_address2) 

447 score += self.get_recog_score( 

448 (self.mem_recall_address4 == 1 and self.mem_recall_address5 == 1), 

449 self.mem_recognize_address3) 

450 score += self.get_recog_score( 

451 (self.mem_recall_address6 == 1), 

452 self.mem_recognize_address4) 

453 score += self.get_recog_score( 

454 (self.mem_recall_address7 == 1), 

455 self.mem_recognize_address5) 

456 return score 

457 

458 def mem_score(self) -> int: 

459 return ( 

460 self.sum_fields(self.MEM_NON_RECOG_SCORE_FIELDS) + 

461 self.get_mem_recognition_score() 

462 ) 

463 

464 def fluency_score(self) -> int: 

465 return ( 

466 score_zero_for_absent(self.fluency_letters_score) + 

467 score_zero_for_absent(self.fluency_animals_score) 

468 ) 

469 

470 def get_follow_command_score(self) -> int: 

471 if self.lang_follow_command_practice != 1: 

472 return 0 

473 return self.sum_fields(self.LANG_FOLLOW_CMD_FIELDS) 

474 

475 def get_repeat_word_score(self) -> int: 

476 n = self.sum_fields(self.LANG_REPEAT_WORD_FIELDS) 

477 return 2 if n >= 4 else (1 if n == 3 else 0) 

478 

479 def lang_score(self) -> int: 

480 return (self.sum_fields(self.LANG_SIMPLE_SCORE_FIELDS) + 

481 self.get_follow_command_score() + 

482 self.get_repeat_word_score() + 

483 score_zero_for_absent(self.lang_read_words_aloud)) 

484 

485 def vsp_score(self) -> int: 

486 return (self.sum_fields(self.VSP_SIMPLE_SCORE_FIELDS) + 

487 score_zero_for_absent(self.vsp_copy_infinity) + 

488 score_zero_for_absent(self.vsp_copy_cube) + 

489 score_zero_for_absent(self.vsp_draw_clock)) 

490 

491 def total_score(self) -> int: 

492 return (self.attn_score() + 

493 self.mem_score() + 

494 self.fluency_score() + 

495 self.lang_score() + 

496 self.vsp_score()) 

497 

498 # noinspection PyUnresolvedReferences 

499 def is_recognition_complete(self) -> bool: 

500 return ( 

501 ((self.mem_recall_address1 == 1 and 

502 self.mem_recall_address2 == 1) or 

503 self.mem_recognize_address1 is not None) and 

504 (self.mem_recall_address3 == 1 or 

505 self.mem_recognize_address2 is not None) and 

506 ((self.mem_recall_address4 == 1 and 

507 self.mem_recall_address5 == 1) or 

508 self.mem_recognize_address3 is not None) and 

509 (self.mem_recall_address6 == 1 or 

510 self.mem_recognize_address4 is not None) and 

511 (self.mem_recall_address7 == 1 or 

512 self.mem_recognize_address5 is not None) 

513 ) 

514 

515 def is_complete(self) -> bool: 

516 if self.any_fields_none(self.BASIC_COMPLETENESS_FIELDS): 

517 return False 

518 if not self.field_contents_valid(): 

519 return False 

520 if (self.lang_follow_command_practice == 1 and 

521 self.any_fields_none(self.LANG_FOLLOW_CMD_FIELDS)): 

522 return False 

523 return self.is_recognition_complete() 

524 

525 # noinspection PyUnresolvedReferences 

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

527 def percent(score: int, maximum: int) -> str: 

528 return ws.number_to_dp(100 * score / maximum, PERCENT_DP) 

529 

530 a = self.attn_score() 

531 m = self.mem_score() 

532 f = self.fluency_score() 

533 lang = self.lang_score() 

534 v = self.vsp_score() 

535 t = a + m + f + lang + v 

536 if self.is_complete(): 

537 figsize = ( 

538 PlotDefaults.FULLWIDTH_PLOT_WIDTH / 3, 

539 PlotDefaults.FULLWIDTH_PLOT_WIDTH / 4 

540 ) 

541 width = 0.9 

542 fig = req.create_figure(figsize=figsize) 

543 ax = fig.add_subplot(1, 1, 1) 

544 scores = numpy.array([a, m, f, lang, v]) 

545 maxima = numpy.array([ATTN_MAX, MEMORY_MAX, FLUENCY_MAX, 

546 LANG_MAX, VSP_MAX]) 

547 y = 100 * scores / maxima 

548 x_labels = ["Attn", "Mem", "Flu", "Lang", "VSp"] 

549 # noinspection PyTypeChecker 

550 n = len(y) 

551 xvar = numpy.arange(n) 

552 ax.bar(xvar, y, width, color="b") 

553 ax.set_ylabel("%", fontdict=req.fontdict) 

554 ax.set_xticks(xvar) 

555 x_offset = -0.5 

556 ax.set_xlim(0 + x_offset, len(scores) + x_offset) 

557 ax.set_xticklabels(x_labels, fontdict=req.fontdict) 

558 fig.tight_layout() # or the ylabel drops off the figure 

559 # fig.autofmt_xdate() 

560 req.set_figure_font_sizes(ax) 

561 figurehtml = req.get_html_from_pyplot_figure(fig) 

562 else: 

563 figurehtml = "<i>Incomplete; not plotted</i>" 

564 return ( 

565 self.get_standard_clinician_comments_block(req, self.comments) + 

566 f""" 

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

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

569 <tr> 

570 {self.get_is_complete_td_pair(req)} 

571 <td class="{CssClass.FIGURE}" 

572 rowspan="7">{figurehtml}</td> 

573 </tr> 

574 """ + 

575 tr("Total ACE-III score <sup>[1]</sup>", answer(t) + " / 100") + 

576 tr("Attention", 

577 answer(a) + f" / {ATTN_MAX} ({percent(a, ATTN_MAX)}%)") + 

578 tr("Memory", 

579 answer(m) + f" / {MEMORY_MAX} ({percent(m, MEMORY_MAX)}%)") + 

580 tr("Fluency", 

581 answer(f) + f" / {FLUENCY_MAX} ({percent(f, FLUENCY_MAX)}%)") + 

582 tr("Language", 

583 answer(lang) + f" / {LANG_MAX} ({percent(lang, LANG_MAX)}%)") + 

584 tr("Visuospatial", 

585 answer(v) + f" / {VSP_MAX} ({percent(v, VSP_MAX)}%)") + 

586 f""" 

587 </table> 

588 </div> 

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

590 <tr> 

591 <th width="75%">Question</th> 

592 <th width="25%">Answer/score</td> 

593 </tr> 

594 """ + 

595 tr_qa("Age on leaving full-time education", 

596 self.age_at_leaving_full_time_education) + 

597 tr_qa("Occupation", ws.webify(self.occupation)) + 

598 tr_qa("Handedness", ws.webify(self.handedness)) + 

599 

600 subheading_spanning_two_columns("Attention") + 

601 tr("Day? Date? Month? Year? Season?", 

602 ", ".join([answer(x) for x in [self.attn_time1, 

603 self.attn_time2, 

604 self.attn_time3, 

605 self.attn_time4, 

606 self.attn_time5]])) + 

607 tr("House number/floor? Street/hospital? Town? County? Country?", 

608 ", ".join([answer(x) for x in [self.attn_place1, 

609 self.attn_place2, 

610 self.attn_place3, 

611 self.attn_place4, 

612 self.attn_place5]])) + 

613 tr("Repeat: Lemon? Key? Ball?", 

614 ", ".join([answer(x) for x in [self.attn_repeat_word1, 

615 self.attn_repeat_word2, 

616 self.attn_repeat_word3]])) + 

617 tr("Repetition: number of trials <i>(not scored)</i>", 

618 answer(self.attn_num_registration_trials, 

619 formatter_answer=italic)) + 

620 tr( 

621 "Serial subtractions: First correct? Second? Third? Fourth? " 

622 "Fifth?", 

623 ", ".join([answer(x) for x in [ 

624 self.attn_serial7_subtraction1, 

625 self.attn_serial7_subtraction2, 

626 self.attn_serial7_subtraction3, 

627 self.attn_serial7_subtraction4, 

628 self.attn_serial7_subtraction5]])) + 

629 

630 subheading_spanning_two_columns("Memory (1)") + 

631 tr("Recall: Lemon? Key? Ball?", 

632 ", ".join([answer(x) for x in [self.mem_recall_word1, 

633 self.mem_recall_word2, 

634 self.mem_recall_word3]])) + 

635 

636 subheading_spanning_two_columns("Fluency") + 

637 tr("Score for words beginning with ‘P’ <i>(≥18 scores 7, 14–17 " 

638 "scores 6, 11–13 scores 5, 8–10 scores 4, 6–7 scores 3, " 

639 "4–5 scores 2, 2–3 scores 1, 0–1 scores 0)</i>", 

640 answer(self.fluency_letters_score) + " / 7") + 

641 tr("Score for animals <i>(≥22 scores 7, 17–21 scores 6, " 

642 "14–16 scores 5, 11–13 scores 4, 9–10 scores 3, " 

643 "7–8 scores 2, 5–6 scores 1, &lt;5 scores 0)</i>", 

644 answer(self.fluency_animals_score) + " / 7") + 

645 

646 subheading_spanning_two_columns("Memory (2)") + 

647 tr( 

648 "Third trial of address registration: Harry? Barnes? 73? " 

649 "Orchard? Close? Kingsbridge? Devon?", 

650 ", ".join([answer(x) for x in [ 

651 self.mem_repeat_address_trial3_1, 

652 self.mem_repeat_address_trial3_2, 

653 self.mem_repeat_address_trial3_3, 

654 self.mem_repeat_address_trial3_4, 

655 self.mem_repeat_address_trial3_5, 

656 self.mem_repeat_address_trial3_6, 

657 self.mem_repeat_address_trial3_7]])) + 

658 tr("Current PM? Woman who was PM? USA president? USA president " 

659 "assassinated in 1960s?", 

660 ", ".join([answer(x) for x in [self.mem_famous1, 

661 self.mem_famous2, 

662 self.mem_famous3, 

663 self.mem_famous4]])) + 

664 

665 subheading_spanning_two_columns("Language") + 

666 tr("<i>Practice trial (“Pick up the pencil and then the " 

667 "paper”)</i>", 

668 answer(self.lang_follow_command_practice, 

669 formatter_answer=italic)) + 

670 tr_qa("“Place the paper on top of the pencil”", 

671 self.lang_follow_command1) + 

672 tr_qa("“Pick up the pencil but not the paper”", 

673 self.lang_follow_command2) + 

674 tr_qa("“Pass me the pencil after touching the paper”", 

675 self.lang_follow_command3) + 

676 tr( 

677 "Sentence-writing: point for ≥2 complete sentences about " 

678 "the one topic? Point for correct grammar and spelling?", 

679 ", ".join([answer(x) for x in [ 

680 self.lang_write_sentences_point1, 

681 self.lang_write_sentences_point2]])) + 

682 tr( 

683 "Repeat: caterpillar? eccentricity? unintelligible? " 

684 "statistician? <i>(score 2 if all correct, 1 if 3 correct, " 

685 "0 if ≤2 correct)</i>", 

686 "<i>{}, {}, {}, {}</i> (score <b>{}</b> / 2)".format( 

687 answer(self.lang_repeat_word1, formatter_answer=italic), 

688 answer(self.lang_repeat_word2, formatter_answer=italic), 

689 answer(self.lang_repeat_word3, formatter_answer=italic), 

690 answer(self.lang_repeat_word4, formatter_answer=italic), 

691 self.get_repeat_word_score(), 

692 )) + 

693 tr_qa("Repeat: “All that glitters is not gold”?", 

694 self.lang_repeat_sentence1) + 

695 tr_qa("Repeat: “A stitch in time saves nine”?", 

696 self.lang_repeat_sentence2) + 

697 tr("Name pictures: spoon, book, kangaroo/wallaby", 

698 ", ".join([answer(x) for x in [self.lang_name_picture1, 

699 self.lang_name_picture2, 

700 self.lang_name_picture3]])) + 

701 tr("Name pictures: penguin, anchor, camel/dromedary", 

702 ", ".join([answer(x) for x in [self.lang_name_picture4, 

703 self.lang_name_picture5, 

704 self.lang_name_picture6]])) + 

705 tr("Name pictures: harp, rhinoceros/rhino, barrel/keg/tub", 

706 ", ".join([answer(x) for x in [self.lang_name_picture7, 

707 self.lang_name_picture8, 

708 self.lang_name_picture9]])) + 

709 tr("Name pictures: crown, alligator/crocodile, " 

710 "accordion/piano accordion/squeeze box", 

711 ", ".join([answer(x) for x in [self.lang_name_picture10, 

712 self.lang_name_picture11, 

713 self.lang_name_picture12]])) + 

714 tr( 

715 "Identify pictures: monarchy? marsupial? Antarctic? nautical?", 

716 ", ".join([answer(x) for x in [self.lang_identify_concept1, 

717 self.lang_identify_concept2, 

718 self.lang_identify_concept3, 

719 self.lang_identify_concept4]]) 

720 ) + 

721 tr_qa("Read all successfully: sew, pint, soot, dough, height", 

722 self.lang_read_words_aloud) + 

723 

724 subheading_spanning_two_columns("Visuospatial") + 

725 tr("Copy infinity", answer(self.vsp_copy_infinity) + " / 1") + 

726 tr("Copy cube", answer(self.vsp_copy_cube) + " / 2") + 

727 tr("Draw clock with numbers and hands at 5:10", 

728 answer(self.vsp_draw_clock) + " / 5") + 

729 tr("Count dots: 8, 10, 7, 9", 

730 ", ".join([answer(x) for x in [self.vsp_count_dots1, 

731 self.vsp_count_dots2, 

732 self.vsp_count_dots3, 

733 self.vsp_count_dots4]])) + 

734 tr("Identify letters: K, M, A, T", 

735 ", ".join([answer(x) for x in [self.vsp_identify_letter1, 

736 self.vsp_identify_letter2, 

737 self.vsp_identify_letter3, 

738 self.vsp_identify_letter4]])) + 

739 

740 subheading_spanning_two_columns("Memory (3)") + 

741 tr("Recall address: Harry? Barnes? 73? Orchard? Close? " 

742 "Kingsbridge? Devon?", 

743 ", ".join([answer(x) for x in [self.mem_recall_address1, 

744 self.mem_recall_address2, 

745 self.mem_recall_address3, 

746 self.mem_recall_address4, 

747 self.mem_recall_address5, 

748 self.mem_recall_address6, 

749 self.mem_recall_address7]])) + 

750 tr("Recognize address: Jerry Barnes/Harry Barnes/Harry Bradford?", 

751 self.get_recog_text((self.mem_recall_address1 == 1 and 

752 self.mem_recall_address2 == 1), 

753 self.mem_recognize_address1)) + 

754 tr("Recognize address: 37/73/76?", 

755 self.get_recog_text((self.mem_recall_address3 == 1), 

756 self.mem_recognize_address2)) + 

757 tr( 

758 "Recognize address: Orchard Place/Oak Close/Orchard " 

759 "Close?", 

760 self.get_recog_text( 

761 (self.mem_recall_address4 == 1 and 

762 self.mem_recall_address5 == 1), 

763 self.mem_recognize_address3)) + 

764 tr("Recognize address: Oakhampton/Kingsbridge/Dartington?", 

765 self.get_recog_text((self.mem_recall_address6 == 1), 

766 self.mem_recognize_address4)) + 

767 tr("Recognize address: Devon/Dorset/Somerset?", 

768 self.get_recog_text((self.mem_recall_address7 == 1), 

769 self.mem_recognize_address5)) + 

770 

771 subheading_spanning_two_columns("Photos of test sheet") + 

772 tr_span_col(get_blob_img_html(self.picture1), 

773 td_class=CssClass.PHOTO) + 

774 tr_span_col(get_blob_img_html(self.picture2), 

775 td_class=CssClass.PHOTO) + 

776 f""" 

777 </table> 

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

779 [1] In the ACE-R (the predecessor of the ACE-III), 

780 scores ≤82 had sensitivity 0.84 and specificity 1.0 for 

781 dementia, and scores ≤88 had sensitivity 0.94 and 

782 specificity 0.89 for dementia, in a context of patients 

783 with AlzD, FTD, LBD, MCI, and controls 

784 (Mioshi et al., 2006, PMID 16977673). 

785 </div> 

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

787 ACE-III: Copyright © 2012, John Hodges. 

788 “The ACE-III is available for free. The copyright is held 

789 by Professor John Hodges who is happy for the test to be 

790 used in clinical practice and research projects. There is 

791 no need to contact us if you wish to use the ACE-III in 

792 clinical practice.” 

793 (ACE-III FAQ, 7 July 2013, www.neura.edu.au). 

794 </div> 

795 """ 

796 ) 

797 

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

799 codes = [SnomedExpression(req.snomed(SnomedLookup.ACE_R_PROCEDURE_ASSESSMENT))] # noqa 

800 # add(SnomedLookup.ACE_R_PROCEDURE_ASSESSMENT_SUBSCALE_ATTENTION_ORIENTATION) # noqa 

801 # add(SnomedLookup.ACE_R_PROCEDURE_ASSESSMENT_SUBSCALE_MEMORY) 

802 # add(SnomedLookup.ACE_R_PROCEDURE_ASSESSMENT_SUBSCALE_FLUENCY) 

803 # add(SnomedLookup.ACE_R_PROCEDURE_ASSESSMENT_SUBSCALE_LANGUAGE) 

804 # add(SnomedLookup.ACE_R_PROCEDURE_ASSESSMENT_SUBSCALE_VISUOSPATIAL) 

805 if self.is_complete(): # could refine: is each subscale complete? 

806 a = self.attn_score() 

807 m = self.mem_score() 

808 f = self.fluency_score() 

809 lang = self.lang_score() 

810 v = self.vsp_score() 

811 t = a + m + f + lang + v 

812 codes.append(SnomedExpression( 

813 req.snomed(SnomedLookup.ACE_R_SCALE), 

814 { 

815 req.snomed(SnomedLookup.ACE_R_SCORE): t, 

816 req.snomed(SnomedLookup.ACE_R_SUBSCORE_ATTENTION_ORIENTATION): a, # noqa 

817 req.snomed(SnomedLookup.ACE_R_SUBSCORE_MEMORY): m, 

818 req.snomed(SnomedLookup.ACE_R_SUBSCORE_FLUENCY): f, 

819 req.snomed(SnomedLookup.ACE_R_SUBSCORE_LANGUAGE): lang, 

820 req.snomed(SnomedLookup.ACE_R_SUBSCORE_VISUOSPATIAL): v, 

821 } 

822 )) 

823 return codes