Coverage for tasks/cardinal_expectationdetection.py: 31%

457 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-11-08 23:14 +0000

1#!/usr/bin/env python 

2 

3""" 

4camcops_server/tasks/cardinal_expectationdetection.py 

5 

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

7 

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

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

10 

11 This file is part of CamCOPS. 

12 

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

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

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

16 (at your option) any later version. 

17 

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

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

20 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

21 GNU General Public License for more details. 

22 

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

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

25 

26=============================================================================== 

27 

28""" 

29 

30import logging 

31from typing import Any, Dict, List, Optional, Sequence, Tuple, Type 

32 

33from cardinal_pythonlib.logs import BraceStyleAdapter 

34from matplotlib.axes import Axes 

35import numpy 

36import scipy.stats # http://docs.scipy.org/doc/scipy/reference/stats.html 

37from sqlalchemy.sql.schema import Column, ForeignKey 

38from sqlalchemy.sql.sqltypes import Float, Integer 

39 

40from camcops_server.cc_modules.cc_constants import ( 

41 CssClass, 

42 MatplotlibConstants, 

43 PlotDefaults, 

44) 

45from camcops_server.cc_modules.cc_db import ( 

46 ancillary_relationship, 

47 GenericTabletRecordMixin, 

48 TaskDescendant, 

49) 

50from camcops_server.cc_modules.cc_html import ( 

51 answer, 

52 div, 

53 get_yes_no_none, 

54 identity, 

55 italic, 

56 td, 

57 tr, 

58 tr_qa, 

59) 

60from camcops_server.cc_modules.cc_request import CamcopsRequest 

61from camcops_server.cc_modules.cc_sqla_coltypes import ( 

62 PendulumDateTimeAsIsoTextColType, 

63) 

64from camcops_server.cc_modules.cc_sqlalchemy import Base 

65from camcops_server.cc_modules.cc_summaryelement import ( 

66 ExtraSummaryTable, 

67 SummaryElement, 

68) 

69from camcops_server.cc_modules.cc_task import Task, TaskHasPatientMixin 

70 

71log = BraceStyleAdapter(logging.getLogger(__name__)) 

72 

73 

74CONVERT_0_P_TO = 0.001 # for Z-transformed ROC plot 

75CONVERT_1_P_TO = 0.999 # for Z-transformed ROC plot 

76 

77NRATINGS = 5 # numbered 0-4 in the database 

78# -- to match DETECTION_OPTIONS.length in the original task 

79N_CUES = 8 # to match magic number in original task 

80 

81ERROR_RATING_OUT_OF_RANGE = f""" 

82 <div class="{CssClass.ERROR}">Can't draw figure: rating out of range</div> 

83""" 

84WARNING_INSUFFICIENT_DATA = f""" 

85 <div class="{CssClass.WARNING}">Insufficient data</div> 

86""" 

87WARNING_RATING_MISSING = f""" 

88 <div class="{CssClass.WARNING}">One or more ratings are missing</div> 

89""" 

90PLAIN_ROC_TITLE = "ROC" 

91Z_ROC_TITLE = ( 

92 f"ROC in Z coordinates (0/1 first mapped to " 

93 f"{CONVERT_0_P_TO}/{CONVERT_1_P_TO})" 

94) 

95 

96AUDITORY = 0 

97VISUAL = 1 

98 

99 

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

101 """Answer formatting for this task.""" 

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

103 

104 

105# ============================================================================= 

106# Cardinal_ExpectationDetection 

107# ============================================================================= 

108 

109 

110class ExpDetTrial(GenericTabletRecordMixin, TaskDescendant, Base): 

111 __tablename__ = "cardinal_expdet_trials" 

112 

113 cardinal_expdet_id = Column( 

114 "cardinal_expdet_id", 

115 Integer, 

116 nullable=False, 

117 comment="FK to cardinal_expdet", 

118 ) 

119 trial = Column( 

120 "trial", Integer, nullable=False, comment="Trial number (0-based)" 

121 ) 

122 

123 # Config determines these (via an autogeneration process): 

124 block = Column("block", Integer, comment="Block number (0-based)") 

125 group_num = Column("group_num", Integer, comment="Group number (0-based)") 

126 cue = Column("cue", Integer, comment="Cue number (0-based)") 

127 raw_cue_number = Column( 

128 "raw_cue_number", 

129 Integer, 

130 comment="Raw cue number (following counterbalancing) (0-based)", 

131 ) 

132 target_modality = Column( 

133 "target_modality", 

134 Integer, 

135 comment="Target modality (0 auditory, 1 visual)", 

136 ) 

137 target_number = Column( 

138 "target_number", Integer, comment="Target number (0-based)" 

139 ) 

140 target_present = Column( 

141 "target_present", Integer, comment="Target present? (0 no, 1 yes)" 

142 ) 

143 iti_length_s = Column( 

144 "iti_length_s", Float, comment="Intertrial interval (s)" 

145 ) 

146 

147 # Task determines these (on the fly): 

148 pause_given_before_trial = Column( 

149 "pause_given_before_trial", 

150 Integer, 

151 comment="Pause given before trial? (0 no, 1 yes)", 

152 ) 

153 pause_start_time = Column( 

154 "pause_start_time", 

155 PendulumDateTimeAsIsoTextColType, 

156 comment="Pause start time (ISO-8601)", 

157 ) 

158 pause_end_time = Column( 

159 "pause_end_time", 

160 PendulumDateTimeAsIsoTextColType, 

161 comment="Pause end time (ISO-8601)", 

162 ) 

163 trial_start_time = Column( 

164 "trial_start_time", 

165 PendulumDateTimeAsIsoTextColType, 

166 comment="Trial start time (ISO-8601)", 

167 ) 

168 cue_start_time = Column( 

169 "cue_start_time", 

170 PendulumDateTimeAsIsoTextColType, 

171 comment="Cue start time (ISO-8601)", 

172 ) 

173 target_start_time = Column( 

174 "target_start_time", 

175 PendulumDateTimeAsIsoTextColType, 

176 comment="Target start time (ISO-8601)", 

177 ) 

178 detection_start_time = Column( 

179 "detection_start_time", 

180 PendulumDateTimeAsIsoTextColType, 

181 comment="Detection response start time (ISO-8601)", 

182 ) 

183 iti_start_time = Column( 

184 "iti_start_time", 

185 PendulumDateTimeAsIsoTextColType, 

186 comment="Intertrial interval start time (ISO-8601)", 

187 ) 

188 iti_end_time = Column( 

189 "iti_end_time", 

190 PendulumDateTimeAsIsoTextColType, 

191 comment="Intertrial interval end time (ISO-8601)", 

192 ) 

193 trial_end_time = Column( 

194 "trial_end_time", 

195 PendulumDateTimeAsIsoTextColType, 

196 comment="Trial end time (ISO-8601)", 

197 ) 

