Coverage for tasks/cardinal_expectationdetection.py: 31%

458 statements  

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

1""" 

2camcops_server/tasks/cardinal_expectationdetection.py 

3 

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

5 

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

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

8 

9 This file is part of CamCOPS. 

10 

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

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

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

14 (at your option) any later version. 

15 

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

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

18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

19 GNU General Public License for more details. 

20 

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

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

23 

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

25 

26""" 

27 

28import logging 

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

30 

31from cardinal_pythonlib.logs import BraceStyleAdapter 

32from matplotlib.axes import Axes 

33import numpy 

34from pendulum import DateTime as Pendulum 

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

36from sqlalchemy.orm import Mapped, mapped_column 

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: Mapped[int] = mapped_column( 

114 comment="FK to cardinal_expdet", 

115 ) 

116 trial: Mapped[int] = mapped_column(comment="Trial number (0-based)") 

117 

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

119 block: Mapped[Optional[int]] = mapped_column( 

120 comment="Block number (0-based)" 

121 ) 

122 group_num: Mapped[Optional[int]] = mapped_column( 

123 comment="Group number (0-based)" 

124 ) 

125 cue: Mapped[Optional[int]] = mapped_column(comment="Cue number (0-based)") 

126 raw_cue_number: Mapped[Optional[int]] = mapped_column( 

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

128 ) 

129 target_modality: Mapped[Optional[int]] = mapped_column( 

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

131 ) 

132 target_number: Mapped[Optional[int]] = mapped_column( 

133 comment="Target number (0-based)" 

134 ) 

135 target_present: Mapped[Optional[int]] = mapped_column( 

136 comment="Target present? (0 no, 1 yes)" 

137 ) 

138 iti_length_s: Mapped[Optional[float]] = mapped_column( 

139 comment="Intertrial interval (s)" 

140 ) 

141 

142 # Task determines these (on the fly): 

143 pause_given_before_trial: Mapped[Optional[int]] = mapped_column( 

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

145 ) 

146 pause_start_time: Mapped[Optional[Pendulum]] = mapped_column( 

147 PendulumDateTimeAsIsoTextColType, 

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

149 ) 

150 pause_end_time: Mapped[Optional[Pendulum]] = mapped_column( 

151 PendulumDateTimeAsIsoTextColType, 

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

153 ) 

154 trial_start_time: Mapped[Optional[Pendulum]] = mapped_column( 

155 PendulumDateTimeAsIsoTextColType, 

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

157 ) 

158 cue_start_time: Mapped[Optional[Pendulum]] = mapped_column( 

159 PendulumDateTimeAsIsoTextColType, 

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

161 ) 

162 target_start_time: Mapped[Optional[Pendulum]] = mapped_column( 

163 PendulumDateTimeAsIsoTextColType, 

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

165 ) 

166 detection_start_time: Mapped[Optional[Pendulum]] = mapped_column( 

167 PendulumDateTimeAsIsoTextColType, 

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

169 ) 

170 iti_start_time: Mapped[Optional[Pendulum]] = mapped_column( 

171 PendulumDateTimeAsIsoTextColType, 

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

173 ) 

174 iti_end_time: Mapped[Optional[Pendulum]] = mapped_column( 

175 PendulumDateTimeAsIsoTextColType, 

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

177 ) 

178 trial_end_time: Mapped[Optional[Pendulum]] = mapped_column( 

179 PendulumDateTimeAsIsoTextColType, 

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

181 ) 

182 

183 # Subject decides these: 

184 responded: Mapped[Optional[int]] = mapped_column( 

185 comment="Responded? (0 no, 1 yes)" 

186 ) 

187 response_time: Mapped[Optional[Pendulum]] = mapped_column( 

188 PendulumDateTimeAsIsoTextColType, 

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

190 ) 

191 response_latency_ms: Mapped[Optional[int]] = mapped_column( 

192 comment="Response latency (ms)" 

193 ) 

194 rating: Mapped[Optional[int]] = mapped_column( 

195 comment="Rating (0 definitely not - 4 definitely)" 

196 ) 

197 correct: Mapped[Optional[int]] = mapped_column( 

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

199 ) 

200 points: Mapped[Optional[int]] = mapped_column( 

201 comment="Points earned this trial" 

202 ) 

203 cumulative_points: Mapped[Optional[int]] = mapped_column( 

204 comment="Cumulative points earned" 

205 ) 

206 

207 @classmethod 

208 def get_html_table_header(cls) -> str: 

