Experiment

This module contains the main experiment and experiment session objects.

What’s the difference between the Experiment and the ExperimentSession?

  • The Experiment class is the first thing you create in your script.py. Its purpose is simply to collect all pages and functions that you add to the experiment. For this purpose it offers decorators like setup and member. From this collection, it creates a new ExperimentSession for every new subject.

  • The ExperimentSession class is the main experiment organizer. This is the class that actually coordinates all the work in an ongoing experiment. When you interact with a Section’s or a Page’s exp attribute, you are actually interacting with the current ExperimentSession object.

class alfred3.experiment.Experiment

Bases: object

Holds pages and section and creates experiment sessions.

Experiment members can be accessed via dictionary-style square brackets and added with the augmented assigment operator += (see exampples).

The class acts as an organizing container for pages and section for orderly instantiation of the ExperimentSession.

Examples

Create an experiment and add simple page in linear style

>>> exp = al.Experiment()
>>> exp += al.Page(name="page1")
>>> exp.members
{"page1": Page(class="Page", name="page1")}
>>> exp["page1"]
Page(class="Page", name="page1")

New in version 2.0.

members: dict = {}

A dictionary of all pages and sections added to the experiment.

setup_functions: List[callable] = []

A list of function that will be called upon creation of an experiment session. They are added with the setup() decorator

finish_functions: List[callable] = []

A list of function that will be called upon finishing an experiment session. They are added with the finish() decorator

setup(func)

Decorator for functions that work on the experiment session.

The decorated function can have an arbitrary name. It must take an ExperimentSession object as its only argument (usually spelled as exp). You can use this decorator on as many function as you like.

The purpose of this decorator is to allow manipulation of the ExperimentSession object generated by Experiment.

Examples

In this example, we use the the @exp.setup decorator to add a plugin to the experiment session’s plugin dictionary and access that same plugin later in a page hook.

>>> import alfred3 as al
>>> exp = al.Experiment()
...
>>> @exp.setup
>>> def setup(exp):  # the decorated function can have any name
...     exp.plugins["a"] = "mock plugin"
...
>>> @exp.member
>>> class HelloWorld(al.Page):
...     name = "hello_world"
...
...     def on_exp_access(self):
...         print(self.exp.plugins["a"])
mock plugin
finish(func)

Decorator for code to be run upon ExperimentSession.finish().

The decorated function can have an arbitrary name. It must take an ExperimentSession object as its only argument (usually spelled as exp). You can use this decorator on as many functions as you like.

The purpose of this decorator is to allow manipulation of the ExperimentSession object generated by Experiment a last time before the final page is shown. The decorated functions are the first things to be called in the ExperimentSession.finish() method.

A common usecase would be to conditionally assign the experiment’s final page based on subject input during the experiment.

Examples

Using the @exp.finish decorator for conditionally changing the final page:

import alfred3 as al

exp = al.Experiment()

exp += al.Page(name="page1")
exp.page1 += al.TextEntry("Enter text", name="text1")

@exp.finish
def set_final_page(exp):  # the decorated function can have any name
    if exp.values["text1"] == "value":
       exp.final_page = al.Page("Option A", name="final_page_a")
   else:
       exp.final_page = al.Page("Option B", name="final_page_b")
member(_member=None, *, of_section: str = '_content')

Decorator for adding pages and sections to the experiment.

Works both with and without arguments.

Parameters

of_section – Name of the section to which the new member belongs.

Examples

Adding a page directly to the main content section:

>>> exp = al.Experiment()
...
>>> @exp.member
>>> class HelloWorld(al.Page):
...     name = "hello_world"
...
...     def on_exp_access(self):
...         self += al.Text("This is a 'hello, world!' Page.")
...
>>> exp.members
{"hello_world": Page(class="HelloWorld", name="hello_world")}

Adding a page to a specific section:

>>> exp = al.Experiment()
...
>>> exp += al.Section(name="main")
>>> @exp.member(of_section="main")
>>> class HelloWorld(al.Page):
...     name = "hello_world"
...     title = "Hello, World!"
...     def on_exp_access(self):
...         self += al.Text("This is a 'hello, world!' Page.")
...
>>> exp["hello_world"]
Page(class="HelloWorld", name="hello_world")
property final_page