198 

199 # Subject decides these: 

200 responded = Column( 

201 "responded", Integer, comment="Responded? (0 no, 1 yes)" 

202 ) 

203 response_time = Column( 

204 "response_time", 

205 PendulumDateTimeAsIsoTextColType, 

206 comment="Response time (ISO-8601)", 

207 ) 

208 response_latency_ms = Column( 

209 "response_latency_ms", Integer, comment="Response latency (ms)" 

210 ) 

211 rating = Column( 

212 "rating", Integer, comment="Rating (0 definitely not - 4 definitely)" 

213 ) 

214 correct = Column( 

215 "correct", 

216 Integer, 

217 comment="Correct side of the middle rating? (0 no, 1 yes)", 

218 ) 

219 points = Column("points", Integer, comment="Points earned this trial") 

220 cumulative_points = Column( 

221 "cumulative_points", Integer, comment="Cumulative points earned" 

222 ) 

223 

224 @classmethod 

225 def get_html_table_header(cls) -> str: 

226 return f""" 

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

228 <tr> 

229 <th>Trial# (0-based)</th> 

230 <th>Block# (0-based)</th> 

231 <th>Group# (0-based)</th> 

232 <th>Cue</th> 

233 <th>Raw cue</th> 

234 <th>Target modality</th> 

235 <th>Target#</th> 

236 <th>Target present?</th> 

237 <th>ITI (s)</th> 

238 <th>Pause before trial?</th> 

239 </tr> 

240 <tr class="{CssClass.EXTRADETAIL2}"> 

241 <th>...</th> 

242 <th>Pause start@</th> 

243 <th>Pause end@</th> 

244 <th>Trial start@</th> 

245 <th>Cue@</th> 

246 <th>Target@</th> 

247 <th>Detection start@</th> 

248 <th>ITI start@</th> 

249 <th>ITI end@</th> 

250 <th>Trial end@</th> 

251 </tr> 

252 <tr class="{CssClass.EXTRADETAIL2}"> 

253 <th>...</th> 

254 <th>Responded?</th> 

255 <th>Responded@</th> 

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

257 <th>Rating</th> 

258 <th>Correct?</th> 

259 <th>Points</th> 

260 <th>Cumulative points</th> 

261 </tr> 

262 """ 

263 

264 # ratings: 0, 1 absent -- 2 don't know -- 3, 4 present 

265 def judged_present(self) -> Optional[bool]: 

266 if not self.responded: 

267 return None 

268 elif self.rating >= 3: 

269 return True 

270 else: 

271 return False 

272 

273 def judged_absent(self) -> Optional[bool]: 

274 if not self.responded: 

275 return None 

276 elif self.rating <= 1: 

277 return True 

278 else: 

279 return False 

280 

281 def didnt_know(self) -> Optional[bool]: 

282 if not self.responded: 

283 return None 

284 return self.rating == 2 

285 

286 def get_html_table_row(self) -> str: 

287 return ( 

288 tr( 

289 a(self.trial), 

290 a(self.block), 

291 a(self.group_num), 

292 a(self.cue), 

293 a(self.raw_cue_number), 

294 a(self.target_modality), 

295 a(self.target_number), 

296 a(self.target_present), 

297 a(self.iti_length_s), 

298 a(self.pause_given_before_trial), 

299 ) 

300 + tr( 

301 "...", 

302 a(self.pause_start_time), 

303 a(self.pause_end_time), 

304 a(self.trial_start_time), 

305 a(self.cue_start_time), 

306 a(self.target_start_time), 

307 a(self.detection_start_time), 

308 a(self.iti_start_time), 

309 a(self.iti_end_time), 

310 a(self.trial_end_time), 

311 tr_class=CssClass.EXTRADETAIL2, 

312 ) 

313 + tr( 

314 "...", 

315 a(self.responded), 

316 a(self.response_time), 

317 a(self.response_latency_ms), 

318 a(self.rating), 

319 a(self.correct), 

320 a(self.points), 

321 a(self.cumulative_points), 

322 tr_class=CssClass.EXTRADETAIL2, 

323 ) 

324 ) 

325 

326 # ------------------------------------------------------------------------- 

327 # TaskDescendant overrides 

328 # ------------------------------------------------------------------------- 

329 

330 @classmethod 

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

332 return CardinalExpectationDetection 

333 

334 def task_ancestor(self) -> Optional["CardinalExpectationDetection"]: 

335 return CardinalExpectationDetection.get_linked( 

336 self.cardinal_expdet_id, self 

337 ) 

338 

339 

340class ExpDetTrialGroupSpec(GenericTabletRecordMixin, TaskDescendant, Base): 

341 __tablename__ = "cardinal_expdet_trialgroupspec" 

342 

343 cardinal_expdet_id = Column( 

344 "cardinal_expdet_id", 

345 Integer, 

346 nullable=False, 

347 comment="FK to cardinal_expdet", 

348 ) 

349 group_num = Column( 

350 "group_num", Integer, nullable=False, comment="Group number (0-based)" 

351 ) 

352 

353 # Group spec 

354 cue = Column("cue", Integer, comment="Cue number (0-based)") 

355 target_modality = Column( 

356 "target_modality", 

357 Integer, 

358 comment="Target modality (0 auditory, 1 visual)", 

359 ) 

360 target_number = Column( 

361 "target_number", Integer, comment="Target number (0-based)" 

362 ) 

363 n_target = Column( 

364 "n_target", Integer, comment="Number of trials with target present" 

365 ) 

366 n_no_target = Column( 

367 "n_no_target", Integer, comment="Number of trials with target absent" 

368 ) 

369 

370 DP = 3 

371 

372 @classmethod 

373 def get_html_table_header(cls) -> str: 

374 return f""" 

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

376 <tr> 

377 <th>Group# (0-based)</th> 

378 <th>Cue (0-based)</th> 

379 <th>Target modality (0 auditory, 1 visual)</th> 

380 <th>Target# (0-based)</th> 

381 <th># target trials</th> 

382 <th># no-target trials</th> 

383 </tr> 

384 """ 

385 

386 def get_html_table_row(self) -> str: 

387 return tr( 

388 a(self.group_num), 

389 a(self.cue), 

390 a(self.target_modality), 

391 a(self.target_number), 

392 a(self.n_target), 

393 a(self.n_no_target), 

394 ) 

395 

396 # ------------------------------------------------------------------------- 

397 # TaskDescendant overrides 

398 # ------------------------------------------------------------------------- 

399 

400 @classmethod 

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

402 return CardinalExpectationDetection 

403 

404 def task_ancestor(self) -> Optional["CardinalExpectationDetection"]: 

405 return CardinalExpectationDetection.get_linked( 

406 self.cardinal_expdet_id, self 

407 ) 

