Skip to content

Assays

Generate snail assays.

AssayParams

Bases: BaseModel

Parameters for assay generation.

Parameters:

Name Type Description Default
baseline float

Baseline reading value

2.0
degrade float

Rate at which sample responses decrease per day after first day (0..1)

0.05
delay int

Maximum number of days between specimen collection and assay

5
mutant float

Mutant reading value (must be positive)

5.0
noise float

Noise level for readings (must be positive)

0.2
plate_size int

Size of assay plate (must be positive)

4
image_noise int

Plate image noise (grayscale 0-255)

32
Source code in src/snailz/assays.py
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
class AssayParams(BaseModel):
    """Parameters for assay generation."""

    baseline: float = Field(default=2.0, ge=0.0, description="Baseline reading value")
    degrade: float = Field(
        default=0.05,
        ge=0.0,
        le=1.0,
        description="Rate at which sample responses decrease per day after first day (0..1)",
    )
    delay: int = Field(
        default=5,
        gt=0,
        description="Maximum number of days between specimen collection and assay",
    )
    mutant: float = Field(
        default=5.0, gt=0.0, description="Mutant reading value (must be positive)"
    )
    noise: float = Field(
        default=0.2, ge=0.0, description="Noise level for readings (must be positive)"
    )
    plate_size: int = Field(
        default=DEFAULT_PLATE_SIZE,
        gt=0,
        description="Size of assay plate (must be positive)",
    )
    image_noise: int = Field(
        default=32,
        ge=0,
        le=255,
        description="Plate image noise (grayscale 0-255)",
    )

    model_config = {"extra": "forbid"}

    @model_validator(mode="after")
    def validate_fields(self):
        """Validate requirements on fields."""
        if self.mutant < self.baseline:
            raise ValueError("mutant value must be greater than baseline")
        return self

validate_fields()

Validate requirements on fields.

Source code in src/snailz/assays.py
54
55
56
57
58
59
@model_validator(mode="after")
def validate_fields(self):
    """Validate requirements on fields."""
    if self.mutant < self.baseline:
        raise ValueError("mutant value must be greater than baseline")
    return self

Assay

Bases: BaseModel

A single assay.

Parameters:

Name Type Description Default
ident str

unique identifier

required
specimen str

which specimen

required
person str

who did the assay

required
performed date

date assay was performed

required
readings Grid[float]

assay readings

required
treatments Grid[str]

samples or controls

required
Source code in src/snailz/assays.py
 62
 63
 64
 65
 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
class Assay(BaseModel):
    """A single assay."""

    ident: str = Field(description="unique identifier")
    specimen: str = Field(description="which specimen")
    person: str = Field(description="who did the assay")
    performed: date = Field(description="date assay was performed")
    readings: Grid[float] = Field(description="assay readings")
    treatments: Grid[str] = Field(description="samples or controls")

    model_config = {"extra": "forbid"}

    @model_validator(mode="after")
    def show_fields(self):
        return self

    def to_csv(self, kind: str) -> str:
        """Return a CSV string representation of the assay data.

        Parameters:
            kind: Either "readings" or "treatments"

        Returns:
            A CSV-formatted string with the assay data.

        Raises:
            ValueError: If 'kind' is not "readings" or "treatments"
        """
        if kind not in ["readings", "treatments"]:
            raise ValueError("data_type must be 'readings' or 'treatments'")

        # Get the appropriate data based on data_type
        data = self.readings if kind == "readings" else self.treatments
        assert isinstance(data, Grid)

        # Generate column headers (A, B, C, etc.) and calculate metadata padding
        column_headers = [""] + [chr(ord("A") + i) for i in range(data.width)]
        max_columns = len(column_headers)
        padding = [""] * (max_columns - 2)

        # Write data
        output = io.StringIO()
        writer = csv.writer(output, lineterminator="\n")
        pre = [
            ["id", self.ident] + padding,
            ["specimen", self.specimen] + padding,
            ["date", self.performed.isoformat()] + padding,
            ["by", self.person] + padding,
            column_headers,
        ]
        for row in pre:
            writer.writerow(row)

        for i, y in enumerate(range(data.height - 1, -1, -1)):
            row = [i + 1] + [data[x, y] for x in range(data.width)]
            writer.writerow(row)

        return output.getvalue()

to_csv(kind)

Return a CSV string representation of the assay data.

Parameters:

Name Type Description Default
kind str

Either "readings" or "treatments"

required

Returns:

Type Description
str

A CSV-formatted string with the assay data.

Raises:

Type Description
ValueError

If 'kind' is not "readings" or "treatments"

Source code in src/snailz/assays.py
 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