209 return f""" 

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

211 <tr> 

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

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

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

215 <th>Cue</th> 

216 <th>Raw cue</th> 

217 <th>Target modality</th> 

218 <th>Target#</th> 

219 <th>Target present?</th> 

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

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

222 </tr> 

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

224 <th>...</th> 

225 <th>Pause start@</th> 

226 <th>Pause end@</th> 

227 <th>Trial start@</th> 

228 <th>Cue@</th> 

229 <th>Target@</th> 

230 <th>Detection start@</th> 

231 <th>ITI start@</th> 

232 <th>ITI end@</th> 

233 <th>Trial end@</th> 

234 </tr> 

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

236 <th>...</th> 

237 <th>Responded?</th> 

238 <th>Responded@</th> 

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

240 <th>Rating</th> 

241 <th>Correct?</th> 

242 <th>Points</th> 

243 <th>Cumulative points</th> 

244 </tr> 

245 """ 

246 

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

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

249 if not self.responded: 

250 return None 

251 elif self.rating >= 3: 

252 return True 

253 else: 

254 return False 

255 

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

257 if not self.responded: 

258 return None 

259 elif self.rating <= 1: 

260 return True 

261 else: 

262 return False 

263 

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

265 if not self.responded: 

266 return None 

267 return self.rating == 2 

268 

269 def get_html_table_row(self) -> str: 

270 return ( 

271 tr( 

272 a(self.trial), 

273 a(self.block), 

274 a(self.group_num), 

275 a(self.cue), 

276 a(self.raw_cue_number), 

277 a(self.target_modality), 

278 a(self.target_number), 

279 a(self.target_present), 

280 a(self.iti_length_s), 

281 a(self.pause_given_before_trial), 

282 ) 

283 + tr( 

284 "...", 

285 a(self.pause_start_time), 

286 a(self.pause_end_time), 

287 a(self.trial_start_time), 

288 a(self.cue_start_time), 

289 a(self.target_start_time), 

290 a(self.detection_start_time), 

291 a(self.iti_start_time), 

292 a(self.iti_end_time), 

293 a(self.trial_end_time), 

294 tr_class=CssClass.EXTRADETAIL2, 

295 ) 

296 + tr( 

297 "...", 

298 a(self.responded), 

299 a(self.response_time), 

300 a(self.response_latency_ms), 

301 a(self.rating), 

302 a(self.correct), 

303 a(self.points), 

304 a(self.cumulative_points), 

305 tr_class=CssClass.EXTRADETAIL2, 

306 ) 

307 ) 

308 

309 # ------------------------------------------------------------------------- 

310 # TaskDescendant overrides 

311 # ------------------------------------------------------------------------- 

312 

313 @classmethod 

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

315 return CardinalExpectationDetection 

316 

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

318 return CardinalExpectationDetection.get_linked( # type: ignore[return-value] # noqa: E501 

319 self.cardinal_expdet_id, self 

320 ) 

321 

322 

323class ExpDetTrialGroupSpec(GenericTabletRecordMixin, TaskDescendant, Base): 

324 __tablename__ = "cardinal_expdet_trialgroupspec" 

325 

326 cardinal_expdet_id: Mapped[int] = mapped_column( 

327 comment="FK to cardinal_expdet", 

328 ) 

329 group_num: Mapped[int] = mapped_column(comment="Group number (0-based)") 

330 

331 # Group spec 

332 cue: Mapped[Optional[int]] = mapped_column(comment="Cue number (0-based)") 

333 target_modality: Mapped[Optional[int]] = mapped_column( 

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

335 ) 

336 target_number: Mapped[Optional[int]] = mapped_column( 

337 comment="Target number (0-based)" 

338 ) 

339 n_target: Mapped[Optional[int]] = mapped_column( 

340 comment="Number of trials with target present" 

341 ) 

342 n_no_target: Mapped[Optional[int]] = mapped_column( 

343 comment="Number of trials with target absent" 

344 ) 

345 

346 DP = 3 

347 

348 @classmethod 

349 def get_html_table_header(cls) -> str: 

350 return f""" 

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

352 <tr> 

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

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

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

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

357 <th># target trials</th> 

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

359 </tr> 

360 """ 

361 

362 def get_html_table_row(self) -> str: 

363 return tr( 

364 a(self.group_num), 

365 a(self.cue), 

366 a(self.target_modality), 

367 a(self.target_number), 

368 a(self.n_target), 

369 a(self.n_no_target), 

370 ) 

371 

372 # ------------------------------------------------------------------------- 

373 # TaskDescendant overrides 

374 # ------------------------------------------------------------------------- 

375 

376 @classmethod 

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

378 return CardinalExpectationDetection 

379 

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

381 return CardinalExpectationDetection.get_linked( # type: ignore[return-value] # noqa: E501 

382 self.cardinal_expdet_id, self 

383 ) 

384 

385 

386class CardinalExpectationDetection(TaskHasPatientMixin, Task): # type: ignore[misc] # noqa: E501 

387 """ 

388 Server implementation of the Cardinal_ExpDet task. 

389 """ 

390 

391 __tablename__ = "cardinal_expdet" 

392 shortname = "Cardinal_ExpDet" 

393 use_landscape_for_pdf = True 

394 

395 # Config 

