Assays - Snailz Docs

Skip to content

# Assays

## `Assay` `dataclass`

A single assay with date, unique identifier, specimen ID, person ID, readings and treatments.

Source code in `src/snailz/assays.py`

    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    @dataclass
    class Assay:
        """A single assay with date, unique identifier, specimen ID, person ID, readings and treatments."""

        performed: date
        ident: str
        specimen_id: str
        person_id: str
        readings: list[list[float]]
        treatments: list[list[str]]

## `Assays` `dataclass`

Keep track of generated assays.

Source code in `src/snailz/assays.py`

    40
    41
    42
    43
    44
    45
    @dataclass
    class Assays:
        """Keep track of generated assays."""

        items: list[Assay]
        params: dict[str, object]

## `assays_check(params)`

Check parameters for assay generation.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`params` | `dict[str, object]` | Dictionary containing assay generation parameters | required

Raises:

Type | Description
--- | ---
`ValueError` | If parameters are missing, have wrong types, or have invalid values

Source code in `src/snailz/assays.py`

    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    def assays_check(params: dict[str, object]) -> None:
        """Check parameters for assay generation.

        Parameters:
            params: Dictionary containing assay generation parameters

        Raises:
            ValueError: If parameters are missing, have wrong types, or have invalid values
        """
        utils.check_keys_and_types(ASSAYS_PARAMS, params)
        for name in ["plate_size", "noise", "baseline", "mutant"]:
            utils.require(0 < params[name], f"{name} must be positive")
        utils.require(
            params["start_date"] <= params["end_date"],
            "start date must be less than or equal to end date",
        )

## `assays_generate(params, specimens, people)`

Generate an assay for each specimen.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`params` | `dict[str, object]` | Dictionary containing assay generation parameters | required
`specimens` | `Specimens` | Specimens object with individual specimens to generate assays for | required
`people` | `People` | People object with staff members | required

Returns:

Type | Description
--- | ---
`Assays` | Assays object containing generated assays and parameters

Source code in `src/snailz/assays.py`

     66
     67
     68
     69
     70
     71
     72
     73
     74
     75
     76
     77
     78
     79
     80
     81
     82
     83
     84
     85
     86
     87
     88
     89
     90
     91
     92
     93
     94
     95
     96
     97
     98
     99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    def assays_generate(
        params: dict[str, object], specimens: Specimens, people: People
    ) -> Assays:
        """Generate an assay for each specimen.

        Parameters:
            params: Dictionary containing assay generation parameters
            specimens: Specimens object with individual specimens to generate assays for
            people: People object with staff members

        Returns:
            Assays object containing generated assays and parameters
        """
        assays_check(params)

        start = params["start_date"]
        end = params["end_date"]
        plate_size = params["plate_size"]
        noise = params["noise"]
        baseline = params["baseline"]
        mutant = params["mutant"]

        days_delta = (end - start).days + 1
        individuals = specimens.individuals
        susc_locus = specimens.susceptible_locus
        susc_base = specimens.susceptible_base
        items = []

        gen = utils.UniqueIdGenerator("assays", lambda: f"{random.randint(0, 999999):06d}")

        for individual in individuals:
            assay_date = start + timedelta(days=random.randint(0, days_delta - 1))
            assay_id = gen.next()

            # Generate treatments randomly with equal probability
            treatments = []
            for row in range(plate_size):
                treatment_row = []
                for col in range(plate_size):
                    treatment_row.append(random.choice(["S", "C"]))
                treatments.append(treatment_row)

            # Generate readings based on treatments and susceptibility
            readings = []
            is_susceptible = individual.genome[susc_locus] == susc_base
            for row in range(plate_size):
                reading_row = []
                for col in range(plate_size):
                    if treatments[row][col] == "C":
                        # Control cells have values uniformly distributed between 0 and noise
                        reading_row.append(random.uniform(0, noise))
                    elif is_susceptible:
                        # Susceptible specimens (with susceptible base at susceptible locus)
                        # Base mutant value plus noise scaled by mutant/baseline ratio
                        scaled_noise = round(noise * mutant / baseline, utils.PRECISION)
                        reading_row.append(mutant + random.uniform(0, scaled_noise))
                    else:
                        # Non-susceptible specimens
                        # Base baseline value plus uniform noise
                        reading_row.append(baseline + random.uniform(0, noise))
                # Handle limited precision.
                reading_row = [round(r, utils.PRECISION) for r in reading_row]
                readings.append(reading_row)

            # Randomly select a person to perform the assay
            person = random.choice(people.individuals)

            # Create the assay with reference to the specimen ID and person ID
            items.append(
                Assay(
                    performed=assay_date,
                    ident=assay_id,
                    specimen_id=individual.ident,
                    person_id=person.ident,
                    readings=readings,
                    treatments=treatments,
                )
            )

        return Assays(items=items, params=params)

