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,241 @@
|
|
|
1
|
+
// This file is generated from Python OrderingConstraint specifications
|
|
2
|
+
// Do not edit manually - regenerate using bead.deployment.jspsych.randomizer
|
|
3
|
+
|
|
4
|
+
// Embedded metadata for constraint checking
|
|
5
|
+
const trialMetadata = {{ metadata | tojson }};
|
|
6
|
+
|
|
7
|
+
// Constraint specifications
|
|
8
|
+
const constraints = {{ constraints | tojson }};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Shuffle array in place using Fisher-Yates algorithm
|
|
12
|
+
* @param {Array} array - Array to shuffle
|
|
13
|
+
* @param {function} rng - Random number generator
|
|
14
|
+
*/
|
|
15
|
+
function shuffle(array, rng) {
|
|
16
|
+
for (let i = array.length - 1; i > 0; i--) {
|
|
17
|
+
const j = Math.floor(rng() * (i + 1));
|
|
18
|
+
[array[i], array[j]] = [array[j], array[i]];
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
{% if has_precedence %}
|
|
23
|
+
/**
|
|
24
|
+
* Check if trial order satisfies precedence constraints
|
|
25
|
+
* @param {Array} trials - Array of trial objects with item_id property
|
|
26
|
+
* @param {Array} pairs - Array of [itemA_id, itemB_id] precedence pairs
|
|
27
|
+
* @returns {boolean} True if all precedence constraints satisfied
|
|
28
|
+
*/
|
|
29
|
+
function checkPrecedence(trials, pairs) {
|
|
30
|
+
const positions = {};
|
|
31
|
+
trials.forEach((trial, idx) => {
|
|
32
|
+
positions[trial.item_id] = idx;
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
for (const [itemA, itemB] of pairs) {
|
|
36
|
+
if (positions[itemA] !== undefined && positions[itemB] !== undefined) {
|
|
37
|
+
if (positions[itemA] >= positions[itemB]) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
{% endif %}
|
|
45
|
+
|
|
46
|
+
{% if has_no_adjacent %}
|
|
47
|
+
/**
|
|
48
|
+
* Check if no adjacent trials have same property value
|
|
49
|
+
* @param {Array} trials - Array of trial objects
|
|
50
|
+
* @param {string} property - Property path to check
|
|
51
|
+
* @param {Object} metadata - Trial metadata
|
|
52
|
+
* @returns {boolean} True if no adjacent items have same value
|
|
53
|
+
*/
|
|
54
|
+
function checkNoAdjacent(trials, property, metadata) {
|
|
55
|
+
for (let i = 0; i < trials.length - 1; i++) {
|
|
56
|
+
const valueA = getPropertyValue(metadata[trials[i].item_id], property);
|
|
57
|
+
const valueB = getPropertyValue(metadata[trials[i + 1].item_id], property);
|
|
58
|
+
|
|
59
|
+
if (valueA !== undefined && valueB !== undefined && valueA === valueB) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
{% endif %}
|
|
66
|
+
|
|
67
|
+
{% if has_min_distance %}
|
|
68
|
+
/**
|
|
69
|
+
* Check if minimum distance constraint is satisfied
|
|
70
|
+
* @param {Array} trials - Array of trial objects
|
|
71
|
+
* @param {string} property - Property path to check
|
|
72
|
+
* @param {number} minDist - Minimum distance required
|
|
73
|
+
* @param {Object} metadata - Trial metadata
|
|
74
|
+
* @returns {boolean} True if minimum distance satisfied
|
|
75
|
+
*/
|
|
76
|
+
function checkMinDistance(trials, property, minDist, metadata) {
|
|
77
|
+
const valuePositions = {};
|
|
78
|
+
|
|
79
|
+
trials.forEach((trial, idx) => {
|
|
80
|
+
const value = getPropertyValue(metadata[trial.item_id], property);
|
|
81
|
+
if (value !== undefined) {
|
|
82
|
+
if (!valuePositions[value]) {
|
|
83
|
+
valuePositions[value] = [];
|
|
84
|
+
}
|
|
85
|
+
valuePositions[value].push(idx);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
for (const positions of Object.values(valuePositions)) {
|
|
90
|
+
for (let i = 0; i < positions.length - 1; i++) {
|
|
91
|
+
const distance = positions[i + 1] - positions[i] - 1;
|
|
92
|
+
if (distance < minDist) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
{% endif %}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Get property value from nested object using dot notation
|
|
103
|
+
* @param {Object} obj - Object to query
|
|
104
|
+
* @param {string} path - Dot-notation path (e.g., "item_metadata.condition")
|
|
105
|
+
* @returns {*} Property value or undefined
|
|
106
|
+
*/
|
|
107
|
+
function getPropertyValue(obj, path) {
|
|
108
|
+
const parts = path.split('.');
|
|
109
|
+
let current = obj;
|
|
110
|
+
for (const part of parts) {
|
|
111
|
+
if (current === undefined || current === null) {
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
current = current[part];
|
|
115
|
+
}
|
|
116
|
+
return current;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Check if trial order satisfies all constraints
|
|
121
|
+
* @param {Array} trials - Array of trial objects
|
|
122
|
+
* @param {Object} metadata - Trial metadata
|
|
123
|
+
* @returns {boolean} True if all constraints satisfied
|
|
124
|
+
*/
|
|
125
|
+
function checkAllConstraints(trials, metadata) {
|
|
126
|
+
{% if has_precedence %}
|
|
127
|
+
// Check precedence constraints
|
|
128
|
+
for (const constraint of constraints) {
|
|
129
|
+
if (constraint.precedence_pairs && constraint.precedence_pairs.length > 0) {
|
|
130
|
+
if (!checkPrecedence(trials, constraint.precedence_pairs)) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
{% endif %}
|
|
136
|
+
|
|
137
|
+
{% if has_no_adjacent %}
|
|
138
|
+
// Check no-adjacent constraints
|
|
139
|
+
for (const constraint of constraints) {
|
|
140
|
+
if (constraint.no_adjacent_property) {
|
|
141
|
+
if (!checkNoAdjacent(trials, constraint.no_adjacent_property, metadata)) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
{% endif %}
|
|
147
|
+
|
|
148
|
+
{% if has_min_distance %}
|
|
149
|
+
// Check minimum distance constraints
|
|
150
|
+
for (const constraint of constraints) {
|
|
151
|
+
if (constraint.min_distance && constraint.no_adjacent_property) {
|
|
152
|
+
if (!checkMinDistance(trials, constraint.no_adjacent_property, constraint.min_distance, metadata)) {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
{% endif %}
|
|
158
|
+
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Main entry point: randomize trials with constraint enforcement
|
|
164
|
+
* @param {Array} trials - Array of jsPsych trial objects with item_id property
|
|
165
|
+
* @param {string|number} seed - Random seed (usually participant ID)
|
|
166
|
+
* @returns {Array} Randomized trials satisfying all constraints
|
|
167
|
+
*/
|
|
168
|
+
function randomizeTrials(trials, seed) {
|
|
169
|
+
const rng = new Math.seedrandom(seed);
|
|
170
|
+
|
|
171
|
+
{% if has_practice %}
|
|
172
|
+
// Separate practice items (must come first)
|
|
173
|
+
const practiceTrials = trials.filter(t => {
|
|
174
|
+
const meta = trialMetadata[t.item_id];
|
|
175
|
+
const value = getPropertyValue(meta, '{{ practice_property }}');
|
|
176
|
+
return value === true;
|
|
177
|
+
});
|
|
178
|
+
const mainTrials = trials.filter(t => {
|
|
179
|
+
const meta = trialMetadata[t.item_id];
|
|
180
|
+
const value = getPropertyValue(meta, '{{ practice_property }}');
|
|
181
|
+
return value !== true;
|
|
182
|
+
});
|
|
183
|
+
{% else %}
|
|
184
|
+
const practiceTrials = [];
|
|
185
|
+
const mainTrials = trials.slice();
|
|
186
|
+
{% endif %}
|
|
187
|
+
|
|
188
|
+
{% if has_blocking %}
|
|
189
|
+
// Group main trials by block property
|
|
190
|
+
const blocks = {};
|
|
191
|
+
mainTrials.forEach(t => {
|
|
192
|
+
const blockValue = getPropertyValue(trialMetadata[t.item_id], '{{ block_property }}');
|
|
193
|
+
const blockKey = blockValue !== undefined ? String(blockValue) : '__undefined__';
|
|
194
|
+
if (!blocks[blockKey]) {
|
|
195
|
+
blocks[blockKey] = [];
|
|
196
|
+
}
|
|
197
|
+
blocks[blockKey].push(t);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Randomize block order
|
|
201
|
+
const blockKeys = Object.keys(blocks);
|
|
202
|
+
shuffle(blockKeys, rng);
|
|
203
|
+
|
|
204
|
+
let randomizedMain = [];
|
|
205
|
+
blockKeys.forEach(key => {
|
|
206
|
+
const blockTrials = blocks[key];
|
|
207
|
+
{% if randomize_within_blocks %}
|
|
208
|
+
// Randomize within blocks
|
|
209
|
+
shuffle(blockTrials, rng);
|
|
210
|
+
{% endif %}
|
|
211
|
+
randomizedMain = randomizedMain.concat(blockTrials);
|
|
212
|
+
});
|
|
213
|
+
{% else %}
|
|
214
|
+
// Rejection sampling: try to find valid order
|
|
215
|
+
const maxAttempts = 1000;
|
|
216
|
+
let randomizedMain = mainTrials.slice();
|
|
217
|
+
let lastAttempt = randomizedMain.slice();
|
|
218
|
+
|
|
219
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
220
|
+
shuffle(randomizedMain, rng);
|
|
221
|
+
lastAttempt = randomizedMain.slice();
|
|
222
|
+
|
|
223
|
+
if (checkAllConstraints(randomizedMain, trialMetadata)) {
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (attempt === maxAttempts - 1) {
|
|
228
|
+
console.warn('Could not find constraint-satisfying order after ' +
|
|
229
|
+
maxAttempts + ' attempts. Using last attempt.');
|
|
230
|
+
randomizedMain = lastAttempt;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
{% endif %}
|
|
234
|
+
|
|
235
|
+
return practiceTrials.concat(randomizedMain);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Export for use in jsPsych experiments
|
|
239
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
240
|
+
module.exports = { randomizeTrials };
|
|
241
|
+
}
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Constraint-Aware Trial Randomization
|
|
3
|
+
* Generated by sash deployment system
|
|
4
|
+
*
|
|
5
|
+
* This code implements runtime trial randomization while satisfying
|
|
6
|
+
* ordering constraints (precedence, no-adjacency, blocking, distance).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Item metadata for constraint checking
|
|
10
|
+
const ITEM_METADATA = {{ metadata_json }};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Main entry point for trial randomization
|
|
14
|
+
* @param {Array} trials - Array of jsPsych trial objects
|
|
15
|
+
* @param {string} participantId - Participant ID for seeding
|
|
16
|
+
* @returns {Array} Randomized trials satisfying all constraints
|
|
17
|
+
*/
|
|
18
|
+
function randomizeTrials(trials, participantId) {
|
|
19
|
+
// Initialize seeded random number generator
|
|
20
|
+
const rng = new Math.seedrandom(participantId);
|
|
21
|
+
|
|
22
|
+
{% if has_practice_items %}
|
|
23
|
+
// Separate practice and main trials
|
|
24
|
+
const practiceTrials = trials.filter(t => {
|
|
25
|
+
const itemId = t.data?.item_id || t.item_id;
|
|
26
|
+
return itemId && ITEM_METADATA[itemId]?.{{ practice_property }};
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const mainTrials = trials.filter(t => {
|
|
30
|
+
const itemId = t.data?.item_id || t.item_id;
|
|
31
|
+
return !itemId || !ITEM_METADATA[itemId]?.{{ practice_property }};
|
|
32
|
+
});
|
|
33
|
+
{% else %}
|
|
34
|
+
const practiceTrials = [];
|
|
35
|
+
const mainTrials = trials.slice();
|
|
36
|
+
{% endif %}
|
|
37
|
+
|
|
38
|
+
{% if has_blocking %}
|
|
39
|
+
// Block by property: {{ block_property }}
|
|
40
|
+
const blocks = {};
|
|
41
|
+
mainTrials.forEach(trial => {
|
|
42
|
+
const itemId = trial.data?.item_id || trial.item_id;
|
|
43
|
+
const blockKey = itemId && ITEM_METADATA[itemId]?.{{ block_property }}
|
|
44
|
+
? ITEM_METADATA[itemId].{{ block_property }}
|
|
45
|
+
: 'default';
|
|
46
|
+
|
|
47
|
+
if (!blocks[blockKey]) {
|
|
48
|
+
blocks[blockKey] = [];
|
|
49
|
+
}
|
|
50
|
+
blocks[blockKey].push(trial);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Randomize within blocks
|
|
54
|
+
{% if randomize_within_blocks %}
|
|
55
|
+
const randomizedBlocks = Object.values(blocks).map(block =>
|
|
56
|
+
shuffleWithConstraints(block, rng)
|
|
57
|
+
);
|
|
58
|
+
{% else %}
|
|
59
|
+
const randomizedBlocks = Object.values(blocks);
|
|
60
|
+
{% endif %}
|
|
61
|
+
|
|
62
|
+
// Shuffle block order
|
|
63
|
+
const finalMainTrials = shuffle(randomizedBlocks, rng).flat();
|
|
64
|
+
{% else %}
|
|
65
|
+
// Randomize main trials with constraint checking
|
|
66
|
+
const finalMainTrials = shuffleWithConstraints(mainTrials, rng);
|
|
67
|
+
{% endif %}
|
|
68
|
+
|
|
69
|
+
// Combine practice (randomized) + main trials
|
|
70
|
+
const randomizedPractice = shuffle(practiceTrials, rng);
|
|
71
|
+
return [...randomizedPractice, ...finalMainTrials];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Shuffle trials while satisfying all constraints
|
|
76
|
+
* Uses greedy construction with constraint checking
|
|
77
|
+
* @param {Array} trials - Trials to shuffle
|
|
78
|
+
* @param {Function} rng - Seeded random number generator
|
|
79
|
+
* @returns {Array} Shuffled trials satisfying constraints
|
|
80
|
+
*/
|
|
81
|
+
function shuffleWithConstraints(trials, rng) {
|
|
82
|
+
if (trials.length === 0) return [];
|
|
83
|
+
|
|
84
|
+
// First try greedy construction for distance/no-adjacent constraints
|
|
85
|
+
{% if has_distance or has_no_adjacent %}
|
|
86
|
+
const constructed = greedyConstruction(trials, rng);
|
|
87
|
+
if (constructed && checkAllConstraints(constructed)) {
|
|
88
|
+
return constructed;
|
|
89
|
+
}
|
|
90
|
+
{% endif %}
|
|
91
|
+
|
|
92
|
+
// Fall back to rejection sampling with limited attempts
|
|
93
|
+
const maxAttempts = 10000;
|
|
94
|
+
let attempt = 0;
|
|
95
|
+
|
|
96
|
+
while (attempt < maxAttempts) {
|
|
97
|
+
const shuffled = shuffle(trials.slice(), rng);
|
|
98
|
+
|
|
99
|
+
if (checkAllConstraints(shuffled)) {
|
|
100
|
+
return shuffled;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
attempt++;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Final fallback: return random shuffle
|
|
107
|
+
console.error(`Could not find valid trial order after ${maxAttempts} attempts`);
|
|
108
|
+
return shuffle(trials.slice(), rng);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
{% if has_distance or has_no_adjacent %}
|
|
112
|
+
/**
|
|
113
|
+
* Greedy construction algorithm for spacing out items
|
|
114
|
+
* @param {Array} trials - Trials to place
|
|
115
|
+
* @param {Function} rng - Random number generator
|
|
116
|
+
* @returns {Array|null} Constructed array or null if failed
|
|
117
|
+
*/
|
|
118
|
+
function greedyConstruction(trials, rng) {
|
|
119
|
+
const result = [];
|
|
120
|
+
const remaining = trials.slice();
|
|
121
|
+
shuffle(remaining, rng); // Randomize initial order
|
|
122
|
+
|
|
123
|
+
while (remaining.length > 0) {
|
|
124
|
+
let placed = false;
|
|
125
|
+
|
|
126
|
+
// Try to place each remaining item
|
|
127
|
+
for (let i = 0; i < remaining.length; i++) {
|
|
128
|
+
const trial = remaining[i];
|
|
129
|
+
result.push(trial);
|
|
130
|
+
|
|
131
|
+
// Check if this placement is valid
|
|
132
|
+
if (checkAllConstraints(result)) {
|
|
133
|
+
remaining.splice(i, 1);
|
|
134
|
+
placed = true;
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Invalid, remove and try next
|
|
139
|
+
result.pop();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!placed) {
|
|
143
|
+
// Couldn't place any item, construction failed
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
{% endif %}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Check if trial order satisfies all constraints
|
|
154
|
+
* @param {Array} trials - Trial order to check
|
|
155
|
+
* @returns {boolean} True if all constraints satisfied
|
|
156
|
+
*/
|
|
157
|
+
function checkAllConstraints(trials) {
|
|
158
|
+
{% if has_precedence %}
|
|
159
|
+
// Check precedence constraints
|
|
160
|
+
if (!checkPrecedenceConstraints(trials)) {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
{% endif %}
|
|
164
|
+
|
|
165
|
+
{% if has_no_adjacent %}
|
|
166
|
+
// Check no-adjacency constraints
|
|
167
|
+
if (!checkNoAdjacentConstraints(trials)) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
{% endif %}
|
|
171
|
+
|
|
172
|
+
{% if has_distance %}
|
|
173
|
+
// Check distance constraints
|
|
174
|
+
if (!checkDistanceConstraints(trials)) {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
{% endif %}
|
|
178
|
+
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
{% if has_precedence %}
|
|
183
|
+
/**
|
|
184
|
+
* Check precedence constraints (item A must appear before item B)
|
|
185
|
+
* @param {Array} trials - Trial order to check
|
|
186
|
+
* @returns {boolean} True if precedence constraints satisfied
|
|
187
|
+
*/
|
|
188
|
+
function checkPrecedenceConstraints(trials) {
|
|
189
|
+
const precedencePairs = {{ precedence_pairs_json }};
|
|
190
|
+
|
|
191
|
+
for (const [beforeId, afterId] of precedencePairs) {
|
|
192
|
+
let beforeIndex = -1;
|
|
193
|
+
let afterIndex = -1;
|
|
194
|
+
|
|
195
|
+
for (let i = 0; i < trials.length; i++) {
|
|
196
|
+
const itemId = trials[i].data?.item_id || trials[i].item_id;
|
|
197
|
+
if (itemId === beforeId) beforeIndex = i;
|
|
198
|
+
if (itemId === afterId) afterIndex = i;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// If both items present, check ordering
|
|
202
|
+
if (beforeIndex !== -1 && afterIndex !== -1 && beforeIndex >= afterIndex) {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
{% endif %}
|
|
210
|
+
|
|
211
|
+
{% if has_no_adjacent %}
|
|
212
|
+
/**
|
|
213
|
+
* Check no-adjacency constraints (no adjacent items with same property)
|
|
214
|
+
* @param {Array} trials - Trial order to check
|
|
215
|
+
* @returns {boolean} True if no-adjacency constraints satisfied
|
|
216
|
+
*/
|
|
217
|
+
function checkNoAdjacentConstraints(trials) {
|
|
218
|
+
const property = "{{ no_adjacent_property }}";
|
|
219
|
+
|
|
220
|
+
for (let i = 0; i < trials.length - 1; i++) {
|
|
221
|
+
const itemId1 = trials[i].data?.item_id || trials[i].item_id;
|
|
222
|
+
const itemId2 = trials[i + 1].data?.item_id || trials[i + 1].item_id;
|
|
223
|
+
|
|
224
|
+
if (!itemId1 || !itemId2) continue;
|
|
225
|
+
|
|
226
|
+
const value1 = ITEM_METADATA[itemId1]?.[property];
|
|
227
|
+
const value2 = ITEM_METADATA[itemId2]?.[property];
|
|
228
|
+
|
|
229
|
+
if (value1 !== undefined && value2 !== undefined && value1 === value2) {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
{% endif %}
|
|
237
|
+
|
|
238
|
+
{% if has_distance %}
|
|
239
|
+
/**
|
|
240
|
+
* Check distance constraints (min/max distance between items)
|
|
241
|
+
* @param {Array} trials - Trial order to check
|
|
242
|
+
* @returns {boolean} True if distance constraints satisfied
|
|
243
|
+
*/
|
|
244
|
+
function checkDistanceConstraints(trials) {
|
|
245
|
+
const distanceConstraints = {{ distance_constraints_json }};
|
|
246
|
+
|
|
247
|
+
for (const constraint of distanceConstraints) {
|
|
248
|
+
const {item1_id, item2_id, min_distance, max_distance} = constraint;
|
|
249
|
+
|
|
250
|
+
let index1 = -1;
|
|
251
|
+
let index2 = -1;
|
|
252
|
+
|
|
253
|
+
for (let i = 0; i < trials.length; i++) {
|
|
254
|
+
const itemId = trials[i].data?.item_id || trials[i].item_id;
|
|
255
|
+
if (itemId === item1_id) index1 = i;
|
|
256
|
+
if (itemId === item2_id) index2 = i;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// If both items present, check distance
|
|
260
|
+
if (index1 !== -1 && index2 !== -1) {
|
|
261
|
+
// Distance is number of items between them (not including the items themselves)
|
|
262
|
+
const distance = Math.abs(index2 - index1) - 1;
|
|
263
|
+
|
|
264
|
+
if (min_distance !== null && distance < min_distance) {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (max_distance !== null && distance > max_distance) {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
{% endif %}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Seeded Fisher-Yates shuffle
|
|
280
|
+
* @param {Array} array - Array to shuffle
|
|
281
|
+
* @param {Function} rng - Seeded random number generator
|
|
282
|
+
* @returns {Array} Shuffled array
|
|
283
|
+
*/
|
|
284
|
+
function shuffle(array, rng) {
|
|
285
|
+
const arr = array.slice();
|
|
286
|
+
for (let i = arr.length - 1; i > 0; i--) {
|
|
287
|
+
const j = Math.floor(rng() * (i + 1));
|
|
288
|
+
[arr[i], arr[j]] = [arr[j], arr[i]];
|
|
289
|
+
}
|
|
290
|
+
return arr;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Get nested property from object using dot notation
|
|
295
|
+
* @param {Object} obj - Object to query
|
|
296
|
+
* @param {string} path - Property path (e.g., "item_metadata.condition")
|
|
297
|
+
* @returns {*} Property value or undefined
|
|
298
|
+
*/
|
|
299
|
+
function getNestedProperty(obj, path) {
|
|
300
|
+
if (!obj || !path) return undefined;
|
|
301
|
+
|
|
302
|
+
const parts = path.split('.');
|
|
303
|
+
let current = obj;
|
|
304
|
+
|
|
305
|
+
for (const part of parts) {
|
|
306
|
+
if (current === null || current === undefined) {
|
|
307
|
+
return undefined;
|
|
308
|
+
}
|
|
309
|
+
current = current[part];
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return current;
|
|
313
|
+
}
|