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()