408 

409 

410class CardinalExpectationDetection(TaskHasPatientMixin, Task): 

411 """ 

412 Server implementation of the Cardinal_ExpDet task. 

413 """ 

414 

415 __tablename__ = "cardinal_expdet" 

416 shortname = "Cardinal_ExpDet" 

417 use_landscape_for_pdf = True 

418 

419 # Config 

420 num_blocks = Column("num_blocks", Integer, comment="Number of blocks") 

421 stimulus_counterbalancing = Column( 

422 "stimulus_counterbalancing", 

423 Integer, 

424 comment="Stimulus counterbalancing condition", 

425 ) 

426 is_detection_response_on_right = Column( 

427 "is_detection_response_on_right", 

428 Integer, 

429 comment='Is the "detection" response on the right? (0 no, 1 yes)', 

430 ) 

431 pause_every_n_trials = Column( 

432 "pause_every_n_trials", Integer, comment="Pause every n trials" 

433 ) 

434 # ... cue 

435 cue_duration_s = Column( 

436 "cue_duration_s", Float, comment="Cue duration (s)" 

437 ) 

438 visual_cue_intensity = Column( 

439 "visual_cue_intensity", Float, comment="Visual cue intensity (0.0-1.0)" 

440 ) 

441 auditory_cue_intensity = Column( 

442 "auditory_cue_intensity", 

443 Float, 

444 comment="Auditory cue intensity (0.0-1.0)", 

445 ) 

446 # ... ISI 

447 isi_duration_s = Column( 

448 "isi_duration_s", Float, comment="Interstimulus interval (s)" 

449 ) 

450 # .. target 

451 visual_target_duration_s = Column( 

452 "visual_target_duration_s", Float, comment="Visual target duration (s)" 

453 ) 

454 visual_background_intensity = Column( 

455 "visual_background_intensity", 

456 Float, 

457 comment="Visual background intensity (0.0-1.0)", 

458 ) 

459 visual_target_0_intensity = Column( 

460 "visual_target_0_intensity", 

461 Float, 

462 comment="Visual target 0 intensity (0.0-1.0)", 

463 ) 

464 visual_target_1_intensity = Column( 

465 "visual_target_1_intensity", 

466 Float, 

467 comment="Visual target 1 intensity (0.0-1.0)", 

468 ) 

469 auditory_background_intensity = Column( 

470 "auditory_background_intensity", 

471 Float, 

472 comment="Auditory background intensity (0.0-1.0)", 

473 ) 

474 auditory_target_0_intensity = Column( 

475 "auditory_target_0_intensity", 

476 Float, 

477 comment="Auditory target 0 intensity (0.0-1.0)", 

478 ) 

479 auditory_target_1_intensity = Column( 

480 "auditory_target_1_intensity", 

481 Float, 

482 comment="Auditory target 1 intensity (0.0-1.0)", 

483 ) 

484 # ... ITI 

485 iti_min_s = Column( 

486 "iti_min_s", Float, comment="Intertrial interval minimum (s)" 

487 ) 

488 iti_max_s = Column( 

489 "iti_max_s", Float, comment="Intertrial interval maximum (s)" 

490 ) 

491 

492 # Results 

493 aborted = Column( 

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

495 ) 

496 finished = Column( 

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

498 ) 

499 last_trial_completed = Column( 

500 "last_trial_completed", 

501 Integer, 

502 comment="Number of last trial completed", 

503 ) 

504 

505 # Relationships 

506 trials = ancillary_relationship( 

507 parent_class_name="CardinalExpectationDetection", 

508 ancillary_class_name="ExpDetTrial", 

509 ancillary_fk_to_parent_attr_name="cardinal_expdet_id", 

510 ancillary_order_by_attr_name="trial", 

511 ) # type: List[ExpDetTrial] 

512 groupspecs = ancillary_relationship( 

513 parent_class_name="CardinalExpectationDetection", 

514 ancillary_class_name="ExpDetTrialGroupSpec", 

515 ancillary_fk_to_parent_attr_name="cardinal_expdet_id", 

516 ancillary_order_by_attr_name="group_num", 

517 ) # type: List[ExpDetTrialGroupSpec] 

518 

519 @staticmethod 

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

521 _ = req.gettext 

522 return _("Cardinal RN – Expectation–Detection task") 

523 

524 def is_complete(self) -> bool: 

525 return bool(self.finished) 

526 

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

528 return self.standard_task_summary_fields() + [ 

529 SummaryElement( 

530 name="final_score", 

531 coltype=Integer(), 

532 value=self.get_final_score(), 

533 ), 

534 SummaryElement( 

535 name="overall_p_detect_present", 

536 coltype=Float(), 

537 value=self.get_overall_p_detect_present(), 

538 ), 

539 SummaryElement( 

540 name="overall_p_detect_absent", 

541 coltype=Float(), 

542 value=self.get_overall_p_detect_absent(), 

543 ), 

544 SummaryElement( 

545 name="overall_c", coltype=Float(), value=self.get_overall_c() 

546 ), 

547 SummaryElement( 

548 name="overall_d", coltype=Float(), value=self.get_overall_d() 

549 ), 

550 ] 

551 

552 def get_final_score(self) -> Optional[int]: 

553 trialarray = self.trials 

554 if not trialarray: 

555 return None 

556 return trialarray[-1].cumulative_points 

557 

558 def get_group_html(self) -> str: 

559 grouparray = self.groupspecs 

560 html = ExpDetTrialGroupSpec.get_html_table_header() 

561 for g in grouparray: 

562 html += g.get_html_table_row() 

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

564 return html 

565 

566 @staticmethod 

567 def get_c_dprime( 

568 h: Optional[float], 

569 fa: Optional[float], 

570 two_alternative_forced_choice: bool = False, 

571 ) -> Tuple[Optional[float], Optional[float]]: 

572 if h is None or fa is None: 

573 return None, None 

574 # In this task, we're only presenting a single alternative. 

575 if fa == 0: 

576 fa = CONVERT_0_P_TO 

577 if fa == 1: 

578 fa = CONVERT_1_P_TO 

579 if h == 0: 

580 h = CONVERT_0_P_TO 

581 if h == 1: 

582 h = CONVERT_1_P_TO 

583 z_fa = scipy.stats.norm.ppf(fa) 

584 z_h = scipy.stats.norm.ppf(h) 

585 if two_alternative_forced_choice: 

586 dprime = (1.0 / numpy.sqrt(2)) * (z_h - z_fa) 

587 else: 

588 dprime = z_h - z_fa 

589 c = -0.5 * (z_h + z_fa) 

590 # c is zero when FA rate and miss rate (1 - H) are equal 

591 # c is negative when FA > miss 

592 # c is positive when miss > FA 

593 return c, dprime 

