@adriansteffan/reactive 0.1.2 → 0.1.3

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.
@@ -18,7 +18,19 @@
18
18
  "Bash(npm test:*)",
19
19
  "WebFetch(domain:raw.githubusercontent.com)",
20
20
  "Bash(ls /Users/adriansteffan/Projects/reactive/README*)",
21
- "Bash(ls /Users/adriansteffan/Projects/reactive/*.md)"
21
+ "Bash(ls /Users/adriansteffan/Projects/reactive/*.md)",
22
+ "Bash(npm search:*)",
23
+ "Bash(node -e \"const d=require\\('fs'\\).readFileSync\\('/dev/stdin','utf8'\\); const r=JSON.parse\\(d\\); r.slice\\(0,5\\).forEach\\(p => console.log\\(p.name, '-', p.description, '- v'+p.version\\)\\)\")",
24
+ "Bash(npm info:*)",
25
+ "Bash(node:*)",
26
+ "Bash(npm uninstall:*)",
27
+ "Bash(curl -s 'https://web.maths.unsw.edu.au/~fkuo/sobol/joe-kuo-old.1111')",
28
+ "WebFetch(domain:doi.org)",
29
+ "WebFetch(domain:dl.acm.org)",
30
+ "WebFetch(domain:web.maths.unsw.edu.au)",
31
+ "WebFetch(domain:www.netlib.org)",
32
+ "WebFetch(domain:calgo.acm.org)",
33
+ "Bash(npx vitest:*)"
22
34
  ],
23
35
  "deny": [],
24
36
  "ask": []
package/README.md CHANGED
@@ -38,6 +38,8 @@ Premade components available so far:
38
38
  * Quest: SurveyJS questionnaires
39
39
  * ... all questiontypes supported by SurveyJS can be used
40
40
  * voicerecorder: a custom question type that allows participants to record voice
41
+ * Tutorial: A paginated slide deck with navigation, dot indicators, and optional interactive slides via `useTutorialSlide`
42
+ * RandomDotKinematogram: A random dot kinematogram (RDK) stimulus for motion perception experiments
41
43
  * Upload: Uploads the collected data on a button press by the participant
42
44
 
43
45
 
@@ -52,10 +54,10 @@ Define your participant generator in `Experiment.tsx`:
52
54
 
53
55
  ```tsx
54
56
  export const simulationConfig = {
55
- participants: {
56
- generator: (i) => ({ id: i, nickname: `participant_${i}` }),
57
- count: 10,
58
- },
57
+ seed: 42, // optional: makes simulations fully reproducible
58
+ participants: () => sampleParticipants('sobol', 10, {
59
+ needForCognition: { distribution: 'normal', mean: 3.5, sd: 0.8 },
60
+ }).map((p, i) => ({ ...p, id: i })),
59
61
  };
60
62
  ```
61
63
 
@@ -127,6 +129,64 @@ Trials with `simulators` or `simulate: true` defined on them will auto-advance.
127
129
 
128
130
  Hybrid mode is enabled by default during development. For production, set `VITE_DISABLE_HYBRID_SIMULATION=true` to disable it regardless of URL parameters.
129
131
 