The experiment’s final page.

You can set this property to a Page class or Page instance directly. For more complex final pages, we recommend the use of the as_final_page() decorator.

Returns

The experiment’s final page.

Return type

page.Page

Examples

This property will return None, if the final page was not set manually. The experiment session will then use a default final page.

>>> exp = al.Experiment()
>>> exp.final_page
None

Setting a class instance as final page:

>>> exp = al.Experiment()
>>> exp.final_page = al.Page(name="final_page")
>>> exp.final_page
Page(class='Page', name='final_page')

See also

Type

page.Page

as_final_page(page)

Class decorator for adding a custom final page to the experiment.

Use this decorator, if you want to define a new final page with full access to all experiment hooks.

Examples

>>> exp = al.Experiment()
...
>>> @exp.as_final_page
>>> class Final(al.Page):
...     name = "final_page"
...     def on_exp_access(self):
...         self += al.Text("This is the final page.")
...
>>> exp.final_page.name
final_page

See also

final_page: The final page as a property.

init_members()dict

Initializes all pages and sections in the members dictionary.

Also appends all members to their respective parents. If a member is already instantiated, the instance is copied to ensure uniqueness of instances across experiments.

Returns

Dictionary of initialized sections and pages.

Return type

dict

create_session(session_id: str, config: alfred3.config.ExperimentConfig, secrets: alfred3.config.ExperimentSecrets, **urlargs)

Creates an experiment session.

The arguments get passed on directly to the ExperimentSession object and are documented there.

See also

ExperimentSession contains documentation on how to interact with an experiment session object.

append(*members, to_section: str = '_content')

Append members to the experiment.

Parameters
  • *members – The members to append

  • to_section – The parent section for the members to append. All members in a function call will be appended to the same section.

Members can be classes that inherit from Section or Page, as well as instances of these classes or their subclasses. To append Page or Section classes to the experiment, you should use the member() decorator.

While it is perfectly possible to use the append() method, usually it is preferable to use to augmented assigment operator += to append members to the experiment.

Note

All members must have a unique name.

Examples

>>> exp = al.Experiment()
>>> exp.append(Page(name="page1"))
>>> exp.members
{"page1": Page(class="Page", name="page1")}
class alfred3.experiment.ExperimentSession(session_id: str, config: alfred3.config.ExperimentConfig = None, secrets: alfred3.config.ExperimentSecrets = None, **urlargs)

Bases: object

Coordinates all parts of an experiment.

Parameters
  • session_id – Unique session identifier

  • config – Non-secret experiment configuration

  • secrets – Secret experiment configuration, such as database credentials

  • **urlargs – Keyword arguments from url parameters. Will be stored in the attribute urlargs for access in the experiment session.

Usually, there is no need to initialize an ExperimentSession manually. The Experiment object will take care of that for you.

You have access to this object in Page and Section hooks through their accessors Page.exp and Section.exp. It connects all parts of an experiment and allows you to access data from other pages and even other experiment sessions.

Note

Because the experiment session is newly created for every new subject, you can only access it in the object-oriented style of writing an alfred experiment by deriving new Page and/or Section classes and using their hooks to add elements.

See also

  • Object-oriented style

  • Page hooks

  • Section hooks

  • Experiment object

Changed in version 2.0: Many changes, including new methods and properties for improved usability. Name changed from “Experiment” to “ExperimentSession”.

plugins

A dictionary of experiment plugins for use in the experiment session.

finish_functions

A list of functions that will be called upon finishing an experiment session. They are added with the finish() decorator

finished

Indicates whether the experiment session is finished

start_timestamp

Timestamp saved upon experiment start

start_time

Time of experiment start in seconds since epoch

alfred_version

The alfred3 version used for this experiment session

session_id

Unique session identifier

config

Non-sensitive experiment configuration. This is an ExperimentConfig object. It offers access to all values defined in the experiment’s config.conf through the methods get, getint, getfloat, and getboolean.

secrets

Sensitive experiment configuration (e.g. database credentials). This is an ExperimentSecrets object. It offers access to all values defined in the experiment’s secrets.conf through the methods get, getint, getfloat, and getboolean.

urlargs

A dictionary of parameters passed as url parameters for use in the experiment session.

log