594 

595 @staticmethod 

596 def get_sdt_values( 

597 count_stimulus: Sequence[int], count_nostimulus: Sequence[int] 

598 ) -> Dict: 

599 # Probabilities and cumulative probabilities 

600 sum_count_stimulus = numpy.sum(count_stimulus) 

601 sum_count_nostimulus = numpy.sum(count_nostimulus) 

602 if sum_count_stimulus == 0 or sum_count_nostimulus == 0: 

603 fa = [] 

604 h = [] 

605 z_fa = [] 

606 z_h = [] 

607 else: 

608 p_stimulus = count_stimulus / sum_count_stimulus 

609 p_nostimulus = count_nostimulus / sum_count_nostimulus 

610 # ... may produce a RuntimeWarning in case of division by zero 

611 cump_stimulus = numpy.cumsum(p_stimulus) # hit rates 

612 cump_nostimulus = numpy.cumsum(p_nostimulus) # false alarm rates 

613 # We're interested in all pairs except the last: 

614 fa = cump_stimulus[:-1] 

615 h = cump_nostimulus[:-1] 

616 # WHICH WAY ROUND YOU ASSIGN THESE DETERMINES THE ROC'S APPEARANCE. 

617 # However, it's arbitrary, in the sense that the left/right 

618 # assignment of the ratings is arbitrary. To make the ROC look 

619 # conventional (top left), assign this way round, so that "fa" 

620 # starts low and grows, and "h" starts high and falls. Hmm... 

621 fa[fa == 0] = CONVERT_0_P_TO 

622 fa[fa == 1] = CONVERT_1_P_TO 

623 h[h == 0] = CONVERT_0_P_TO 

624 h[h == 1] = CONVERT_1_P_TO 

625 z_fa = scipy.stats.norm.ppf(fa) 

626 z_h = scipy.stats.norm.ppf(h) 

627 

628 # log.debug("p_stimulus: " + str(p_stimulus)) 

629 # log.debug("p_nostimulus: " + str(p_nostimulus)) 

630 # log.debug("cump_stimulus: " + str(cump_stimulus)) 

631 # log.debug("cump_nostimulus: " + str(cump_nostimulus)) 

632 

633 # log.debug("h: " + str(h)) 

634 # log.debug("fa: " + str(fa)) 

635 # log.debug("z_h: " + str(z_h)) 

636 # log.debug("z_fa: " + str(z_fa)) 

637 return {"fa": fa, "h": h, "z_fa": z_fa, "z_h": z_h} 

638 

639 def plot_roc( 

640 self, 

641 req: CamcopsRequest, 

642 ax: Axes, 

643 count_stimulus: Sequence[int], 

644 count_nostimulus: Sequence[int], 

645 show_x_label: bool, 

646 show_y_label: bool, 

647 plainroc: bool, 

648 subtitle: str, 

649 ) -> None: 

650 extraspace = 0.05 

651 sdtval = self.get_sdt_values(count_stimulus, count_nostimulus) 

652 

653 # Calculate d' for all pairs but the last 

654 if plainroc: 

655 x = sdtval["fa"] 

656 y = sdtval["h"] 

657 xlabel = "FA" 

658 ylabel = "H" 

659 ax.set_xlim(0 - extraspace, 1 + extraspace) 

660 ax.set_ylim(0 - extraspace, 1 + extraspace) 

661 else: 

662 x = sdtval["z_fa"] 

663 y = sdtval["z_h"] 

664 xlabel = "Z(FA)" 

665 ylabel = "Z(H)" 

666 # Plot 

667 ax.plot( 

668 x, 

669 y, 

670 marker=MatplotlibConstants.MARKER_PLUS, 

671 color=MatplotlibConstants.COLOUR_BLUE, 

672 linestyle=MatplotlibConstants.LINESTYLE_SOLID, 

673 ) 

674 ax.set_xlabel(xlabel if show_x_label else "", fontdict=req.fontdict) 

675 ax.set_ylabel(ylabel if show_y_label else "", fontdict=req.fontdict) 

676 ax.set_title(subtitle, fontdict=req.fontdict) 

677 req.set_figure_font_sizes(ax) 

678 

679 @staticmethod 

680 def get_roc_info( 

681 trialarray: List[ExpDetTrial], 

682 blocks: List[int], 

683 groups: Optional[List[int]], 

684 ) -> Dict: 

685 # Collect counts (Macmillan & Creelman p61) 

686 total_n = 0 

687 count_stimulus = numpy.zeros(NRATINGS) 

688 count_nostimulus = numpy.zeros(NRATINGS) 

689 rating_missing = False 

690 rating_out_of_range = False 

691 for t in trialarray: 

692 if t.rating is None: 

693 rating_missing = True 

694 continue 

695 if t.rating < 0 or t.rating >= NRATINGS: 

696 rating_out_of_range = True 

697 break 

698 if groups and t.group_num not in groups: 

699 continue 

700 if blocks and t.block not in blocks: 

701 continue 

702 total_n += 1 

703 if t.target_present: 

704 count_stimulus[t.rating] += 1 

705 else: 

706 count_nostimulus[t.rating] += 1 

707 return { 

708 "total_n": total_n, 

709 "count_stimulus": count_stimulus, 

710 "count_nostimulus": count_nostimulus, 

711 "rating_missing": rating_missing, 

712 "rating_out_of_range": rating_out_of_range, 

713 } 

714 

715 def get_roc_figure_by_group( 

716 self, 

717 req: CamcopsRequest, 

718 trialarray: List[ExpDetTrial], 

719 grouparray: List[ExpDetTrialGroupSpec], 

720 plainroc: bool, 

721 ) -> str: 

722 if not trialarray or not grouparray: 

723 return WARNING_INSUFFICIENT_DATA 

724 figsize = ( 

725 PlotDefaults.FULLWIDTH_PLOT_WIDTH * 2, 

726 PlotDefaults.FULLWIDTH_PLOT_WIDTH, 

727 ) 

728 html = "" 

729 fig = req.create_figure(figsize=figsize) 

730 warned = False 

731 for groupnum in range(len(grouparray)): 

732 ax = fig.add_subplot(2, 4, groupnum + 1) 

733 # ... rows, cols, plotnum (in reading order from 1) 

734 rocinfo = self.get_roc_info(trialarray, [], [groupnum]) 

735 if rocinfo["rating_out_of_range"]: 

736 return ERROR_RATING_OUT_OF_RANGE 

737 if rocinfo["rating_missing"] and not warned: 

738 html += WARNING_RATING_MISSING 

739 warned = True 

740 show_x_label = groupnum > 3 

741 show_y_label = groupnum % 4 == 0 

742 subtitle = f"Group {groupnum} (n = {rocinfo['total_n']})" 

