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/ided3d.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, List, Optional, Type 

30 

31import cardinal_pythonlib.rnc_web as ws 

32from sqlalchemy.sql.schema import Column 

33from sqlalchemy.sql.sqltypes import Boolean, Float, Integer, Text 

34 

35from camcops_server.cc_modules.cc_constants import CssClass 

36from camcops_server.cc_modules.cc_db import ( 

37 ancillary_relationship, 

38 GenericTabletRecordMixin, 

39 TaskDescendant, 

40) 

41from camcops_server.cc_modules.cc_html import ( 

42 answer, 

43 get_yes_no_none, 

44 identity, 

45 tr, 

46 tr_qa, 

47) 

48from camcops_server.cc_modules.cc_request import CamcopsRequest 

49from camcops_server.cc_modules.cc_sqla_coltypes import ( 

50 BIT_CHECKER, 

51 CamcopsColumn, 

52 PendulumDateTimeAsIsoTextColType, 

53) 

54from camcops_server.cc_modules.cc_sqlalchemy import Base 

55from camcops_server.cc_modules.cc_task import Task, TaskHasPatientMixin 

56from camcops_server.cc_modules.cc_text import SS 

57 

58 

59def a(x: Any) -> str: 

60 """Answer formatting for this task.""" 

61 return answer(x, formatter_answer=identity, default="") 

62 

63 

64# ============================================================================= 

65# IDED3D 

66# ============================================================================= 

67 

68class IDED3DTrial(GenericTabletRecordMixin, TaskDescendant, Base): 

69 __tablename__ = "ided3d_trials" 

70 

71 ided3d_id = Column( 

72 "ided3d_id", Integer, 

73 nullable=False, 

74 comment="FK to ided3d" 

75 ) 

76 trial = Column( 

77 "trial", Integer, 

78 nullable=False, 

79 comment="Trial number (1-based)" 

80 ) 

81 stage = Column( 

82 "stage", Integer, 

83 comment="Stage number (1-based)" 

84 ) 

85 

86 # Locations 

87 correct_location = Column( 

88 "correct_location", Integer, 

89 comment="Location of correct stimulus " 

90 "(0 top, 1 right, 2 bottom, 3 left)" 

91 ) 

92 incorrect_location = Column( 

93 "incorrect_location", Integer, 

94 comment="Location of incorrect stimulus " 

95 "(0 top, 1 right, 2 bottom, 3 left)" 

96 ) 

97 

98 # Stimuli 

99 correct_shape = Column( 

100 "correct_shape", Integer, 

101 comment="Shape# of correct stimulus" 

102 ) 

103 correct_colour = CamcopsColumn( 

104 "correct_colour", Text, 

105 exempt_from_anonymisation=True, 

106 comment="HTML colour of correct stimulus" 

107 ) 

108 correct_number = Column( 

109 "correct_number", Integer, 

110 comment="Number of copies of correct stimulus" 

111 ) 

112 incorrect_shape = Column( 

113 "incorrect_shape", Integer, 

114 comment="Shape# of incorrect stimulus" 

115 ) 

116 incorrect_colour = CamcopsColumn( 

117 "incorrect_colour", Text, 

118 exempt_from_anonymisation=True, 

119 comment="HTML colour of incorrect stimulus" 

120 ) 

121 incorrect_number = Column( 

122 "incorrect_number", Integer, 

123 comment="Number of copies of incorrect stimulus" 

124 ) 

125 

126 # Trial 

127 trial_start_time = Column( 

128 "trial_start_time", PendulumDateTimeAsIsoTextColType, 

129 comment="Trial start time / stimuli presented at (ISO-8601)" 

130 ) 

131 

132 # Response 

133 responded = CamcopsColumn( 

134 "responded", Boolean, 

135 permitted_value_checker=BIT_CHECKER, 

136 comment="Did the subject respond?" 

137 ) 

138 response_time = Column( 

139 "response_time", PendulumDateTimeAsIsoTextColType, 

140 comment="Time of response (ISO-8601)" 

141 ) 

142 response_latency_ms = Column( 

143 "response_latency_ms", Integer, 

144 comment="Response latency (ms)" 

145 ) 