A QueuedLoggingInterface, offering logging through the methods debug, info, warning, error, exception, and log.

property progress_bar

The experiment’s progress bar.

There are two options in config.conf that control the progress bar, both in the layout section:

  • show_progress (true/false) toggles whether a progress bar is displayed at all

  • fix_progress_top (true/false) toggles whether the progress bar should stay at a fixed position at the top of the page, when subjects scroll down on a long page.

The progress bar can be customized by redefining it with a element.ProgressBar of your choosing.

See also

See element.ProgressBar for more information on how to specify a custom progress bar.

Notes

The experiment-wide progress bar always receives a name of “progress_bar_”.

Examples

Example of controlling the progress bar in the config.conf:

[layout]
show_progress = true
fix_progress_top = false

Example of redefining the experiment-wide progress bar:

import alfred3 as al
exp = al.Experiment()

@exp.setup
def setup(exp):
    exp.progress_bar = al.ProgressBar(show_text=True, bar_height="10px")
start()

Starts the experiment.

Usually, this method does not need to be called manually. It will be called automatically, when the /experiment/start url route is called.

finish()

Closes all pages and saves data.

Usually, this method does not need to be called manually. It will be called automatically upon entering the finished section.

save_data(sync: bool = False)

Saves data with the main and unlinked saving agents.

Usually, there is no need to call this method manually, as data is saved automatically on every move.

Parameters

sync – If True, the experiment will only proceed after the saving task was completed.

Warning

Note that a call to this function will NOT prompt a call to the save_data() method of CustomSavingPage instances in the experiment. You need to call those manually.

property all_members

Dictionary of all sections and pages in the experiment.

Excludes the final page.

Type

dict

property all_sections

Dictionary of all sections in the experiment.

Type

dict

property all_pages

Dictionary of all pages in the experiment.

Type

dict

property final_page

The experiment’s final page.

This page will be displayed after a subject has finished the experiment. It will not contain any navigation elements. You can use this property to change the final page by assigning a page of your design.

See also

  • You can change the final page in a similar way using Experiment.final_page.

  • For conditional assignment of the experiment’s final page, doing so using a function with the Experiment.finish() decorator is the recommended way to go.

Examples

>>> import alfred3 as al
>>> exp = al.ExperimentSession()
>>> exp.final_page = al.Page(name="my_final_page")
>>> exp.final_page
Page(class="Page", name="my_final_page")
subpath(path: Union[str, pathlib.Path])pathlib.Path

Returns the full path of an experiment subdirectory.

If the given path is absolute, it will not be altered, but transformed to a pathlib.Path object.

Returns

Absolute path

Return type

pathlib.Path

read_csv_todict(path: Union[str, pathlib.Path], encoding: str = 'utf-8', **kwargs) → Iterator[dict]

Iterates over the rows in a .csv file, yielding dictionaries.

Parameters
  • path – The path to the .csv file. Usually, you want this to be a relative path to a file in a subdirectory of the experiment directory.

  • encoding – Encoding of the .csv file. Defaults to ‘utf-8’.

  • **kwargs – Further arguments passed on to csv.DictReader

Yields

dict – A dictionary in which the keys are the column names.

Examples

Consider the following csv-file, located at files/data.csv in your experiment directory:

col1    ,   col2    ,   col3
text_a  ,   text_b  ,   text_c
text_d  ,   text_e  ,   text_f

When building a page, usual usage would be:

import alfred3 as al
exp = al.Experiment()

@exp.member
class CSVDemoPage(al.Page):     # this could also be a Section
    name = "csv_demo"

    def on_exp_access(self):

        for row in self.exp.read_csv_todict("files/data.csv"):
            print(row)

The output would be the following:

{"col1": "text_a", "col2": "text_b", "col3": "text_c"}  # first iteration
{"col1": "text_d", "col2": "text_e", "col3": "text_f"}  # second iteration

If you need a full list of the rows, you can wrap the function call in list():

import alfred3 as al
exp = al.Experiment()

@exp.member
class CSVDemoPage(al.Page):     # this could also be a Section
    name = "csv_demo"

    def on_exp_access(self):

        data = list(self.exp.read_csv_todict("files/data.csv"))
        print(data)

The output would be the following:

[{"col1": "text_a", "col2": "text_b", "col3": "text_c"},
{"col1": "text_d", "col2": "text_e", "col3": "text_f"}]

New in version 2.0.

read_csv_tolist(path: Union[str, pathlib.Path], encoding: str = 'utf-8', **kwargs) → Iterator[list]

Iterates over the rows in a .csv file, yielding lists.

Parameters
  • path – The path to the .csv file. Usually, you want this to be a relative path to a file in a subdirectory of the experiment directory.

  • encoding – Encoding of the .csv file. Defaults to ‘utf-8’.

  • **kwargs – Further arguments passed on to csv.reader

Yields

list – A list of the values in one row.

Examples

Consider the following csv-file:

col1    ,   col2    ,   col3
text_a  ,   text_b  ,   text_c
text_d  ,   text_e  ,   text_f

When building a page, usual usage would be:

import alfred3 as al
exp = al.Experiment()

@exp.member
class CSVDemoPage(al.Page):     # this could also be a Section
    name = "csv_demo"

    def on_exp_access(self):

        for row in self.exp.read_csv_tolist("files/data.csv"):
            print(row)

The output would be the following:

["col1", "col2", "col3"]        # first iteration yields column names
["text_a", "text_b", "text_c"]  # second iteration
["text_a", "text_b", "text_c"]  # third iteration

If you need a full list of the rows, you can wrap the function call in list():

import alfred3 as al
exp = al.Experiment()

@exp.member
class CSVDemoPage(al.Page):     # this could also be a Section
    name = "csv_demo"

    def on_exp_access(self):

        data = list(self.exp.read_csv_tolist("files/data.csv"))
        print(data)

The output would be the following:

[["col1", "col2", "col3"],
["text_a", "text_b", "text_c"],
["text_a", "text_b", "text_c"]]

New in version 2.0.

property author

Returns the experiment author.

Type

str

property type

Type of the experiment

Type

str

property version

Experiment version

Type

str

property title

Experiment title

Type

str

property exp_id

Experiment id

Type

str

property path

Path to the experiment directory

Type

Path

property adata

Shortcut for accessing the additional data dictionary.

The additional data dictionary is meant to be a place where you can store data manually. It will be saved to the final dataset, where all additional data for a session will be stored as a single string.

See also

You can add specific values to a page with the Value element. For each Value element, alfred will save an individual column to the final dataset.

New in version 2.0.

Type

dict

property additional_data

The additional data dictionary. See adata.

Type

dict

property condition

Current experiment condition

Type

str

property session

read-only

Returns

Current TestCondition (str or unicode)

append(*items)

Appends Sections or Pages to the experiment’s root section.

While it is perfectly possible to use the append() method, usually it is preferable to use to augmented assigment operator += to append members to the experiment.

All members must have a unique name.

Examples

>>> import alfred3 as al
>>> exp = al.ExperimentSession()
>>> exp.append(al.Page(name="page1"))
>>> exp.members
{"page1": Page(class="Page", name="page1")}
encrypt(data: Union[str, int, float])str

Encrypts the input and returns the encrypted string.

In web experiments deployed via mortimer, a safe, user-specific secret key will be used for encryption.

Warning

The method will also work in offline experiments, but does NOT provide safe encryption in this case, as a PUBLIC key is used for encryption. This is only ok for testing purposes.

Parameters

data – Input object that you want to encrypt. If the input is None, the function will return None.

Returns

Encrypted data

Return type

str

decrypt(data: Union[str, bytes])str

Decrypts input and returns the decrypted object as str.

The method uses the built-in Fernet instance to decrypt the input.

Parameters

data – Encrypted string or bytes object.

Returns

Decrypted data

Return type

str

set_additional_data(key: str, value)

Shortcut for DataManager.add_additional_data().

get_additional_data(key: str)

Shortcut for DataManager.get_additional_data_by_key().

forward()

Moves the experiment forward one page.

Can be called in the Page.custom_move() hook to implement very specific movement behavior.

See also

Refer to the documentation of Page.custom_move() for guidance on how to implement a custom movement method.

New in version 2.0.

backward()

Moves the experiment backward one page.

Can be called in the Page.custom_move() hook to implement very specific movement behavior.

See also

Refer to the documentation of Page.custom_move() for guidance on how to implement a custom movement method.