132
+ ### Reproducible simulations
133
+
134
+ Add `seed` to your simulation config to make runs fully reproducible:
135
+
136
+ ```tsx
137
+ export const simulationConfig = {
138
+ seed: 42,
139
+ participants: () => sampleParticipants('sobol', 100, {
140
+ needForCognition: { distribution: 'normal', mean: 3.5, sd: 0.8 },
141
+ agreeableness: { distribution: 'normal', mean: 3.0, sd: 1.0 },
142
+ age: { distribution: 'uniform', min: 18, max: 65 },
143
+ }).map((p, i) => ({ ...p, id: i })),
144
+ };
145
+ ```
146
+
147
+ Each simulated participant runs in its own subprocess. The `seed` controls two separate phases:
148
+
149
+ - **Participant generation**: The `participants` factory function is called with the base `seed` (same for all workers), so every worker generates the same participant list.
150
+ - **Simulation behavior**: Module-level randomness (group assignment, trial order) and simulator callbacks are seeded with `seed + participantIndex`, so each participant gets a unique but reproducible random stream.
151
+
152
+ Without `seed`, simulations use random entropy and will vary between runs. When seeded, `Math.random()` is also patched to use the seeded PRNG, so existing code and third-party libraries are automatically reproducible.
153
+
154
+ **Note:** Do not close over module-level random values into the factory function — module-level code runs with a per-worker seed, so captured values would differ between workers. Keep all participant generation logic inside the factory.
155
+
156
+ ```tsx
157
+ // Good: all randomness is inside the factory
158
+ participants: () => {
159
+ const base = sampleParticipants('random', 100, {
160
+ openness: { distribution: 'normal', mean: 3.5, sd: 0.8 },
161
+ });
162
+ return base.map((p, i) => ({
163
+ ...p,
164
+ curiosity: p.openness * 0.7 + normal(0, 0.3),
165
+ id: i,
166
+ }));
167
+ },
168
+
169
+ // Bad: module-level value captured into factory
170
+ const noise = normal(0, 1); // different per worker!
171
+ participants: () => [{ trait: noise }],
172
+ ```
173
+
174
+ To model distinct populations, combine them inside the factory:
175
+
176
+ ```tsx
177
+ participants: () => {
178
+ const smokers = sampleParticipants('sobol', 50, {
179
+ alertness: { distribution: 'normal', mean: 2.8, sd: 0.7 },
180
+ }).map((p) => ({ ...p, smoker: true }));
181
+
182
+ const nonSmokers = sampleParticipants('sobol', 50, {
183
+ alertness: { distribution: 'normal', mean: 3.6, sd: 0.5 },
184
+ }).map((p) => ({ ...p, smoker: false }));
185
+
186
+ return [...smokers, ...nonSmokers].map((p, i) => ({ ...p, id: i }));
187
+ },
188
+ ```
189
+
130
190
  ### Built-in simulator decision functions
131
191
 
132
192
  | Component | Decision functions | Default behavior |
