bead 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bead/__init__.py +11 -0
- bead/__main__.py +11 -0
- bead/active_learning/__init__.py +15 -0
- bead/active_learning/config.py +231 -0
- bead/active_learning/loop.py +566 -0
- bead/active_learning/models/__init__.py +24 -0
- bead/active_learning/models/base.py +852 -0
- bead/active_learning/models/binary.py +910 -0
- bead/active_learning/models/categorical.py +943 -0
- bead/active_learning/models/cloze.py +862 -0
- bead/active_learning/models/forced_choice.py +956 -0
- bead/active_learning/models/free_text.py +773 -0
- bead/active_learning/models/lora.py +365 -0
- bead/active_learning/models/magnitude.py +835 -0
- bead/active_learning/models/multi_select.py +795 -0
- bead/active_learning/models/ordinal_scale.py +811 -0
- bead/active_learning/models/peft_adapter.py +155 -0
- bead/active_learning/models/random_effects.py +639 -0
- bead/active_learning/selection.py +354 -0
- bead/active_learning/strategies.py +391 -0
- bead/active_learning/trainers/__init__.py +26 -0
- bead/active_learning/trainers/base.py +210 -0
- bead/active_learning/trainers/data_collator.py +172 -0
- bead/active_learning/trainers/dataset_utils.py +261 -0
- bead/active_learning/trainers/huggingface.py +304 -0
- bead/active_learning/trainers/lightning.py +324 -0
- bead/active_learning/trainers/metrics.py +424 -0
- bead/active_learning/trainers/mixed_effects.py +551 -0
- bead/active_learning/trainers/model_wrapper.py +509 -0
- bead/active_learning/trainers/registry.py +104 -0
- bead/adapters/__init__.py +11 -0
- bead/adapters/huggingface.py +61 -0
- bead/behavioral/__init__.py +116 -0
- bead/behavioral/analytics.py +646 -0
- bead/behavioral/extraction.py +343 -0
- bead/behavioral/merging.py +343 -0
- bead/cli/__init__.py +11 -0
- bead/cli/active_learning.py +513 -0
- bead/cli/active_learning_commands.py +779 -0
- bead/cli/completion.py +359 -0
- bead/cli/config.py +624 -0
- bead/cli/constraint_builders.py +286 -0
- bead/cli/deployment.py +859 -0
- bead/cli/deployment_trials.py +493 -0
- bead/cli/deployment_ui.py +332 -0
- bead/cli/display.py +378 -0
- bead/cli/items.py +960 -0
- bead/cli/items_factories.py +776 -0
- bead/cli/list_constraints.py +714 -0
- bead/cli/lists.py +490 -0
- bead/cli/main.py +430 -0
- bead/cli/models.py +877 -0
- bead/cli/resource_loaders.py +621 -0
- bead/cli/resources.py +1036 -0
- bead/cli/shell.py +356 -0
- bead/cli/simulate.py +840 -0
- bead/cli/templates.py +1158 -0
- bead/cli/training.py +1080 -0
- bead/cli/utils.py +614 -0
- bead/cli/workflow.py +1273 -0
- bead/config/__init__.py +68 -0
- bead/config/active_learning.py +1009 -0
- bead/config/config.py +192 -0
- bead/config/defaults.py +118 -0
- bead/config/deployment.py +217 -0
- bead/config/env.py +147 -0
- bead/config/item.py +45 -0
- bead/config/list.py +193 -0
- bead/config/loader.py +149 -0
- bead/config/logging.py +42 -0
- bead/config/model.py +49 -0
- bead/config/paths.py +46 -0
- bead/config/profiles.py +320 -0
- bead/config/resources.py +47 -0
- bead/config/serialization.py +210 -0
- bead/config/simulation.py +206 -0
- bead/config/template.py +238 -0
- bead/config/validation.py +267 -0
- bead/data/__init__.py +65 -0
- bead/data/base.py +87 -0
- bead/data/identifiers.py +97 -0
- bead/data/language_codes.py +61 -0
- bead/data/metadata.py +270 -0
- bead/data/range.py +123 -0
- bead/data/repository.py +358 -0
- bead/data/serialization.py +249 -0
- bead/data/timestamps.py +89 -0
- bead/data/validation.py +349 -0
- bead/data_collection/__init__.py +11 -0
- bead/data_collection/jatos.py +223 -0
- bead/data_collection/merger.py +154 -0
- bead/data_collection/prolific.py +198 -0
- bead/deployment/__init__.py +5 -0
- bead/deployment/distribution.py +402 -0
- bead/deployment/jatos/__init__.py +1 -0
- bead/deployment/jatos/api.py +200 -0
- bead/deployment/jatos/exporter.py +210 -0
- bead/deployment/jspsych/__init__.py +9 -0
- bead/deployment/jspsych/biome.json +44 -0
- bead/deployment/jspsych/config.py +411 -0
- bead/deployment/jspsych/generator.py +598 -0
- bead/deployment/jspsych/package.json +51 -0
- bead/deployment/jspsych/pnpm-lock.yaml +2141 -0
- bead/deployment/jspsych/randomizer.py +299 -0
- bead/deployment/jspsych/src/lib/list-distributor.test.ts +327 -0
- bead/deployment/jspsych/src/lib/list-distributor.ts +1282 -0
- bead/deployment/jspsych/src/lib/randomizer.test.ts +232 -0
- bead/deployment/jspsych/src/lib/randomizer.ts +367 -0
- bead/deployment/jspsych/src/plugins/cloze-dropdown.ts +252 -0
- bead/deployment/jspsych/src/plugins/forced-choice.ts +265 -0
- bead/deployment/jspsych/src/plugins/plugins.test.ts +141 -0
- bead/deployment/jspsych/src/plugins/rating.ts +248 -0
- bead/deployment/jspsych/src/slopit/index.ts +9 -0
- bead/deployment/jspsych/src/types/jatos.d.ts +256 -0
- bead/deployment/jspsych/src/types/jspsych.d.ts +228 -0
- bead/deployment/jspsych/templates/experiment.css +1 -0
- bead/deployment/jspsych/templates/experiment.js.template +289 -0
- bead/deployment/jspsych/templates/index.html +51 -0
- bead/deployment/jspsych/templates/randomizer.js +241 -0
- bead/deployment/jspsych/templates/randomizer.js.template +313 -0
- bead/deployment/jspsych/trials.py +723 -0
- bead/deployment/jspsych/tsconfig.json +23 -0
- bead/deployment/jspsych/tsup.config.ts +30 -0
- bead/deployment/jspsych/ui/__init__.py +1 -0
- bead/deployment/jspsych/ui/components.py +383 -0
- bead/deployment/jspsych/ui/styles.py +411 -0
- bead/dsl/__init__.py +80 -0
- bead/dsl/ast.py +168 -0
- bead/dsl/context.py +178 -0
- bead/dsl/errors.py +71 -0
- bead/dsl/evaluator.py +570 -0
- bead/dsl/grammar.lark +81 -0
- bead/dsl/parser.py +231 -0
- bead/dsl/stdlib.py +929 -0
- bead/evaluation/__init__.py +13 -0
- bead/evaluation/convergence.py +485 -0
- bead/evaluation/interannotator.py +398 -0
- bead/items/__init__.py +40 -0
- bead/items/adapters/__init__.py +70 -0
- bead/items/adapters/anthropic.py +224 -0
- bead/items/adapters/api_utils.py +167 -0
- bead/items/adapters/base.py +216 -0
- bead/items/adapters/google.py +259 -0
- bead/items/adapters/huggingface.py +1074 -0
- bead/items/adapters/openai.py +323 -0
- bead/items/adapters/registry.py +202 -0
- bead/items/adapters/sentence_transformers.py +224 -0
- bead/items/adapters/togetherai.py +309 -0
- bead/items/binary.py +515 -0
- bead/items/cache.py +558 -0
- bead/items/categorical.py +593 -0
- bead/items/cloze.py +757 -0
- bead/items/constructor.py +784 -0
- bead/items/forced_choice.py +413 -0
- bead/items/free_text.py +681 -0
- bead/items/generation.py +432 -0
- bead/items/item.py +396 -0
- bead/items/item_template.py +787 -0
- bead/items/magnitude.py +573 -0
- bead/items/multi_select.py +621 -0
- bead/items/ordinal_scale.py +569 -0
- bead/items/scoring.py +448 -0
- bead/items/validation.py +723 -0
- bead/lists/__init__.py +30 -0
- bead/lists/balancer.py +263 -0
- bead/lists/constraints.py +1067 -0
- bead/lists/experiment_list.py +286 -0
- bead/lists/list_collection.py +378 -0
- bead/lists/partitioner.py +1141 -0
- bead/lists/stratification.py +254 -0
- bead/participants/__init__.py +73 -0
- bead/participants/collection.py +699 -0
- bead/participants/merging.py +312 -0
- bead/participants/metadata_spec.py +491 -0
- bead/participants/models.py +276 -0
- bead/resources/__init__.py +29 -0
- bead/resources/adapters/__init__.py +19 -0
- bead/resources/adapters/base.py +104 -0
- bead/resources/adapters/cache.py +128 -0
- bead/resources/adapters/glazing.py +508 -0
- bead/resources/adapters/registry.py +117 -0
- bead/resources/adapters/unimorph.py +796 -0
- bead/resources/classification.py +856 -0
- bead/resources/constraint_builders.py +329 -0
- bead/resources/constraints.py +165 -0
- bead/resources/lexical_item.py +223 -0
- bead/resources/lexicon.py +744 -0
- bead/resources/loaders.py +209 -0
- bead/resources/template.py +441 -0
- bead/resources/template_collection.py +707 -0
- bead/resources/template_generation.py +349 -0
- bead/simulation/__init__.py +29 -0
- bead/simulation/annotators/__init__.py +15 -0
- bead/simulation/annotators/base.py +175 -0
- bead/simulation/annotators/distance_based.py +135 -0
- bead/simulation/annotators/lm_based.py +114 -0
- bead/simulation/annotators/oracle.py +182 -0
- bead/simulation/annotators/random.py +181 -0
- bead/simulation/dsl_extension/__init__.py +3 -0
- bead/simulation/noise_models/__init__.py +13 -0
- bead/simulation/noise_models/base.py +42 -0
- bead/simulation/noise_models/random_noise.py +82 -0
- bead/simulation/noise_models/systematic.py +132 -0
- bead/simulation/noise_models/temperature.py +86 -0
- bead/simulation/runner.py +144 -0
- bead/simulation/strategies/__init__.py +23 -0
- bead/simulation/strategies/base.py +123 -0
- bead/simulation/strategies/binary.py +103 -0
- bead/simulation/strategies/categorical.py +123 -0
- bead/simulation/strategies/cloze.py +224 -0
- bead/simulation/strategies/forced_choice.py +127 -0
- bead/simulation/strategies/free_text.py +105 -0
- bead/simulation/strategies/magnitude.py +116 -0
- bead/simulation/strategies/multi_select.py +129 -0
- bead/simulation/strategies/ordinal_scale.py +131 -0
- bead/templates/__init__.py +27 -0
- bead/templates/adapters/__init__.py +17 -0
- bead/templates/adapters/base.py +128 -0
- bead/templates/adapters/cache.py +178 -0
- bead/templates/adapters/huggingface.py +312 -0
- bead/templates/combinatorics.py +103 -0
- bead/templates/filler.py +605 -0
- bead/templates/renderers.py +177 -0
- bead/templates/resolver.py +178 -0
- bead/templates/strategies.py +1806 -0
- bead/templates/streaming.py +195 -0
- bead-0.1.0.dist-info/METADATA +212 -0
- bead-0.1.0.dist-info/RECORD +231 -0
- bead-0.1.0.dist-info/WHEEL +4 -0
- bead-0.1.0.dist-info/entry_points.txt +2 -0
- bead-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,723 @@
|
|
|
1
|
+
"""Trial generators for jsPsych experiments.
|
|
2
|
+
|
|
3
|
+
This module provides functions to generate jsPsych trial objects from
|
|
4
|
+
Item models. It supports various trial types including rating scales,
|
|
5
|
+
forced choice, and binary choice trials.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from bead.data.base import JsonValue
|
|
11
|
+
from bead.deployment.jspsych.config import (
|
|
12
|
+
ChoiceConfig,
|
|
13
|
+
DemographicsConfig,
|
|
14
|
+
DemographicsFieldConfig,
|
|
15
|
+
ExperimentConfig,
|
|
16
|
+
InstructionsConfig,
|
|
17
|
+
RatingScaleConfig,
|
|
18
|
+
)
|
|
19
|
+
from bead.items.item import Item
|
|
20
|
+
from bead.items.item_template import ItemTemplate
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _serialize_item_metadata(
|
|
24
|
+
item: Item, template: ItemTemplate
|
|
25
|
+
) -> dict[str, JsonValue]:
|
|
26
|
+
"""Serialize complete item and template metadata for trial data.
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
item : Item
|
|
31
|
+
The item to serialize metadata from.
|
|
32
|
+
template : ItemTemplate
|
|
33
|
+
The item template to serialize metadata from.
|
|
34
|
+
|
|
35
|
+
Returns
|
|
36
|
+
-------
|
|
37
|
+
dict[str, JsonValue]
|
|
38
|
+
Metadata dictionary containing all item and template fields.
|
|
39
|
+
"""
|
|
40
|
+
return {
|
|
41
|
+
# Item identification
|
|
42
|
+
"item_id": str(item.id),
|
|
43
|
+
"item_created": item.created_at.isoformat(),
|
|
44
|
+
"item_modified": item.modified_at.isoformat(),
|
|
45
|
+
# Item template reference
|
|
46
|
+
"item_template_id": str(item.item_template_id),
|
|
47
|
+
# Filled template references
|
|
48
|
+
"filled_template_refs": [str(ref) for ref in item.filled_template_refs],
|
|
49
|
+
# Options (for forced_choice/multi_select)
|
|
50
|
+
"options": list(item.options),
|
|
51
|
+
# Rendered elements
|
|
52
|
+
"rendered_elements": dict(item.rendered_elements),
|
|
53
|
+
# Unfilled slots (for cloze tasks)
|
|
54
|
+
"unfilled_slots": [
|
|
55
|
+
{
|
|
56
|
+
"slot_name": slot.slot_name,
|
|
57
|
+
"position": slot.position,
|
|
58
|
+
"constraint_ids": [str(cid) for cid in slot.constraint_ids],
|
|
59
|
+
}
|
|
60
|
+
for slot in item.unfilled_slots
|
|
61
|
+
],
|
|
62
|
+
# Model outputs
|
|
63
|
+
"model_outputs": [
|
|
64
|
+
{
|
|
65
|
+
"model_name": output.model_name,
|
|
66
|
+
"model_version": output.model_version,
|
|
67
|
+
"operation": output.operation,
|
|
68
|
+
"inputs": output.inputs,
|
|
69
|
+
"output": output.output,
|
|
70
|
+
"cache_key": output.cache_key,
|
|
71
|
+
"computation_metadata": output.computation_metadata,
|
|
72
|
+
}
|
|
73
|
+
for output in item.model_outputs
|
|
74
|
+
],
|
|
75
|
+
# Constraint satisfaction
|
|
76
|
+
"constraint_satisfaction": {
|
|
77
|
+
str(k): v for k, v in item.constraint_satisfaction.items()
|
|
78
|
+
},
|
|
79
|
+
# Item-specific metadata
|
|
80
|
+
"item_metadata": dict(item.item_metadata),
|
|
81
|
+
# Template information
|
|
82
|
+
"template_name": template.name,
|
|
83
|
+
"template_description": template.description,
|
|
84
|
+
"judgment_type": template.judgment_type,
|
|
85
|
+
"task_type": template.task_type,
|
|
86
|
+
# Template elements
|
|
87
|
+
"template_elements": [
|
|
88
|
+
{
|
|
89
|
+
"element_type": elem.element_type,
|
|
90
|
+
"element_name": elem.element_name,
|
|
91
|
+
"content": elem.content,
|
|
92
|
+
"filled_template_ref_id": (
|
|
93
|
+
str(elem.filled_template_ref_id)
|
|
94
|
+
if elem.filled_template_ref_id
|
|
95
|
+
else None
|
|
96
|
+
),
|
|
97
|
+
"element_metadata": elem.element_metadata,
|
|
98
|
+
"order": elem.order,
|
|
99
|
+
}
|
|
100
|
+
for elem in template.elements
|
|
101
|
+
],
|
|
102
|
+
# Template constraints
|
|
103
|
+
"template_constraints": [str(c) for c in template.constraints],
|
|
104
|
+
# Task specification
|
|
105
|
+
"task_spec": {
|
|
106
|
+
"prompt": template.task_spec.prompt,
|
|
107
|
+
"scale_bounds": template.task_spec.scale_bounds,
|
|
108
|
+
"scale_labels": template.task_spec.scale_labels,
|
|
109
|
+
"options": template.task_spec.options,
|
|
110
|
+
"min_selections": template.task_spec.min_selections,
|
|
111
|
+
"max_selections": template.task_spec.max_selections,
|
|
112
|
+
"text_validation_pattern": template.task_spec.text_validation_pattern,
|
|
113
|
+
"max_length": template.task_spec.max_length,
|
|
114
|
+
},
|
|
115
|
+
# Presentation specification
|
|
116
|
+
"presentation_spec": {
|
|
117
|
+
"mode": template.presentation_spec.mode,
|
|
118
|
+
"chunking": (
|
|
119
|
+
{
|
|
120
|
+
"unit": template.presentation_spec.chunking.unit,
|
|
121
|
+
"parse_type": (template.presentation_spec.chunking.parse_type),
|
|
122
|
+
"constituent_labels": (
|
|
123
|
+
template.presentation_spec.chunking.constituent_labels
|
|
124
|
+
),
|
|
125
|
+
"parser": template.presentation_spec.chunking.parser,
|
|
126
|
+
"parse_language": (
|
|
127
|
+
template.presentation_spec.chunking.parse_language
|
|
128
|
+
),
|
|
129
|
+
"custom_boundaries": (
|
|
130
|
+
template.presentation_spec.chunking.custom_boundaries
|
|
131
|
+
),
|
|
132
|
+
}
|
|
133
|
+
if template.presentation_spec.chunking
|
|
134
|
+
else None
|
|
135
|
+
),
|
|
136
|
+
"timing": (
|
|
137
|
+
{
|
|
138
|
+
"duration_ms": template.presentation_spec.timing.duration_ms,
|
|
139
|
+
"isi_ms": template.presentation_spec.timing.isi_ms,
|
|
140
|
+
"timeout_ms": template.presentation_spec.timing.timeout_ms,
|
|
141
|
+
"mask_char": template.presentation_spec.timing.mask_char,
|
|
142
|
+
"cumulative": template.presentation_spec.timing.cumulative,
|
|
143
|
+
}
|
|
144
|
+
if template.presentation_spec.timing
|
|
145
|
+
else None
|
|
146
|
+
),
|
|
147
|
+
"display_format": template.presentation_spec.display_format,
|
|
148
|
+
},
|
|
149
|
+
# Presentation order
|
|
150
|
+
"presentation_order": template.presentation_order,
|
|
151
|
+
# Template metadata
|
|
152
|
+
"template_metadata": dict(template.template_metadata),
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def create_trial(
|
|
157
|
+
item: Item,
|
|
158
|
+
template: ItemTemplate,
|
|
159
|
+
experiment_config: ExperimentConfig,
|
|
160
|
+
trial_number: int,
|
|
161
|
+
rating_config: RatingScaleConfig | None = None,
|
|
162
|
+
choice_config: ChoiceConfig | None = None,
|
|
163
|
+
) -> dict[str, JsonValue]:
|
|
164
|
+
"""Create a jsPsych trial object from an Item.
|
|
165
|
+
|
|
166
|
+
Parameters
|
|
167
|
+
----------
|
|
168
|
+
item : Item
|
|
169
|
+
The item to create a trial from.
|
|
170
|
+
template : ItemTemplate
|
|
171
|
+
The item template for this item.
|
|
172
|
+
experiment_config : ExperimentConfig
|
|
173
|
+
The experiment configuration.
|
|
174
|
+
trial_number : int
|
|
175
|
+
The trial number (for tracking).
|
|
176
|
+
rating_config : RatingScaleConfig | None
|
|
177
|
+
Configuration for rating scale trials (required for rating types).
|
|
178
|
+
choice_config : ChoiceConfig | None
|
|
179
|
+
Configuration for choice trials (required for choice types).
|
|
180
|
+
|
|
181
|
+
Returns
|
|
182
|
+
-------
|
|
183
|
+
dict[str, JsonValue]
|
|
184
|
+
A jsPsych trial object with item and template metadata.
|
|
185
|
+
|
|
186
|
+
Raises
|
|
187
|
+
------
|
|
188
|
+
ValueError
|
|
189
|
+
If required configuration is missing for the experiment type.
|
|
190
|
+
|
|
191
|
+
Examples
|
|
192
|
+
--------
|
|
193
|
+
>>> from uuid import UUID
|
|
194
|
+
>>> from bead.items.item_template import TaskSpec, PresentationSpec
|
|
195
|
+
>>> item = Item(
|
|
196
|
+
... item_template_id=UUID("12345678-1234-5678-1234-567812345678"),
|
|
197
|
+
... rendered_elements={"sentence": "The cat broke the vase"}
|
|
198
|
+
... )
|
|
199
|
+
>>> template = ItemTemplate(
|
|
200
|
+
... name="test",
|
|
201
|
+
... judgment_type="acceptability",
|
|
202
|
+
... task_type="ordinal_scale",
|
|
203
|
+
... task_spec=TaskSpec(prompt="Rate this"),
|
|
204
|
+
... presentation_spec=PresentationSpec(mode="static")
|
|
205
|
+
... )
|
|
206
|
+
>>> config = ExperimentConfig(
|
|
207
|
+
... experiment_type="likert_rating",
|
|
208
|
+
... title="Test",
|
|
209
|
+
... description="Test",
|
|
210
|
+
... instructions="Test"
|
|
211
|
+
... )
|
|
212
|
+
>>> rating_config = RatingScaleConfig()
|
|
213
|
+
>>> trial = create_trial(item, template, config, 0, rating_config=rating_config)
|
|
214
|
+
>>> trial["type"]
|
|
215
|
+
'html-slider-response'
|
|
216
|
+
"""
|
|
217
|
+
if experiment_config.experiment_type == "likert_rating":
|
|
218
|
+
if rating_config is None:
|
|
219
|
+
raise ValueError("rating_config required for likert_rating experiments")
|
|
220
|
+
return _create_likert_trial(item, template, rating_config, trial_number)
|
|
221
|
+
elif experiment_config.experiment_type == "slider_rating":
|
|
222
|
+
if rating_config is None:
|
|
223
|
+
raise ValueError("rating_config required for slider_rating experiments")
|
|
224
|
+
return _create_slider_trial(item, template, rating_config, trial_number)
|
|
225
|
+
elif experiment_config.experiment_type == "binary_choice":
|
|
226
|
+
if choice_config is None:
|
|
227
|
+
raise ValueError("choice_config required for binary_choice experiments")
|
|
228
|
+
return _create_binary_choice_trial(item, template, choice_config, trial_number)
|
|
229
|
+
elif experiment_config.experiment_type == "forced_choice":
|
|
230
|
+
if choice_config is None:
|
|
231
|
+
raise ValueError("choice_config required for forced_choice experiments")
|
|
232
|
+
return _create_forced_choice_trial(item, template, choice_config, trial_number)
|
|
233
|
+
else:
|
|
234
|
+
raise ValueError(
|
|
235
|
+
f"Unknown experiment type: {experiment_config.experiment_type}"
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _create_likert_trial(
|
|
240
|
+
item: Item,
|
|
241
|
+
template: ItemTemplate,
|
|
242
|
+
config: RatingScaleConfig,
|
|
243
|
+
trial_number: int,
|
|
244
|
+
) -> dict[str, JsonValue]:
|
|
245
|
+
"""Create a Likert rating trial.
|
|
246
|
+
|
|
247
|
+
Parameters
|
|
248
|
+
----------
|
|
249
|
+
item : Item
|
|
250
|
+
The item to create a trial from.
|
|
251
|
+
template : ItemTemplate
|
|
252
|
+
The item template.
|
|
253
|
+
config : RatingScaleConfig
|
|
254
|
+
Rating scale configuration.
|
|
255
|
+
trial_number : int
|
|
256
|
+
The trial number.
|
|
257
|
+
|
|
258
|
+
Returns
|
|
259
|
+
-------
|
|
260
|
+
dict[str, JsonValue]
|
|
261
|
+
A jsPsych html-button-response trial object.
|
|
262
|
+
"""
|
|
263
|
+
# Generate stimulus HTML from rendered elements
|
|
264
|
+
stimulus_html = _generate_stimulus_html(item)
|
|
265
|
+
|
|
266
|
+
# Generate button labels for Likert scale
|
|
267
|
+
labels: list[str] = []
|
|
268
|
+
for i in range(config.scale.min, config.scale.max + 1, config.step):
|
|
269
|
+
if config.show_numeric_labels:
|
|
270
|
+
labels.append(str(i))
|
|
271
|
+
else:
|
|
272
|
+
labels.append("")
|
|
273
|
+
|
|
274
|
+
prompt_html = (
|
|
275
|
+
f'<p style="margin-top: 20px;">'
|
|
276
|
+
f'<span style="float: left;">{config.min_label}</span>'
|
|
277
|
+
f'<span style="float: right;">{config.max_label}</span>'
|
|
278
|
+
f"</p>"
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
# Serialize complete metadata
|
|
282
|
+
metadata = _serialize_item_metadata(item, template)
|
|
283
|
+
metadata["trial_number"] = trial_number
|
|
284
|
+
metadata["trial_type"] = "likert_rating"
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
"type": "html-button-response",
|
|
288
|
+
"stimulus": stimulus_html,
|
|
289
|
+
"choices": labels,
|
|
290
|
+
"prompt": prompt_html,
|
|
291
|
+
"data": metadata,
|
|
292
|
+
"button_html": '<button class="jspsych-btn likert-button">%choice%</button>',
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _create_slider_trial(
|
|
297
|
+
item: Item,
|
|
298
|
+
template: ItemTemplate,
|
|
299
|
+
config: RatingScaleConfig,
|
|
300
|
+
trial_number: int,
|
|
301
|
+
) -> dict[str, JsonValue]:
|
|
302
|
+
"""Create a slider rating trial.
|
|
303
|
+
|
|
304
|
+
Parameters
|
|
305
|
+
----------
|
|
306
|
+
item : Item
|
|
307
|
+
The item to create a trial from.
|
|
308
|
+
template : ItemTemplate
|
|
309
|
+
The item template.
|
|
310
|
+
config : RatingScaleConfig
|
|
311
|
+
Rating scale configuration.
|
|
312
|
+
trial_number : int
|
|
313
|
+
The trial number.
|
|
314
|
+
|
|
315
|
+
Returns
|
|
316
|
+
-------
|
|
317
|
+
dict[str, JsonValue]
|
|
318
|
+
A jsPsych html-slider-response trial object.
|
|
319
|
+
"""
|
|
320
|
+
stimulus_html = _generate_stimulus_html(item)
|
|
321
|
+
|
|
322
|
+
# Serialize complete metadata
|
|
323
|
+
metadata = _serialize_item_metadata(item, template)
|
|
324
|
+
metadata["trial_number"] = trial_number
|
|
325
|
+
metadata["trial_type"] = "slider_rating"
|
|
326
|
+
|
|
327
|
+
return {
|
|
328
|
+
"type": "html-slider-response",
|
|
329
|
+
"stimulus": stimulus_html,
|
|
330
|
+
"labels": [config.min_label, config.max_label],
|
|
331
|
+
"min": config.scale.min,
|
|
332
|
+
"max": config.scale.max,
|
|
333
|
+
"step": config.step,
|
|
334
|
+
"slider_start": (config.scale.min + config.scale.max) // 2,
|
|
335
|
+
"require_movement": config.required,
|
|
336
|
+
"data": metadata,
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _create_binary_choice_trial(
|
|
341
|
+
item: Item,
|
|
342
|
+
template: ItemTemplate,
|
|
343
|
+
config: ChoiceConfig,
|
|
344
|
+
trial_number: int,
|
|
345
|
+
) -> dict[str, JsonValue]:
|
|
346
|
+
"""Create a binary choice trial.
|
|
347
|
+
|
|
348
|
+
Parameters
|
|
349
|
+
----------
|
|
350
|
+
item : Item
|
|
351
|
+
The item to create a trial from.
|
|
352
|
+
template : ItemTemplate
|
|
353
|
+
The item template.
|
|
354
|
+
config : ChoiceConfig
|
|
355
|
+
Choice configuration.
|
|
356
|
+
trial_number : int
|
|
357
|
+
The trial number.
|
|
358
|
+
|
|
359
|
+
Returns
|
|
360
|
+
-------
|
|
361
|
+
dict[str, JsonValue]
|
|
362
|
+
A jsPsych html-button-response trial object.
|
|
363
|
+
"""
|
|
364
|
+
stimulus_html = _generate_stimulus_html(item)
|
|
365
|
+
|
|
366
|
+
# Serialize complete metadata
|
|
367
|
+
metadata = _serialize_item_metadata(item, template)
|
|
368
|
+
metadata["trial_number"] = trial_number
|
|
369
|
+
metadata["trial_type"] = "binary_choice"
|
|
370
|
+
|
|
371
|
+
return {
|
|
372
|
+
"type": "html-button-response",
|
|
373
|
+
"stimulus": stimulus_html,
|
|
374
|
+
"choices": ["Yes", "No"],
|
|
375
|
+
"data": metadata,
|
|
376
|
+
"button_html": config.button_html
|
|
377
|
+
or '<button class="jspsych-btn">%choice%</button>',
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _create_forced_choice_trial(
|
|
382
|
+
item: Item,
|
|
383
|
+
template: ItemTemplate,
|
|
384
|
+
config: ChoiceConfig,
|
|
385
|
+
trial_number: int,
|
|
386
|
+
) -> dict[str, JsonValue]:
|
|
387
|
+
"""Create a forced choice trial.
|
|
388
|
+
|
|
389
|
+
Parameters
|
|
390
|
+
----------
|
|
391
|
+
item : Item
|
|
392
|
+
The item to create a trial from. Must have at least 2 options in
|
|
393
|
+
the item.options list.
|
|
394
|
+
template : ItemTemplate
|
|
395
|
+
The item template.
|
|
396
|
+
config : ChoiceConfig
|
|
397
|
+
Choice configuration.
|
|
398
|
+
trial_number : int
|
|
399
|
+
The trial number.
|
|
400
|
+
|
|
401
|
+
Returns
|
|
402
|
+
-------
|
|
403
|
+
dict[str, JsonValue]
|
|
404
|
+
A jsPsych html-button-response trial object.
|
|
405
|
+
|
|
406
|
+
Raises
|
|
407
|
+
------
|
|
408
|
+
ValueError
|
|
409
|
+
If item.options is empty or has fewer than 2 options.
|
|
410
|
+
"""
|
|
411
|
+
# For forced choice, use the prompt from the template as the stimulus
|
|
412
|
+
# (not the choices themselves)
|
|
413
|
+
prompt = (
|
|
414
|
+
template.task_spec.prompt
|
|
415
|
+
if template.task_spec
|
|
416
|
+
else "Which option do you choose?"
|
|
417
|
+
)
|
|
418
|
+
stimulus_html = (
|
|
419
|
+
f'<div class="stimulus-container"><p class="prompt">{prompt}</p></div>'
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
# Extract choices from item.options
|
|
423
|
+
if not item.options:
|
|
424
|
+
raise ValueError(
|
|
425
|
+
f"Item {item.id} has no options. "
|
|
426
|
+
f"Forced choice items must have at least 2 options in item.options. "
|
|
427
|
+
f"Use create_forced_choice_item() to create items with options."
|
|
428
|
+
)
|
|
429
|
+
if len(item.options) < 2:
|
|
430
|
+
raise ValueError(
|
|
431
|
+
f"Item {item.id} has only {len(item.options)} option(s). "
|
|
432
|
+
f"Forced choice items require at least 2 options."
|
|
433
|
+
)
|
|
434
|
+
choices = list(item.options)
|
|
435
|
+
|
|
436
|
+
# Serialize complete metadata
|
|
437
|
+
metadata = _serialize_item_metadata(item, template)
|
|
438
|
+
metadata["trial_number"] = trial_number
|
|
439
|
+
metadata["trial_type"] = "forced_choice"
|
|
440
|
+
|
|
441
|
+
return {
|
|
442
|
+
"type": "html-button-response",
|
|
443
|
+
"stimulus": stimulus_html,
|
|
444
|
+
"choices": choices,
|
|
445
|
+
"data": metadata,
|
|
446
|
+
"button_html": config.button_html
|
|
447
|
+
or '<button class="jspsych-btn">%choice%</button>',
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def _generate_stimulus_html(item: Item, include_all: bool = True) -> str:
|
|
452
|
+
"""Generate HTML for stimulus presentation.
|
|
453
|
+
|
|
454
|
+
Parameters
|
|
455
|
+
----------
|
|
456
|
+
item : Item
|
|
457
|
+
The item to generate HTML for.
|
|
458
|
+
include_all : bool
|
|
459
|
+
Whether to include all rendered elements (True) or just the first one (False).
|
|
460
|
+
|
|
461
|
+
Returns
|
|
462
|
+
-------
|
|
463
|
+
str
|
|
464
|
+
HTML string for the stimulus.
|
|
465
|
+
"""
|
|
466
|
+
if not item.rendered_elements:
|
|
467
|
+
return "<p>No stimulus available</p>"
|
|
468
|
+
|
|
469
|
+
# Get rendered elements in a consistent order
|
|
470
|
+
sorted_keys = sorted(item.rendered_elements.keys())
|
|
471
|
+
|
|
472
|
+
if include_all:
|
|
473
|
+
# Include all rendered elements
|
|
474
|
+
elements = [
|
|
475
|
+
f'<div class="stimulus-element"><p>{item.rendered_elements[k]}</p></div>'
|
|
476
|
+
for k in sorted_keys
|
|
477
|
+
]
|
|
478
|
+
return '<div class="stimulus-container">' + "".join(elements) + "</div>"
|
|
479
|
+
else:
|
|
480
|
+
# Include only the first element (for forced choice where others are options)
|
|
481
|
+
first_key = sorted_keys[0]
|
|
482
|
+
element_html = item.rendered_elements[first_key]
|
|
483
|
+
return f'<div class="stimulus-container"><p>{element_html}</p></div>'
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def create_consent_trial(consent_text: str) -> dict[str, JsonValue]:
|
|
487
|
+
"""Create a consent trial.
|
|
488
|
+
|
|
489
|
+
Parameters
|
|
490
|
+
----------
|
|
491
|
+
consent_text : str
|
|
492
|
+
The consent text to display.
|
|
493
|
+
|
|
494
|
+
Returns
|
|
495
|
+
-------
|
|
496
|
+
dict[str, JsonValue]
|
|
497
|
+
A jsPsych html-button-response trial object.
|
|
498
|
+
"""
|
|
499
|
+
stimulus_html = (
|
|
500
|
+
f'<div class="consent"><h2>Consent</h2><div>{consent_text}</div></div>'
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
return {
|
|
504
|
+
"type": "html-button-response",
|
|
505
|
+
"stimulus": stimulus_html,
|
|
506
|
+
"choices": ["I agree", "I do not agree"],
|
|
507
|
+
"data": {
|
|
508
|
+
"trial_type": "consent",
|
|
509
|
+
},
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def create_completion_trial(
|
|
514
|
+
completion_message: str = "Thank you for participating!",
|
|
515
|
+
) -> dict[str, JsonValue]:
|
|
516
|
+
"""Create a completion trial.
|
|
517
|
+
|
|
518
|
+
Parameters
|
|
519
|
+
----------
|
|
520
|
+
completion_message : str
|
|
521
|
+
The completion message to display.
|
|
522
|
+
|
|
523
|
+
Returns
|
|
524
|
+
-------
|
|
525
|
+
dict[str, JsonValue]
|
|
526
|
+
A jsPsych html-keyboard-response trial object.
|
|
527
|
+
"""
|
|
528
|
+
stimulus_html = (
|
|
529
|
+
f'<div class="completion"><h2>Complete</h2><p>{completion_message}</p></div>'
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
return {
|
|
533
|
+
"type": "html-keyboard-response",
|
|
534
|
+
"stimulus": stimulus_html,
|
|
535
|
+
"choices": "NO_KEYS",
|
|
536
|
+
"data": {
|
|
537
|
+
"trial_type": "completion",
|
|
538
|
+
},
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def _create_survey_question(field: DemographicsFieldConfig) -> dict[str, JsonValue]:
|
|
543
|
+
"""Create a jsPsych survey question from a demographics field config.
|
|
544
|
+
|
|
545
|
+
Parameters
|
|
546
|
+
----------
|
|
547
|
+
field : DemographicsFieldConfig
|
|
548
|
+
The field configuration.
|
|
549
|
+
|
|
550
|
+
Returns
|
|
551
|
+
-------
|
|
552
|
+
dict[str, JsonValue]
|
|
553
|
+
A jsPsych survey question object.
|
|
554
|
+
"""
|
|
555
|
+
question: dict[str, JsonValue] = {
|
|
556
|
+
"name": field.name,
|
|
557
|
+
"prompt": field.label,
|
|
558
|
+
"required": field.required,
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if field.field_type == "text":
|
|
562
|
+
question["type"] = "text"
|
|
563
|
+
if field.placeholder:
|
|
564
|
+
question["placeholder"] = field.placeholder
|
|
565
|
+
|
|
566
|
+
elif field.field_type == "number":
|
|
567
|
+
question["type"] = "text"
|
|
568
|
+
question["input_type"] = "number"
|
|
569
|
+
if field.placeholder:
|
|
570
|
+
question["placeholder"] = field.placeholder
|
|
571
|
+
if field.range is not None:
|
|
572
|
+
question["min"] = field.range.min
|
|
573
|
+
question["max"] = field.range.max
|
|
574
|
+
|
|
575
|
+
elif field.field_type == "dropdown":
|
|
576
|
+
question["type"] = "drop-down"
|
|
577
|
+
if field.options:
|
|
578
|
+
question["options"] = field.options
|
|
579
|
+
|
|
580
|
+
elif field.field_type == "radio":
|
|
581
|
+
question["type"] = "multi-choice"
|
|
582
|
+
if field.options:
|
|
583
|
+
question["options"] = field.options
|
|
584
|
+
|
|
585
|
+
elif field.field_type == "checkbox":
|
|
586
|
+
question["type"] = "multi-select"
|
|
587
|
+
if field.options:
|
|
588
|
+
question["options"] = field.options
|
|
589
|
+
|
|
590
|
+
return question
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def create_demographics_trial(config: DemographicsConfig) -> dict[str, JsonValue]:
|
|
594
|
+
"""Create a demographics survey trial.
|
|
595
|
+
|
|
596
|
+
Parameters
|
|
597
|
+
----------
|
|
598
|
+
config : DemographicsConfig
|
|
599
|
+
The demographics form configuration.
|
|
600
|
+
|
|
601
|
+
Returns
|
|
602
|
+
-------
|
|
603
|
+
dict[str, JsonValue]
|
|
604
|
+
A jsPsych survey trial object.
|
|
605
|
+
|
|
606
|
+
Examples
|
|
607
|
+
--------
|
|
608
|
+
>>> from bead.deployment.jspsych.config import (
|
|
609
|
+
... DemographicsConfig, DemographicsFieldConfig
|
|
610
|
+
... )
|
|
611
|
+
>>> config = DemographicsConfig(
|
|
612
|
+
... enabled=True,
|
|
613
|
+
... title="About You",
|
|
614
|
+
... fields=[
|
|
615
|
+
... DemographicsFieldConfig(
|
|
616
|
+
... name="age",
|
|
617
|
+
... field_type="number",
|
|
618
|
+
... label="Your Age",
|
|
619
|
+
... required=True,
|
|
620
|
+
... ),
|
|
621
|
+
... ],
|
|
622
|
+
... )
|
|
623
|
+
>>> trial = create_demographics_trial(config)
|
|
624
|
+
>>> trial["type"]
|
|
625
|
+
'survey'
|
|
626
|
+
"""
|
|
627
|
+
questions = [_create_survey_question(field) for field in config.fields]
|
|
628
|
+
|
|
629
|
+
return {
|
|
630
|
+
"type": "survey",
|
|
631
|
+
"title": config.title,
|
|
632
|
+
"pages": [questions],
|
|
633
|
+
"button_label_finish": config.submit_button_text,
|
|
634
|
+
"data": {
|
|
635
|
+
"trial_type": "demographics",
|
|
636
|
+
},
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def create_instructions_trial(
|
|
641
|
+
instructions: str | InstructionsConfig,
|
|
642
|
+
) -> dict[str, JsonValue]:
|
|
643
|
+
"""Create an instruction trial supporting both simple strings and rich config.
|
|
644
|
+
|
|
645
|
+
Parameters
|
|
646
|
+
----------
|
|
647
|
+
instructions : str | InstructionsConfig
|
|
648
|
+
Either a simple instruction string (single page, keyboard response)
|
|
649
|
+
or an InstructionsConfig for multi-page instructions.
|
|
650
|
+
|
|
651
|
+
Returns
|
|
652
|
+
-------
|
|
653
|
+
dict[str, JsonValue]
|
|
654
|
+
A jsPsych trial object. For simple strings, returns html-keyboard-response.
|
|
655
|
+
For InstructionsConfig, returns an instructions plugin trial.
|
|
656
|
+
|
|
657
|
+
Examples
|
|
658
|
+
--------
|
|
659
|
+
>>> # Simple string instructions
|
|
660
|
+
>>> trial = create_instructions_trial("Rate each sentence from 1-7.")
|
|
661
|
+
>>> trial["type"]
|
|
662
|
+
'html-keyboard-response'
|
|
663
|
+
|
|
664
|
+
>>> # Multi-page instructions
|
|
665
|
+
>>> from bead.deployment.jspsych.config import InstructionsConfig, InstructionPage
|
|
666
|
+
>>> config = InstructionsConfig(
|
|
667
|
+
... pages=[
|
|
668
|
+
... InstructionPage(title="Welcome", content="<p>Welcome!</p>"),
|
|
669
|
+
... InstructionPage(title="Task", content="<p>Rate sentences.</p>"),
|
|
670
|
+
... ],
|
|
671
|
+
... )
|
|
672
|
+
>>> trial = create_instructions_trial(config)
|
|
673
|
+
>>> trial["type"]
|
|
674
|
+
'instructions'
|
|
675
|
+
>>> len(trial["pages"])
|
|
676
|
+
2
|
|
677
|
+
"""
|
|
678
|
+
if isinstance(instructions, str):
|
|
679
|
+
# Simple string: use html-keyboard-response (backward compatible)
|
|
680
|
+
stimulus_html = (
|
|
681
|
+
f'<div class="instructions">'
|
|
682
|
+
f"<h2>Instructions</h2>"
|
|
683
|
+
f"<p>{instructions}</p>"
|
|
684
|
+
f"<p><em>Press any key to continue</em></p>"
|
|
685
|
+
f"</div>"
|
|
686
|
+
)
|
|
687
|
+
return {
|
|
688
|
+
"type": "html-keyboard-response",
|
|
689
|
+
"stimulus": stimulus_html,
|
|
690
|
+
"data": {
|
|
691
|
+
"trial_type": "instructions",
|
|
692
|
+
},
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
# InstructionsConfig: use jsPsych instructions plugin
|
|
696
|
+
pages: list[str] = []
|
|
697
|
+
for i, page in enumerate(instructions.pages):
|
|
698
|
+
page_html = '<div class="instructions-page">'
|
|
699
|
+
if page.title:
|
|
700
|
+
page_html += f"<h2>{page.title}</h2>"
|
|
701
|
+
page_html += f"<div>{page.content}</div>"
|
|
702
|
+
|
|
703
|
+
# Add page numbers if enabled
|
|
704
|
+
if instructions.show_page_numbers and len(instructions.pages) > 1:
|
|
705
|
+
page_html += (
|
|
706
|
+
f'<p class="page-number">Page {i + 1} of {len(instructions.pages)}</p>'
|
|
707
|
+
)
|
|
708
|
+
|
|
709
|
+
page_html += "</div>"
|
|
710
|
+
pages.append(page_html)
|
|
711
|
+
|
|
712
|
+
return {
|
|
713
|
+
"type": "instructions",
|
|
714
|
+
"pages": pages,
|
|
715
|
+
"show_clickable_nav": True,
|
|
716
|
+
"allow_backward": instructions.allow_backwards,
|
|
717
|
+
"button_label_next": instructions.button_label_next,
|
|
718
|
+
"button_label_previous": "Previous",
|
|
719
|
+
"button_label_finish": instructions.button_label_finish,
|
|
720
|
+
"data": {
|
|
721
|
+
"trial_type": "instructions",
|
|
722
|
+
},
|
|
723
|
+
}
|