New in version 2.0.

jump(to: Union[str, int])

Jumps to a specific page in the experiment.

Parameters

to (str, int) – The name or index of the target page. Preferred usage is by name, because that reduces ambiguity.

Can be called in the Page.custom_move() hook to implement very specific movement behavior.

See also

Refer to the documentation of Page.custom_move() for guidance on how to implement a custom movement method.

New in version 2.0.

property values

Dictionary of input elements and their current values.

Elements are identified by their name. Only elements from pages that have already been shown to a user appear in this dictionary.

You cannot (and should not) change subject data by manipulating the dictionary returned by this property.

Returns

Dictionary of input elements and their current values.

Return type

dict

New in version 2.0.

property session_data

Full dataset of the current experimental session.

Contains client information, experiment metadata, element values, movement history, and additional data.

You cannot (and should not) change subject data by manipulating the dictionary returned by this property.

Returns

Full dataset of the current experimental session.

Return type

dict

New in version 2.0.

property move_history

A list, containing the movement history for the current session.

Each entry in this list is a dictionary. The entries in each of these dictionaries are based on the Move dataclass.

You cannot (and should not) change subject data by manipulating the dictionary returned by this property.

Returns

A list, containing a dictionary for each move of the subject in the current session.

Return type

list

See also

Please refer to the documentation of Move for an explanation of the saved fields.

New in version 2.0.

property metadata

A dict of information about the experiment.

This dict contains general information about the experiment, such as

  • Start time

  • Experiment version

  • Title

  • Author

  • (…)

Returns

A dict of information about the experiment.

Return type

dict

New in version 2.0.

get_page_data(name: str)dict

Get the data dictionary of a specific page.

Parameters

name – The name of the target page

Returns

Data dictionary of a specific page.

Return type

dict

get_section_data(name: str)dict

Get the data dictionary for all pages in a specific section.

Includes pages in child-sections of the target section.

Parameters

name – The name of the target section

Returns

Data dictionary of a specific section.

Return type

dict

New in version 2.0.

post_message(msg: str, title: str = '', level: str = 'info')

Post a message for one-time display after the next move.

Both the message and its title can contain github flavored markdown and emoji shortcodes.

The message will always be displayed only once. If the current page cannot be left, e.g. due to invalid input, it will be displayed on the current page. Otherwise, it will always be displayed on the next displayed page.

Parameters
  • msg – Message main text.

  • title – Message title, will be displayed as a bold heading.

  • level – Message level, controls the display style. Can be ‘info’ (default), ‘warning’, ‘danger’, ‘success’, ‘primary’, ‘secondary’, ‘dark’, or ‘light’.

New in version 2.0.

property db

Database object of the main mongo saving agent.

You can use this property to access multiple collections in the used database, if your monogo saving agent is connected through an account with the necessary privileges.

Note

This is the database object, which can contain multiple collections of documents. Data is always saved to a collection in a database, not to a database directly.

See also

db_main returns the collection, to which the main mongo saving agent writes its data.

Returns

A database object. If no mongo saving agent is present in the experiment, None is returned.

Return type

pymongo.database.Database

New in version 2.0.

property db_main

Document collection of the main mongo saving agent.

You can use this property to interact with the collection through the pymongo api: https://pymongo.readthedocs.io/en/stable/

Returns

A collection object. If no mongo saving agent is present in the experiment, None is returned.

Return type

pymongo.collections.Collection

New in version 2.0.

property db_unlinked

Document collection of the unlinked mongo saving agent.

You can use this property to interact with the collection through the pymongo api: https://pymongo.readthedocs.io/en/stable/

Returns

A collection object. If no unlinked mongo saving agent is present in the experiment, None is returned.

Return type

pymongo.collections.Collection

New in version 2.0.

property db_misc

Document collection for miscellaneous data.

You can use this property to interact with the collection through the pymongo api: https://pymongo.readthedocs.io/en/stable/

Note

This property assumes that the database credentials used by the main mongo saving agent are valid for a collection with the name misc in the same database aswell. If that is not the case, you will receive authentication errors when interacting with this property.

Returns

A collection object. If no mongo saving agent is present in the experiment, None is returned.

Return type

pymongo.collections.Collection

New in version 2.0.