396 num_blocks: Mapped[Optional[int]] = mapped_column( 

397 comment="Number of blocks" 

398 ) 

399 stimulus_counterbalancing: Mapped[Optional[int]] = mapped_column( 

400 comment="Stimulus counterbalancing condition", 

401 ) 

402 is_detection_response_on_right: Mapped[Optional[int]] = mapped_column( 

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

404 ) 

405 pause_every_n_trials: Mapped[Optional[int]] = mapped_column( 

406 comment="Pause every n trials" 

407 ) 

408 # ... cue 

409 cue_duration_s: Mapped[Optional[float]] = mapped_column( 

410 comment="Cue duration (s)" 

411 ) 

412 visual_cue_intensity: Mapped[Optional[float]] = mapped_column( 

413 comment="Visual cue intensity (0.0-1.0)" 

414 ) 

415 auditory_cue_intensity: Mapped[Optional[float]] = mapped_column( 

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

417 ) 

418 # ... ISI 

419 isi_duration_s: Mapped[Optional[float]] = mapped_column( 

420 comment="Interstimulus interval (s)" 

421 ) 

422 # .. target 

423 visual_target_duration_s: Mapped[Optional[float]] = mapped_column( 

424 comment="Visual target duration (s)" 

425 ) 

426 visual_background_intensity: Mapped[Optional[float]] = mapped_column( 

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

428 ) 

429 visual_target_0_intensity: Mapped[Optional[float]] = mapped_column( 

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

431 ) 

432 visual_target_1_intensity: Mapped[Optional[float]] = mapped_column( 

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

434 ) 

435 auditory_background_intensity: Mapped[Optional[float]] = mapped_column( 

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

437 ) 

438 auditory_target_0_intensity: Mapped[Optional[float]] = mapped_column( 

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

440 ) 

441 auditory_target_1_intensity: Mapped[Optional[float]] = mapped_column( 

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

443 ) 

444 # ... ITI 

445 iti_min_s: Mapped[Optional[float]] = mapped_column( 

446 comment="Intertrial interval minimum (s)" 

447 ) 

448 iti_max_s: Mapped[Optional[float]] = mapped_column( 

449 comment="Intertrial interval maximum (s)" 

450 ) 

451 

452 # Results 

453 aborted: Mapped[Optional[int]] = mapped_column( 

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

455 ) 

456 finished: Mapped[Optional[int]] = mapped_column( 

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

458 ) 

459 last_trial_completed: Mapped[Optional[int]] = mapped_column( 

460 comment="Number of last trial completed", 

461 ) 

462 

463 # Relationships 

464 trials = ancillary_relationship( # type: ignore[assignment] 

465 parent_class_name="CardinalExpectationDetection", 

466 ancillary_class_name="ExpDetTrial", 

467 ancillary_fk_to_parent_attr_name="cardinal_expdet_id", 

468 ancillary_order_by_attr_name="trial", 

469 ) # type: List[ExpDetTrial] 

470 groupspecs = ancillary_relationship( # type: ignore[assignment] 

471 parent_class_name="CardinalExpectationDetection", 

472 ancillary_class_name="ExpDetTrialGroupSpec", 

473 ancillary_fk_to_parent_attr_name="cardinal_expdet_id", 

474 ancillary_order_by_attr_name="group_num", 

475 ) # type: List[ExpDetTrialGroupSpec] 

476 

477 @staticmethod 

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

479 _ = req.gettext 

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

481 

482 def is_complete(self) -> bool: 

483 return bool(self.finished) 

484 

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

486 return self.standard_task_summary_fields() + [ 

487 SummaryElement( 

488 name="final_score", 

489 coltype=Integer(), 

490 value=self.get_final_score(), 

491 ), 

492 SummaryElement( 

493 name="overall_p_detect_present", 

494 coltype=Float(), 

495 value=self.get_overall_p_detect_present(), 

496 ), 

497 SummaryElement( 

498 name="overall_p_detect_absent", 

499 coltype=Float(), 

500 value=self.get_overall_p_detect_absent(), 

501 ), 

502 SummaryElement( 

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

504 ), 

505 SummaryElement( 

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

507 ), 

508 ] 

509 

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

511 trialarray = self.trials 

512 if not trialarray: 

513 return None 

514 return trialarray[-1].cumulative_points 

515 

516 def get_group_html(self) -> str: 

517 grouparray = self.groupspecs 

518 html = ExpDetTrialGroupSpec.get_html_table_header() 

519 for g in grouparray: 

520 html += g.get_html_table_row() 

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

522 return html 

523 

524 @staticmethod 