146 correct = CamcopsColumn( 

147 "correct", Boolean, 

148 permitted_value_checker=BIT_CHECKER, 

149 comment="Response was correct" 

150 ) 

151 incorrect = CamcopsColumn( 

152 "incorrect", Boolean, 

153 permitted_value_checker=BIT_CHECKER, 

154 comment="Response was incorrect" 

155 ) 

156 

157 @classmethod 

158 def get_html_table_header(cls) -> str: 

159 return f""" 

160 <table class="{CssClass.EXTRADETAIL}"> 

161 <tr> 

162 <th>Trial#</th> 

163 <th>Stage#</th> 

164 <th>Correct location</th> 

165 <th>Incorrect location</th> 

166 <th>Correct shape</th> 

167 <th>Correct colour</th> 

168 <th>Correct number</th> 

169 <th>Incorrect shape</th> 

170 <th>Incorrect colour</th> 

171 <th>Incorrect number</th> 

172 <th>Trial start time</th> 

173 <th>Responded?</th> 

174 <th>Response time</th> 

175 <th>Response latency (ms)</th> 

176 <th>Correct?</th> 

177 <th>Incorrect?</th> 

178 </tr> 

179 """ 

180 

181 def get_html_table_row(self) -> str: 

182 return tr( 

183 a(self.trial), 

184 a(self.stage), 

185 a(self.correct_location), 

186 a(self.incorrect_location), 

187 a(self.correct_shape), 

188 a(self.correct_colour), 

189 a(self.correct_number), 

190 a(self.incorrect_shape), 

191 a(self.incorrect_colour), 

192 a(self.incorrect_number), 

193 a(self.trial_start_time), 

194 a(self.responded), 

195 a(self.response_time), 

196 a(self.response_latency_ms), 

197 a(self.correct), 

198 a(self.incorrect), 

199 ) 

200 

201 # ------------------------------------------------------------------------- 

202 # TaskDescendant overrides 

203 # ------------------------------------------------------------------------- 

204 

205 @classmethod 

206 def task_ancestor_class(cls) -> Optional[Type["Task"]]: 

207 return IDED3D 

208 

209 def task_ancestor(self) -> Optional["IDED3D"]: 

210 return IDED3D.get_linked(self.ided3d_id, self) 

211 

212 

213class IDED3DStage(GenericTabletRecordMixin, TaskDescendant, Base): 

214 __tablename__ = "ided3d_stages" 

215 

216 ided3d_id = Column( 

217 "ided3d_id", Integer, 

218 nullable=False, 

219 comment="FK to ided3d" 

220 ) 

221 stage = Column( 

222 "stage", Integer, 

223 nullable=False, 

224 comment="Stage number (1-based)" 

225 ) 

226 

227 # Config 

228 stage_name = CamcopsColumn( 

229 "stage_name", Text, 

230 exempt_from_anonymisation=True, 

231 comment="Name of the stage (e.g. SD, EDr)" 

232 ) 

233 relevant_dimension = CamcopsColumn( 

234 "relevant_dimension", Text, 

235 exempt_from_anonymisation=True, 

236 comment="Relevant dimension (e.g. shape, colour, number)" 

237 ) 

238 correct_exemplar = CamcopsColumn( 

239 "correct_exemplar", Text, 

240 exempt_from_anonymisation=True, 

241 comment="Correct exemplar (from relevant dimension)" 

242 ) 

243 incorrect_exemplar = CamcopsColumn( 

244 "incorrect_exemplar", Text, 

245 exempt_from_anonymisation=True, 

246 comment="Incorrect exemplar (from relevant dimension)" 

247 ) 

248 correct_stimulus_shapes = CamcopsColumn( 

249 "correct_stimulus_shapes", Text, 

250 exempt_from_anonymisation=True, 

251 comment="Possible shapes for correct stimulus " 

252 "(CSV list of shape numbers)" 

253 ) 

254 correct_stimulus_colours = CamcopsColumn( 

255 "correct_stimulus_colours", Text, 

256 exempt_from_anonymisation=True, 

257 comment="Possible colours for correct stimulus " 

258 "(CSV list of HTML colours)" 

259 ) 

