Markov Chain Monte Carlo with People (MCMCP)

Markov Chain Monte Carlo with People (MCMCP) is an adaptive procedure related to Gibbs Sampling with People (GSP). Like GSP, it is intended to map participants’ associations of a stimulus space. In each trial, the participant is presented with a pair of stimuli: a ‘current state’ and a ‘proposal state’. They are asked to decide which stimulus best matches a given criterion. The chosen stimulus is then accepted as the next state, and a new proposal is generated from that state by making a small random jump in the stimulus space.

Source: demos/mcmcp

# pylint: disable=unused-import,abstract-method,unused-argument

import random

import psynet.experiment
from psynet.consent import MainConsent
from psynet.modular_page import Prompt, PushButtonControl
from psynet.page import InfoPage, ModularPage, SuccessfulEndPage
from psynet.timeline import Timeline
from psynet.trial.mcmcp import MCMCPNode, MCMCPTrial, MCMCPTrialMaker
from psynet.utils import get_logger

logger = get_logger()

from .test_imports import CustomCls  # noqa -- this is to test custom class import

MAX_AGE = 100
OCCUPATIONS = ["doctor", "babysitter", "teacher"]
SAMPLE_RANGE = 5


class CustomTrial(MCMCPTrial):
    time_estimate = 5

    def show_trial(self, experiment, participant):
        occupation = self.context["occupation"]
        age_1 = self.first_stimulus["age"]
        age_2 = self.second_stimulus["age"]
        prompt = (
            f"Person A is {age_1} years old. "
            f"Person B is {age_2} years old. "
            f"Which one is the {occupation}?"
        )
        return ModularPage(
            "mcmcp_trial",
            Prompt(prompt),
            control=PushButtonControl(
                ["0", "1"], labels=["Person A", "Person B"], arrange_vertically=False
            ),
            time_estimate=self.time_estimate,
        )


class CustomNode(MCMCPNode):
    def create_initial_seed(self, experiment, participant):
        return {"age": random.randint(0, MAX_AGE)}

    def get_proposal(self, state, experiment, participant):
        age = state["age"] + random.randint(-SAMPLE_RANGE, SAMPLE_RANGE)
        age = age % (MAX_AGE + 1)
        return {"age": age}


def start_nodes(participant):
    return [
        CustomNode(
            context={
                "occupation": occupation,
            },
        )
        for occupation in OCCUPATIONS
    ]


class Exp(psynet.experiment.Experiment):
    label = "MCMCP demo experiment"

    timeline = Timeline(
        MainConsent(),
        MCMCPTrialMaker(
            id_="mcmcp_demo",
            start_nodes=start_nodes,
            trial_class=CustomTrial,
            node_class=CustomNode,
            chain_type="within",  # can be "within" or "across"
            expected_trials_per_participant=9,
            max_trials_per_participant=9,
            chains_per_participant=3,  # set to None if chain_type="across"
            chains_per_experiment=None,  # set to None if chain_type="within"
            max_nodes_per_chain=3,
            trials_per_node=1,
            balance_across_chains=True,
            check_performance_at_end=False,
            check_performance_every_trial=False,
            fail_trials_on_participant_performance_check=True,
            recruit_mode="n_participants",
            target_n_participants=1,
        ),
        InfoPage("You finished the experiment!", time_estimate=0),
        SuccessfulEndPage(),
    )

    def __init__(self, session=None):
        super().__init__(session)
        self.initial_recruitment_size = 1