Coverage for tasks/cardinal_expectationdetection.py: 31%

1"""

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

28 import logging

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

30

31 from cardinal_pythonlib.logs import BraceStyleAdapter

32 from matplotlib.axes import Axes

33 import numpy

34 from pendulum import DateTime as Pendulum

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

36 from sqlalchemy.orm import Mapped, mapped_column

37 from sqlalchemy.sql.schema import Column, ForeignKey

38 from sqlalchemy.sql.sqltypes import Float, Integer

39

40 from camcops_server.cc_modules.cc_constants import (

41 CssClass,

42 MatplotlibConstants,

43 PlotDefaults,

44)

45 from camcops_server.cc_modules.cc_db import (

46 ancillary_relationship,

47 GenericTabletRecordMixin,

48 TaskDescendant,

49)

50 from 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)

60 from camcops_server.cc_modules.cc_request import CamcopsRequest

61 from camcops_server.cc_modules.cc_sqla_coltypes import (

62 PendulumDateTimeAsIsoTextColType,

63)

64 from camcops_server.cc_modules.cc_sqlalchemy import Base

65 from camcops_server.cc_modules.cc_summaryelement import (

66 ExtraSummaryTable,

67 SummaryElement,

68)

69 from camcops_server.cc_modules.cc_task import Task, TaskHasPatientMixin

70

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

72

73

74 CONVERT_0_P_TO = 0.001 # for Z-transformed ROC plot

75 CONVERT_1_P_TO = 0.999 # for Z-transformed ROC plot

76

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

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

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

80

81 ERROR_RATING_OUT_OF_RANGE = f"""

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

83"""

84 WARNING_INSUFFICIENT_DATA = f"""

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

86"""

87 WARNING_RATING_MISSING = f"""

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

89"""

90 PLAIN_ROC_TITLE = "ROC"

91 Z_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

96 AUDITORY = 0

97 VISUAL = 1

98

99

100 def 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

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

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

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