260 correct_stimulus_numbers = CamcopsColumn( 

261 "correct_stimulus_numbers", Text, 

262 exempt_from_anonymisation=True, 

263 comment="Possible numbers for correct stimulus " 

264 "(CSV list of numbers)" 

265 ) 

266 incorrect_stimulus_shapes = CamcopsColumn( 

267 "incorrect_stimulus_shapes", Text, 

268 exempt_from_anonymisation=True, 

269 comment="Possible shapes for incorrect stimulus " 

270 "(CSV list of shape numbers)" 

271 ) 

272 incorrect_stimulus_colours = CamcopsColumn( 

273 "incorrect_stimulus_colours", Text, 

274 exempt_from_anonymisation=True, 

275 comment="Possible colours for incorrect stimulus " 

276 "(CSV list of HTML colours)" 

277 ) 

278 incorrect_stimulus_numbers = CamcopsColumn( 

279 "incorrect_stimulus_numbers", Text, 

280 exempt_from_anonymisation=True, 

281 comment="Possible numbers for incorrect stimulus " 

282 "(CSV list of numbers)" 

283 ) 

284 

285 # Results 

286 first_trial_num = Column( 

287 "first_trial_num", Integer, 

288 comment="Number of the first trial of the stage (1-based)" 

289 ) 

290 n_completed_trials = Column( 

291 "n_completed_trials", Integer, 

292 comment="Number of trials completed" 

293 ) 

294 n_correct = Column( 

295 "n_correct", Integer, 

296 comment="Number of trials performed correctly" 

297 ) 

298 n_incorrect = Column( 

299 "n_incorrect", Integer, 

300 comment="Number of trials performed incorrectly" 

301 ) 

302 stage_passed = CamcopsColumn( 

303 "stage_passed", Boolean, 

304 permitted_value_checker=BIT_CHECKER, 

305 comment="Subject met criterion and passed stage" 

306 ) 

307 stage_failed = CamcopsColumn( 

308 "stage_failed", Boolean, 

309 permitted_value_checker=BIT_CHECKER, 

310 comment="Subject took too many trials and failed stage" 

311 ) 

312 

313 @classmethod 

314 def get_html_table_header(cls) -> str: 

315 return f""" 

316 <table class="{CssClass.EXTRADETAIL}"> 

317 <tr> 

318 <th>Stage#</th> 

319 <th>Stage name</th> 

320 <th>Relevant dimension</th> 

321 <th>Correct exemplar</th> 

322 <th>Incorrect exemplar</th> 

323 <th>Shapes for correct</th> 

324 <th>Colours for correct</th> 

325 <th>Numbers for correct</th> 

326 <th>Shapes for incorrect</th> 

327 <th>Colours for incorrect</th> 

328 <th>Numbers for incorrect</th> 

329 <th>First trial#</th> 

330 <th>#completed trials</th> 

331 <th>#correct</th> 

332 <th>#incorrect</th> 

333 <th>Passed?</th> 

334 <th>Failed?</th> 

335 </tr> 

336 """ 

337 

338 def get_html_table_row(self) -> str: 

339 return tr( 

340 a(self.stage), 

341 a(self.stage_name), 

342 a(self.relevant_dimension), 

343 a(self.correct_exemplar), 

344 a(self.incorrect_exemplar), 

345 a(self.correct_stimulus_shapes), 

346 a(self.correct_stimulus_colours), 

347 a(self.correct_stimulus_numbers), 

348 a(self.incorrect_stimulus_shapes), 

349 a(self.incorrect_stimulus_colours), 

350 a(self.incorrect_stimulus_numbers), 

351 a(self.first_trial_num), 

352 a(self.n_completed_trials), 

353 a(self.n_correct), 

354 a(self.n_incorrect), 

355 a(self.stage_passed), 

356 a(self.stage_failed), 

357 ) 

358 

359 # ------------------------------------------------------------------------- 

360 # TaskDescendant overrides 

361 # ------------------------------------------------------------------------- 

362 

363 @classmethod 

364 def task_ancestor_class(cls) -> Optional[Type["Task"]]: 

365 return IDED3D 

366 

367 def task_ancestor(self) -> Optional["IDED3D"]: 