743 self.plot_roc( 

744 req, 

745 ax, 

746 rocinfo["count_stimulus"], 

747 rocinfo["count_nostimulus"], 

748 show_x_label, 

749 show_y_label, 

750 plainroc, 

751 subtitle, 

752 ) 

753 title = PLAIN_ROC_TITLE if plainroc else Z_ROC_TITLE 

754 fontprops = req.fontprops 

755 fontprops.set_weight("bold") 

756 fig.suptitle(title, fontproperties=fontprops) 

757 html += req.get_html_from_pyplot_figure(fig) 

758 return html 

759 

760 def get_roc_figure_firsthalf_lasthalf( 

761 self, 

762 req: CamcopsRequest, 

763 trialarray: List[ExpDetTrial], 

764 plainroc: bool, 

765 ) -> str: 

766 if not trialarray or not self.num_blocks: 

767 return WARNING_INSUFFICIENT_DATA 

768 figsize = ( 

769 PlotDefaults.FULLWIDTH_PLOT_WIDTH, 

770 PlotDefaults.FULLWIDTH_PLOT_WIDTH / 2, 

771 ) 

772 html = "" 

773 fig = req.create_figure(figsize=figsize) 

774 warned = False 

775 for half in range(2): 

776 ax = fig.add_subplot(1, 2, half + 1) 

777 # ... rows, cols, plotnum (in reading order from 1) 

778 blocks = list( 

779 range( 

780 half * self.num_blocks // 2, self.num_blocks // (2 - half) 

781 ) 

782 ) 

783 rocinfo = self.get_roc_info(trialarray, blocks, None) 

784 if rocinfo["rating_out_of_range"]: 

785 return ERROR_RATING_OUT_OF_RANGE 

786 if rocinfo["rating_missing"] and not warned: 

787 html += WARNING_RATING_MISSING 

788 warned = True 

789 show_x_label = True 

790 show_y_label = half == 0 

791 subtitle = "First half" if half == 0 else "Second half" 

792 self.plot_roc( 

793 req, 

794 ax, 

795 rocinfo["count_stimulus"], 

796 rocinfo["count_nostimulus"], 

797 show_x_label, 

798 show_y_label, 

799 plainroc, 

800 subtitle, 

801 ) 

802 title = PLAIN_ROC_TITLE if plainroc else Z_ROC_TITLE 

803 fontprops = req.fontprops 

804 fontprops.set_weight("bold") 

805 fig.suptitle(title, fontproperties=fontprops) 

806 html += req.get_html_from_pyplot_figure(fig) 

807 return html 

808 

809 def get_trial_html(self) -> str: 

810 trialarray = self.trials 

811 html = ExpDetTrial.get_html_table_header() 

812 for t in trialarray: 

813 html += t.get_html_table_row() 

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

815 return html 

816 

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

818 grouparray = self.groupspecs 

819 trialarray = self.trials 

820 # THIS IS A NON-EDITABLE TASK, so we *ignore* the problem 

821 # of matching to no-longer-current records. 

822 # (See PhotoSequence.py for a task that does it properly.) 

823 

824 # Provide HTML 

825 # HTML 

826 h = f""" 

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

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

829 {self.get_is_complete_tr(req)} 

830 </table> 

831 </div> 

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

833 Putative assay of propensity to hallucinations. 

834 </div> 

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

836 <tr> 

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

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

839 </tr> 

840 """ 

841 h += tr_qa("Number of blocks", self.num_blocks) 

842 h += tr_qa("Stimulus counterbalancing", self.stimulus_counterbalancing) 

843 h += tr_qa( 

844 "“Detection” response on right?", 

845 self.is_detection_response_on_right, 

846 ) 

847 h += tr_qa( 

848 "Pause every <i>n</i> trials (0 = no pauses)", 

849 self.pause_every_n_trials, 

850 ) 

851 h += tr_qa("Cue duration (s)", self.cue_duration_s) 

852 h += tr_qa("Visual cue intensity (0–1)", self.visual_cue_intensity) 

853 h += tr_qa("Auditory cue intensity (0–1)", self.auditory_cue_intensity) 

854 h += tr_qa("ISI duration (s)", self.isi_duration_s) 

855 h += tr_qa("Visual target duration (s)", self.visual_target_duration_s) 

856 h += tr_qa( 

857 "Visual background intensity", self.visual_background_intensity 

858 ) 

859 h += tr_qa( 

860 "Visual target 0 (circle) intensity", 

861 self.visual_target_0_intensity, 

862 ) 

863 h += tr_qa( 

864 "Visual target 1 (“sun”) intensity", self.visual_target_1_intensity 

865 ) 

866 h += tr_qa( 

867 "Auditory background intensity", self.auditory_background_intensity 

868 ) 

869 h += tr_qa( 

870 "Auditory target 0 (tone) intensity", 

871 self.auditory_target_0_intensity, 

872 ) 

873 h += tr_qa( 

874 "Auditory target 1 (“moon”) intensity", 

875 self.auditory_target_1_intensity, 

876 ) 

877 h += tr_qa("ITI minimum (s)", self.iti_min_s) 

878 h += tr_qa("ITI maximum (s)", self.iti_max_s) 

879 h += f""" 

880 </table> 

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

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

883 """ 

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

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

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

887 h += ( 

888 """ 

889 </table> 

890 <div> 

891 Trial group specifications (one block is a full set of 

892 all these trials): 

893 </div> 

894 """ 

895 + self.get_group_html() 

896 + """ 

897 <div> 

898 Detection probabilities by block and group (c &gt; 0 when 

899 miss rate &gt; false alarm rate; c &lt; 0 when false alarm 

900 rate &gt; miss rate): 

901 </div> 

902 """ 

903 + self.get_html_correct_by_group_and_block(trialarray) 

904 + "<div>Detection probabilities by block:</div>" 

905 + self.get_html_correct_by_block(trialarray) 

906 + "<div>Detection probabilities by group:</div>" 

907 + self.get_html_correct_by_group(trialarray) 

908 + """ 

909 <div> 

910 Detection probabilities by half and high/low association 

911 probability: 

912 </div> 

913 """ 

914 + self.get_html_correct_by_half_and_probability( 

915 trialarray, grouparray 

916 ) 

917 + """ 

918 <div> 

919 Detection probabilities by block and high/low association 

920 probability: 

921 </div> 

922 """ 

923 + self.get_html_correct_by_block_and_probability( 

924 trialarray, grouparray 

925 ) 

926 + """ 

927 <div> 

928 Receiver operating characteristic (ROC) curves by group: 

929 </div> 

930 """ 

931 + self.get_roc_figure_by_group(req, trialarray, grouparray, True) 

932 + self.get_roc_figure_by_group(req, trialarray, grouparray, False) 

933 + "<div>First-half/last-half ROCs:</div>" 

934 + self.get_roc_figure_firsthalf_lasthalf(req, trialarray, True) 

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

936 + self.get_trial_html() 

937 ) 

