bead.deployment

Stage 5 of the bead pipeline: jsPsych 8.x batch experiment generation for JATOS.

Distribution Strategies

distribution

List distribution configuration and strategies for batch experiments.

Models for configuring list distribution strategies in JATOS batch experiments. Supports eight distribution strategies (random, sequential, balanced, latin square, stratified, weighted random, quota-based, and metadata-based) for assigning participants to experiment lists.

DistributionStrategyType

Bases: StrEnum

Named distribution strategies for list assignment.

QuotaConfig

Bases: BeadBaseModel

Configuration for quota-based assignment.

Attributes:

Name Type Description
participants_per_list int

Target participants per list (> 0).

allow_overflow bool

Allow continued assignment after quota is reached.

WeightedRandomConfig

Bases: BeadBaseModel

Configuration for weighted random assignment.

Attributes:

Name Type Description
weight_expression str

JavaScript expression evaluated with list_metadata in scope.

normalize_weights bool

Whether to normalize weights to sum to 1.0.

LatinSquareConfig

Bases: BeadBaseModel

Configuration for Latin square counterbalancing.

Attributes:

Name Type Description
balanced bool

Use a balanced Latin square (Bradley's 1958 algorithm).

MetadataBasedConfig

Bases: BeadBaseModel

Configuration for metadata-based assignment.

Attributes:

Name Type Description
filter_expression str | None

JavaScript boolean expression filtering eligible lists.

rank_expression str | None

JavaScript expression ranking lists.

rank_ascending bool

Sort ascending when using rank_expression.

StratifiedConfig

Bases: BeadBaseModel

Configuration for stratified assignment.

Attributes:

Name Type Description
factors tuple[str, ...]

Metadata keys used as stratification factors. Must be non-empty and distinct.

ListDistributionStrategy

Bases: BeadBaseModel

Configuration for list distribution in batch experiments.

Attributes:

Name Type Description
strategy_type DistributionStrategyType

Type of distribution strategy.

strategy_config dict[str, JsonValue]

Strategy-specific configuration parameters.

max_participants int | None

Maximum total participants across all lists.

error_on_exhaustion bool

Raise an error when max_participants is reached.

debug_mode bool

Always assign the same list (development aid).

debug_list_index int

List index to use in debug mode (>= 0).

validate_metadata_based_config(config: MetadataBasedConfig) -> None

Raise ValueError if neither expression is supplied.

validate_list_distribution_strategy(strategy: ListDistributionStrategy) -> None

Raise ValueError if strategy's config doesn't match its type.

jsPsych Experiment Generation

generator

jsPsych batch experiment generator.

Generates complete jsPsych 8.x experiments using JATOS batch sessions for server-side list distribution.

JsPsychExperimentGenerator

Generator for jsPsych 8.x experiments.

This class orchestrates the generation of complete jsPsych experiments, including HTML, CSS, JavaScript, and data files. It converts bead's ExperimentList and Item models into a deployable jsPsych experiment.

Parameters:

Name Type Description Default
config ExperimentConfig

Experiment configuration.

required
output_dir Path

Output directory for generated files.

required
rating_config RatingScaleConfig | None

Configuration for rating scale trials (required for rating experiments). Defaults to RatingScaleConfig() if not provided.

None
choice_config ChoiceConfig | None

Configuration for choice trials (required for choice experiments). Defaults to ChoiceConfig() if not provided.

None

Attributes:

Name Type Description
config ExperimentConfig

Experiment configuration.

output_dir Path

Output directory for generated files.

rating_config RatingScaleConfig

Configuration for rating scale trials.

choice_config ChoiceConfig

Configuration for choice trials.

jinja_env Environment

Jinja2 environment for template rendering.

Examples:

>>> from pathlib import Path
>>> config = ExperimentConfig(
...     experiment_type="likert_rating",
...     title="Acceptability Study",
...     description="Rate sentences",
...     instructions="Rate each sentence from 1 to 7"
... )
>>> generator = JsPsychExperimentGenerator(
...     config=config,
...     output_dir=Path("/tmp/experiment")
... )
>>> # generator.generate(lists, items)

generate(lists: list[ExperimentList], items: dict[UUID, Item], templates: dict[UUID, ItemTemplate]) -> Path

Generate complete jsPsych batch experiment.

Creates a unified batch experiment that uses JATOS batch sessions for server-side list distribution. All participants are automatically assigned to lists according to the distribution strategy specified in the experiment configuration.

Parameters:

Name Type Description Default
lists list[ExperimentList]

Experiment lists for batch distribution (required, must be non-empty). All lists will be serialized to lists.jsonl and made available for participant assignment.

required
items dict[UUID, Item]

Dictionary of items keyed by UUID (required, must be non-empty). All items referenced by lists must be present in this dictionary.

required
templates dict[UUID, ItemTemplate]

Dictionary of item templates keyed by UUID (required, must be non-empty). All templates referenced by items must be present in this dictionary.

required

Returns:

Type Description
Path

Path to the generated experiment directory containing: - index.html - js/experiment.js, js/list_distributor.js - css/experiment.css - data/config.json, data/lists.jsonl, data/items.jsonl, data/distribution.json

Raises:

Type Description
ValueError

If lists is empty, items is empty, templates is empty, or if any referenced UUIDs are not found in the provided dictionaries.

SerializationError

If writing JSONL files fails.

Examples:

>>> from pathlib import Path
>>> from bead.deployment.distribution import (
...     ListDistributionStrategy, DistributionStrategyType
... )
>>> strategy = ListDistributionStrategy(
...     strategy_type=DistributionStrategyType.BALANCED
... )
>>> config = ExperimentConfig(
...     experiment_type="forced_choice",
...     title="Test",
...     description="Test",
...     instructions="Test",
...     distribution_strategy=strategy
... )
>>> generator = JsPsychExperimentGenerator(
...     config=config, output_dir=Path("/tmp/exp")
... )
>>> # output_dir = generator.generate(lists, items, templates)

config

Configuration models for jsPsych experiment generation.

SpanDisplayConfig

Bases: Model

Visual configuration for span rendering.

Attributes:

Name Type Description
highlight_style Literal['background', 'underline', 'border']

How to visually indicate spans.

color_palette tuple[str, ...]

CSS colors for span highlighting (light backgrounds).

dark_color_palette tuple[str, ...]

CSS colors for subscript label badges (dark, index-aligned with color_palette).

show_labels bool

Show span labels inline.

show_tooltips bool

Show tooltips on hover.

token_delimiter str

Delimiter between tokens in the display.

label_position Literal['inline', 'below', 'tooltip']

Where span labels are placed.

DemographicsFieldConfig

Bases: Model

Configuration for a single demographics form field.

Attributes:

Name Type Description
name str

Field name (used as key in collected data).

field_type Literal['text', 'number', 'dropdown', 'radio', 'checkbox']

Type of form input.

label str

Display label.

required bool

Whether the field is required.

options tuple[str, ...] | None

Options for dropdown / radio fields.

range Range[float] | None

Numeric range constraint for number fields.

placeholder str | None

Placeholder text.

help_text str | None

Help text displayed below the field.

DemographicsConfig

Bases: Model

Configuration for the participant demographics form.

Attributes:

Name Type Description
enabled bool

Show the demographics form.

title str

Form title.

fields tuple[DemographicsFieldConfig, ...]

Fields to include in the form.

submit_button_text str

Submit button label.

InstructionPage

Bases: Model

A single instruction page.

Attributes:

Name Type Description
content str

HTML content.

title str | None

Optional page title.

InstructionsConfig

Bases: Model

Configuration for multi-page experiment instructions.

Attributes:

Name Type Description
pages tuple[InstructionPage, ...]

Instruction pages.

show_page_numbers bool

Show page numbers.

allow_backwards bool

Allow navigation to previous pages.

button_label_next str

Label for the next button.

button_label_finish str

Label for the final button.

from_text(text: str) -> InstructionsConfig classmethod

Build a single-page instructions config from plain text or HTML.

ExperimentConfig

Bases: Model

Configuration for jsPsych experiment generation.

Attributes:

Name Type Description
experiment_type ExperimentType

Type of experiment.

title str

Experiment title.

description str

Brief description.

instructions InstructionsConfig

Instructions shown to participants. Use InstructionsConfig.from_text("...") for a single-page instruction set built from a plain string.

distribution_strategy ListDistributionStrategy

List distribution strategy for batch mode.

demographics DemographicsConfig | None

Demographics form shown before instructions.

randomize_trial_order bool

Randomize trial order.

show_progress_bar bool

Show a progress bar.

ui_theme UITheme

UI theme.

on_finish_url str | None

URL to redirect to after completion.

allow_backwards bool

Allow navigation to previous trials.

show_click_target bool

Show click target for accuracy tracking.

minimum_duration_ms int

Minimum trial duration in milliseconds (>= 0).

use_jatos bool

Enable JATOS integration.

prolific_completion_code str | None

Prolific completion code; when set, on_finish_url is auto-generated.

slopit SlopitIntegrationConfig

Slopit behavioral capture integration.

span_display SpanDisplayConfig | None

Span display configuration; auto-enabled when items contain span annotations.

RatingScaleConfig

Bases: Model

Configuration for rating-scale trials.

Attributes:

Name Type Description
scale Range[int]

Numeric range for the rating scale.

min_label str

Label for the minimum value.

max_label str

Label for the maximum value.

step int

Step size between values (>= 1).

show_numeric_labels bool

Show numeric labels on the scale.

required bool

Whether a response is required.

ChoiceConfig

Bases: Model

Configuration for choice trials.

Attributes:

Name Type Description
button_html str | None

Custom HTML for choice buttons.

required bool

Whether a response is required.

randomize_choice_order bool

Randomize the order of choices.

layout Literal['horizontal', 'vertical']

Button layout.

trials

Trial generators for jsPsych experiments.

This module provides functions to generate jsPsych trial objects from Item models. It supports various trial types including rating scales, forced choice, binary choice, and span labeling trials. Composite tasks (e.g., rating with span highlights) are also supported.

SpanColorMap dataclass

Light and dark color assignments for spans.

Attributes:

Name Type Description
light_by_span_id dict[str, str]

Light (background) colors keyed by span_id.

dark_by_span_id dict[str, str]

Dark (badge) colors keyed by span_id.

light_by_label dict[str, str]

Light (background) colors keyed by label name.

dark_by_label dict[str, str]

Dark (badge) colors keyed by label name.

create_trial(item: Item, template: ItemTemplate, experiment_config: ExperimentConfig, trial_number: int, rating_config: RatingScaleConfig | None = None, choice_config: ChoiceConfig | None = None) -> dict[str, JsonValue]

Create a jsPsych trial object from an Item.

Parameters:

Name Type Description Default
item Item

The item to create a trial from.

required
template ItemTemplate

The item template for this item.

required
experiment_config ExperimentConfig

The experiment configuration.

required
trial_number int

The trial number (for tracking).

required
rating_config RatingScaleConfig | None

Configuration for rating scale trials (required for rating types).

None
choice_config ChoiceConfig | None

Configuration for choice trials (required for choice types).

None

Returns:

Type Description
dict[str, JsonValue]

A jsPsych trial object with item and template metadata.

Raises:

Type Description
ValueError

If required configuration is missing for the experiment type.

Examples:

>>> from uuid import UUID
>>> from bead.items.item_template import TaskSpec, PresentationSpec
>>> item = Item(
...     item_template_id=UUID("12345678-1234-5678-1234-567812345678"),
...     rendered_elements={"sentence": "The cat broke the vase"}
... )
>>> template = ItemTemplate(
...     name="test",
...     judgment_type="acceptability",
...     task_type="ordinal_scale",
...     task_spec=TaskSpec(prompt="Rate this"),
...     presentation_spec=PresentationSpec(mode="static")
... )
>>> config = ExperimentConfig(
...     experiment_type="likert_rating",
...     title="Test",
...     description="Test",
...     instructions="Test"
... )
>>> rating_config = RatingScaleConfig()
>>> trial = create_trial(item, template, config, 0, rating_config=rating_config)
>>> trial["type"]
'bead-slider-rating'

Create a consent trial.

Parameters:

Name Type Description Default
consent_text str

The consent text to display.

required

Returns:

Type Description
dict[str, JsonValue]

A jsPsych html-button-response trial object.

create_completion_trial(completion_message: str = 'Thank you for participating!') -> dict[str, JsonValue]

Create a completion trial.

Parameters:

Name Type Description Default
completion_message str

The completion message to display.

'Thank you for participating!'

Returns:

Type Description
dict[str, JsonValue]

A jsPsych html-keyboard-response trial object.

create_demographics_trial(config: DemographicsConfig) -> dict[str, JsonValue]

Create a demographics survey trial.

Parameters:

Name Type Description Default
config DemographicsConfig

The demographics form configuration.

required

Returns:

Type Description
dict[str, JsonValue]

A jsPsych survey trial object.

Examples:

>>> from bead.deployment.jspsych.config import (
...     DemographicsConfig, DemographicsFieldConfig
... )
>>> config = DemographicsConfig(
...     enabled=True,
...     title="About You",
...     fields=[
...         DemographicsFieldConfig(
...             name="age",
...             field_type="number",
...             label="Your Age",
...             required=True,
...         ),
...     ],
... )
>>> trial = create_demographics_trial(config)
>>> trial["type"]
'survey'

create_instructions_trial(instructions: str | InstructionsConfig) -> dict[str, JsonValue]

Create an instruction trial supporting both simple strings and rich config.

Parameters:

Name Type Description Default
instructions str | InstructionsConfig

Either a simple instruction string (single page, keyboard response) or an InstructionsConfig for multi-page instructions.

required

Returns:

Type Description
dict[str, JsonValue]

A jsPsych trial object. For simple strings, returns html-keyboard-response. For InstructionsConfig, returns an instructions plugin trial.

Examples:

>>> # Simple string instructions
>>> trial = create_instructions_trial("Rate each sentence from 1-7.")
>>> trial["type"]
'html-keyboard-response'
>>> # Multi-page instructions
>>> from bead.deployment.jspsych.config import InstructionsConfig, InstructionPage
>>> config = InstructionsConfig(
...     pages=[
...         InstructionPage(title="Welcome", content="<p>Welcome!</p>"),
...         InstructionPage(title="Task", content="<p>Rate sentences.</p>"),
...     ],
... )
>>> trial = create_instructions_trial(config)
>>> trial["type"]
'instructions'
>>> len(trial["pages"])
2

randomizer

JavaScript randomizer code generator from OrderingConstraints.

This module converts Python OrderingConstraint models into JavaScript code that performs constraint-aware trial randomization at jsPsych runtime. This enables per-participant randomization while satisfying all ordering constraints.

generate_randomizer_function(item_ids: list[UUID], constraints: list[OrderingConstraint], metadata: dict[UUID, dict[str, JsonValue]]) -> str

Generate JavaScript code for constraint-aware trial randomization.

This function converts OrderingConstraints into JavaScript code that can randomize trial order at runtime while satisfying all constraints. The generated code uses seeded randomization for reproducibility and rejection sampling to satisfy constraints.

Parameters:

Name Type Description Default
item_ids list[UUID]

List of item IDs included in the experiment.

required
constraints list[OrderingConstraint]

Ordering constraints to enforce.

required
metadata dict[UUID, dict[str, JsonValue]]

Item metadata needed for constraint checking (keyed by item UUID).

required

Returns:

Type Description
str

JavaScript code implementing randomizeTrials() function.

Examples:

>>> from uuid import UUID
>>> item1 = UUID("12345678-1234-5678-1234-567812345678")
>>> item2 = UUID("87654321-4321-8765-4321-876543218765")
>>> constraint = OrderingConstraint(constraint_type="ordering",
...     no_adjacent_property="item_metadata.condition"
... )
>>> metadata = {
...     item1: {"condition": "A"},
...     item2: {"condition": "B"}
... }
>>> js_code = generate_randomizer_function(
...     [item1, item2],
...     [constraint],
...     metadata
... )
>>> "function randomizeTrials" in js_code
True
>>> "checkNoAdjacentConstraints" in js_code
True

JATOS Export

exporter

JATOS exporter for jsPsych experiments.

This module provides the JATOSExporter class for creating JATOS study packages (.jzip) from generated jsPsych experiments.

JATOSExporter

Bases: BeadBaseModel

Exports jsPsych experiments as JATOS study packages (.jzip).

A .jzip file is a ZIP archive containing: - study.json: JATOS metadata - experiment/: All experiment files (HTML, JS, CSS, data)

Attributes:

Name Type Description
study_title str

Title of the JATOS study.

study_description str

Description of the study.

Examples:

>>> from pathlib import Path
>>> exporter = JATOSExporter("Test Study", "A test study")
>>> # exporter.export(Path("experiment"), Path("study.jzip"))

export(experiment_dir: Path, output_path: Path, component_title: str = 'Main Experiment') -> None

Create JATOS .jzip file.

Parameters:

Name Type Description Default
experiment_dir Path

Directory containing experiment files (from JsPsychExperimentGenerator). Expected structure: - index.html - css/experiment.css - js/experiment.js - data/timeline.json - data/config.json

required
output_path Path

Output path for .jzip file.

required
component_title str

Title for the JATOS component.

'Main Experiment'

Raises:

Type Description
ValueError

If experiment_dir does not exist or is missing required files.

FileNotFoundError

If required experiment files are not found.

Examples:

>>> exporter = JATOSExporter("Test Study")
>>> exporter.export(Path("exp"), Path("study.jzip"))

api

JATOS REST API client.

This module provides the JATOSClient class for interacting with JATOS servers via the REST API.

JATOSClient

Client for JATOS REST API.

Supports uploading study packages (.jzip), listing studies, deleting studies, and retrieving study results.

Parameters:

Name Type Description Default
base_url str

Base URL for JATOS instance (e.g., "https://jatos.example.com").

required
api_token str

API token for authentication.

required

Attributes:

Name Type Description
base_url str

Base URL for JATOS instance (trailing slash removed).

api_token str

API token for authentication.

session Session

HTTP session with authentication headers configured.

Examples:

>>> client = JATOSClient("https://jatos.example.com", "my-api-token")
>>> # studies = client.list_studies()

upload_study(jzip_path: Path) -> dict[str, JsonValue]

Upload study package to JATOS.

POST /api/v1/studies

Parameters:

Name Type Description Default
jzip_path Path

Path to .jzip file to upload.

required

Returns:

Type Description
dict[str, JsonValue]

Response with study ID, UUID, and URL.

Raises:

Type Description
HTTPError

If the upload fails.

FileNotFoundError

If the .jzip file does not exist.

Examples:

>>> client = JATOSClient("https://jatos.example.com", "token")
>>> # result = client.upload_study(Path("study.jzip"))
>>> # print(result["id"])

list_studies() -> list[dict[str, JsonValue]]

List all studies.

GET /api/v1/studies

Returns:

Type Description
list[dict[str, JsonValue]]

List of study dictionaries.

Raises:

Type Description
HTTPError

If the request fails.

Examples:

>>> client = JATOSClient("https://jatos.example.com", "token")
>>> # studies = client.list_studies()
>>> # print(len(studies))

get_study(study_id: int) -> dict[str, JsonValue]

Get study details.

GET /api/v1/studies/{study_id}

Parameters:

Name Type Description Default
study_id int

Study ID.

required

Returns:

Type Description
dict[str, JsonValue]

Study details dictionary.

Raises:

Type Description
HTTPError

If the request fails.

Examples:

>>> client = JATOSClient("https://jatos.example.com", "token")
>>> # study = client.get_study(123)
>>> # print(study["title"])

delete_study(study_id: int) -> None

Delete study.

DELETE /api/v1/studies/{study_id}

Parameters:

Name Type Description Default
study_id int

Study ID to delete.

required

Raises:

Type Description
HTTPError

If the request fails.

Examples:

>>> client = JATOSClient("https://jatos.example.com", "token")
>>> # client.delete_study(123)

get_results(study_id: int) -> list[int]

Get all result IDs for a study.

GET /api/v1/studies/{study_id}/results

Parameters:

Name Type Description Default
study_id int

Study ID.

required

Returns:

Type Description
list[int]

List of result IDs.

Raises:

Type Description
HTTPError

If the request fails.

Examples:

>>> client = JATOSClient("https://jatos.example.com", "token")
>>> # result_ids = client.get_results(123)
>>> # print(len(result_ids))

Protocol-Layer Bridge

Single canonical bridge from a configured :class:~bead.protocol.AnnotationProtocol and a sequence of :class:~bead.protocol.ProtocolContext records to a flat list of jsPsych trial dicts.

protocol_trials

Bridge from the protocol layer to jsPsych deployment.

End-to-end path from a configured :class:AnnotationProtocol and a sequence of :class:~bead.protocol.ProtocolContext records to a list of jsPsych trial objects ready for batch deployment.

This is the canonical bridge to deployment. There is no other way to materialize a protocol-defined experiment.

protocol_to_jspsych_trials(protocol: AnnotationProtocol, contexts: Iterable[ProtocolContext], *, experiment_config: ExperimentConfig, judgment_type: JudgmentType, presentation_spec: PresentationSpec | None = None, rating_config: RatingScaleConfig | None = None, choice_config: ChoiceConfig | None = None) -> list[dict[str, JsonValue]]

Materialize an entire protocol as a flat list of jsPsych trials.

Each :class:ProtocolContext is realized through every applicable :class:~bead.protocol.QuestionFamily. Each resulting realization is packaged as an :class:~bead.items.item.Item bound to the family's :class:ItemTemplate and turned into a jsPsych trial via :func:bead.deployment.jspsych.trials.create_trial. Trials are returned in (context_order, family_order) order: every realized question for the first context comes first, then the second context, and so on.

Parameters:

Name Type Description Default
protocol AnnotationProtocol

Configured protocol whose families to realize.

required
contexts Iterable[ProtocolContext]

Contexts to realize, one per annotation target.

required
experiment_config ExperimentConfig

Shared experiment configuration applied to every trial.

required
judgment_type JudgmentType

Common judgment type assigned to every per-family :class:ItemTemplate.

required
presentation_spec PresentationSpec | None

Common presentation spec across families. Defaults to a fresh :class:PresentationSpec per template.

None
rating_config RatingScaleConfig | None

Configuration for rating-scale trials (ordinal task type).

None
choice_config ChoiceConfig | None

Configuration for choice trials (binary, categorical, or forced-choice task types).

None

Returns:

Type Description
list[dict[str, JsonValue]]

Flat list of jsPsych trial dicts in trial_number order.