Skip to content

crispr_analysis_utils.guide_alignment.library

crispr_analysis_utils.guide_alignment.library

The guide library: reading and validating the guide table.

A guide is known by its row, its 0-based position in the list read_guides returns, and identified by Guide.guide_id: the sequence that is aligned, plus the PAM. The name the table gives it is Guide.alias.

Guide dataclass

One guide of the library.

Attributes:

  • guide_id (str) –

    The sequence that is aligned, ending in the PAM pattern. It identifies the guide, so two entries share it only when their sequences are equal.

  • alias (str) –

    The name the guide table gives this entry, as a string.

  • input_sequence (str) –

    The sequence from the guide table, stripped of surrounding whitespace and uppercased.

  • aligned_sequence (str) –

    What is aligned, without the PAM: input_sequence, after a 5' G when read_guides was told to add one. Nothing is ever trimmed from it.

  • spacer (str) –

    The 3'-most spacer_length bases of aligned_sequence, 5' to 3': the protospacer the coordinates are reported for.

  • n_extra (int) –

    The bases of aligned_sequence 5' of the spacer, so 1 for a guide carrying an added G and 0 for one that does not. Their mismatches are reported but never counted.

  • extra_5p (bool) –

    Whether n_extra is more than zero: the sequence carries a base 5' of the protospacer, read from its length and not from any alignment.

  • duplicate_of (str | None) –

    The alias of the first entry whose guide_id is the same, or None for that first entry itself.

Source code in src/crispr_analysis_utils/guide_alignment/library.py
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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
@dataclass(frozen=True)
class Guide:
    """One guide of the library.

    Attributes
    ----------
    guide_id
        The sequence that is aligned, ending in the PAM pattern. It identifies
        the guide, so two entries share it only when their sequences are
        equal.
    alias
        The name the guide table gives this entry, as a string.
    input_sequence
        The sequence from the guide table, stripped of surrounding whitespace
        and uppercased.
    aligned_sequence
        What is aligned, without the PAM: `input_sequence`, after a 5' G when
        `read_guides` was told to add one. Nothing is ever trimmed from it.
    spacer
        The 3'-most `spacer_length` bases of `aligned_sequence`, 5' to 3':
        the protospacer the coordinates are reported for.
    n_extra
        The bases of `aligned_sequence` 5' of the spacer, so 1 for a guide
        carrying an added G and 0 for one that does not. Their mismatches are
        reported but never counted.
    extra_5p
        Whether `n_extra` is more than zero: the sequence carries a base 5'
        of the protospacer, read from its length and not from any alignment.
    duplicate_of
        The alias of the first entry whose `guide_id` is the same, or None for
        that first entry itself.
    """

    guide_id: str
    alias: str
    input_sequence: str
    aligned_sequence: str
    spacer: str
    n_extra: int
    duplicate_of: str | None

    @property
    def extra_5p(self) -> bool:
        """Whether the sequence carries a base 5' of the protospacer."""
        return self.n_extra > 0

extra_5p property

extra_5p: bool

Whether the sequence carries a base 5' of the protospacer.

read_guides

read_guides(source: str | PathLike[str] | DataFrame, *, id_col: int | str = 0, spacer_col: int | str = 1, sep: str = '\t', header: bool = False, spacer_length: int = 20, add_leading_g: bool = False, pam: str = 'NGG') -> list[Guide]

Read and validate a guide table.

Parameters:

  • source (str | PathLike[str] | DataFrame) –

    A delimited text file, or a pandas DataFrame. A file is read with every column as a string, so an alias such as 0001 stays 0001 and one such as NA is not taken for a missing value.

  • id_col (int | str, default: 0 ) –

    The column of the name the table gives each guide, which becomes Guide.alias: a 0-based position, or a column name.

  • spacer_col (int | str, default: 1 ) –

    The sequence column: a 0-based position, or a column name. Sequences are given 5' to 3'.

  • sep (str, default: '\t' ) –

    The field separator of a file. Ignored for a DataFrame.

  • header (bool, default: False ) –

    Whether the file's first line holds column names. Ignored for a DataFrame.

  • spacer_length (int, default: 20 ) –

    The spacer is the 3'-most spacer_length bases of each sequence.

  • add_leading_g (bool, default: False ) –

    Put a 5' G in front of every sequence, for a table that gives bare protospacers while the construct adds a G. Leave it False when the sequences already carry the G they were ordered with: whether a guide has one then follows from its length, not from its first base.

  • pam (str, default: 'NGG' ) –

    The PAM pattern, which ends Guide.guide_id.

Returns:

  • list of Guide –

    One guide per table row, in table order. A guide whose spacer equals an earlier guide's has duplicate_of set to that earlier guide's alias; a warning is logged when there are any.

Raises:

  • ValueError –

    If the table has no rows; a column is missing; an alias or a sequence is missing; an alias is empty or holds a tab or a line break; aliases repeat; a sequence, once stripped and uppercased, holds a character other than A, C, G and T; or a sequence is shorter than spacer_length.

