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.
Files changed (231) hide show
  1. bead/__init__.py +11 -0
  2. bead/__main__.py +11 -0
  3. bead/active_learning/__init__.py +15 -0
  4. bead/active_learning/config.py +231 -0
  5. bead/active_learning/loop.py +566 -0
  6. bead/active_learning/models/__init__.py +24 -0
  7. bead/active_learning/models/base.py +852 -0
  8. bead/active_learning/models/binary.py +910 -0
  9. bead/active_learning/models/categorical.py +943 -0
  10. bead/active_learning/models/cloze.py +862 -0
  11. bead/active_learning/models/forced_choice.py +956 -0
  12. bead/active_learning/models/free_text.py +773 -0
  13. bead/active_learning/models/lora.py +365 -0
  14. bead/active_learning/models/magnitude.py +835 -0
  15. bead/active_learning/models/multi_select.py +795 -0
  16. bead/active_learning/models/ordinal_scale.py +811 -0
  17. bead/active_learning/models/peft_adapter.py +155 -0
  18. bead/active_learning/models/random_effects.py +639 -0
  19. bead/active_learning/selection.py +354 -0
  20. bead/active_learning/strategies.py +391 -0
  21. bead/active_learning/trainers/__init__.py +26 -0
  22. bead/active_learning/trainers/base.py +210 -0
  23. bead/active_learning/trainers/data_collator.py +172 -0
  24. bead/active_learning/trainers/dataset_utils.py +261 -0
  25. bead/active_learning/trainers/huggingface.py +304 -0
  26. bead/active_learning/trainers/lightning.py +324 -0
  27. bead/active_learning/trainers/metrics.py +424 -0
  28. bead/active_learning/trainers/mixed_effects.py +551 -0
  29. bead/active_learning/trainers/model_wrapper.py +509 -0
  30. bead/active_learning/trainers/registry.py +104 -0
  31. bead/adapters/__init__.py +11 -0
  32. bead/adapters/huggingface.py +61 -0
  33. bead/behavioral/__init__.py +116 -0
  34. bead/behavioral/analytics.py +646 -0
  35. bead/behavioral/extraction.py +343 -0
  36. bead/behavioral/merging.py +343 -0
  37. bead/cli/__init__.py +11 -0
  38. bead/cli/active_learning.py +513 -0
  39. bead/cli/active_learning_commands.py +779 -0
  40. bead/cli/completion.py +359 -0
  41. bead/cli/config.py +624 -0
  42. bead/cli/constraint_builders.py +286 -0
  43. bead/cli/deployment.py +859 -0
  44. bead/cli/deployment_trials.py +493 -0
  45. bead/cli/deployment_ui.py +332 -0
  46. bead/cli/display.py +378 -0
  47. bead/cli/items.py +960 -0
  48. bead/cli/items_factories.py +776 -0
  49. bead/cli/list_constraints.py +714 -0
  50. bead/cli/lists.py +490 -0
  51. bead/cli/main.py +430 -0
  52. bead/cli/models.py +877 -0
  53. bead/cli/resource_loaders.py +621 -0
  54. bead/cli/resources.py +1036 -0
  55. bead/cli/shell.py +356 -0
  56. bead/cli/simulate.py +840 -0
  57. bead/cli/templates.py +1158 -0
  58. bead/cli/training.py +1080 -0
  59. bead/cli/utils.py +614 -0
  60. bead/cli/workflow.py +1273 -0
  61. bead/config/__init__.py +68 -0
  62. bead/config/active_learning.py +1009 -0
  63. bead/config/config.py +192 -0
  64. bead/config/defaults.py +118 -0
  65. bead/config/deployment.py +217 -0
  66. bead/config/env.py +147 -0
  67. bead/config/item.py +45 -0
  68. bead/config/list.py +193 -0
  69. bead/config/loader.py +149 -0
  70. bead/config/logging.py +42 -0
  71. bead/config/model.py +49 -0
  72. bead/config/paths.py +46 -0
  73. bead/config/profiles.py +320 -0
  74. bead/config/resources.py +47 -0
  75. bead/config/serialization.py +210 -0
  76. bead/config/simulation.py +206 -0
  77. bead/config/template.py +238 -0
  78. bead/config/validation.py +267 -0
  79. bead/data/__init__.py +65 -0
  80. bead/data/base.py +87 -0
  81. bead/data/identifiers.py +97 -0
  82. bead/data/language_codes.py +61 -0
  83. bead/data/metadata.py +270 -0
  84. bead/data/range.py +123 -0
  85. bead/data/repository.py +358 -0
  86. bead/data/serialization.py +249 -0
  87. bead/data/timestamps.py +89 -0
  88. bead/data/validation.py +349 -0
  89. bead/data_collection/__init__.py +11 -0
  90. bead/data_collection/jatos.py +223 -0
  91. bead/data_collection/merger.py +154 -0
  92. bead/data_collection/prolific.py +198 -0
  93. bead/deployment/__init__.py +5 -0
  94. bead/deployment/distribution.py +402 -0
  95. bead/deployment/jatos/__init__.py +1 -0
  96. bead/deployment/jatos/api.py +200 -0
  97. bead/deployment/jatos/exporter.py +210 -0
  98. bead/deployment/jspsych/__init__.py +9 -0
  99. bead/deployment/jspsych/biome.json +44 -0
  100. bead/deployment/jspsych/config.py +411 -0
  101. bead/deployment/jspsych/generator.py +598 -0
  102. bead/deployment/jspsych/package.json +51 -0
  103. bead/deployment/jspsych/pnpm-lock.yaml +2141 -0
  104. bead/deployment/jspsych/randomizer.py +299 -0
  105. bead/deployment/jspsych/src/lib/list-distributor.test.ts +327 -0
  106. bead/deployment/jspsych/src/lib/list-distributor.ts +1282 -0
  107. bead/deployment/jspsych/src/lib/randomizer.test.ts +232 -0
  108. bead/deployment/jspsych/src/lib/randomizer.ts +367 -0
  109. bead/deployment/jspsych/src/plugins/cloze-dropdown.ts +252 -0
  110. bead/deployment/jspsych/src/plugins/forced-choice.ts +265 -0
  111. bead/deployment/jspsych/src/plugins/plugins.test.ts +141 -0
  112. bead/deployment/jspsych/src/plugins/rating.ts +248 -0
  113. bead/deployment/jspsych/src/slopit/index.ts +9 -0
  114. bead/deployment/jspsych/src/types/jatos.d.ts +256 -0
  115. bead/deployment/jspsych/src/types/jspsych.d.ts +228 -0
  116. bead/deployment/jspsych/templates/experiment.css +1 -0
  117. bead/deployment/jspsych/templates/experiment.js.template +289 -0
  118. bead/deployment/jspsych/templates/index.html +51 -0
  119. bead/deployment/jspsych/templates/randomizer.js +241 -0
  120. bead/deployment/jspsych/templates/randomizer.js.template +313 -0
  121. bead/deployment/jspsych/trials.py +723 -0
  122. bead/deployment/jspsych/tsconfig.json +23 -0
  123. bead/deployment/jspsych/tsup.config.ts +30 -0
  124. bead/deployment/jspsych/ui/__init__.py +1 -0
  125. bead/deployment/jspsych/ui/components.py +383 -0
  126. bead/deployment/jspsych/ui/styles.py +411 -0
  127. bead/dsl/__init__.py +80 -0
  128. bead/dsl/ast.py +168 -0
  129. bead/dsl/context.py +178 -0
  130. bead/dsl/errors.py +71 -0
  131. bead/dsl/evaluator.py +570 -0
  132. bead/dsl/grammar.lark +81 -0
  133. bead/dsl/parser.py +231 -0
  134. bead/dsl/stdlib.py +929 -0
  135. bead/evaluation/__init__.py +13 -0
  136. bead/evaluation/convergence.py +485 -0
  137. bead/evaluation/interannotator.py +398 -0
  138. bead/items/__init__.py +40 -0
  139. bead/items/adapters/__init__.py +70 -0
  140. bead/items/adapters/anthropic.py +224 -0
  141. bead/items/adapters/api_utils.py +167 -0
  142. bead/items/adapters/base.py +216 -0
  143. bead/items/adapters/google.py +259 -0
  144. bead/items/adapters/huggingface.py +1074 -0
  145. bead/items/adapters/openai.py +323 -0
  146. bead/items/adapters/registry.py +202 -0
  147. bead/items/adapters/sentence_transformers.py +224 -0
  148. bead/items/adapters/togetherai.py +309 -0
  149. bead/items/binary.py +515 -0
  150. bead/items/cache.py +558 -0
  151. bead/items/categorical.py +593 -0
  152. bead/items/cloze.py +757 -0
  153. bead/items/constructor.py +784 -0
  154. bead/items/forced_choice.py +413 -0
  155. bead/items/free_text.py +681 -0
  156. bead/items/generation.py +432 -0
  157. bead/items/item.py +396 -0
  158. bead/items/item_template.py +787 -0
  159. bead/items/magnitude.py +573 -0
  160. bead/items/multi_select.py +621 -0
  161. bead/items/ordinal_scale.py +569 -0
  162. bead/items/scoring.py +448 -0
  163. bead/items/validation.py +723 -0
  164. bead/lists/__init__.py +30 -0
  165. bead/lists/balancer.py +263 -0
  166. bead/lists/constraints.py +1067 -0
  167. bead/lists/experiment_list.py +286 -0
  168. bead/lists/list_collection.py +378 -0
  169. bead/lists/partitioner.py +1141 -0
  170. bead/lists/stratification.py +254 -0
  171. bead/participants/__init__.py +73 -0
  172. bead/participants/collection.py +699 -0
  173. bead/participants/merging.py +312 -0
  174. bead/participants/metadata_spec.py +491 -0
  175. bead/participants/models.py +276 -0
  176. bead/resources/__init__.py +29 -0
  177. bead/resources/adapters/__init__.py +19 -0
  178. bead/resources/adapters/base.py +104 -0
  179. bead/resources/adapters/cache.py +128 -0
  180. bead/resources/adapters/glazing.py +508 -0
  181. bead/resources/adapters/registry.py +117 -0
  182. bead/resources/adapters/unimorph.py +796 -0
  183. bead/resources/classification.py +856 -0
  184. bead/resources/constraint_builders.py +329 -0
  185. bead/resources/constraints.py +165 -0
  186. bead/resources/lexical_item.py +223 -0
  187. bead/resources/lexicon.py +744 -0
  188. bead/resources/loaders.py +209 -0
  189. bead/resources/template.py +441 -0
  190. bead/resources/template_collection.py +707 -0
  191. bead/resources/template_generation.py +349 -0
  192. bead/simulation/__init__.py +29 -0
  193. bead/simulation/annotators/__init__.py +15 -0
  194. bead/simulation/annotators/base.py +175 -0
  195. bead/simulation/annotators/distance_based.py +135 -0
  196. bead/simulation/annotators/lm_based.py +114 -0
  197. bead/simulation/annotators/oracle.py +182 -0
  198. bead/simulation/annotators/random.py +181 -0
  199. bead/simulation/dsl_extension/__init__.py +3 -0
  200. bead/simulation/noise_models/__init__.py +13 -0
  201. bead/simulation/noise_models/base.py +42 -0
  202. bead/simulation/noise_models/random_noise.py +82 -0
  203. bead/simulation/noise_models/systematic.py +132 -0
  204. bead/simulation/noise_models/temperature.py +86 -0
  205. bead/simulation/runner.py +144 -0
  206. bead/simulation/strategies/__init__.py +23 -0
  207. bead/simulation/strategies/base.py +123 -0
  208. bead/simulation/strategies/binary.py +103 -0
  209. bead/simulation/strategies/categorical.py +123 -0
  210. bead/simulation/strategies/cloze.py +224 -0
  211. bead/simulation/strategies/forced_choice.py +127 -0
  212. bead/simulation/strategies/free_text.py +105 -0
  213. bead/simulation/strategies/magnitude.py +116 -0
  214. bead/simulation/strategies/multi_select.py +129 -0
  215. bead/simulation/strategies/ordinal_scale.py +131 -0
  216. bead/templates/__init__.py +27 -0
  217. bead/templates/adapters/__init__.py +17 -0
  218. bead/templates/adapters/base.py +128 -0
  219. bead/templates/adapters/cache.py +178 -0
  220. bead/templates/adapters/huggingface.py +312 -0
  221. bead/templates/combinatorics.py +103 -0
  222. bead/templates/filler.py +605 -0
  223. bead/templates/renderers.py +177 -0
  224. bead/templates/resolver.py +178 -0
  225. bead/templates/strategies.py +1806 -0
  226. bead/templates/streaming.py +195 -0
  227. bead-0.1.0.dist-info/METADATA +212 -0
  228. bead-0.1.0.dist-info/RECORD +231 -0
  229. bead-0.1.0.dist-info/WHEEL +4 -0
  230. bead-0.1.0.dist-info/entry_points.txt +2 -0
  231. 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
+ }