938 return h 

939 

940 def get_html_correct_by_group_and_block( 

941 self, trialarray: List[ExpDetTrial] 

942 ) -> str: 

943 if not trialarray: 

944 return div(italic("No trials")) 

945 html = f""" 

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

947 <tr> 

948 <th>Block</th> 

949 """ 

950 for g in range(N_CUES): 

951 # Have spaces around | to allow web browsers to word-wrap 

952 html += f""" 

953 <th>Group {g} P(detected | present)</th> 

954 <th>Group {g} P(detected | absent)</th> 

955 <th>Group {g} c</th> 

956 <th>Group {g} d'</th> 

957 """ 

958 html += """ 

959 </th> 

960 </tr> 

961 """ 

962 for b in range(self.num_blocks): 

963 html += "<tr>" + td(str(b)) 

964 for g in range(N_CUES): 

965 ( 

966 p_detected_given_present, 

967 p_detected_given_absent, 

968 c, 

969 dprime, 

970 n_trials, 

971 ) = self.get_p_detected(trialarray, [b], [g]) 

972 html += td(a(p_detected_given_present)) 

973 html += td(a(p_detected_given_absent)) 

974 html += td(a(c)) 

975 html += td(a(dprime)) 

976 html += "</tr>\n" 

977 html += """ 

978 </table> 

979 """ 

980 return html 

981 

982 def get_html_correct_by_half_and_probability( 

983 self, 

984 trialarray: List[ExpDetTrial], 

985 grouparray: List[ExpDetTrialGroupSpec], 

986 ) -> str: 

987 if (not trialarray) or (not grouparray): 

988 return div(italic("No trials or no groups")) 

989 n_target_highprob = max([x.n_target for x in grouparray]) 

990 n_target_lowprob = min([x.n_target for x in grouparray]) 

991 groups_highprob = [ 

992 x.group_num for x in grouparray if x.n_target == n_target_highprob 

993 ] 

994 groups_lowprob = [ 

995 x.group_num for x in grouparray if x.n_target == n_target_lowprob 

996 ] 

997 html = f""" 

998 <div><i> 

999 High probability groups (cues): 

1000 {", ".join([str(x) for x in groups_highprob])}.\n 

1001 Low probability groups (cues): 

1002 {", ".join([str(x) for x in groups_lowprob])}.\n 

1003 </i></div> 

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

1005 <tr> 

1006 <th>Half (0 first, 1 second)</th> 

1007 <th>Target probability given stimulus (0 low, 1 high)</th> 

1008 <th>P(detected | present)</th> 

1009 <th>P(detected | absent)</th> 

1010 <th>c</th> 

1011 <th>d'</th> 

1012 </tr> 

1013 """ 

1014 for half in (0, 1): 

1015 for prob in (0, 1): 

1016 blocks = list( 

1017 range( 

1018 half * self.num_blocks // 2, 

1019 self.num_blocks // (2 - half), 

1020 ) 

1021 ) 

1022 groups = groups_lowprob if prob == 0 else groups_highprob 

1023 ( 

1024 p_detected_given_present, 

1025 p_detected_given_absent, 

1026 c, 

1027 dprime, 

1028 n_trials, 

1029 ) = self.get_p_detected(trialarray, blocks, groups) 

1030 html += tr( 

1031 half, 

1032 a(prob), 

1033 a(p_detected_given_present), 

1034 a(p_detected_given_absent), 

1035 a(c), 

1036 a(dprime), 

1037 ) 

1038 html += """ 

1039 </table> 

1040 """ 

1041 return html 

1042 

1043 def get_html_correct_by_block_and_probability( 

1044 self, 

1045 trialarray: List[ExpDetTrial], 

1046 grouparray: List[ExpDetTrialGroupSpec], 

1047 ) -> str: 

1048 if (not trialarray) or (not grouparray): 

1049 return div(italic("No trials or no groups")) 

1050 n_target_highprob = max([x.n_target for x in grouparray]) 

1051 n_target_lowprob = min([x.n_target for x in grouparray]) 

1052 groups_highprob = [ 

1053 x.group_num for x in grouparray if x.n_target == n_target_highprob 

1054 ] 

1055 groups_lowprob = [ 

1056 x.group_num for x in grouparray if x.n_target == n_target_lowprob 

1057 ] 

1058 html = f""" 

1059 <div><i> 

1060 High probability groups (cues): 

1061 {", ".join([str(x) for x in groups_highprob])}.\n 

1062 Low probability groups (cues): 

1063 {", ".join([str(x) for x in groups_lowprob])}.\n 

1064 </i></div> 

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

1066 <tr> 

1067 <th>Block (0-based)</th> 

1068 <th>Target probability given stimulus (0 low, 1 high)</th> 

1069 <th>P(detected | present)</th> 

1070 <th>P(detected | absent)</th> 

1071 <th>c</th> 

1072 <th>d'</th> 

1073 </tr> 

1074 """ 

1075 for b in range(self.num_blocks): 

1076 for prob in (0, 1): 

1077 groups = groups_lowprob if prob == 0 else groups_highprob 

1078 ( 

1079 p_detected_given_present, 

1080 p_detected_given_absent, 

1081 c, 

1082 dprime, 

1083 n_trials, 

1084 ) = self.get_p_detected(trialarray, [b], groups) 

1085 html += tr( 

1086 b, 

1087 prob, 

1088 a(p_detected_given_present), 

1089 a(p_detected_given_absent), 

1090 a(c), 

1091 a(dprime), 

1092 ) 

1093 html += """ 

1094 </table> 

1095 """ 

1096 return html 

1097 

1098 def get_html_correct_by_group(self, trialarray: List[ExpDetTrial]) -> str: 

1099 if not trialarray: 

1100 return div(italic("No trials")) 

1101 html = f""" 

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

1103 <tr> 

1104 <th>Group</th> 

1105 <th>P(detected | present)</th> 

1106 <th>P(detected | absent)</th> 

1107 <th>c</th> 

1108 <th>d'</th> 

1109 </tr> 

1110 """ 

1111 for g in range(N_CUES): 

1112 ( 

1113 p_detected_given_present, 

1114 p_detected_given_absent, 

1115 c, 

1116 dprime, 

1117 n_trials, 

1118 ) = self.get_p_detected(trialarray, None, [g]) 

1119 html += tr( 

1120 g, 

1121 a(p_detected_given_present), 

1122 a(p_detected_given_absent), 

1123 a(c), 

1124 a(dprime), 

1125 ) 

1126 html += """ 

1127 </table> 

1128 """ 

1129 return html 

1130 

1131 def get_html_correct_by_block(self, trialarray: List[ExpDetTrial]) -> str: 