## `assays_to_csv(assays, directory, ident=None)`

Write assay data to CSV files.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`assays` | `Assays` | An Assays instance containing assay data | required
`directory` | `str | None` | Directory to save output. | required
`ident` | `str | None` | Optional assay ID to output only a specific assay | `None`

If multiple files are created, writes assays.csv with fields: - ident: assay identifier - specimen_id: specimen identifier - performed: date the assay was performed

For each assay (or just the specified one if ident is provided), creates: - assays/ID_design.csv: containing treatment data - assays/ID_assay.csv: containing reading data

Each file has the format: id, specimen, performed, ,A,B,C,... 1,,,... 2,,,... ...

Source code in `src/snailz/assays.py`

    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    def assays_to_csv(
        assays: Assays, directory: str | None, ident: str | None = None
    ) -> None:
        """Write assay data to CSV files.

        Args:
            assays: An Assays instance containing assay data
            directory: Directory to save output.
            ident: Optional assay ID to output only a specific assay

        If multiple files are created, writes assays.csv with fields:
        - ident: assay identifier
        - specimen_id: specimen identifier
        - performed: date the assay was performed

        For each assay (or just the specified one if ident is provided), creates:
        - assays/ID_design.csv: containing treatment data
        - assays/ID_assay.csv: containing reading data

        Each file has the format:
        id,<assay_id>
        specimen,<specimen_id>
        performed,<performed_date>
        ,A,B,C,...
        1,<data>,<data>,...
        2,<data>,<data>,...
        ...
        """
        # Filter assays if an ident is provided
        items_to_process = [
            item for item in assays.items if ident is None or item.ident == ident
        ]
        if not items_to_process and ident is not None:
            raise ValueError(f"No assay with ID {ident} found")

        # Write summary assays.csv file if multiple files are output.
        if directory is not None and ident is None:
            summary_file = Path(directory, "assays.csv")
            with open(summary_file, "w", newline="") as stream:
                writer = csv.writer(stream)
                writer.writerow(["ident", "specimen_id", "performed", "performed_by"])
                for assay in assays.items:
                    writer.writerow(
                        [
                            assay.ident,
                            assay.specimen_id,
                            assay.performed.isoformat(),
                            assay.person_id,
                        ]
                    )

        # Process each assay
        for assay in items_to_process:
            # Generate column headers (A, B, C, etc.)
            plate_size = len(assay.readings)
            column_headers = [""] + [chr(65 + i) for i in range(plate_size)]

            # Write treatments to ID_design.csv
            _write_assay_csv(
                assay,
                directory,
                f"{assay.ident}_design.csv",
                assay.treatments,
                column_headers,
            )

            # Write readings to ID_assay.csv
            _write_assay_csv(
                assay, directory, f"{assay.ident}_assay.csv", assay.readings, column_headers
            )

## `_write_assay_csv(assay, directory, filename, data, column_headers)`

Helper function to write a single assay CSV file.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`assay` | `Assay` | The Assay instance | required
`directory` | `str | None` | Directory to save file (None for stdout) | required
`filename` | `str` | Name of the file to create | required
`data` | `Sequence[Sequence[float | str]]` | The data to write (treatments or readings) | required
`column_headers` | `list[str]` | Column headers including empty first cell | required

Side effects

Either writes to a file in the specified directory or prints to stdout

Source code in `src/snailz/assays.py`

    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    def _write_assay_csv(
        assay: Assay,
        directory: str | None,
        filename: str,
        data: Sequence[Sequence[float | str]],
        column_headers: list[str],
    ) -> None:
        """Helper function to write a single assay CSV file.

        Parameters:
            assay: The Assay instance
            directory: Directory to save file (None for stdout)
            filename: Name of the file to create
            data: The data to write (treatments or readings)
            column_headers: Column headers including empty first cell

        Side effects:
            Either writes to a file in the specified directory or prints to stdout
        """
        # If directory is None, write to stdout
        if directory is None:
            print(f"--- {filename}")
            stream = sys.stdout
        else:
            Path(directory, ASSAYS_SUBDIR).mkdir(exist_ok=True)
            stream = open(Path(directory, ASSAYS_SUBDIR, filename), "w", newline="")

        writer = csv.writer(stream)

        max_columns = len(column_headers)
        padding = [""] * (max_columns - 2)
        writer.writerow(["id", assay.ident] + padding)
        writer.writerow(["specimen", assay.specimen_id] + padding)
        writer.writerow(["performed", assay.performed.isoformat()] + padding)
        writer.writerow(["performed_by", assay.person_id] + padding)

        # Write column headers, padding if necessary
        writer.writerow(column_headers)

        # Write data rows with row numbers
        for i, row in enumerate(data, 1):
            writer.writerow([i] + row)

        # Close file if we opened one
        if directory is not None:
            stream.close()