@@ -135,6 +195,8 @@ Hybrid mode is enabled by default during development. For production, set `VITE_
135
195
  | PlainInput | `respond` | Returns `'simulated_input'` |
136
196
  | Quest | `answerQuestion` | Random valid answer per question type |
137
197
  | CanvasBlock | `respondToSlide` | Random key from `allowedKeys`, random RT |
198
+ | Tutorial | `respondToSlide` | Advances through slides, no interaction data |
199
+ | RandomDotKinematogram | `respond` | Random key from `validKeys`, random RT (may timeout) |
138
200
  | Upload | *(none)* | Builds CSVs and POSTs to backend |
139
201
  | StoreUI | *(none)* | Uses field default values |
140
202
  | CheckDevice | *(none)* | Returns simulated device info |
@@ -169,7 +231,9 @@ Built-in components come pre-registered. The Upload component produces CSVs auto
169
231
  | Text | `text` | One row per Text component |
170
232
  | CanvasBlock | `canvas` | One row per slide, with built-in flattener |
171
233
  | StoreUI | `storeui` | One row per StoreUI occurrence |
172
- | ProlificEnding, Upload | *(none)* | No CSV output |
234
+ | Tutorial | `session` | Merged into session row |
235
+ | RandomDotKinematogram | `rdk` | One row per RDK trial |
236
+ | ProlificEnding, Upload, RequestFilePermission | *(none)* | No CSV output |
173
237
 
174
238
  ### Output files
175
239
 
@@ -277,6 +341,121 @@ Add `metadata` to timeline items to include extra columns in every CSV row that
277
341
  For session-level items, metadata is namespaced by trial name (e.g. `block1_difficulty`). For non-session items, metadata columns appear unprefixed.
278
342
 
279
343
 
344
+ ## Utilities
345
+
346
+ Reactive exports helper functions for common experiment-building tasks.
347
+
348
+ ```tsx
349
+ import { shuffle, sample, chunk, pipe, normal, uniform, poisson, seedDistributions, sobol, halton, sampleParticipants } from '@adriansteffan/reactive';
350
+ ```
351
+
352
+ ### Array functions
353
+
354
+ These are available both as standalone functions and as Array prototype extensions (after calling `registerArrayExtensions()`).
355
+
356
+ | Function | Signature | Description |
357
+ |---|---|---|
358
+ | `shuffle` | `shuffle(arr)` | Returns a new array with elements randomly reordered (Fisher-Yates) |
359
+ | `sample` | `sample(arr, n?)` | Returns `n` random elements from the array (default 1, with replacement) |
360
+ | `chunk` | `chunk(arr, n)` | Splits the array into `n` roughly equal chunks |
361
+ | `pipe` | `pipe(arr, fn)` | Passes the array to `fn` and returns the result |
362
+
363
+ As prototype methods:
364
+
365
+ ```tsx
366
+ import { registerArrayExtensions } from '@adriansteffan/reactive';
367
+ registerArrayExtensions();
368
+
369
+ const trials = [1, 2, 3, 4, 5].shuffle();
370
+ const picked = trials.sample(2);
371
+ const blocks = trials.chunk(3);
372
+ ```
373
+
374
+ ### Distributions
375
+
376
+ Random number generators backed by [@stdlib](https://github.com/stdlib-js/stdlib), useful for writing realistic simulations.
377
+
378
+ | Function | Signature | Description |
379
+ |---|---|---|
380
+ | `uniform` | `uniform(a, b)` | Sample from a continuous uniform distribution over `[a, b)` |
381
+ | `normal` | `normal(mu, sigma)` | Sample from a normal (Gaussian) distribution with mean `mu` and standard deviation `sigma` |
382
+ | `poisson` | `poisson(lambda)` | Sample from a Poisson distribution with rate `lambda` |
383
+
384
+ All built-in simulation functions use these distributions internally.
385
+
386
+ #### Global seeding
387
+
388
+ Call `seedDistributions` to seed all three generators from a single seed, making simulation runs fully reproducible:
389
+
390
+ ```tsx
391
+ import { seedDistributions } from '@adriansteffan/reactive';
392
+
393
+ seedDistributions(42);
394
+ // All subsequent calls to normal(), uniform(), poisson() produce the same sequence
395
+ ```
396
+
397
+ Without seeding, the generators use random entropy (non-reproducible, same as `Math.random()`).
398
+
399
+ ### Quasi-Monte Carlo sequences
400
+
401
+ Low-discrepancy sequences for more uniform coverage of parameter spaces than pseudorandom sampling. Useful for generating participant parameters in simulations.
402
+
403
+ Both `sobol` and `halton` take a count and an array of dimension specs. Each dimension describes a distribution to sample from.
404
+
405
+ | Function | Signature | Description |
406
+ |---|---|---|
407
+ | `sobol` | `sobol(count, specs)` | Generate `count` points using a Sobol sequence. Supports 1–21 dimensions. |
408
+ | `halton` | `halton(count, specs)` | Generate `count` points using a Halton sequence (auto-selects prime bases). |
409
+
410
+ Each dimension spec is one of:
411
+
412
+ | Distribution | Spec | Description |
413
+ |---|---|---|
414
+ | Uniform | `{ distribution: 'uniform', min, max }` | Uniform over `[min, max)` |
415
+ | Normal | `{ distribution: 'normal', mean, sd }` | Gaussian with given mean and standard deviation |
416
+ | Poisson | `{ distribution: 'poisson', mean }` | Poisson with given mean (discrete) |
417
+
418
+ For a single dimension, both return a flat `number[]`. For multiple dimensions, they return `number[][]`.
419
+
420
+ ```tsx
421
+ import { sobol, halton } from '@adriansteffan/reactive';
422
+
423
+ // Uniform
424
+ sobol(5, [{ distribution: 'uniform', min: 200, max: 800 }]);
425
+
426
+ // Normal: 10 reaction times ~ N(500, 100)
427
+ sobol(10, [{ distribution: 'normal', mean: 500, sd: 100 }]);
428
+
429
+ // Poisson: 8 counts ~ Poisson(5)
430
+ sobol(8, [{ distribution: 'poisson', mean: 5 }]);
431
+
432
+ // Multi-dimensional: uniform RT + normally distributed threshold
433
+ sobol(5, [
434
+ { distribution: 'uniform', min: 200, max: 800 },
435
+ { distribution: 'normal', mean: 0.5, sd: 0.1 },
436
+ ]);
437
+
438
+ // Halton — same API, different sequence
439
+ halton(5, [{ distribution: 'uniform', min: 200, max: 800 }]);
440
+ ```
441
+
442
+ #### Sampling participants
443
+
444
+ `sampleParticipants` wraps the QMC sequences into a convenient API for generating participant parameter sets. Each key in the spec becomes a named field on the returned objects.
445
+
446
+ ```tsx
447
+ import { sampleParticipants } from '@adriansteffan/reactive';
448
+
449
+ const participants = sampleParticipants('sobol', 100, {
450
+ needForCognition: { distribution: 'normal', mean: 3.5, sd: 0.8 },
451
+ agreeableness: { distribution: 'normal', mean: 3.0, sd: 1.0 },
452
+ }).map((p, i) => ({ ...p, id: i }));
453
+ // → [{ needForCognition: 3.5, agreeableness: 3.0, id: 0 }, ...]
454
+ ```
455
+
456
+ The first argument is the sampling method: `'sobol'`, `'halton'`, or `'random'`.
457
+
458
+
280
459
  ## Development
281
460
 
282
461