368 return IDED3D.get_linked(self.ided3d_id, self) 

369 

370 

371class IDED3D(TaskHasPatientMixin, Task): 

372 """ 

373 Server implementation of the ID/ED-3D task. 

374 """ 

375 __tablename__ = "ided3d" 

376 shortname = "ID/ED-3D" 

377 

378 # Config 

379 last_stage = Column( 

380 "last_stage", Integer, 

381 comment="Last stage to offer (1 [SD] - 8 [EDR])" 

382 ) 

383 max_trials_per_stage = Column( 

384 "max_trials_per_stage", Integer, 

385 comment="Maximum number of trials allowed per stage before " 

386 "the task aborts" 

387 ) 

388 progress_criterion_x = Column( 

389 "progress_criterion_x", Integer, 

390 comment='Criterion to proceed to next stage: X correct out of' 

391 ' the last Y trials, where this is X' 

392 ) 

393 progress_criterion_y = Column( 

394 "progress_criterion_y", Integer, 

395 comment='Criterion to proceed to next stage: X correct out of' 

396 ' the last Y trials, where this is Y' 

397 ) 

398 min_number = Column( 

399 "min_number", Integer, 

400 comment="Minimum number of stimulus element to use" 

401 ) 

402 max_number = Column( 

403 "max_number", Integer, 

404 comment="Maximum number of stimulus element to use" 

405 ) 

406 pause_after_beep_ms = Column( 

407 "pause_after_beep_ms", Integer, 

408 comment="Time to continue visual feedback after auditory " 

409 "feedback finished (ms)" 

410 ) 

411 iti_ms = Column( 

412 "iti_ms", Integer, 

413 comment="Intertrial interval (ms)" 

414 ) 

415 counterbalance_dimensions = Column( 

416 "counterbalance_dimensions", Integer, 

417 comment="Dimensional counterbalancing condition (0-5)" 

418 ) 

419 volume = Column( 

420 "volume", Float, 

421 comment="Sound volume (0.0-1.0)" 

422 ) 

423 offer_abort = CamcopsColumn( 

424 "offer_abort", Boolean, 

425 permitted_value_checker=BIT_CHECKER, 

426 comment="Offer an abort button?" 

427 ) 

428 debug_display_stimuli_only = CamcopsColumn( 

429 "debug_display_stimuli_only", Boolean, 

430 permitted_value_checker=BIT_CHECKER, 

431 comment="DEBUG: show stimuli only, don't run task" 

432 ) 

433 

434 # Intrinsic config 

435 shape_definitions_svg = CamcopsColumn( 

436 "shape_definitions_svg", Text, 

437 exempt_from_anonymisation=True, 

438 comment="JSON-encoded version of shape definition" 

439 " array in SVG format (with arbitrary scale of -60 to" 

440 " +60 in both X and Y dimensions)" 

441 ) 

442 colour_definitions_rgb = CamcopsColumn( # v2.0.0 

443 "colour_definitions_rgb", Text, 

444 exempt_from_anonymisation=True, 

445 comment="JSON-encoded version of colour RGB definitions" 

446 ) 

447 

448 # Results 

449 aborted = Column( 

450 "aborted", Integer, 

451 comment="Was the task aborted? (0 no, 1 yes)" 

452 ) 

453 finished = Column( 

454 "finished", Integer, 

455 comment="Was the task finished? (0 no, 1 yes)" 

456 ) 

457 last_trial_completed = Column( 

458 "last_trial_completed", Integer, 

459 comment="Number of last trial completed" 

460 ) 

461 

462 # Relationships 

463 trials = ancillary_relationship( 

464 parent_class_name="IDED3D", 

465 ancillary_class_name="IDED3DTrial", 

466 ancillary_fk_to_parent_attr_name="ided3d_id", 

467 ancillary_order_by_attr_name="trial" 

468 ) # type: List[IDED3DTrial] 

469 stages = ancillary_relationship( 

470 parent_class_name="IDED3D", 

471 ancillary_class_name="IDED3DStage", 

472 ancillary_fk_to_parent_attr_name="ided3d_id", 

473 ancillary_order_by_attr_name="stage" 

474 ) # type: List[IDED3DStage] 