1132 if not trialarray: 

1133 return div(italic("No trials")) 

1134 html = f""" 

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

1136 <tr> 

1137 <th>Block</th> 

1138 <th>P(detected | present)</th> 

1139 <th>P(detected | absent)</th> 

1140 <th>c</th> 

1141 <th>d'</th> 

1142 </tr> 

1143 """ 

1144 for b in range(self.num_blocks): 

1145 ( 

1146 p_detected_given_present, 

1147 p_detected_given_absent, 

1148 c, 

1149 dprime, 

1150 n_trials, 

1151 ) = self.get_p_detected(trialarray, [b], None) 

1152 html += tr( 

1153 b, 

1154 a(p_detected_given_present), 

1155 a(p_detected_given_absent), 

1156 a(c), 

1157 a(dprime), 

1158 ) 

1159 html += """ 

1160 </table> 

1161 """ 

1162 return html 

1163 

1164 def get_p_detected( 

1165 self, 

1166 trialarray: List[ExpDetTrial], 

1167 blocks: Optional[List[int]], 

1168 groups: Optional[List[int]], 

1169 ) -> Tuple[ 

1170 Optional[float], Optional[float], Optional[float], Optional[float], int 

1171 ]: 

1172 n_present = 0 

1173 n_absent = 0 

1174 n_detected_given_present = 0 

1175 n_detected_given_absent = 0 

1176 n_trials = 0 

1177 for t in trialarray: 

1178 if ( 

1179 not t.responded 

1180 or (blocks is not None and t.block not in blocks) 

1181 or (groups is not None and t.group_num not in groups) 

1182 ): 

1183 continue 

1184 if t.target_present: 

1185 n_present += 1 

1186 if t.judged_present(): 

1187 n_detected_given_present += 1 

1188 else: 

1189 n_absent += 1 

1190 if t.judged_present(): 

1191 n_detected_given_absent += 1 

1192 n_trials += 1 

1193 p_detected_given_present = ( 

1194 (float(n_detected_given_present) / float(n_present)) 

1195 if n_present > 0 

1196 else None 

1197 ) 

1198 p_detected_given_absent = ( 

1199 (float(n_detected_given_absent) / float(n_absent)) 

1200 if n_absent > 0 

1201 else None 

1202 ) 

1203 (c, dprime) = self.get_c_dprime( 

1204 p_detected_given_present, p_detected_given_absent 

1205 ) 

1206 # hits: p_detected_given_present 

1207 # false alarms: p_detected_given_absent 

1208 return ( 

1209 p_detected_given_present, 

1210 p_detected_given_absent, 

1211 c, 

1212 dprime, 

1213 n_trials, 

1214 ) 

1215 

1216 def get_extra_summary_tables( 

1217 self, req: CamcopsRequest 

1218 ) -> List[ExtraSummaryTable]: 

1219 grouparray = self.groupspecs 

1220 trialarray = self.trials 

1221 trialarray_auditory = [ 

1222 x for x in trialarray if x.target_modality == AUDITORY 

1223 ] 

1224 blockprob_values = [] # type: List[Dict[str, Any]] 

1225 halfprob_values = [] # type: List[Dict[str, Any]] 

1226 

1227 if grouparray and trialarray: 

1228 n_target_highprob = max([x.n_target for x in grouparray]) 

1229 n_target_lowprob = min([x.n_target for x in grouparray]) 

1230 groups_highprob = [ 

1231 x.group_num 

1232 for x in grouparray 

1233 if x.n_target == n_target_highprob 

1234 ] 

1235 groups_lowprob = [ 

1236 x.group_num 

1237 for x in grouparray 

1238 if x.n_target == n_target_lowprob 

1239 ] 

1240 for block in range(self.num_blocks): 

1241 for target_probability_low_high in (0, 1): 

1242 groups = ( 

1243 groups_lowprob 

1244 if target_probability_low_high == 0 

1245 else groups_highprob 

1246 ) 

1247 ( 

1248 p_detected_given_present, 

1249 p_detected_given_absent, 

1250 c, 

1251 dprime, 

1252 n_trials, 

1253 ) = self.get_p_detected(trialarray, [block], groups) 

1254 ( 

1255 auditory_p_detected_given_present, 

1256 auditory_p_detected_given_absent, 

1257 auditory_c, 

1258 auditory_dprime, 

1259 auditory_n_trials, 

1260 ) = self.get_p_detected( 

1261 trialarray_auditory, [block], groups 

1262 ) 

1263 blockprob_values.append( 

1264 dict( 

1265 cardinal_expdet_pk=self._pk, # tablename_pk 

1266 n_blocks_overall=self.num_blocks, 

1267 block=block, 

1268 target_probability_low_high=target_probability_low_high, # noqa 

1269 n_trials=n_trials, 

1270 p_detect_present=p_detected_given_present, 

1271 p_detect_absent=p_detected_given_absent, 

1272 c=c, 

1273 d=dprime, 

1274 auditory_n_trials=auditory_n_trials, 

1275 auditory_p_detect_present=auditory_p_detected_given_present, # noqa 

1276 auditory_p_detect_absent=auditory_p_detected_given_absent, # noqa 

1277 auditory_c=auditory_c, 

1278 auditory_d=auditory_dprime, 

1279 ) 

1280 ) 

1281 

1282 # Now another one... 

1283 for half in range(2): 

1284 blocks = list( 

1285 range( 

1286 half * self.num_blocks // 2, 

1287 self.num_blocks // (2 - half), 

1288 ) 

1289 ) 

1290 for target_probability_low_high in (0, 1): 

1291 groups = ( 

1292 groups_lowprob 

1293 if target_probability_low_high == 0 

1294 else groups_highprob 

1295 ) 

1296 ( 

1297 p_detected_given_present, 

1298 p_detected_given_absent, 

1299 c, 

1300 dprime, 

1301 n_trials, 

1302 ) = self.get_p_detected(trialarray, blocks, groups) 

1303 ( 

1304 auditory_p_detected_given_present, 

1305 auditory_p_detected_given_absent, 

1306 auditory_c, 

1307 auditory_dprime, 

1308 auditory_n_trials, 

1309 ) = self.get_p_detected( 

1310 trialarray_auditory, blocks, groups 

1311 ) 

1312 halfprob_values.append( 

1313 dict( 

1314 cardinal_expdet_pk=self._pk, # tablename_pk 

1315 half=half, 

1316 target_probability_low_high=target_probability_low_high, # noqa 

1317 n_trials=n_trials, 

1318 p_detect_present=p_detected_given_present, 

1319 p_detect_absent=p_detected_given_absent, 

1320 c=c, 

1321 d=dprime, 

1322 auditory_n_trials=auditory_n_trials, 

1323 auditory_p_detect_present=auditory_p_detected_given_present, # noqa 

1324 auditory_p_detect_absent=auditory_p_detected_given_absent, # noqa 

1325 auditory_c=auditory_c, 

1326 auditory_d=auditory_dprime, 

1327 ) 

1328 ) 