Source code in src/crispr_analysis_utils/guide_alignment/library.py
 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
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
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
218
219
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
def read_guides(
    source: str | os.PathLike[str] | pd.DataFrame,
    *,
    id_col: int | str = 0,
    spacer_col: int | str = 1,
    sep: str = "\t",
    header: bool = False,
    spacer_length: int = 20,
    add_leading_g: bool = False,
    pam: str = "NGG",
) -> list[Guide]:
    """Read and validate a guide table.

    Parameters
    ----------
    source
        A delimited text file, or a pandas DataFrame. A file is read with every
        column as a string, so an alias such as ``0001`` stays ``0001`` and one
        such as ``NA`` is not taken for a missing value.
    id_col
        The column of the name the table gives each guide, which becomes
        `Guide.alias`: a 0-based position, or a column name.
    spacer_col
        The sequence column: a 0-based position, or a column name. Sequences
        are given 5' to 3'.
    sep
        The field separator of a file. Ignored for a DataFrame.
    header
        Whether the file's first line holds column names. Ignored for a
        DataFrame.
    spacer_length
        The spacer is the 3'-most `spacer_length` bases of each sequence.
    add_leading_g
        Put a 5' G in front of every sequence, for a table that gives bare
        protospacers while the construct adds a G. Leave it False when the
        sequences already carry the G they were ordered with: whether a guide
        has one then follows from its length, not from its first base.
    pam
        The PAM pattern, which ends `Guide.guide_id`.

    Returns
    -------
    list of Guide
        One guide per table row, in table order. A guide whose spacer equals
        an earlier guide's has `duplicate_of` set to that earlier guide's
        alias; a warning is logged when there are any.

    Raises
    ------
    ValueError
        If the table has no rows; a column is missing; an alias or a sequence
        is missing; an alias is empty or holds a tab or a line break; aliases
        repeat; a sequence, once stripped and uppercased, holds a character
        other than A, C, G and T; or a sequence is shorter than
        `spacer_length`.
    """
    pam = validate_pams([pam])[0]
    if (
        isinstance(spacer_length, bool)
        or not isinstance(spacer_length, int)
        or spacer_length < 1
    ):
        raise ValueError("spacer_length must be a positive integer.")

    if isinstance(source, pd.DataFrame):
        table = source
        origin = "the guide DataFrame"
    else:
        origin = os.fspath(source)
        try:
            table = pd.read_csv(
                source,
                sep=sep,
                header=0 if header else None,
                dtype=str,
                keep_default_na=False,
            )
        except pd.errors.EmptyDataError:
            raise ValueError(f"No guides in {origin}.") from None
    if table.empty:
        raise ValueError(f"No guides in {origin}.")

    raw_aliases = _column(table, id_col, "id_col", origin)
    raw_sequences = _column(table, spacer_col, "spacer_col", origin)
    _check_present(raw_aliases, "guide alias", origin)
    _check_present(raw_sequences, "guide sequence", origin)

    ids = [str(value) for value in raw_aliases]
    sequences = [str(value).strip().upper() for value in raw_sequences]

    empty = [str(row + 1) for row, alias in enumerate(ids) if alias == ""]
    if empty:
        raise ValueError(
            f"Guide aliases must not be empty in {origin}: data rows "
            f"{_listing(empty)} (counted from 1)."
        )

    unsafe = [repr(alias) for alias in ids if _TABLE_BREAKS & set(alias)]
    if unsafe:
        raise ValueError(
            f"Guide aliases must not hold tabs or line breaks in {origin}: "
            f"{_listing(unsafe)}."
        )

    repeated = [
        f"{alias!r} ({count} times)"
        for alias, count in Counter(ids).items()
        if count > 1
    ]
    if repeated:
        raise ValueError(
            f"Guide aliases must be unique in {origin}: {_listing(repeated)}."
        )

    invalid = [
        f"{alias!r} ({''.join(sorted(set(sequence) - _ACGT))!r})"
        for alias, sequence in zip(ids, sequences, strict=True)
        if not set(sequence) <= _ACGT
    ]
    if invalid:
        raise ValueError(
            f"Guide sequences may hold only A, C, G and T in {origin}: "
            f"{_listing(invalid)}."
        )

    short = [
        f"{alias!r} ({len(sequence)} bases)"
        for alias, sequence in zip(ids, sequences, strict=True)
        if len(sequence) < spacer_length
    ]
    if short:
        raise ValueError(
            f"Guide sequences must be at least {spacer_length} bases long in "
            f"{origin}: {_listing(short)}."
        )

    first_by_id: dict[str, str] = {}
    guides = []
    for alias, sequence in zip(ids, sequences, strict=True):
        aligned = "G" + sequence if add_leading_g else sequence
        guide_id = aligned + pam
        first = first_by_id.setdefault(guide_id, alias)
        guides.append(
            Guide(
                guide_id=guide_id,
                alias=alias,
                input_sequence=sequence,
                aligned_sequence=aligned,
                spacer=aligned[-spacer_length:],
                n_extra=len(aligned) - spacer_length,
                duplicate_of=None if first == alias else first,
            )
        )

    duplicates = [
        f"{guide.alias!r} (same sequence as {guide.duplicate_of!r})"
        for guide in guides
        if guide.duplicate_of is not None
    ]
    if duplicates:
        logger.warning(
            "%d entries in %s repeat an earlier entry's sequence under another "
            "name; the sequence is aligned once and its results are copied to "
            "every entry: %s",
            len(duplicates),
            origin,
            _listing(duplicates),
        )
    return guides