475 

476 @staticmethod 

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

478 _ = req.gettext 

479 return _("Three-dimensional ID/ED task") 

480 

481 def is_complete(self) -> bool: 

482 return bool(self.debug_display_stimuli_only) or bool(self.finished) 

483 

484 def get_stage_html(self) -> str: 

485 html = IDED3DStage.get_html_table_header() 

486 # noinspection PyTypeChecker 

487 for s in self.stages: 

488 html += s.get_html_table_row() 

489 html += """</table>""" 

490 return html 

491 

492 def get_trial_html(self) -> str: 

493 html = IDED3DTrial.get_html_table_header() 

494 # noinspection PyTypeChecker 

495 for t in self.trials: 

496 html += t.get_html_table_row() 

497 html += """</table>""" 

498 return html 

499 

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

501 h = f""" 

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

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

504 {self.get_is_complete_tr(req)} 

505 </table> 

506 </div> 

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

508 1. Simple discrimination (SD), and 2. reversal (SDr); 

509 3. compound discrimination (CD), and 4. reversal (CDr); 

510 5. intradimensional shift (ID), and 6. reversal (IDr); 

511 7. extradimensional shift (ED), and 8. reversal (EDr). 

512 </div> 

513 <table class="{CssClass.TASKCONFIG}"> 

514 <tr> 

515 <th width="50%">Configuration variable</th> 

516 <th width="50%">Value</th> 

517 </tr> 

518 """ 

519 h += tr_qa(self.wxstring(req, "last_stage"), self.last_stage) 

520 h += tr_qa(self.wxstring(req, "max_trials_per_stage"), 

521 self.max_trials_per_stage) 

522 h += tr_qa(self.wxstring(req, "progress_criterion_x"), 

523 self.progress_criterion_x) 

524 h += tr_qa(self.wxstring(req, "progress_criterion_y"), 

525 self.progress_criterion_y) 

526 h += tr_qa(self.wxstring(req, "min_number"), self.min_number) 

527 h += tr_qa(self.wxstring(req, "max_number"), self.max_number) 

528 h += tr_qa(self.wxstring(req, "pause_after_beep_ms"), 

529 self.pause_after_beep_ms) 

530 h += tr_qa(self.wxstring(req, "iti_ms"), self.iti_ms) 

531 h += tr_qa( 

532 self.wxstring(req, "counterbalance_dimensions") + "<sup>[1]</sup>", 

533 self.counterbalance_dimensions) 

534 h += tr_qa(req.sstring(SS.VOLUME_0_TO_1), self.volume) 

535 h += tr_qa(self.wxstring(req, "offer_abort"), self.offer_abort) 

536 h += tr_qa(self.wxstring(req, "debug_display_stimuli_only"), 

537 self.debug_display_stimuli_only) 

538 h += tr_qa("Shapes (as a JSON-encoded array of SVG " 

539 "definitions; X and Y range both –60 to +60)", 

540 ws.webify(self.shape_definitions_svg)) 

541 h += f""" 

542 </table> 

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

544 <tr><th width="50%">Measure</th><th width="50%">Value</th></tr> 

545 """ 

546 h += tr_qa("Aborted?", get_yes_no_none(req, self.aborted)) 

547 h += tr_qa("Finished?", get_yes_no_none(req, self.finished)) 

548 h += tr_qa("Last trial completed", self.last_trial_completed) 

549 h += ( 

550 """ 

551 </table> 

552 <div>Stage specifications and results:</div> 

553 """ + 

554 self.get_stage_html() + 

555 "<div>Trial-by-trial results:</div>" + 

556 self.get_trial_html() + 

557 f""" 

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

559 [1] Counterbalancing of dimensions is as follows, with 

560 notation X/Y indicating that X is the first relevant 

561 dimension (for stages SD–IDr) and Y is the second relevant 

562 dimension (for stages ED–EDr). 

563 0: shape/colour. 

564 1: colour/number. 

565 2: number/shape. 

566 3: shape/number. 

567 4: colour/shape. 

568 5: number/colour. 

569 </div> 

570 """ 

571 ) 

572 return h