525 def get_c_dprime( 

526 h: Optional[float], 

527 fa: Optional[float], 

528 two_alternative_forced_choice: bool = False, 

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

530 if h is None or fa is None: 

531 return None, None 

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

533 if fa == 0: 

534 fa = CONVERT_0_P_TO 

535 if fa == 1: 

536 fa = CONVERT_1_P_TO 

537 if h == 0: 

538 h = CONVERT_0_P_TO 

539 if h == 1: 

540 h = CONVERT_1_P_TO 

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

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

543 if two_alternative_forced_choice: 

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

545 else: 

546 dprime = z_h - z_fa 

547 c = -0.5 * (z_h + z_fa) 

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

549 # c is negative when FA > miss 

550 # c is positive when miss > FA 

551 return c, dprime 

552 

553 @staticmethod 

554 def get_sdt_values( 

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

556 ) -> Dict: 

557 # Probabilities and cumulative probabilities 

558 sum_count_stimulus = numpy.sum(count_stimulus) 

559 sum_count_nostimulus = numpy.sum(count_nostimulus) 

560 if sum_count_stimulus == 0 or sum_count_nostimulus == 0: 

561 fa = [] 

562 h = [] 

563 z_fa = [] 

564 z_h = [] 

565 else: 

566 p_stimulus = count_stimulus / sum_count_stimulus 

567 p_nostimulus = count_nostimulus / sum_count_nostimulus 

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

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

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

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

572 fa = cump_stimulus[:-1] 

573 h = cump_nostimulus[:-1] 

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

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

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

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

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

579 fa[fa == 0] = CONVERT_0_P_TO 

580 fa[fa == 1] = CONVERT_1_P_TO 

581 h[h == 0] = CONVERT_0_P_TO 

582 h[h == 1] = CONVERT_1_P_TO 

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

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

585 

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

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

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

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

590 

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

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

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

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

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

596 

597 def plot_roc( 

598 self, 

599 req: CamcopsRequest, 

600 ax: Axes, 

601 count_stimulus: Sequence[int], 

602 count_nostimulus: Sequence[int], 

603 show_x_label: bool, 

604 show_y_label: bool, 

605 plainroc: bool, 

606 subtitle: str, 

607 ) -> None: 

608 extraspace = 0.05 

609 sdtval = self.get_sdt_values(count_stimulus, count_nostimulus) 

610 

611 # Calculate d' for all pairs but the last 

612 if plainroc: 

613 x = sdtval["fa"] 

614 y = sdtval["h"] 

615 xlabel = "FA" 

616 ylabel = "H" 

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

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

619 else: 

620 x = sdtval["z_fa"] 

621 y = sdtval["z_h"] 

622 xlabel = "Z(FA)" 

623 ylabel = "Z(H)" 

624 # Plot 

625 ax.plot( 

626 x, 

627 y, 

628 marker=MatplotlibConstants.MARKER_PLUS, 

629 color=MatplotlibConstants.COLOUR_BLUE, 

630 linestyle=MatplotlibConstants.LINESTYLE_SOLID, 

631 ) 

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

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

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

635 req.set_figure_font_sizes(ax) 

636 

637 @staticmethod 

638 def get_roc_info( 

639 trialarray: List[ExpDetTrial], 

640 blocks: List[int], 

641 groups: Optional[List[int]], 

642 ) -> Dict: 

643 # Collect counts (Macmillan & Creelman p61) 

644 total_n = 0 

645 count_stimulus = numpy.zeros(NRATINGS) 

646 count_nostimulus = numpy.zeros(NRATINGS) 

647 rating_missing = False 

648 rating_out_of_range = False 

649 for t in trialarray: 

650 if t.rating is None: 

651 rating_missing = True 

652 continue 

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

654 rating_out_of_range = True 

655 break 

656 if groups and t.group_num not in groups: 

657 continue 

658 if blocks and t.block not in blocks: 

659 continue 

660 total_n += 1 

661 if t.target_present: 

662 count_stimulus[t.rating] += 1 

663 else: 

664 count_nostimulus[t.rating] += 1 

665 return { 

666 "total_n": total_n, 

667 "count_stimulus": count_stimulus, 

668 "count_nostimulus": count_nostimulus, 

669 "rating_missing": rating_missing, 

670 "rating_out_of_range": rating_out_of_range, 

671 } 

672 

673 def get_roc_figure_by_group( 

674 self, 

675 req: CamcopsRequest, 

676 trialarray: List[ExpDetTrial], 

677 grouparray: List[ExpDetTrialGroupSpec], 

678 plainroc: bool, 

679 ) -> str: 

680 if not trialarray or not grouparray: 

681 return WARNING_INSUFFICIENT_DATA 

682 figsize = ( 

683 PlotDefaults.FULLWIDTH_PLOT_WIDTH * 2, 

684 PlotDefaults.FULLWIDTH_PLOT_WIDTH, 

685 ) 

686 html = "" 

687 fig = req.create_figure(figsize=figsize) 

688 warned = False 

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

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

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

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

693 if rocinfo["rating_out_of_range"]: 

694 return ERROR_RATING_OUT_OF_RANGE 

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

696 html += WARNING_RATING_MISSING 

697 warned = True 

698 show_x_label = groupnum > 3 

699 show_y_label = groupnum % 4 == 0 

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

701 self.plot_roc( 

702 req, 

703 ax, 

704 rocinfo["count_stimulus"], 

705 rocinfo["count_nostimulus"], 

706 show_x_label, 

707 show_y_label, 

708 plainroc, 

709 subtitle, 

710 ) 

711 title = PLAIN_ROC_TITLE if plainroc else Z_ROC_TITLE 

712 fontprops = req.fontprops 

713 fontprops.set_weight("bold") 

714 fig.suptitle(title, fontproperties=fontprops) 

715 html += req.get_html_from_pyplot_figure(fig) 

716 return html 

717 

718 def get_roc_figure_firsthalf_lasthalf( 

719 self, 

720 req: CamcopsRequest, 

721 trialarray: List[ExpDetTrial], 

722 plainroc: bool, 

723 ) -> str: 

724 if not trialarray or not self.num_blocks: 

725 return WARNING_INSUFFICIENT_DATA 

726 figsize = ( 

727 PlotDefaults.FULLWIDTH_PLOT_WIDTH, 

728 PlotDefaults.FULLWIDTH_PLOT_WIDTH / 2, 

729 ) 

730 html = "" 

731 fig = req.create_figure(figsize=figsize) 

732 warned = False 

733 for half in range(2): 

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

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

736 blocks = list( 

737 range( 

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

739 ) 

740 ) 

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

742 if rocinfo["rating_out_of_range"]: 

743 return ERROR_RATING_OUT_OF_RANGE 

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

745 html += WARNING_RATING_MISSING 

746 warned = True 

747 show_x_label = True 

748 show_y_label = half == 0 

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

750 self.plot_roc( 

751 req, 

752 ax, 

753 rocinfo["count_stimulus"], 

754 rocinfo["count_nostimulus"], 

755 show_x_label, 

756 show_y_label, 

757 plainroc, 

758 subtitle, 

759 ) 

760 title = PLAIN_ROC_TITLE if plainroc else Z_ROC_TITLE 

761 fontprops = req.fontprops 

762 fontprops.set_weight("bold") 

763 fig.suptitle(title, fontproperties=fontprops) 

764 html += req.get_html_from_pyplot_figure(fig) 

765 return html 

766 

767 def get_trial_html(self) -> str: 

768 trialarray = self.trials 

769 html = ExpDetTrial.get_html_table_header() 

770 for t in trialarray: 

771 html += t.get_html_table_row() 

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

773 return html 

774 

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

776 grouparray = self.groupspecs 

777 trialarray = self.trials 

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

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

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

781 

782 # Provide HTML 

783 # HTML 

784 h = f""" 

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

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

787 {self.get_is_complete_tr(req)} 

788 </table> 

789 </div> 

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

791 Putative assay of propensity to hallucinations. 

792 </div> 

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

794 <tr> 

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

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

797 </tr> 

798 """ 

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

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

801 h += tr_qa( 

802 "“Detection” response on right?", 

803 self.is_detection_response_on_right, 

804 ) 

805 h += tr_qa( 

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

807 self.pause_every_n_trials, 

808 ) 

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

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

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

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

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

814 h += tr_qa( 

815 "Visual background intensity", self.visual_background_intensity 

816 ) 

817 h += tr_qa( 

818 "Visual target 0 (circle) intensity", 

819 self.visual_target_0_intensity, 

820 ) 

821 h += tr_qa( 

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

823 ) 

824 h += tr_qa( 

825 "Auditory background intensity", self.auditory_background_intensity 

826 ) 

827 h += tr_qa( 

828 "Auditory target 0 (tone) intensity", 

829 self.auditory_target_0_intensity, 

830 ) 

831 h += tr_qa( 

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

833 self.auditory_target_1_intensity, 

834 ) 

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

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

837 h += f""" 

838 </table> 

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

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

841 """ 

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

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

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

845 h += ( 

846 """ 

847 </table> 

848 <div> 

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

850 all these trials): 

851 </div> 

852 """ 

853 + self.get_group_html() 

854 + """ 

855 <div> 

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

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

858 rate &gt; miss rate): 

859 </div> 

860 """ 

861 + self.get_html_correct_by_group_and_block(trialarray) 

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

863 + self.get_html_correct_by_block(trialarray) 

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

865 + self.get_html_correct_by_group(trialarray) 

866 + """ 

867 <div> 

868 Detection probabilities by half and high/low association 

869 probability: 

870 </div> 

871 """ 

872 + self.get_html_correct_by_half_and_probability( 

873 trialarray, grouparray 

874 ) 

875 + """ 

876 <div> 

877 Detection probabilities by block and high/low association 

878 probability: 

879 </div> 

880 """ 

881 + self.get_html_correct_by_block_and_probability( 

882 trialarray, grouparray 

883 ) 

884 + """ 

885 <div> 

886 Receiver operating characteristic (ROC) curves by group: 

887 </div> 

888 """ 

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

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

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

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

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

894 + self.get_trial_html() 

895 ) 

896 return h 

897 

898 def get_html_correct_by_group_and_block( 

899 self, trialarray: List[ExpDetTrial] 

900 ) -> str: 

901 if not trialarray: 

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

903 html = f""" 

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

905 <tr> 

906 <th>Block</th> 

907 """ 

908 for g in range(N_CUES): 

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

910 html += f""" 

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

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

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

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

915 """ 

916 html += """ 

917 </th> 

918 </tr> 

919 """ 

920 for b in range(self.num_blocks): 

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

922 for g in range(N_CUES): 

923 ( 

924 p_detected_given_present, 

925 p_detected_given_absent, 

926 c, 

927 dprime, 

928 n_trials, 

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

930 html += td(a(p_detected_given_present)) 

931 html += td(a(p_detected_given_absent)) 

932 html += td(a(c)) 

933 html += td(a(dprime)) 

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

935 html += """ 

936 </table> 

937 """ 

938 return html 

939 

940 def get_html_correct_by_half_and_probability( 

941 self, 

942 trialarray: List[ExpDetTrial], 

943 grouparray: List[ExpDetTrialGroupSpec], 

944 ) -> str: 

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

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

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

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

949 groups_highprob = [ 

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

951 ] 

952 groups_lowprob = [ 

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

954 ] 

955 html = f""" 

956 <div><i> 

957 High probability groups (cues): 

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

959 Low probability groups (cues): 

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

961 </i></div> 

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

963 <tr> 

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

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

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

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

968 <th>c</th> 

969 <th>d'</th> 

970 </tr> 

971 """ 

972 for half in (0, 1): 

973 for prob in (0, 1): 

974 blocks = list( 

975 range( 

976 half * self.num_blocks // 2, 

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

978 ) 

979 ) 

980 groups = groups_lowprob if prob == 0 else groups_highprob 

981 ( 

982 p_detected_given_present, 

983 p_detected_given_absent, 

984 c, 

985 dprime, 

986 n_trials, 

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

988 html += tr( 

989 half, 

990 a(prob), 

991 a(p_detected_given_present), 

992 a(p_detected_given_absent), 

993 a(c), 

994 a(dprime), 

995 ) 

996 html += """ 

997 </table> 

998 """ 

999 return html 

1000 

1001 def get_html_correct_by_block_and_probability( 

1002 self, 

1003 trialarray: List[ExpDetTrial], 

1004 grouparray: List[ExpDetTrialGroupSpec], 

1005 ) -> str: 

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

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

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

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

1010 groups_highprob = [ 

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

1012 ] 

1013 groups_lowprob = [ 

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

1015 ] 

1016 html = f""" 

1017 <div><i> 

1018 High probability groups (cues): 

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

1020 Low probability groups (cues): 

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

1022 </i></div> 

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

1024 <tr> 

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

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

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

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

1029 <th>c</th> 

1030 <th>d'</th> 

1031 </tr> 

1032 """ 

1033 for b in range(self.num_blocks): 

1034 for prob in (0, 1): 

1035 groups = groups_lowprob if prob == 0 else groups_highprob 

1036 ( 

1037 p_detected_given_present, 

1038 p_detected_given_absent, 

1039 c, 

1040 dprime, 

1041 n_trials, 

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

1043 html += tr( 

1044 b, 

1045 prob, 

1046 a(p_detected_given_present), 

1047 a(p_detected_given_absent), 

1048 a(c), 

1049 a(dprime), 

1050 ) 

1051 html += """ 

1052 </table> 

1053 """ 

1054 return html 

1055 

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

1057 if not trialarray: 

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

1059 html = f""" 

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

1061 <tr> 

1062 <th>Group</th> 

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

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

1065 <th>c</th> 

1066 <th>d'</th> 

1067 </tr> 

1068 """ 

1069 for g in range(N_CUES): 

1070 ( 

1071 p_detected_given_present, 

1072 p_detected_given_absent, 

1073 c, 

1074 dprime, 

1075 n_trials, 

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

1077 html += tr( 

1078 g, 

1079 a(p_detected_given_present), 

1080 a(p_detected_given_absent), 

1081 a(c), 

1082 a(dprime), 

1083 ) 

1084 html += """ 

1085 </table> 

1086 """ 

1087 return html 

1088 

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

1090 if not trialarray: 

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

1092 html = f""" 

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

1094 <tr> 

1095 <th>Block</th> 

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

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

1098 <th>c</th> 

1099 <th>d'</th> 

1100 </tr> 

1101 """ 

1102 for b in range(self.num_blocks): 

1103 ( 

1104 p_detected_given_present, 

1105 p_detected_given_absent, 

1106 c, 

1107 dprime, 

1108 n_trials, 

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

1110 html += tr( 

1111 b, 

1112 a(p_detected_given_present), 

1113 a(p_detected_given_absent), 

1114 a(c), 

1115 a(dprime), 

1116 ) 

1117 html += """ 

1118 </table> 

1119 """ 

1120 return html 

1121 

1122 def get_p_detected( 

1123 self, 

1124 trialarray: List[ExpDetTrial], 

1125 blocks: Optional[List[int]], 

1126 groups: Optional[List[int]], 

1127 ) -> Tuple[ 

1128 Optional[float], Optional[float], Optional[float], Optional[float], int 

1129 ]: 

1130 n_present = 0 

1131 n_absent = 0 

1132 n_detected_given_present = 0 

1133 n_detected_given_absent = 0 

1134 n_trials = 0 

1135 for t in trialarray: 

1136 if ( 

1137 not t.responded 

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

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

1140 ): 

1141 continue 

1142 if t.target_present: 

1143 n_present += 1 

1144 if t.judged_present(): 

1145 n_detected_given_present += 1 

1146 else: 

1147 n_absent += 1 

1148 if t.judged_present(): 

1149 n_detected_given_absent += 1 

1150 n_trials += 1 

1151 p_detected_given_present = ( 

1152 (float(n_detected_given_present) / float(n_present)) 

1153 if n_present > 0 

1154 else None 

1155 ) 

1156 p_detected_given_absent = ( 

1157 (float(n_detected_given_absent) / float(n_absent)) 

1158 if n_absent > 0 

1159 else None 

1160 ) 

1161 (c, dprime) = self.get_c_dprime( 

1162 p_detected_given_present, p_detected_given_absent 

1163 ) 

1164 # hits: p_detected_given_present 

1165 # false alarms: p_detected_given_absent 

1166 return ( 

1167 p_detected_given_present, 

1168 p_detected_given_absent, 

1169 c, 

1170 dprime, 

1171 n_trials, 

1172 ) 

1173 

1174 def get_extra_summary_tables( 

1175 self, req: CamcopsRequest 

1176 ) -> List[ExtraSummaryTable]: 

1177 grouparray = self.groupspecs 

1178 trialarray = self.trials 

1179 trialarray_auditory = [ 

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

1181 ] 

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

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

1184 

1185 if grouparray and trialarray: 

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

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

1188 groups_highprob = [ 

1189 x.group_num 

1190 for x in grouparray 

1191 if x.n_target == n_target_highprob 

1192 ] 

1193 groups_lowprob = [ 

1194 x.group_num 

1195 for x in grouparray 

1196 if x.n_target == n_target_lowprob 

1197 ] 

1198 for block in range(self.num_blocks): 

1199 for target_probability_low_high in (0, 1): 

1200 groups = ( 

1201 groups_lowprob 

1202 if target_probability_low_high == 0 

1203 else groups_highprob 

1204 ) 

1205 ( 

1206 p_detected_given_present, 

1207 p_detected_given_absent, 

1208 c, 

1209 dprime, 

1210 n_trials, 

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

1212 ( 

1213 auditory_p_detected_given_present, 

1214 auditory_p_detected_given_absent, 

1215 auditory_c, 

1216 auditory_dprime, 

1217 auditory_n_trials, 

1218 ) = self.get_p_detected( 

1219 trialarray_auditory, [block], groups 

1220 ) 

1221 blockprob_values.append( 

1222 dict( 

1223 cardinal_expdet_pk=self._pk, # tablename_pk 

1224 n_blocks_overall=self.num_blocks, 

1225 block=block, 

1226 target_probability_low_high=target_probability_low_high, # noqa 

1227 n_trials=n_trials, 

1228 p_detect_present=p_detected_given_present, 

1229 p_detect_absent=p_detected_given_absent, 

1230 c=c, 

1231 d=dprime, 

1232 auditory_n_trials=auditory_n_trials, 

1233 auditory_p_detect_present=auditory_p_detected_given_present, # noqa 

1234 auditory_p_detect_absent=auditory_p_detected_given_absent, # noqa 

1235 auditory_c=auditory_c, 

1236 auditory_d=auditory_dprime, 

1237 ) 

1238 ) 

1239 

1240 # Now another one... 

1241 for half in range(2): 

1242 blocks = list( 

1243 range( 

1244 half * self.num_blocks // 2, 

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

1246 ) 

1247 ) 

1248 for target_probability_low_high in (0, 1): 

1249 groups = ( 

1250 groups_lowprob 

1251 if target_probability_low_high == 0 

1252 else groups_highprob 

1253 ) 

1254 ( 

1255 p_detected_given_present, 

1256 p_detected_given_absent, 

1257 c, 

1258 dprime, 

1259 n_trials, 

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

1261 ( 

1262 auditory_p_detected_given_present, 

1263 auditory_p_detected_given_absent, 

1264 auditory_c, 

1265 auditory_dprime, 

1266 auditory_n_trials, 

1267 ) = self.get_p_detected( 

1268 trialarray_auditory, blocks, groups 

1269 ) 

1270 halfprob_values.append( 

1271 dict( 

1272 cardinal_expdet_pk=self._pk, # tablename_pk 

1273 half=half, 

1274 target_probability_low_high=target_probability_low_high, # noqa 

1275 n_trials=n_trials, 

1276 p_detect_present=p_detected_given_present, 

1277 p_detect_absent=p_detected_given_absent, 

1278 c=c, 

1279 d=dprime, 

1280 auditory_n_trials=auditory_n_trials, 

1281 auditory_p_detect_present=auditory_p_detected_given_present, # noqa 

1282 auditory_p_detect_absent=auditory_p_detected_given_absent, # noqa 

1283 auditory_c=auditory_c, 

1284 auditory_d=auditory_dprime, 

1285 ) 

1286 ) 

1287 

1288 blockprob_table = ExtraSummaryTable( 

1289 tablename="cardinal_expdet_blockprobs", 

1290 xmlname="blockprobs", 

1291 columns=self.get_blockprob_columns(), 

1292 rows=blockprob_values, 

1293 task=self, 

1294 ) 

1295 halfprob_table = ExtraSummaryTable( 

1296 tablename="cardinal_expdet_halfprobs", 

1297 xmlname="halfprobs", 

1298 columns=self.get_halfprob_columns(), 

1299 rows=halfprob_values, 

1300 task=self, 

1301 ) 

1302 return [blockprob_table, halfprob_table] 

1303 

1304 @staticmethod 

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

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

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

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

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

1310 # already assigned to Table 'cardinal_expdet_blockprobs'" 

1311 return [ 

1312 Column( 

1313 "id", 

1314 Integer, 

1315 primary_key=True, 

1316 autoincrement=True, 

1317 comment="Arbitrary PK", 

1318 ), 

1319 Column( 

1320 "cardinal_expdet_pk", 

1321 Integer, 

1322 ForeignKey("cardinal_expdet._pk"), 

1323 nullable=False, 

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

1325 ), 

1326 Column( 

1327 "n_blocks_overall", 

1328 Integer, 

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

1330 ), 

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

1332 Column( 

1333 "target_probability_low_high", 

1334 Integer, 

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

1336 ), 

1337 Column( 

1338 "n_trials", 

1339 Integer, 

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

1341 ), 

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

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

1344 Column( 

1345 "c", 

1346 Float, 

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

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

1349 ), 

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

1351 Column( 

1352 "auditory_n_trials", 

1353 Integer, 

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

1355 ), 

1356 Column( 

1357 "auditory_p_detect_present", 

1358 Float, 

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

1360 ), 

1361 Column( 

1362 "auditory_p_detect_absent", 

1363 Float, 

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

1365 ), 

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

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

1368 ] 

1369 

1370 @staticmethod 

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

1372 return [ 

1373 Column( 

1374 "id", 

1375 Integer, 

1376 primary_key=True, 

1377 autoincrement=True, 

1378 comment="Arbitrary PK", 

1379 ), 

1380 Column( 

1381 "cardinal_expdet_pk", 

1382 Integer, 

1383 ForeignKey("cardinal_expdet._pk"), 

1384 nullable=False, 

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

1386 ), 

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

1388 Column( 

1389 "target_probability_low_high", 

1390 Integer, 

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

1392 ), 

1393 Column( 

1394 "n_trials", 

1395 Integer, 

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

1397 ), 

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

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

1400 Column( 

1401 "c", 

1402 Float, 

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

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

1405 ), 

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

1407 Column( 

1408 "auditory_n_trials", 

1409 Integer, 

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

1411 ), 

1412 Column( 

1413 "auditory_p_detect_present", 

1414 Float, 

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

1416 ), 

1417 Column( 

1418 "auditory_p_detect_absent", 

1419 Float, 

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

1421 ), 

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

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

1424 ] 

1425 

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

1427 trialarray = self.trials 

1428 ( 

1429 p_detected_given_present, 

1430 p_detected_given_absent, 

1431 c, 

1432 dprime, 

1433 n_trials, 

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

1435 return p_detected_given_present 

1436 

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

1438 trialarray = self.trials 

1439 ( 

1440 p_detected_given_present, 

1441 p_detected_given_absent, 

1442 c, 

1443 dprime, 

1444 n_trials, 

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

1446 return p_detected_given_absent 

1447 

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

1449 trialarray = self.trials 

1450 ( 

1451 p_detected_given_present, 

1452 p_detected_given_absent, 

1453 c, 

1454 dprime, 

1455 n_trials, 

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

1457 return c 

1458 

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

1460 trialarray = self.trials 

1461 ( 

1462 p_detected_given_present, 

1463 p_detected_given_absent, 

1464 c, 

1465 dprime, 

1466 n_trials, 

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

1468 return dprime