1329 

1330 blockprob_table = ExtraSummaryTable( 

1331 tablename="cardinal_expdet_blockprobs", 

1332 xmlname="blockprobs", 

1333 columns=self.get_blockprob_columns(), 

1334 rows=blockprob_values, 

1335 task=self, 

1336 ) 

1337 halfprob_table = ExtraSummaryTable( 

1338 tablename="cardinal_expdet_halfprobs", 

1339 xmlname="halfprobs", 

1340 columns=self.get_halfprob_columns(), 

1341 rows=halfprob_values, 

1342 task=self, 

1343 ) 

1344 return [blockprob_table, halfprob_table] 

1345 

1346 @staticmethod 

1347 def get_blockprob_columns() -> List[Column]: 

1348 # Must be a function, not a constant, because as SQLAlchemy builds the 

1349 # tables, it assigns the Table object to each Column. Therefore, a 

1350 # constant list works for the first request, but fails on subsequent 

1351 # requests with e.g. "sqlalchemy.exc.ArgumentError: Column object 'id' 

1352 # already assigned to Table 'cardinal_expdet_blockprobs'" 

1353 return [ 

1354 Column( 

1355 "id", 

1356 Integer, 

1357 primary_key=True, 

1358 autoincrement=True, 

1359 comment="Arbitrary PK", 

1360 ), 

1361 Column( 

1362 "cardinal_expdet_pk", 

1363 Integer, 

1364 ForeignKey("cardinal_expdet._pk"), 

1365 nullable=False, 

1366 comment="FK to the source table's _pk field", 

1367 ), 

1368 Column( 

1369 "n_blocks_overall", 

1370 Integer, 

1371 comment="Number of blocks (OVERALL)", 

1372 ), 

1373 Column("block", Integer, comment="Block number"), 

1374 Column( 

1375 "target_probability_low_high", 

1376 Integer, 

1377 comment="Target probability given stimulus " "(0 low, 1 high)", 

1378 ), 

1379 Column( 

1380 "n_trials", 

1381 Integer, 

1382 comment="Number of trials in this condition", 

1383 ), 

1384 Column("p_detect_present", Float, comment="P(detect | present)"), 

1385 Column("p_detect_absent", Float, comment="P(detect | absent)"), 

1386 Column( 

1387 "c", 

1388 Float, 

1389 comment="c (bias; c > 0 when miss rate > false alarm rate; " 

1390 "c < 0 when false alarm rate > miss rate)", 

1391 ), 

1392 Column("d", Float, comment="d' (discriminability)"), 

1393 Column( 

1394 "auditory_n_trials", 

1395 Integer, 

1396 comment="Number of auditory trials in this condition", 

1397 ), 

1398 Column( 

1399 "auditory_p_detect_present", 

1400 Float, 

1401 comment="AUDITORY P(detect | present)", 

1402 ), 

1403 Column( 

1404 "auditory_p_detect_absent", 

1405 Float, 

1406 comment="AUDITORY P(detect | absent)", 

1407 ), 

1408 Column("auditory_c", Float, comment="AUDITORY c"), 

1409 Column("auditory_d", Float, comment="AUDITORY d'"), 

1410 ] 

1411 

1412 @staticmethod 

1413 def get_halfprob_columns() -> List[Column]: 

1414 return [ 

1415 Column( 

1416 "id", 

1417 Integer, 

1418 primary_key=True, 

1419 autoincrement=True, 

1420 comment="Arbitrary PK", 

1421 ), 

1422 Column( 

1423 "cardinal_expdet_pk", 

1424 Integer, 

1425 ForeignKey("cardinal_expdet._pk"), 

1426 nullable=False, 

1427 comment="FK to the source table's _pk field", 

1428 ), 

1429 Column("half", Integer, comment="Half number"), 

1430 Column( 

1431 "target_probability_low_high", 

1432 Integer, 

1433 comment="Target probability given stimulus " "(0 low, 1 high)", 

1434 ), 

1435 Column( 

1436 "n_trials", 

1437 Integer, 

1438 comment="Number of trials in this condition", 

1439 ), 

1440 Column("p_detect_present", Float, comment="P(detect | present)"), 

1441 Column("p_detect_absent", Float, comment="P(detect | absent)"), 

1442 Column( 

1443 "c", 

1444 Float, 

1445 comment="c (bias; c > 0 when miss rate > false alarm rate; " 

1446 "c < 0 when false alarm rate > miss rate)", 

1447 ), 

1448 Column("d", Float, comment="d' (discriminability)"), 

1449 Column( 

1450 "auditory_n_trials", 

1451 Integer, 

1452 comment="Number of auditory trials in this condition", 

1453 ), 

1454 Column( 

1455 "auditory_p_detect_present", 

1456 Float, 

1457 comment="AUDITORY P(detect | present)", 

1458 ), 

1459 Column( 

1460 "auditory_p_detect_absent", 

1461 Float, 

1462 comment="AUDITORY P(detect | absent)", 

1463 ), 

1464 Column("auditory_c", Float, comment="AUDITORY c"), 

1465 Column("auditory_d", Float, comment="AUDITORY d'"), 

1466 ] 

1467 

1468 def get_overall_p_detect_present(self) -> Optional[float]: 

1469 trialarray = self.trials 

1470 ( 

1471 p_detected_given_present, 

1472 p_detected_given_absent, 

1473 c, 

1474 dprime, 

1475 n_trials, 

1476 ) = self.get_p_detected(trialarray, None, None) 

1477 return p_detected_given_present 

1478 

1479 def get_overall_p_detect_absent(self) -> Optional[float]: 

1480 trialarray = self.trials 

1481 ( 

1482 p_detected_given_present, 

1483 p_detected_given_absent, 

1484 c, 

1485 dprime, 

1486 n_trials, 

1487 ) = self.get_p_detected(trialarray, None, None) 

1488 return p_detected_given_absent 

1489 

1490 def get_overall_c(self) -> Optional[float]: 

1491 trialarray = self.trials 

1492 ( 

1493 p_detected_given_present, 

1494 p_detected_given_absent, 

1495 c, 

1496 dprime, 

1497 n_trials, 

1498 ) = self.get_p_detected(trialarray, None, None) 

1499 return c 

1500 

1501 def get_overall_d(self) -> Optional[float]: 

1502 trialarray = self.trials 

1503 ( 

1504 p_detected_given_present, 

1505 p_detected_given_absent, 

1506 c, 

1507 dprime, 

1508 n_trials, 

1509 ) = self.get_p_detected(trialarray, None, None) 

1510 return dprime