def to_csv(self, kind: str) -> str:
    """Return a CSV string representation of the assay data.

    Parameters:
        kind: Either "readings" or "treatments"

    Returns:
        A CSV-formatted string with the assay data.

    Raises:
        ValueError: If 'kind' is not "readings" or "treatments"
    """
    if kind not in ["readings", "treatments"]:
        raise ValueError("data_type must be 'readings' or 'treatments'")

    # Get the appropriate data based on data_type
    data = self.readings if kind == "readings" else self.treatments
    assert isinstance(data, Grid)

    # Generate column headers (A, B, C, etc.) and calculate metadata padding
    column_headers = [""] + [chr(ord("A") + i) for i in range(data.width)]
    max_columns = len(column_headers)
    padding = [""] * (max_columns - 2)

    # Write data
    output = io.StringIO()
    writer = csv.writer(output, lineterminator="\n")
    pre = [
        ["id", self.ident] + padding,
        ["specimen", self.specimen] + padding,
        ["date", self.performed.isoformat()] + padding,
        ["by", self.person] + padding,
        column_headers,
    ]
    for row in pre:
        writer.writerow(row)

    for i, y in enumerate(range(data.height - 1, -1, -1)):
        row = [i + 1] + [data[x, y] for x in range(data.width)]
        writer.writerow(row)

    return output.getvalue()

AllAssays

Bases: BaseModel

All generated assays.

Parameters:

Name Type Description Default
items list[Assay]

actual assays

required
Source code in src/snailz/assays.py
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
class AllAssays(BaseModel):
    """All generated assays."""

    items: list[Assay] = Field(description="actual assays")

    def to_csv(self) -> str:
        """Return a CSV string representation of the assay summary data.

        Returns:
            A CSV-formatted string containing a summary of all assays
        """
        return utils.to_csv(
            self.items,
            ["ident", "specimen", "person", "performed"],
            lambda r: [r.ident, r.specimen, r.person, r.performed.isoformat()],
        )

to_csv()

Return a CSV string representation of the assay summary data.

Returns:

Type Description
str

A CSV-formatted string containing a summary of all assays

Source code in src/snailz/assays.py
127
128
129
130
131
132
133
134
135
136
137
def to_csv(self) -> str:
    """Return a CSV string representation of the assay summary data.

    Returns:
        A CSV-formatted string containing a summary of all assays
    """
    return utils.to_csv(
        self.items,
        ["ident", "specimen", "person", "performed"],
        lambda r: [r.ident, r.specimen, r.person, r.performed.isoformat()],
    )

assays_generate(params, persons, specimens)

Generate an assay for each specimen.

Parameters:

Name Type Description Default
params AssayParams

assay generation parameters

required
persons AllPersons

all staff members

required
specimens AllSpecimens

specimens to generate assays for

required

Returns:

Type Description
AllAssays

Assay list object

Source code in src/snailz/assays.py
140
141
142
143
144
145
146
147
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
def assays_generate(
    params: AssayParams, persons: AllPersons, specimens: AllSpecimens
) -> AllAssays:
    """Generate an assay for each specimen.

    Parameters:
        params: assay generation parameters
        persons: all staff members
        specimens: specimens to generate assays for

    Returns:
        Assay list object
    """
    gen = utils.unique_id("assays", lambda: f"{random.randint(0, 999999):06d}")

    items = []
    for spec in specimens.items:
        performed = spec.collected + timedelta(days=random.randint(0, params.delay))
        person = random.choice(persons.items)
        treatments = _make_treatments(params)
        readings = _make_readings(params, spec, performed, treatments)
        ident = next(gen)
        assert isinstance(ident, str)  # to satisfy type checking
        items.append(
            Assay(
                ident=ident,
                performed=performed,
                specimen=spec.ident,
                person=person.ident,
                treatments=treatments,
                readings=readings,
            )
        )

    return AllAssays(items=items)

_calc_degradation(params, collected, assayed)

Calculate degradation based on days since collection.

Source code in src/snailz/assays.py
177
178
179
def _calc_degradation(params: AssayParams, collected: date, assayed: date) -> float:
    """Calculate degradation based on days since collection."""
    return max(0.0, 1.0 - (params.degrade * (assayed - collected).days))

_make_readings(params, specimen, performed, treatments)

Make a single assay.

Source code in src/snailz/assays.py
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
def _make_readings(
    params: AssayParams,
    specimen: Specimen,
    performed: date,
    treatments: Grid[str],
) -> Grid[float]:
    """Make a single assay."""
    degradation = _calc_degradation(params, specimen.collected, performed)
    readings = Grid(width=params.plate_size, height=params.plate_size, default=0.0)
    for x in range(params.plate_size):
        for y in range(params.plate_size):
            if treatments[x, y] == "C":
                base_value = 0.0
            elif specimen.is_mutant:
                base_value = params.mutant * degradation
            else:
                base_value = params.baseline * degradation
            readings[x, y] = round(
                base_value + random.uniform(0.0, params.noise), utils.PRECISION
            )

    return readings

_make_treatments(params)

Generate random treatments.

Source code in src/snailz/assays.py
206
207
208
209
210
211
212
213
214
215
216
217
def _make_treatments(params: AssayParams) -> Grid[str]:
    """Generate random treatments."""
    size = params.plate_size
    size_sq = size**2
    half = size_sq // 2
    available = list(("S" * half) + ("C" * (size_sq - half)))
    random.shuffle(available)
    treatments = Grid(width=size, height=size, default="")
    for x in range(size):
        for y in range(size):
            treatments[x, y] = available.pop()
    return treatments