"""
camcops_server/tasks/ace3.py
===============================================================================
Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
This file is part of CamCOPS.
CamCOPS is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
CamCOPS is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with CamCOPS. If not, see <https://www.gnu.org/licenses/>.
===============================================================================
ACE-III and Mini-ACE.
"""
from typing import Any, cast, Iterable, List, Optional, Type
from cardinal_pythonlib.stringfunc import strseq
import cardinal_pythonlib.rnc_web as ws
import numpy
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.sql.sqltypes import Integer, String, UnicodeText
from camcops_server.cc_modules.cc_blob import (
Blob,
blob_relationship,
get_blob_img_html,
)
from camcops_server.cc_modules.cc_constants import CssClass, PlotDefaults, PV
from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo
from camcops_server.cc_modules.cc_db import add_multiple_columns
from camcops_server.cc_modules.cc_html import (
answer,
get_yes_no_none,
italic,
pmid,
subheading_spanning_two_columns,
tr,
tr_qa,
tr_span_col,
)
from camcops_server.cc_modules.cc_request import CamcopsRequest
from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup
from camcops_server.cc_modules.cc_sqla_coltypes import (
BIT_CHECKER,
mapped_camcops_column,
PermittedValueChecker,
)
from camcops_server.cc_modules.cc_summaryelement import SummaryElement
from camcops_server.cc_modules.cc_task import (
Task,
TaskHasClinicianMixin,
TaskHasPatientMixin,
)
from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo
# =============================================================================
# Constants
# =============================================================================
ADDRESS_PARTS = [
"forename",
"surname",
"number",
"street_1",
"street_2",
"town",
"county",
]
RECALL_WORDS = ["lemon", "key", "ball"]
PERCENT_DP = 1
TOTAL_MAX = 100
ATTN_MAX = 18
MEMORY_MAX = 26
FLUENCY_MAX = 14
LANG_MAX = 26
VSP_MAX = 16
ATTN_MINIACE_MAX = 4
MEM_MINIACE_MAX = 14
FLUENCY_MINIACE_MAX = 7
VSP_MINIACE_MAX = 5
MINI_ACE_MAX = 30
AGE_FTE = "Age on leaving full-time education"
OCCUPATION = "Occupation"
HANDEDNESS = "Handedness"
N_ATTN_TIME_ACE = 5
N_ATTN_TIME_MINIACE = 4
N_MEM_REPEAT_RECALL_ADDR = 7
ANIMAL_FLUENCY_SCORING_HTML = (
"Score for animals <i>(≥22 scores 7, 17–21 scores 6, 14–16 scores 5, "
"11–13 scores 4, 9–10 scores 3, 7–8 scores 2, 5–6 scores 1, "
"<5 scores 0)</i>"
)
ACE3_COPYRIGHT = """
ACE-III: Copyright © 2012, John Hodges. “The ACE-III is available for free. The
copyright is held by Professor John Hodges who is happy for the test to be used
in clinical practice and research projects. There is no need to contact us if
you wish to use the ACE-III in clinical practice.” (ACE-III FAQ, 7 July 2013,
www.neura.edu.au).
"""
MINI_ACE_THRESHOLDS = f"""
In the mini-ACE, scores ≤21 had sensitivity 0.61 and specificity 1.0 for
dementia, and scores ≤25 had sensitivity 0.85 and specificity 0.87 for
dementia, in a context of patients with Alzheimer’s disease, behavioural
variant frontotemporal dementia, corticobasal syndrome, primary progressive
aphasia, and controls (Hsieh et al. 2015, {pmid(25227877)}.
"""
# =============================================================================
# Ancillary functions
# =============================================================================
[docs]def score_zero_for_absent(x: Optional[int]) -> int:
"""0 if x is None else x"""
return 0 if x is None else x
def percent(score: int, maximum: int) -> str:
return ws.number_to_dp(100 * score / maximum, PERCENT_DP)
def tr_score_with_pct(title: str, score: int, maximum: int) -> str:
return tr(
title,
answer(score) + f" / {maximum} ({percent(score, maximum)}%)",
)
[docs]def qsequence(target_addr_parts: Iterable[str]) -> str:
"""
For e.g. "Harry? Barnes? ..."
"""
return " ".join(f"{x}?" for x in target_addr_parts)
[docs]def tr_heading(left: str, right: str) -> str:
"""
HTML for header row of most tables.
"""
return f"""
<tr>
<th width="67%">{left}</th>
<th width="33%">{right}</th>
</tr>
"""
# =============================================================================
# ACE-III
# =============================================================================
[docs]class Ace3(TaskHasPatientMixin, TaskHasClinicianMixin, Task): # type: ignore[misc] # noqa: E501
"""
Server implementation of the ACE-III task.
"""
__tablename__ = "ace3"
shortname = "ACE-III"
provides_trackers = True
prohibits_commercial = True
@classmethod
def extend_columns(cls: Type["Ace3"], **kwargs: Any) -> None:
add_multiple_columns(
cls,
"attn_time",
1,
5,
pv=PV.BIT,
comment_fmt="Attention, time, {n}/5, {s} (0 or 1)",
comment_strings=["day", "date", "month", "year", "season"],
)
add_multiple_columns(
cls,
"attn_place",
1,
5,
pv=PV.BIT,
comment_fmt="Attention, place, {n}/5, {s} (0 or 1)",
comment_strings=[
"house number/floor",
"street/hospital",
"town",
"county",
"country",
],
)
add_multiple_columns(
cls,
"attn_repeat_word",
1,
3,
pv=PV.BIT,
comment_fmt="Attention, repeat word, {n}/3, {s} (0 or 1)",
comment_strings=RECALL_WORDS,
)
add_multiple_columns(
cls,
"attn_serial7_subtraction",
1,
5,
pv=PV.BIT,
comment_fmt="Attention, serial sevens, {n}/5 (0 or 1)",
)
add_multiple_columns(
cls,
"mem_recall_word",
1,
3,
pv=PV.BIT,
comment_fmt="Memory, recall word, {n}/3, {s} (0 or 1)",
comment_strings=RECALL_WORDS,
)
add_multiple_columns(
cls,
"mem_repeat_address_trial1_",
1,
N_MEM_REPEAT_RECALL_ADDR,
pv=PV.BIT,
comment_fmt="Memory, address registration trial 1/3 "
"(not scored), {s} (0 or 1)",
comment_strings=ADDRESS_PARTS,
)
add_multiple_columns(
cls,
"mem_repeat_address_trial2_",
1,
N_MEM_REPEAT_RECALL_ADDR,
pv=PV.BIT,
comment_fmt="Memory, address registration trial 2/3 "
"(not scored), {s} (0 or 1)",
comment_strings=ADDRESS_PARTS,
)
add_multiple_columns(
cls,
"mem_repeat_address_trial3_",
1,
N_MEM_REPEAT_RECALL_ADDR,
pv=PV.BIT,
comment_fmt="Memory, address registration trial 3/3 "
"(scored), {s} (0 or 1)",
comment_strings=ADDRESS_PARTS,
)
add_multiple_columns(
cls,
"mem_famous",
1,
4,
pv=PV.BIT,
comment_fmt="Memory, famous people, {n}/4, {s} (0 or 1)",
comment_strings=["current PM", "woman PM", "USA president", "JFK"],
)
add_multiple_columns(
cls,
"lang_follow_command",
1,
3,
pv=PV.BIT,
comment_fmt="Language, command {n}/3 (0 or 1)",
)
add_multiple_columns(
cls,
"lang_write_sentences_point",
1,
2,
pv=PV.BIT,
comment_fmt="Language, write sentences, {n}/2, {s} (0 or 1)",
comment_strings=[
"two sentences on same topic",
"grammar/spelling",
],
)
add_multiple_columns(
cls,
"lang_repeat_word",
1,
4,
pv=PV.BIT,
comment_fmt="Language, repeat word, {n}/4, {s} (0 or 1)",
comment_strings=[
"caterpillar",
"eccentricity",
"unintelligible",
"statistician",
],
)
add_multiple_columns(
cls,
"lang_repeat_sentence",
1,
2,
pv=PV.BIT,
comment_fmt="Language, repeat sentence, {n}/2, {s} (0 or 1)",
comment_strings=["glitters_gold", "stitch_time"],
)
add_multiple_columns(
cls,
"lang_name_picture",
1,
12,
pv=PV.BIT,
comment_fmt="Language, name picture, {n}/12, {s} (0 or 1)",
comment_strings=[
"spoon",
"book",
"kangaroo/wallaby",
"penguin",
"anchor",
"camel/dromedary",
"harp",
"rhinoceros",
"barrel/keg/tub",
"crown",
"alligator/crocodile",
"accordion/piano accordion/squeeze box",
],
)
add_multiple_columns(
cls,
"lang_identify_concept",
1,
4,
pv=PV.BIT,
comment_fmt="Language, identify concept, {n}/4, {s} (0 or 1)",
comment_strings=["monarchy", "marsupial", "Antarctic", "nautical"],
)
add_multiple_columns(
cls,
"vsp_count_dots",
1,
4,
pv=PV.BIT,
comment_fmt="Visuospatial, count dots {n}/4, {s} dots (0-1)",
comment_strings=["8", "10", "7", "9"],
)
add_multiple_columns(
cls,
"vsp_identify_letter",
1,
4,
pv=PV.BIT,
comment_fmt="Visuospatial, identify letter {n}/4, {s} (0-1)",
comment_strings=["K", "M", "A", "T"],
)
add_multiple_columns(
cls,
"mem_recall_address",
1,
N_MEM_REPEAT_RECALL_ADDR,
pv=PV.BIT,
comment_fmt="Memory, recall address {n}/7, {s} (0-1)",
comment_strings=ADDRESS_PARTS,
)
add_multiple_columns(
cls,
"mem_recognize_address",
1,
5,
pv=PV.BIT,
comment_fmt="Memory, recognize address {n}/5 (if "
"applicable) ({s}) (0-1)",
comment_strings=["name", "number", "street", "town", "county"],
)
add_multiple_columns( # tablet version 2.0.0 onwards
cls,
"mem_recognize_address_choice",
1,
5,
coltype=String(length=1), # was Text
comment_fmt="Memory, recognize address {n}/5, CHOICE (if "
"applicable) ({s}) (A/B/C)",
comment_strings=["name", "number", "street", "town", "county"],
)
task_edition: Mapped[Optional[str]] = mapped_camcops_column(
String(length=255),
comment="Task edition. Older task instances will have NULL and that "
"indicates UK English, 2012 version.",
)
task_address_version: Mapped[Optional[str]] = mapped_camcops_column(
String(length=1),
comment="Task version, determining the address for recall (A/B/C). "
"Older task instances will have NULL and that indicates version A.",
permitted_value_checker=PermittedValueChecker(
permitted_values=["A", "B", "C"]
),
)
remote_administration: Mapped[Optional[bool]] = mapped_camcops_column(
permitted_value_checker=BIT_CHECKER,
comment="Task performed using remote (videoconferencing) "
"administration?",
)
age_at_leaving_full_time_education: Mapped[Optional[int]] = mapped_column(
comment="Age at leaving full time education",
)
occupation: Mapped[Optional[str]] = mapped_column(
UnicodeText, comment="Occupation"
)
handedness: Mapped[Optional[str]] = mapped_camcops_column(
String(length=1), # was Text
comment="Handedness (L or R)",
permitted_value_checker=PermittedValueChecker(
permitted_values=["L", "R"]
),
)
attn_num_registration_trials: Mapped[Optional[int]] = mapped_column(
comment="Attention, repetition, number of trials (not scored)",
)
fluency_letters_score: Mapped[Optional[int]] = mapped_camcops_column(
comment="Fluency, words beginning with P, score 0-7",
permitted_value_checker=PermittedValueChecker(minimum=0, maximum=7),
)
fluency_animals_score: Mapped[Optional[int]] = mapped_camcops_column(
comment="Fluency, animals, score 0-7",
permitted_value_checker=PermittedValueChecker(minimum=0, maximum=7),
)
lang_follow_command_practice: Mapped[Optional[int]] = (
mapped_camcops_column(
comment="Language, command, practice trial (not scored)",
permitted_value_checker=BIT_CHECKER,
)
)
lang_read_words_aloud: Mapped[Optional[int]] = mapped_camcops_column(
comment="Language, read five irregular words (0 or 1)",
permitted_value_checker=BIT_CHECKER,
)
vsp_copy_infinity: Mapped[Optional[int]] = mapped_camcops_column(
comment="Visuospatial, copy infinity (0-1)",
permitted_value_checker=BIT_CHECKER,
)
vsp_copy_cube: Mapped[Optional[int]] = mapped_camcops_column(
comment="Visuospatial, copy cube (0-2)",
permitted_value_checker=PermittedValueChecker(minimum=0, maximum=2),
)
vsp_draw_clock: Mapped[Optional[int]] = mapped_camcops_column(
comment="Visuospatial, draw clock (0-5)",
permitted_value_checker=PermittedValueChecker(minimum=0, maximum=5),
)
picture1_blobid: Mapped[Optional[int]] = mapped_camcops_column(
comment="Photo 1/2 PNG BLOB ID",
is_blob_id_field=True,
blob_relationship_attr_name="picture1",
)
picture2_blobid: Mapped[Optional[int]] = mapped_camcops_column(
comment="Photo 2/2 PNG BLOB ID",
is_blob_id_field=True,
blob_relationship_attr_name="picture2",
)
comments: Mapped[Optional[str]] = mapped_column(
UnicodeText, comment="Clinician's comments"
)
picture1: Mapped[Optional[Blob]] = blob_relationship( # type: ignore[assignment] # noqa: E501
"Ace3", "picture1_blobid"
)
picture2: Mapped[Optional[Blob]] = blob_relationship( # type: ignore[assignment] # noqa: E501
"Ace3", "picture2_blobid"
)
ATTN_SCORE_FIELDS = (
strseq("attn_time", 1, N_ATTN_TIME_ACE)
+ strseq("attn_place", 1, 5)
+ strseq("attn_repeat_word", 1, 3)
+ strseq("attn_serial7_subtraction", 1, 5)
)
MEM_NON_RECOG_SCORE_FIELDS = (
strseq("mem_recall_word", 1, 3)
+ strseq("mem_repeat_address_trial3_", 1, N_MEM_REPEAT_RECALL_ADDR)
+ strseq("mem_famous", 1, 4)
+ strseq("mem_recall_address", 1, N_MEM_REPEAT_RECALL_ADDR)
)
LANG_SIMPLE_SCORE_FIELDS = (
strseq("lang_write_sentences_point", 1, 2)
+ strseq("lang_repeat_sentence", 1, 2)
+ strseq("lang_name_picture", 1, 12)
+ strseq("lang_identify_concept", 1, 4)
)
LANG_FOLLOW_CMD_FIELDS = strseq("lang_follow_command", 1, 3)
LANG_REPEAT_WORD_FIELDS = strseq("lang_repeat_word", 1, 4)
VSP_SIMPLE_SCORE_FIELDS = strseq("vsp_count_dots", 1, 4) + strseq(
"vsp_identify_letter", 1, 4
)
BASIC_COMPLETENESS_FIELDS = (
ATTN_SCORE_FIELDS
+ MEM_NON_RECOG_SCORE_FIELDS
+ ["fluency_letters_score", "fluency_animals_score"]
+ ["lang_follow_command_practice"]
+ LANG_SIMPLE_SCORE_FIELDS
+ LANG_REPEAT_WORD_FIELDS
+ [
"lang_read_words_aloud",
"vsp_copy_infinity",
"vsp_copy_cube",
"vsp_draw_clock",
]
+ VSP_SIMPLE_SCORE_FIELDS
+ strseq("mem_recall_address", 1, N_MEM_REPEAT_RECALL_ADDR)
)
MINI_ACE_FIELDS = (
strseq("attn_time", 1, N_ATTN_TIME_MINIACE) # 4 points; not season
+ ["fluency_animals_score"] # 7 points
+ strseq("mem_repeat_address_trial3_", 1, N_MEM_REPEAT_RECALL_ADDR)
# ... 7 points
+ ["vsp_draw_clock"] # 5 points
+ strseq("mem_recall_address", 1, N_MEM_REPEAT_RECALL_ADDR) # 7 points
)
[docs] @staticmethod
def longname(req: "CamcopsRequest") -> str:
_ = req.gettext
return _("Addenbrooke’s Cognitive Examination III")
[docs] def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
return [
TrackerInfo(
value=self.total_score(),
plot_label="ACE-III total score",
axis_label=f"Total score (out of {TOTAL_MAX})",
axis_min=-0.5,
axis_max=TOTAL_MAX + 0.5,
# Traditional cutoffs: ≤82, ≤88
horizontal_lines=[82.5, 88.5],
),
TrackerInfo(
value=self.mini_ace_score(),
plot_label="Mini-ACE score",
axis_label=f"Mini-ACE score (out of {MINI_ACE_MAX})",
axis_min=-0.5,
axis_max=MINI_ACE_MAX + 0.5,
# Traditional cutoffs: ≤21, ≤25
horizontal_lines=[21.5, 25.5],
),
]
[docs] def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
if not self.is_complete():
return CTV_INCOMPLETE
a = self.attn_score()
m = self.mem_score()
f = self.fluency_score()
lang = self.lang_score()
v = self.vsp_score()
t = a + m + f + lang + v
mini = self.mini_ace_score()
text = (
f"ACE-III total: {t}/{TOTAL_MAX} "
f"(attention {a}/{ATTN_MAX}, memory {m}/{MEMORY_MAX}, "
f"fluency {f}/{FLUENCY_MAX}, language {lang}/{LANG_MAX}, "
f"visuospatial {v}/{VSP_MAX}, "
f"mini-ACE score {mini}/{MINI_ACE_MAX})"
)
return [CtvInfo(content=text)]
[docs] def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
return self.standard_task_summary_fields() + [
SummaryElement(
name="total",
coltype=Integer(),
value=self.total_score(),
comment=f"Total score (/{TOTAL_MAX})",
),
SummaryElement(
name="attn",
coltype=Integer(),
value=self.attn_score(),
comment=f"Attention (/{ATTN_MAX})",
),
SummaryElement(
name="mem",
coltype=Integer(),
value=self.mem_score(),
comment=f"Memory (/{MEMORY_MAX})",
),
SummaryElement(
name="fluency",
coltype=Integer(),
value=self.fluency_score(),
comment=f"Fluency (/{FLUENCY_MAX})",
),
SummaryElement(
name="lang",
coltype=Integer(),
value=self.lang_score(),
comment=f"Language (/{LANG_MAX})",
),
SummaryElement(
name="vsp",
coltype=Integer(),
value=self.vsp_score(),
comment=f"Visuospatial (/{VSP_MAX})",
),
SummaryElement(
name="mini_ace",
coltype=Integer(),
value=self.mini_ace_score(),
comment=f"Mini-ACE (/{MINI_ACE_MAX})",
),
]
def attn_score(self) -> int:
return cast(int, self.sum_fields(self.ATTN_SCORE_FIELDS))
@staticmethod
def get_recog_score(
recalled: Optional[int], recognized: Optional[int]
) -> int:
if recalled == 1:
return 1
return score_zero_for_absent(recognized)
@staticmethod
def get_recog_text(
recalled: Optional[int], recognized: Optional[int]
) -> str:
if recalled:
return "<i>1 (already recalled)</i>"
return answer(recognized)
# noinspection PyUnresolvedReferences
def get_mem_recognition_score(self) -> int:
score = 0
score += self.get_recog_score(
(self.mem_recall_address1 == 1 and self.mem_recall_address2 == 1), # type: ignore[attr-defined] # noqa: E501
self.mem_recognize_address1, # type: ignore[attr-defined]
)
score += self.get_recog_score(
(self.mem_recall_address3 == 1), self.mem_recognize_address2 # type: ignore[attr-defined] # noqa: E501
)
score += self.get_recog_score(
(self.mem_recall_address4 == 1 and self.mem_recall_address5 == 1), # type: ignore[attr-defined] # noqa: E501
self.mem_recognize_address3, # type: ignore[attr-defined]
)
score += self.get_recog_score(
(self.mem_recall_address6 == 1), self.mem_recognize_address4 # type: ignore[attr-defined] # noqa: E501
)
score += self.get_recog_score(
(self.mem_recall_address7 == 1), self.mem_recognize_address5 # type: ignore[attr-defined] # noqa: E501
)
return score
def mem_score(self) -> int:
return cast(
int,
(
self.sum_fields(self.MEM_NON_RECOG_SCORE_FIELDS)
+ self.get_mem_recognition_score()
),
)
def fluency_score(self) -> int:
return cast(
int,
(
score_zero_for_absent(self.fluency_letters_score)
+ score_zero_for_absent(self.fluency_animals_score)
),
)
def get_follow_command_score(self) -> int:
if self.lang_follow_command_practice != 1:
return 0
return cast(int, self.sum_fields(self.LANG_FOLLOW_CMD_FIELDS))
def get_repeat_word_score(self) -> int:
n = cast(int, self.sum_fields(self.LANG_REPEAT_WORD_FIELDS))
return 2 if n >= 4 else (1 if n == 3 else 0)
def lang_score(self) -> int:
return (
cast(int, self.sum_fields(self.LANG_SIMPLE_SCORE_FIELDS))
+ self.get_follow_command_score()
+ self.get_repeat_word_score()
+ score_zero_for_absent(self.lang_read_words_aloud)
)
def vsp_score(self) -> int:
return (
cast(int, self.sum_fields(self.VSP_SIMPLE_SCORE_FIELDS))
+ score_zero_for_absent(self.vsp_copy_infinity)
+ score_zero_for_absent(self.vsp_copy_cube)
+ score_zero_for_absent(self.vsp_draw_clock)
)
def total_score(self) -> int:
return (
self.attn_score()
+ self.mem_score()
+ self.fluency_score()
+ self.lang_score()
+ self.vsp_score()
)
def mini_ace_score(self) -> int:
return cast(int, self.sum_fields(self.MINI_ACE_FIELDS))
# noinspection PyUnresolvedReferences
def is_recognition_complete(self) -> bool:
return (
(
(
self.mem_recall_address1 == 1 # type: ignore[attr-defined]
and self.mem_recall_address2 == 1 # type: ignore[attr-defined] # noqa: E501
)
or self.mem_recognize_address1 is not None # type: ignore[attr-defined] # noqa: E501
)
and (
self.mem_recall_address3 == 1 # type: ignore[attr-defined]
or self.mem_recognize_address2 is not None # type: ignore[attr-defined] # noqa: E501
)
and (
(
self.mem_recall_address4 == 1 # type: ignore[attr-defined]
and self.mem_recall_address5 == 1 # type: ignore[attr-defined] # noqa: E501
)
or self.mem_recognize_address3 is not None # type: ignore[attr-defined] # noqa: E501
)
and (
self.mem_recall_address6 == 1 # type: ignore[attr-defined]
or self.mem_recognize_address4 is not None # type: ignore[attr-defined] # noqa: E501
)
and (
self.mem_recall_address7 == 1 # type: ignore[attr-defined]
or self.mem_recognize_address5 is not None # type: ignore[attr-defined] # noqa: E501
)
)
[docs] def is_complete(self) -> bool:
if self.any_fields_none(self.BASIC_COMPLETENESS_FIELDS):
return False
if not self.field_contents_valid():
return False
if self.lang_follow_command_practice == 1 and self.any_fields_none(
self.LANG_FOLLOW_CMD_FIELDS
):
return False
return self.is_recognition_complete()
[docs] @classmethod
def get_target_address_parts(
cls, req: CamcopsRequest, task_address_version: str
) -> List[str]:
"""
Returns the target address components (7 of them). This requires an
xstring (via a request also embodying the currently selected locale)
and the version selected for the task.
We do this as a classmethod so it (a) saves duplication and (b) knows
about the xstrings for ACE-III (which are shared with the Mini-ACE). A
superclass/mixin would be an alternative.
"""
parts = [] # type: List[str]
for i in range(1, N_MEM_REPEAT_RECALL_ADDR + 1):
xstringname = f"task_{task_address_version}_target_address_{i}"
part = cls.xstring(req, xstringname)
parts.append(part)
return parts
# noinspection PyUnresolvedReferences
[docs] def get_task_html(self, req: CamcopsRequest) -> str:
a = self.attn_score()
m = self.mem_score()
f = self.fluency_score()
lang = self.lang_score()
v = self.vsp_score()
t = a + m + f + lang + v
mini = self.mini_ace_score()
target_addr = qsequence(
self.get_target_address_parts(req, self.task_address_version)
)
lkb = qsequence(RECALL_WORDS) # lemon, key, ball
if self.is_complete():
figsize = (
PlotDefaults.FULLWIDTH_PLOT_WIDTH / 3,
PlotDefaults.FULLWIDTH_PLOT_WIDTH / 4,
)
width = 0.9
fig = req.create_figure(figsize=figsize)
ax = fig.add_subplot(1, 1, 1)
scores = numpy.array([a, m, f, lang, v])
maxima = numpy.array(
[ATTN_MAX, MEMORY_MAX, FLUENCY_MAX, LANG_MAX, VSP_MAX]
)
y = 100 * scores / maxima
x_labels = ["Attn", "Mem", "Flu", "Lang", "VSp"]
# noinspection PyTypeChecker
n = len(y)
xvar = numpy.arange(n)
ax.bar(xvar, y, width, color="b")
ax.set_ylabel("%", fontdict=req.fontdict)
ax.set_xticks(xvar)
x_offset = -0.5
ax.set_xlim(0 + x_offset, len(scores) + x_offset)
ax.set_xticklabels(x_labels, fontdict=req.fontdict)
fig.tight_layout() # or the ylabel drops off the figure
# fig.autofmt_xdate()
req.set_figure_font_sizes(ax)
figurehtml = req.get_html_from_pyplot_figure(fig)
else:
figurehtml = "<i>Incomplete; not plotted</i>"
return (
self.get_standard_clinician_comments_block(req, self.comments)
+ f"""
<div class="{CssClass.SUMMARY}">
<table class="{CssClass.SUMMARY}">
<tr>
{self.get_is_complete_td_pair(req)}
<td class="{CssClass.FIGURE}"
rowspan="8">{figurehtml}</td>
</tr>
"""
+ tr(
"Total ACE-III score <sup>[1]</sup>",
answer(t) + f" / {TOTAL_MAX}",
)
+ tr_score_with_pct("Attention", a, ATTN_MAX)
+ tr_score_with_pct("Memory", m, MEMORY_MAX)
+ tr_score_with_pct("Fluency", f, FLUENCY_MAX)
+ tr_score_with_pct("Language", lang, LANG_MAX)
+ tr_score_with_pct("Visuospatial", v, VSP_MAX)
+ tr_score_with_pct(
"Mini-ACE score <sup>[2]</sup>", mini, MINI_ACE_MAX
)
+ f"""
</table>
</div>
<table class="{CssClass.TASKCONFIG}">
"""
+ tr_heading("Task aspect", "Setting")
+ tr_qa("Edition", self.task_edition)
+ tr_qa("Version", self.task_address_version)
+ tr_qa(
"Remote administration?",
get_yes_no_none(req, self.remote_administration),
)
+ f"""
<table class="{CssClass.TASKDETAIL}">
"""
+ tr_heading("Question", "Answer/score")
+ tr_qa(
AGE_FTE,
self.age_at_leaving_full_time_education,
)
+ tr_qa(OCCUPATION, ws.webify(self.occupation))
+ tr_qa(HANDEDNESS, ws.webify(self.handedness))
+ subheading_spanning_two_columns("Attention")
+ tr(
"Day? Date? Month? Year? Season?",
", ".join(
answer(x)
for x in (
self.attn_time1, # type: ignore[attr-defined]
self.attn_time2, # type: ignore[attr-defined]
self.attn_time3, # type: ignore[attr-defined]
self.attn_time4, # type: ignore[attr-defined]
self.attn_time5, # type: ignore[attr-defined]
)
),
)
+ tr(
"House number/floor? Street/hospital? Town? County? Country?",
", ".join(
answer(x)
for x in (
self.attn_place1, # type: ignore[attr-defined]
self.attn_place2, # type: ignore[attr-defined]
self.attn_place3, # type: ignore[attr-defined]
self.attn_place4, # type: ignore[attr-defined]
self.attn_place5, # type: ignore[attr-defined]
)
),
)
+ tr(
"Repeat: " + lkb,
", ".join(
answer(x)
for x in (
self.attn_repeat_word1, # type: ignore[attr-defined]
self.attn_repeat_word2, # type: ignore[attr-defined]
self.attn_repeat_word3, # type: ignore[attr-defined]
)
),
)
+ tr(
"Repetition: number of trials <i>(not scored)</i>",
answer(
self.attn_num_registration_trials, formatter_answer=italic
),
)
+ tr(
"Serial subtractions: First correct? Second? Third? Fourth? "
"Fifth?",
", ".join(
answer(x)
for x in (
self.attn_serial7_subtraction1, # type: ignore[attr-defined] # noqa: E501
self.attn_serial7_subtraction2, # type: ignore[attr-defined] # noqa: E501
self.attn_serial7_subtraction3, # type: ignore[attr-defined] # noqa: E501
self.attn_serial7_subtraction4, # type: ignore[attr-defined] # noqa: E501
self.attn_serial7_subtraction5, # type: ignore[attr-defined] # noqa: E501
)
),
)
+ subheading_spanning_two_columns("Memory (1)")
+ tr(
"Recall: " + lkb,
", ".join(
answer(x)
for x in (
self.mem_recall_word1, # type: ignore[attr-defined]
self.mem_recall_word2, # type: ignore[attr-defined]
self.mem_recall_word3, # type: ignore[attr-defined]
)
),
)
+ subheading_spanning_two_columns("Fluency")
+ tr(
"Score for words beginning with ‘P’ <i>(≥18 scores 7, 14–17 "
"scores 6, 11–13 scores 5, 8–10 scores 4, 6–7 scores 3, "
"4–5 scores 2, 2–3 scores 1, 0–1 scores 0)</i>",
answer(self.fluency_letters_score) + " / 7",
)
+ tr(
ANIMAL_FLUENCY_SCORING_HTML,
answer(self.fluency_animals_score) + " / 7",
)
+ subheading_spanning_two_columns("Memory (2)")
+ tr(
"Third trial of address registration: " + target_addr,
", ".join(
answer(x)
for x in (
self.mem_repeat_address_trial3_1, # type: ignore[attr-defined] # noqa: E501
self.mem_repeat_address_trial3_2, # type: ignore[attr-defined] # noqa: E501
self.mem_repeat_address_trial3_3, # type: ignore[attr-defined] # noqa: E501
self.mem_repeat_address_trial3_4, # type: ignore[attr-defined] # noqa: E501
self.mem_repeat_address_trial3_5, # type: ignore[attr-defined] # noqa: E501
self.mem_repeat_address_trial3_6, # type: ignore[attr-defined] # noqa: E501
self.mem_repeat_address_trial3_7, # type: ignore[attr-defined] # noqa: E501
)
),
)
+ tr(
"Current PM? First female PM? USA president? USA president "
"assassinated in 1960s?",
", ".join(
answer(x)
for x in (
self.mem_famous1, # type: ignore[attr-defined]
self.mem_famous2, # type: ignore[attr-defined]
self.mem_famous3, # type: ignore[attr-defined]
self.mem_famous4, # type: ignore[attr-defined]
)
),
)
+ subheading_spanning_two_columns("Language")
+ tr(
"<i>Practice trial (“Pick up the pencil and then the "
"paper”)</i>",
answer(
self.lang_follow_command_practice, formatter_answer=italic
),
)
+ tr_qa(
"“Place the paper on top of the pencil”",
self.lang_follow_command1, # type: ignore[attr-defined]
)
+ tr_qa(
"“Pick up the pencil but not the paper”",
self.lang_follow_command2, # type: ignore[attr-defined]
)
+ tr_qa(
"“Pass me the pencil after touching the paper”",
self.lang_follow_command3, # type: ignore[attr-defined]
)
+ tr(
"Sentence-writing: point for 2 complete sentences? "
"Point for correct grammar and spelling?",
", ".join(
answer(x)
for x in (
self.lang_write_sentences_point1, # type: ignore[attr-defined] # noqa: E501
self.lang_write_sentences_point2, # type: ignore[attr-defined] # noqa: E501
)
),
)
+ tr(
"Repeat: caterpillar? eccentricity? unintelligible? "
"statistician? <i>(score 2 if all correct, 1 if 3 correct, "
"0 if ≤2 correct)</i>",
"<i>{}, {}, {}, {}</i> (score <b>{}</b> / 2)".format(
answer(self.lang_repeat_word1, formatter_answer=italic), # type: ignore[attr-defined] # noqa: E501
answer(self.lang_repeat_word2, formatter_answer=italic), # type: ignore[attr-defined] # noqa: E501
answer(self.lang_repeat_word3, formatter_answer=italic), # type: ignore[attr-defined] # noqa: E501
answer(self.lang_repeat_word4, formatter_answer=italic), # type: ignore[attr-defined] # noqa: E501
self.get_repeat_word_score(),
),
)
+ tr_qa(
"Repeat: “All that glitters is not gold”?",
self.lang_repeat_sentence1, # type: ignore[attr-defined]
)
+ tr_qa(
"Repeat: “A stitch in time saves nine”?",
self.lang_repeat_sentence2, # type: ignore[attr-defined]
)
+ tr(
"Name pictures: spoon, book, kangaroo/wallaby",
", ".join(
answer(x)
for x in (
self.lang_name_picture1, # type: ignore[attr-defined]
self.lang_name_picture2, # type: ignore[attr-defined]
self.lang_name_picture3, # type: ignore[attr-defined]
)
),
)
+ tr(
"Name pictures: penguin, anchor, camel/dromedary",
", ".join(
answer(x)
for x in (
self.lang_name_picture4, # type: ignore[attr-defined]
self.lang_name_picture5, # type: ignore[attr-defined]
self.lang_name_picture6, # type: ignore[attr-defined]
)
),
)
+ tr(
"Name pictures: harp, rhinoceros/rhino, barrel/keg/tub",
", ".join(
answer(x)
for x in (
self.lang_name_picture7, # type: ignore[attr-defined]
self.lang_name_picture8, # type: ignore[attr-defined]
self.lang_name_picture9, # type: ignore[attr-defined]
)
),
)
+ tr(
"Name pictures: crown, alligator/crocodile, "
"accordion/piano accordion/squeeze box",
", ".join(
answer(x)
for x in (
self.lang_name_picture10, # type: ignore[attr-defined]
self.lang_name_picture11, # type: ignore[attr-defined]
self.lang_name_picture12, # type: ignore[attr-defined]
)
),
)
+ tr(
"Identify pictures: monarchy? marsupial? Antarctic? nautical?",
", ".join(
answer(x)
for x in (
self.lang_identify_concept1, # type: ignore[attr-defined] # noqa: E501
self.lang_identify_concept2, # type: ignore[attr-defined] # noqa: E501
self.lang_identify_concept3, # type: ignore[attr-defined] # noqa: E501
self.lang_identify_concept4, # type: ignore[attr-defined] # noqa: E501
)
),
)
+ tr_qa(
"Read all successfully: sew, pint, soot, dough, height",
self.lang_read_words_aloud,
)
+ subheading_spanning_two_columns("Visuospatial")
+ tr("Copy infinity", answer(self.vsp_copy_infinity) + " / 1")
+ tr("Copy cube", answer(self.vsp_copy_cube) + " / 2")
+ tr(
"Draw clock with numbers and hands at 5:10",
answer(self.vsp_draw_clock) + " / 5",
)
+ tr(
"Count dots: 8, 10, 7, 9",
", ".join(
answer(x)
for x in (
self.vsp_count_dots1, # type: ignore[attr-defined]
self.vsp_count_dots2, # type: ignore[attr-defined]
self.vsp_count_dots3, # type: ignore[attr-defined]
self.vsp_count_dots4, # type: ignore[attr-defined]
)
),
)
+ tr(
"Identify letters: K, M, A, T",
", ".join(
answer(x)
for x in (
self.vsp_identify_letter1, # type: ignore[attr-defined] # noqa: E501
self.vsp_identify_letter2, # type: ignore[attr-defined] # noqa: E501
self.vsp_identify_letter3, # type: ignore[attr-defined] # noqa: E501
self.vsp_identify_letter4, # type: ignore[attr-defined] # noqa: E501
)
),
)
+ subheading_spanning_two_columns("Memory (3)")
+ tr(
"Recall address: " + target_addr,
", ".join(
answer(x)
for x in (
self.mem_recall_address1, # type: ignore[attr-defined]
self.mem_recall_address2, # type: ignore[attr-defined]
self.mem_recall_address3, # type: ignore[attr-defined]
self.mem_recall_address4, # type: ignore[attr-defined]
self.mem_recall_address5, # type: ignore[attr-defined]
self.mem_recall_address6, # type: ignore[attr-defined]
self.mem_recall_address7, # type: ignore[attr-defined]
)
),
)
+ tr(
"Recognize address: forename and surname?",
self.get_recog_text(
(
self.mem_recall_address1 == 1 # type: ignore[attr-defined] # noqa: E501
and self.mem_recall_address2 == 1 # type: ignore[attr-defined] # noqa: E501
),
self.mem_recognize_address1, # type: ignore[attr-defined]
),
)
+ tr(
"Recognize address: house number?",
self.get_recog_text(
(self.mem_recall_address3 == 1), # type: ignore[attr-defined] # noqa: E501
self.mem_recognize_address2, # type: ignore[attr-defined]
),
)
+ tr(
"Recognize address: street?",
self.get_recog_text(
(
self.mem_recall_address4 == 1 # type: ignore[attr-defined] # noqa: E501
and self.mem_recall_address5 == 1 # type: ignore[attr-defined] # noqa: E501
),
self.mem_recognize_address3, # type: ignore[attr-defined]
),
)
+ tr(
"Recognize address: town?",
self.get_recog_text(
(self.mem_recall_address6 == 1), # type: ignore[attr-defined] # noqa: E501
self.mem_recognize_address4, # type: ignore[attr-defined]
),
)
+ tr(
"Recognize address: county?",
self.get_recog_text(
(self.mem_recall_address7 == 1), # type: ignore[attr-defined] # noqa: E501
self.mem_recognize_address5, # type: ignore[attr-defined]
),
)
+ subheading_spanning_two_columns("Photos of test sheet")
+ tr_span_col(
get_blob_img_html(self.picture1), td_class=CssClass.PHOTO
)
+ tr_span_col(
get_blob_img_html(self.picture2), td_class=CssClass.PHOTO
)
+ f"""
</table>
<div class="{CssClass.FOOTNOTES}">
[1] In the ACE-III, scores ≤82 had sensitivity 0.93 and
specificity 1.0 for dementia, and scores ≤88 had
sensitivity 1.0 and specificity 0.98 for dementia, in a
context of patients with Alzheimer’s disease,
frontotemporal dementia, and controls (Hsieh et al. 2013,
{pmid(23949210)}).
[2] {MINI_ACE_THRESHOLDS}
</div>
<div class="{CssClass.COPYRIGHT}">
{ACE3_COPYRIGHT}
</div>
"""
)
[docs] def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]:
codes = [
SnomedExpression(
req.snomed(SnomedLookup.ACE_R_PROCEDURE_ASSESSMENT)
)
]
# add(SnomedLookup.ACE_R_PROCEDURE_ASSESSMENT_SUBSCALE_ATTENTION_ORIENTATION) # noqa
# add(SnomedLookup.ACE_R_PROCEDURE_ASSESSMENT_SUBSCALE_MEMORY)
# add(SnomedLookup.ACE_R_PROCEDURE_ASSESSMENT_SUBSCALE_FLUENCY)
# add(SnomedLookup.ACE_R_PROCEDURE_ASSESSMENT_SUBSCALE_LANGUAGE)
# add(SnomedLookup.ACE_R_PROCEDURE_ASSESSMENT_SUBSCALE_VISUOSPATIAL)
if self.is_complete(): # could refine: is each subscale complete?
a = self.attn_score()
m = self.mem_score()
f = self.fluency_score()
lang = self.lang_score()
v = self.vsp_score()
t = a + m + f + lang + v
codes.append(
SnomedExpression(
req.snomed(SnomedLookup.ACE_R_SCALE),
{
req.snomed(SnomedLookup.ACE_R_SCORE): t,
req.snomed(
SnomedLookup.ACE_R_SUBSCORE_ATTENTION_ORIENTATION
): a,
req.snomed(SnomedLookup.ACE_R_SUBSCORE_MEMORY): m,
req.snomed(SnomedLookup.ACE_R_SUBSCORE_FLUENCY): f,
req.snomed(SnomedLookup.ACE_R_SUBSCORE_LANGUAGE): lang,
req.snomed(
SnomedLookup.ACE_R_SUBSCORE_VISUOSPATIAL
): v,
},
)
)
# There's no mini-ACE code in SNOMED-CT yet, as of 2022-12-01.
return codes
# =============================================================================
# Mini-ACE
# =============================================================================
[docs]class MiniAce( # type: ignore[misc]
TaskHasPatientMixin,
TaskHasClinicianMixin,
Task,
):
"""
Server implementation of the Mini-ACE task.
"""
__tablename__ = "miniace"
shortname = "Mini-ACE"
extrastring_taskname = "ace3" # shares strings with ACE-III
provides_trackers = True
prohibits_commercial = True
@classmethod
def extend_columns(cls: Type["MiniAce"], **kwargs: Any) -> None:
add_multiple_columns(
cls,
"attn_time",
1,
N_ATTN_TIME_MINIACE, # 4, not 5
pv=PV.BIT,
comment_fmt="Attention, time, {n}/4, {s} (0 or 1)",
comment_strings=["day", "date", "month", "year"], # not season
)
add_multiple_columns(
cls,
"mem_repeat_address_trial1_",
1,
N_MEM_REPEAT_RECALL_ADDR,
pv=PV.BIT,
comment_fmt="Memory, address registration trial 1/3 "
"(not scored), {s} (0 or 1)",
comment_strings=ADDRESS_PARTS,
)
add_multiple_columns(
cls,
"mem_repeat_address_trial2_",
1,
N_MEM_REPEAT_RECALL_ADDR,
pv=PV.BIT,
comment_fmt="Memory, address registration trial 2/3 "
"(not scored), {s} (0 or 1)",
comment_strings=ADDRESS_PARTS,
)
add_multiple_columns(
cls,
"mem_repeat_address_trial3_",
1,
N_MEM_REPEAT_RECALL_ADDR,
pv=PV.BIT,
comment_fmt="Memory, address registration trial 3/3 "
"(scored), {s} (0 or 1)",
comment_strings=ADDRESS_PARTS,
)
add_multiple_columns(
cls,
"mem_recall_address",
1,
N_MEM_REPEAT_RECALL_ADDR,
pv=PV.BIT,
comment_fmt="Memory, recall address {n}/7, {s} (0-1)",
comment_strings=ADDRESS_PARTS,
)
task_edition: Mapped[Optional[str]] = mapped_camcops_column(
String(length=255),
comment="Task edition.",
)
task_address_version: Mapped[Optional[str]] = mapped_camcops_column(
String(length=1),
comment="Task version, determining the address for recall (A/B/C).",
permitted_value_checker=PermittedValueChecker(
permitted_values=["A", "B", "C"]
),
)
remote_administration: Mapped[Optional[bool]] = mapped_camcops_column(
permitted_value_checker=BIT_CHECKER,
comment="Task performed using remote (videoconferencing) "
"administration?",
)
age_at_leaving_full_time_education: Mapped[Optional[int]] = mapped_column(
comment="Age at leaving full time education",
)
occupation: Mapped[Optional[str]] = mapped_column(
"occupation", UnicodeText, comment=OCCUPATION
)
handedness: Mapped[Optional[str]] = mapped_camcops_column(
String(length=1), # was Text
comment="Handedness (L or R)",
permitted_value_checker=PermittedValueChecker(
permitted_values=["L", "R"]
),
)
fluency_animals_score: Mapped[Optional[int]] = mapped_camcops_column(
comment="Fluency, animals, score 0-7",
permitted_value_checker=PermittedValueChecker(minimum=0, maximum=7),
)
vsp_draw_clock: Mapped[Optional[int]] = mapped_camcops_column(
comment="Visuospatial, draw clock (0-5)",
permitted_value_checker=PermittedValueChecker(minimum=0, maximum=5),
)
picture1_blobid: Mapped[Optional[int]] = mapped_camcops_column(
comment="Photo 1/2 PNG BLOB ID",
is_blob_id_field=True,
blob_relationship_attr_name="picture1",
)
picture2_blobid: Mapped[Optional[int]] = mapped_camcops_column(
comment="Photo 2/2 PNG BLOB ID",
is_blob_id_field=True,
blob_relationship_attr_name="picture2",
)
comments: Mapped[Optional[str]] = mapped_column(
"comments", UnicodeText, comment="Clinician's comments"
)
picture1 = blob_relationship( # type: ignore[assignment]
"MiniAce", "picture1_blobid"
) # type: Optional[Blob]
picture2 = blob_relationship( # type: ignore[assignment]
"MiniAce", "picture2_blobid"
) # type: Optional[Blob]
MACE_ATTN_FIELDS = strseq("attn_time", 1, N_ATTN_TIME_MINIACE) # 4 points
MACE_MEMORY_FIELDS = strseq("mem_repeat_address_trial3_", 1, 7) + strseq(
"mem_recall_address", 1, 7
) # 14 points
MACE_FLUENCY_FIELDS = ["fluency_animals_score"] # 7 points
MACE_VSP_FIELDS = ["vsp_draw_clock"] # 5 points
MINI_ACE_FIELDS = (
MACE_ATTN_FIELDS
+ MACE_MEMORY_FIELDS
+ MACE_FLUENCY_FIELDS
+ MACE_VSP_FIELDS
)
[docs] @staticmethod
def longname(req: "CamcopsRequest") -> str:
_ = req.gettext
return _("Mini-Addenbrooke’s Cognitive Examination")
[docs] def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
return [
TrackerInfo(
value=self.mini_ace_score(),
plot_label="Mini-ACE score",
axis_label=f"Mini-ACE score (out of {MINI_ACE_MAX})",
axis_min=-0.5,
axis_max=MINI_ACE_MAX + 0.5,
# Traditional cutoffs: ≤21, ≤25
horizontal_lines=[21.5, 25.5],
),
]
[docs] def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
if not self.is_complete():
return CTV_INCOMPLETE
a = self.attn_score()
m = self.mem_score()
f = self.fluency_score()
v = self.vsp_score()
mini = a + m + f + v
text = (
f"Mini-ACE score: {mini}/{MINI_ACE_MAX} "
f"(attention {a}/{ATTN_MINIACE_MAX}, "
f"memory {m}/{MEM_MINIACE_MAX}, "
f"fluency {f}/{FLUENCY_MINIACE_MAX}, "
f"visuospatial {v}/{VSP_MINIACE_MAX})"
)
return [CtvInfo(content=text)]
[docs] def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
return self.standard_task_summary_fields() + [
SummaryElement(
name="mini_ace",
coltype=Integer(),
value=self.mini_ace_score(),
comment=f"Mini-ACE (/{MINI_ACE_MAX})",
),
]
def attn_score(self) -> int:
return cast(int, self.sum_fields(self.MACE_ATTN_FIELDS))
def mem_score(self) -> int:
return cast(int, self.sum_fields(self.MACE_MEMORY_FIELDS))
def fluency_score(self) -> int:
return cast(int, self.sum_fields(self.MACE_FLUENCY_FIELDS))
def vsp_score(self) -> int:
return cast(int, self.sum_fields(self.MACE_VSP_FIELDS))
def mini_ace_score(self) -> int:
return cast(int, self.sum_fields(self.MINI_ACE_FIELDS))
[docs] def is_complete(self) -> bool:
return (
self.all_fields_not_none(self.MINI_ACE_FIELDS)
and self.field_contents_valid()
)
[docs] def get_task_html(self, req: CamcopsRequest) -> str:
a = self.attn_score()
m = self.mem_score()
f = self.fluency_score()
v = self.vsp_score()
mini = a + m + f + v
target_addr = qsequence(
Ace3.get_target_address_parts(req, self.task_address_version)
)
if self.is_complete():
figsize = (
PlotDefaults.FULLWIDTH_PLOT_WIDTH / 3,
PlotDefaults.FULLWIDTH_PLOT_WIDTH / 4,
)
width = 0.9
fig = req.create_figure(figsize=figsize)
ax = fig.add_subplot(1, 1, 1)
scores = numpy.array([a, m, f, v])
maxima = numpy.array(
[
ATTN_MINIACE_MAX,
MEM_MINIACE_MAX,
FLUENCY_MINIACE_MAX,
VSP_MINIACE_MAX,
]
)
y = 100 * scores / maxima
x_labels = ["Attn", "Mem", "Flu", "VSp"]
# noinspection PyTypeChecker
n = len(y)
xvar = numpy.arange(n)
ax.bar(xvar, y, width, color="g")
ax.set_ylabel("%", fontdict=req.fontdict)
ax.set_xticks(xvar)
x_offset = -0.5
ax.set_xlim(0 + x_offset, len(scores) + x_offset)
ax.set_xticklabels(x_labels, fontdict=req.fontdict)
fig.tight_layout() # or the ylabel drops off the figure
# fig.autofmt_xdate()
req.set_figure_font_sizes(ax)
figurehtml = req.get_html_from_pyplot_figure(fig)
else:
figurehtml = "<i>Incomplete; not plotted</i>"
return (
self.get_standard_clinician_comments_block(req, self.comments)
+ f"""
<div class="{CssClass.SUMMARY}">
<table class="{CssClass.SUMMARY}">
<tr>
{self.get_is_complete_td_pair(req)}
<td class="{CssClass.FIGURE}"
rowspan="6">{figurehtml}</td>
</tr>
"""
+ tr_score_with_pct(
"Mini-ACE score <sup>[1]</sup>", mini, MINI_ACE_MAX
)
+ tr_score_with_pct("Attention", a, ATTN_MINIACE_MAX)
+ tr_score_with_pct("Memory", m, MEM_MINIACE_MAX)
+ tr_score_with_pct("Fluency", f, FLUENCY_MINIACE_MAX)
+ tr_score_with_pct("Visuospatial", v, VSP_MINIACE_MAX)
+ f"""
</table>
</div>
<table class="{CssClass.TASKCONFIG}">
"""
+ tr_heading("Task aspect", "Setting")
+ tr_qa("Edition", self.task_edition)
+ tr_qa("Version", self.task_address_version)
+ tr_qa(
"Remote administration?",
get_yes_no_none(req, self.remote_administration),
)
+ f"""
<table class="{CssClass.TASKDETAIL}">
"""
+ tr_heading("Question", "Answer/score")
+ tr_qa(
AGE_FTE,
self.age_at_leaving_full_time_education,
)
+ tr_qa(OCCUPATION, ws.webify(self.occupation))
+ tr_qa(HANDEDNESS, ws.webify(self.handedness))
+ subheading_spanning_two_columns("Attention")
+ tr(
"Day? Date? Month? Year?", # not season
", ".join(
answer(x)
for x in (
self.attn_time1, # type: ignore[attr-defined]
self.attn_time2, # type: ignore[attr-defined]
self.attn_time3, # type: ignore[attr-defined]
self.attn_time4, # type: ignore[attr-defined]
)
),
)
+ subheading_spanning_two_columns("Memory")
+ tr(
"Third trial of address registration: " + target_addr,
", ".join(
answer(x)
for x in (
self.mem_repeat_address_trial3_1, # type: ignore[attr-defined] # noqa: E501
self.mem_repeat_address_trial3_2, # type: ignore[attr-defined] # noqa: E501
self.mem_repeat_address_trial3_3, # type: ignore[attr-defined] # noqa: E501
self.mem_repeat_address_trial3_4, # type: ignore[attr-defined] # noqa: E501
self.mem_repeat_address_trial3_5, # type: ignore[attr-defined] # noqa: E501
self.mem_repeat_address_trial3_6, # type: ignore[attr-defined] # noqa: E501
self.mem_repeat_address_trial3_7, # type: ignore[attr-defined] # noqa: E501
)
),
)
+ subheading_spanning_two_columns("Fluency – animals")
+ tr(
ANIMAL_FLUENCY_SCORING_HTML,
answer(self.fluency_animals_score) + " / 7",
)
+ subheading_spanning_two_columns("Clock drawing")
+ tr(
"Draw clock with numbers and hands at 5:10",
answer(self.vsp_draw_clock) + " / 5",
)
+ subheading_spanning_two_columns("Memory recall")
+ tr(
"Recall address: " + target_addr,
", ".join(
answer(x)
for x in (
self.mem_recall_address1, # type: ignore[attr-defined]
self.mem_recall_address2, # type: ignore[attr-defined]
self.mem_recall_address3, # type: ignore[attr-defined]
self.mem_recall_address4, # type: ignore[attr-defined]
self.mem_recall_address5, # type: ignore[attr-defined]
self.mem_recall_address6, # type: ignore[attr-defined]
self.mem_recall_address7, # type: ignore[attr-defined]
)
),
)
+ subheading_spanning_two_columns("Photos of test sheet")
+ tr_span_col(
get_blob_img_html(self.picture1), td_class=CssClass.PHOTO
)
+ tr_span_col(
get_blob_img_html(self.picture2), td_class=CssClass.PHOTO
)
+ f"""
</table>
<div class="{CssClass.FOOTNOTES}">
[1] {MINI_ACE_THRESHOLDS}
</div>
<div class="{CssClass.COPYRIGHT}">
{ACE3_COPYRIGHT}
</div>
"""
)