Einführung in Rust für Bioinformatiker

von Peter N Robinson;

Mit Dank an die Autoren und Übersetzern des Rust-Books: rustbook-de & rustbook-en.

Vorwort

Dieses Buch dient zur Begleitung des Software-Praktikums (FU Berlin) der AG Robinson am Berlin Institute of Health (BIH).

Einführung

Wir werden git und github einsetzen, um die Grundlagen der Versionskontrolle zu erlernen. Wir werden Code entwickeln, um mehrere Bioinformatik-Algorithmen zu implementieren. Aus didaktischen Gründen werden wir die Algorithmen von Grund auf implementieren, obwohl dies selten der Fall ist, was Sie im Produktionscode tun möchten.

Zeitplan (ungefähr)

  • Woche 1: Hello world! (Grundlagen von rustc, cargo, git, github)
  • Woche 2: Datenstrukturen in Rust: traits
  • Woche 3: Algorithmen in Rust
  • Woche 4 - Woche 8: Programmierübungen -- Arbeiten mit FASTQ Dateien in Rust -- Arbeiten mit BAM Dateien in Rust -- ggf. andere Themen

VS Code

Wir werden in diesem Praktikum VS Code verwenden.

  1. Herunterladen und installieren
  2. Walkthroughs (Einführungen): get started with VS Code
    • Copilot
    • Choose theme (dunkel/hell/Kontrast)
    • Support for languages, browse language extensions, suche nach "Rust", folgende Erweiterungspakete auswählen: rust, rust-analyzer, Rust Extension Pack, Rust Doc Viewer
  3. Den Plugin CodeLLDB installieren (debuggen)

Installation

VS Code Extensions

VS Code lässt sich über Erweiterungen (extensions) für eine ganze Reihe von Programmiersprachen anpassen und erweitern.

In den Einstellungen (Settings), das Extensions-Fenster öffnen und folgende extensions installieren:

  • rust
  • Rust Syntax
  • Rust Doc Viewer
  • rust-analyzer
  • Rust Extension Pack
  • CodeLLDB

Erste Schritte

Grundlagen von Rust

Ziel der ersten Woche: Prakltikumsteilnehmer sollen ein grundlegendes Verständnis von Rust entwickeln, indem sie die ersten Kapitel des Rust-Buches durcharbeiten: [Rust book](https://doc.rust-lang.org/book/). Übersetzungen des Buches in verschiedene Sprache sind verfügbar (z.B., [Deutsch](https://rust-lang-de.github.io/rustbook-de/)).

Es lohnt sich, das ganze Buch zu lesen und alle Übungen auszuführen. Wir werden im Praktikum einige der weichtigsten Themen gemeinsam besprechen.

Hallo Welt

cargo

Cargo ist der Rust-Paketmanager. Cargo lädt die Abhängigkeiten Ihres Rust-Pakets herunter, kompiliert Ihre Pakete, erstellt verteilbare Pakete und lädt sie auf crates.io, die Paketregistrierung der Rust-Community, hoch.

cargo

Hallo, Welt!

Was sonst, natürlich beginnen wir mit einem Hallo-Welt-Programm!

cargo new hello
cd hello
cargo run

Lies hierzu das entsprechende Kapitel des Rustbuchs. Bite auch das Kapitel zum cargo-Paketmanager lesen.

Klassen..?

In objektorientierten Programmiersprachen (z.B., Java, C++, auch Python) bezeichnet der Begriff "Klasse" eine benutzerdefinierte Datentypstruktur, die verwendet wird, um:

  • Daten (Attribute, Eigenschaften oder Felder) zu gruppieren.
  • Funktionen (Methoden) bereitzustellen, die auf diese Daten zugreifen oder sie manipulieren können. Die Vererbung ist ein Mechanismus, durch den eine abgeleitete Klasse die Eigenschaften und Methoden einer anderen Klasse (der Oberklasse oder Basisklasse) übernimmt. Ein Objekt ist dann eine Instanz einer Klasse, die die in der Klasse definierten Attribute und Methoden besitzt.

Rust ist keine OOP und hat keine Klassen. Stattdessen sind Strukturen und Aufzählungen (enums) die Bausteine zum Erstellen neuer Typen in der Domäne eines Rust-Programms.

Strukturen (struct)

Eine Struktur (struct) ist ein benutzerdefinierter Datentyp, mit dem man mehrere zusammenhängende Werte, die eine sinnvolle Gruppe bilden, zusammenpacken und benennen kann.

Hier ist ein aus der Luft gegriffenes Beispiel:

#![allow(unused)]
fn main() {
use std::collections::HashMap;

#[derive(Debug)]
struct DnaSequence {
    id: String,               
    sequence: String,         
    quality_scores: Vec<u8>,   
    annotations: HashMap<String, String>, 
    reference_position: Option<u32>,  
}

impl DnaSequence {
    /// Methoden -- mehr dazu später
}
}

Weitere Informationen:

enums..?

Aufzählungen (Enumerationen, kurz, enumserlauben es, einen Typ durch Aufzählung seiner möglichen Varianten (variants) zu definieren. In Python und vielen anderen Programmiersprachen sehen enums ungefähr wie folgt aus:

from enum import Enum
class Color(Enum):
    RED = 1
    GREEN = 2     
    BLUE = 3

Das heißt, ein enum in Python ist ein Datentyp, der verwendet wird, um eine Menge von benannten Konstanten zu definieren. Diese Konstanten sind typischerweise zusammengehörige Werte, die unveränderlich sind.

In Rust kann man enums analog definieren, z.B.,

#![allow(unused)]
fn main() {
enum Color {
    RED,
    GREEN,
    BLUE,
}

Color::RED;
}

In Rust können enums darüber hinaus mit ggf. unterschiedlichen Datentypen ausgestattet und für Pattern Matching verwendet werden.

S. das enum-Kapitel im Rust-Buch.

Es folgt ein kurzes Beispiel.

enum HttpResponse {
    Ok(String),                     // Variant with associated data (e.g., response body)
    NotFound,                       // Unit variant (no data)
    Redirect { url: String },       // Struct-like variant with named fields
    ServerError(u16, String),       // Tuple-like variant with multiple data types
}

fn handle_response(response: HttpResponse) {
    match response {
        HttpResponse::Ok(body) => {
            println!("Success: {}", body);
        }
        HttpResponse::NotFound => {
            println!("Error: Resource not found.");
        }
        HttpResponse::Redirect { url } => {
            println!("Redirecting to: {}", url);
        }
        HttpResponse::ServerError(code, message) => {
            println!("Server error {}: {}", code, message);
        }
    }
}

fn main() {
    // Example usage
    let response1 = HttpResponse::Ok("Welcome!".to_string());
    let response2 = HttpResponse::NotFound;
    let response3 = HttpResponse::Redirect { url: "https://example.com".to_string() };
    let response4 = HttpResponse::ServerError(500, "Internal Server Error".to_string());

    handle_response(response1);
    handle_response(response2);
    handle_response(response3);
    handle_response(response4);
}
```

Optional..?

In C wird oft der Wert NULL verwendet, um anzuzeigen, dass ein Zeiger keinen gültigen Wert enthält,

Das Problem mit Nullwerten besteht darin, dass du einen Fehler erhältst, wenn du versuchst, einen Nullwert als Nicht-Nullwert zu verwenden.

Aus diesem Grunde haben viele neuere Programmiersprachen wie Python und Java "Optional"-Datentypen.

Optional[int] bedeutet, dass die Funktion entweder einen int oder None zurückgeben kann.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// Funktion, die eine Zeichenkette zurückgibt oder NULL, wenn der Schlüssel nicht gefunden wird
const char* find_value(const char* key) {
    if (strcmp(key, "a") == 0) {
        return "Wert A";
    } else if (strcmp(key, "b") == 0) {
        return "Wert B";
    }
    return NULL;  // Kein Wert gefunden
}

int main() {
    const char* result;

    result = find_value("a");  // Schlüssel gefunden
    if (result != NULL) {
        printf("Gefundener Wert: %s\n", result);
    } else {
        printf("Kein Wert gefunden\n");
    }

    result = find_value("c");  // Schlüssel nicht gefunden
    if (result != NULL) {
        printf("Gefundener Wert: %s\n", result);
    } else {
        printf("Kein Wert gefunden\n");
    }

    return 0;
}

In Java gibt es deshalb die Klasse Optional, die verwendet wird, um anzuzeigen, dass ein Wert entweder vorhanden ist oder fehlt.

import java.util.Optional;

public class Main {
    public static void main(String[] args) {
        // Ein Optional mit einem Wert
        Optional<String> someValue = findValue("a");
        someValue.ifPresentOrElse(
            value -> System.out.println("Gefundener Wert: " + value),
            () -> System.out.println("Kein Wert gefunden")
        );

        // Ein Optional ohne Wert
        Optional<String> noValue = findValue("c");
        noValue.ifPresentOrElse(
            value -> System.out.println("Gefundener Wert: " + value),
            () -> System.out.println("Kein Wert gefunden")
        );
    }

    public static Optional<String> findValue(String key) {
        if ("a".equals(key)) {
            return Optional.of("Wert A");
        } else if ("b".equals(key)) {
            return Optional.of("Wert B");
        }
        return Optional.empty(); // Kein Wert gefunden
    }
}

In Rust gibt es die Aufzählung "Option":

#![allow(unused)]
fn main() {
enum Option<T> {
    None,
    Some(T),
}
}

Sie wird wie folgt verwendet.

fn find_value(key: &str) -> Option<&str> {
    match key {
        "a" => Some("Wert A"),
        "b" => Some("Wert B"),
        _ => None,
    }
}

fn main() {
    if let Some(value) = find_value("a") {
        println!("Gefundener Wert: {}", value);
    } else {
        println!("Kein Wert gefunden");
    }
}

Option ist ein zentrales und unverzichtbares Konstrukt in Rust, das nicht nur für Sicherheit und Klarheit sorgt, sondern auch das Risiko von Fehlern durch null-Referenzen vollständig eliminiert. Das Enum Option in Rust repräsentiert einen Wert, der entweder vorhanden ist (Some(T)) oder fehlt (None). Es wird häufig verwendet, um Werte oder Operationen sicher zu behandeln, die möglicherweise keinen Wert zurückgeben.

S. " Aufzählung Option und ihre Vorteile gegenüber Nullwerten" in dem enum Kapitel des Rust-Buchs.

Das Kontrollflusskonstrukt match

Das Kontrollflusskonstrukt namens "match" ist verglichen mit "if/else" und "switch" bei den meisten anderen Programmiersprachen extrem leistungsfähig und flexibel. Um in Rust zu programmieren ist ein gutes Verständnis von "match" unverzichtbar.

Es folgt ein gekünsteltes Beispiel, wobei nach Übereinstimmung mit drei Elementen geprüft wird (Einzelheiten werden während des Prkatikums besprochen!).

#![allow(unused)]
fn main() {
impl DnaSequence {
    /// Analysiert die Sequenz und liefert eine Beschreibung basierend auf verschiedenen Kriterien.
    fn analyze(&self) -> String {
        match (self.sequence.len(), self.reference_position, self.annotations.get("Gene")) {
            (0, _, _) => "Die Sequenz ist leer.".to_string(),
            (1..=50, Some(pos), Some(gene)) => format!(
                "Kurze Sequenz, referenziert an Position {} und gehört zu Gen {}.",
                pos, gene
            ),
            (1..=50, Some(pos), None) => format!(
                "Kurze Sequenz, referenziert an Position {}, aber kein Gen zugeordnet.",
                pos
            ),
            (51.., _, Some(gene)) => format!("Lange Sequenz, zugeordnet zu Gen {}.", gene),
            (51.., _, None) => "Lange Sequenz ohne Gen-Annotation.".to_string(),
            _ => "Sequenz konnte nicht analysiert werden.".to_string(),
        }
    }
}

}

s. das match-Kapitel im Rust-Buch.

Exceptions... ?

Guter Code muss immer mit dem DAU rechnen (Dem dümmsten anzunehmenden User)... Außerdem können unerwartete Dinge passieren, wie z.B. der Absturz einer API, auf die unser Code angwiesen ist.

Viele Sprachen (z.B. Pyhton, Java) behandeln solche Ausnahmen (Exceptions) mit einer try/catch-Struktur.

public static int divide(int a, int b){
    int c = 0;
    try {
        c = a/b; 
    } catch(ArithmeticException e){
        System.out.printf("Fehler: {e.getMessage}", e);
    }
    return c;
}

oder (besser)

public static int divide(int a, int b) throws ArithmeticException {
    if (b==0) {
        throw new ArithmeticException("Attempt to divide by zero!");
    }
    return a/c;
}

In Rust gibt es dagegen keine herkömmlichen Exceptions. Stattdessen verfolgt Rust einen Ansatz, der auf Ergebnis-Typen und Panics basiert, um Fehler zu handhaben.

Result

Result<T, E> ist der Typ, der zum Zurückgeben und Weitergeben von Fehlern verwendet wird. Es handelt sich um eine Aufzählung mit den Varianten Ok(T), die für Erfolg steht und einen Wert enthält, und Err(E), die für Fehler steht und einen Fehlerwert enthält.

fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        Err(String::from("Division durch Null ist nicht erlaubt"))
    } else {
        Ok(a / b)
    }
}

fn main() {
    let numerator = 10.0;
    let denominator = 0.0;

    match divide(numerator, denominator) {
        Ok(result) => println!("Ergebnis: {}", result),
        Err(e) => eprintln!("Fehler: {}", e),
    }
}

Warum Rust keine Ausnahmen verwendet

  1. Keine versteckten Kontrollflüsse: Fehlerbehandlung ist explizit.
  2. Kostenkontrolle: Keine zusätzlichen Laufzeitkosten durch Exception-Handling.
  3. Sicherheit: Rust fördert die Verwendung von Result oder Option, was den Umgang mit Fehlern zwingend macht.

Panic vs Result

Für schwerwiegende Fehler, die nicht erwartet oder behandelt werden sollen, bietet Rust das Konzept eines Panic. Ein Panic bedeutet, dass das Programm nicht fortgesetzt werden kann, und wird typischerweise verwendet, wenn ein logischer Fehler vorliegt, z. B. ein Index außerhalb der Grenzen eines Arrays.

fn main() {
    let v = vec![1, 2, 3];
    println!("{}", v[10]); // löst panic aus - beendet das Programm
}

Vgl. den Abschnitt When to use panic! vs Result .

unwrap

unwrap wird verwendet, um den enthaltenen Wert eines Option oder Result zu extrahieren. Wenn der Wert nicht existiert (None für Option oder Err für Result), führt unwrap zu einem panic!

fn main() {
    let some_value = Some(42);
    let none_value: Option<i32> = None;

    println!("{}", some_value.unwrap()); // Gibt 42 aus.

    // Führt zu einem panic!
    println!("{}", none_value.unwrap());
}

expect

expect ist wie unwrap, erlaubt aber, eine benutzerdefinierte Fehlermeldung anzugeben, wenn ein Fehlerzustand eintritt. Dies ist hilfreich für Debugging.

Im folgenden Beispiel führt das expect zu einem panic! mit einer benutzerdefinierten Fehlermeldung.

fn main() {
    let none_value: Option<i32> = None;

    println!("{}", none_value.expect("Option war None"));
}

Sicherere Alternativen zu unwrap

match.

Siehe hierzu Kapitel01-06.

fn main() {
    let some_value = Some(42);

    match some_value {
        Some(value) => println!("Wert: {}", value),
        None => println!("Kein Wert gefunden"),
    }
}

unwrap_or

Falls ein Wert nicht existiert, liefert unwrap_or einen Standardwert zurück.

fn main() {
    let none_value: Option<i32> = None;
    println!("{}", none_value.unwrap_or(0)); // Gibt 0 aus.
}

unwrap_or_else

Führt eine Funktion aus, um einen Standardwert zu berechnen, wenn der Wert nicht existiert.

fn main() {
    let none_value: Option<i32> = None;
    println!("{}", none_value.unwrap_or_else(|| 42)); // Gibt 42 aus.
}

?-Operator

Der ?-Operator kann in Funktionen verwendet werden, die einen Result zurückgeben. Er kann verwendet werden, um den Wert aus einem Result oder Option zu extrahieren, ohne jedes Mal explizit match schreiben zu müssen. Wenn ein Fehler auftritt, gibt der ?-Operator den Fehler direkt aus der umgebenden Funktion zurück.

fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        Err(String::from("Division durch Null"))
    } else {
        Ok(a / b)
    }
}

fn calculate() -> Result<(), String> {
    let result = divide(10.0, 2.0)?; // Extrahiert den Wert oder gibt den Fehler (gleich) zurück.
    println!("Ergebnis: {}", result);
    Ok(()) // Rückgabe im Erfolgsfall
}

fn main() {
    match calculate() {
        Ok(()) => println!("Berechnung erfolgreich!"),
        Err(e) => println!("Fehler: {}", e),
    }
}

Interfaces (Schnittstellen)...?

In Sprachen wie Java definiert eine Interface (Schnittstelle) die Signaturen von Methoden (ohne Implementierung), die von einer implementierenden Klasse bereitgestellt werden müssen.

public interface Animal {
    void makeSound();
}

public class Dog implements Animal {
    @Override
    public void makeSound() {
        System.out.println("Wuff!");
    }
}

public class Cat implements Animal {
    @Override
    public void makeSound() {
        System.out.println("Miau!");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal dog = new Dog();
        Animal cat = new Cat();

        dog.makeSound(); // Ausgabe: Wuff!
        cat.makeSound(); // Ausgabe: Miau!
    }
}

In Python gibt es keine Schnittstellen im engeren Sinne, jedoch kann man mit verschiedenen Konstrukten Ähntliches implementieren (vgl Duck typing).

Traits

In Rust haben Traits in etwa den Stellenwert von Interfaces, jedoch werden sie anders implementiert.

Man kann bestehende Traits für eigene Datentypen implementieren oder aber neue Traits definieren. Hier wollen wir den Trait FromString aus der Standardbibliothek für eine eigene Struktur implementieren.

Der Trait FromString ist von der Standardbibliothek.

#![allow(unused)]
fn main() {
pub trait FromStr: Sized {
    type Err;
    fn from_str(s: &str) -> Result<Self, Self::Err>;
}
}

Die parse-Method erwartet nur, dass ein Datentyp diesen Trait implementiert.

#![allow(unused)]
fn main() {
pub fn parse<F: FromStr>(&self) -> Result<F, F::Err> {
    FromStr::from_str(self)
}
}

Stellen wir uns folgende Struktur vor

#![allow(unused)]
fn main() {
struct DnaString {
    dna: String;
}
}

Um unsere Struktur mit parse verwenden zu können, müssen wir FromStr implementieren. Unser Code überprüft, ob alle Zeichen in dem zu parsenden String A, C, G, order T sind.

#![allow(unused)]
fn main() {
use std::str::FromStr;

struct DnaString {
    pub dna: String,
}

#[derive(Debug)]
struct DnaParseError;

impl FromStr for DnaString {
    type Err = DnaParseError;

    fn from_str(s: &str) -> Result<DnaString, DnaParseError> {
        if s.chars().all(|c| matches!(c, 'A' | 'C' | 'G' | 'T')) {
            Ok(DnaString { dna: s.to_string()})
        } else {
            Err(DnaParseError)
        } 
    }
}

#[cfg(test)]
mod test {
    use super::DnaString;

    #[test]
    fn test_dna_parse() {
        let valid_dna = "ATGCTCGCTAG";
        let dna_struct = valid_dna.parse::<DnaString>();
        assert!(dna_struct.is_ok());
        let dna_struct = dna_struct.expect("Could not unwrap DNA");
        assert_eq!(valid_dna, dna_struct.dna);
    }

    #[test]
    fn test_invalid_dna_parse() {
        let valid_dna = "ATGCT??CGCTAG";
        let dna_struct = valid_dna.parse::<DnaString>();
        assert!(dna_struct.is_err());
    }
}
}

Übung 1

Der folgende Code kompiliert nicht

#![allow(unused)]
fn main() {
let d = DnaString { dna: "TGCAACGT".to_string()};
writeln!("{}", d);
}

Verwenden Sie die Fehlermeldung des Rustkompilierers um herauszufinden, warum der Code nicht funktioniert. Es gibt mindests zwei Korrekturen - finden Sie sie und schreiben Sie entsprechenden Rustcode.

Sortieralgorithmen

Wir werden als Übung mehrere Sortieralgorithmen in Rust implementieren. Sinn und Zweck der Überung ist nicht unbedingt, Sortieralgorithmen zu erlernen, sondern zu lernen, wie man mit Traits in Rust umgeht und wie der Debugger eingesetzt werden kann.

Bevor wir mit diesem Abschnitt beginnen, muss der Debugger (CodeLLDB) als VS Code extension installiert sein, s. dazu Extensions.

Bubblesort

Bubblesort ist ein einfacher Sortieralgorithmus, der Elemente in einer Liste durch wiederholtes Vergleichen benachbarter Elemente sortiert. Dabei werden zwei benachbarte Elemente vertauscht, wenn sie in der falschen Reihenfolge stehen. Der Algorithmus wird so lange wiederholt, bis die Liste vollständig sortiert ist. Der Name "Bubblesort" kommt daher, dass kleinere Elemente in der Liste "nach oben steigen", ähnlich wie Luftblasen (bubbles) im Wasser.

Algorithmus

  1. Starte am Anfang der Liste.
  2. Vergleiche Element i (ei) mit Element i+1 (ei+1)
  3. Wenn ei größer als ei+1 ist, vertausche sie. Andernfalls lasse sie unverändert.
  4. Gehe so durch die gesamte Liste.
  5. Wiederhole den Vorgang für die gesamte Liste, bis keine Vertauschungen mehr notwendig sind (die Liste ist dann sortiert).

Dieser Algorithmus hat eine Zeitkomplexität von O(n2).

#![allow(unused)]
fn main() {
pub fn bubble_sort<T: PartialOrd>(v: &mut [T]) {
    for i in 0..v.len() {
        let mut sorted = true;
        for j in 0..(v.len()-1) - i {
            if v[i] > v[i+1] {
                v.swap(i, i+1);
                sorted = false;
            }
        }
        if sorted {
            return;
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_bubble_sort() {
        let mut v = vec![4, 6, 77, 1, 13, 24];
        bubble_sort(&mut v);
        assert_eq!(v, vec![1, 4, 6, 13, 24, 77]);
    }
}
}

Übungen

Erzeuge eine Rust-Applikation namens ch2 mit cargo.

  1. Führe den oben stehenden Rustcode aus.
  2. Definiere einen struct-Datentype mit zwei Ganzzahl-Feldern. Konsulitere die Dokumentation für den PartialOrd-Trait (https://doc.rust-lang.org/std/cmp/trait.PartialOrd.html). Implementiere den Trait für die Struktur, so dass structs gemäß der Summe beider Felder sortiert werden. Sortiere drei Beispiel structs.
  3. Schreibe einen Test der bubble-Sort-Methode für Vektoren von Ganzzahlen
  4. Schreibe einen Test für die oben definierte Struktur
  5. Schreibe eine Testmethode, welche mehrere Tests auf einmal durchführt.

Quicksort

Ziele:
  1. Slices verstehen
  2. quicksort-Algorithmus in idiomatischem Rust programmieren
  3. lldb Debugger verstehen

Quicksort ist ein schneller, rekursiver Sortieralgorithmus, der nach dem Prinzip Teile und herrsche arbeitet. Im Durchschnitt führt der Quicksort-Algorithmus \(O(n\cdot log(n))\) Vergleiche durch.

Zunächst wird die zu sortierende Liste in zwei Teillisten („linke“ und „rechte“ Teilliste) getrennt. Dazu wählt Quicksort ein sogenanntes Pivotelement aus der Liste aus. Alle Elemente, die kleiner als das Pivotelement sind, kommen in die linke Teilliste, und alle, die größer sind, in die rechte Teilliste. Die Elemente, die gleich dem Pivotelement sind, können sich beliebig auf die Teillisten verteilen. Nach der Aufteilung sind die Elemente der linken Liste kleiner oder gleich den Elementen der rechten Liste.

Anschließend muss man also noch jede Teilliste in sich sortieren, um die Sortierung zu vollenden. Dazu wird der Quicksort-Algorithmus jeweils auf der linken und auf der rechten Teilliste ausgeführt. Jede Teilliste wird dann wieder in zwei Teillisten aufgeteilt und auf diese jeweils wieder der Quicksort-Algorithmus angewandt, und so weiter. Diese Selbstaufrufe werden als Rekursion bezeichnet. Wenn eine Teilliste der Länge eins oder null auftritt, so ist diese bereits sortiert und es erfolgt der Abbruch der Rekursion.

Wir haben hier den Java-Code vom oben genannten YouTube-Video leicht angepasst bzw. kommentiert wiedergegeben. Sie können optional das Programm laufen lassen, indem Sie den Code in eine Datei namens Quick.java kopieren und diese Datei mit $ javac Quick.javakompliieren und das Ergebnis mit $ java Quickausführen.

import java.util.Random;

public class Quicksort {

  public static void main(String[] args) {
    int[] numbers = [4, 6, 1, 8, 13, 33, 2];
    printArray(numbers);
    quicksort(numbers);
    printArray(numbers);
  }

  private static void quicksort(int[] array, int lowIndex, int highIndex) {
    // 1. Basisfall der Rekursion: jedes Feld mit nur einem Element ist schon sortiert!
    if (lowIndex >= highIndex) {
      return;
    }
    // 2. Das letzte ElementWir dient als Pivotelement
    int pivot = array[highIndex];
    // 3. Die zu sortierende Liste in zwei Teillisten wird in zwei Unterlisten getrennt.
    // Alle Elemente, die kleiner als das Pivotelement sind, kommen in die linke Teilliste, 
    // und alle, die größer sind, in die rechte Teilliste.
    // Nach Ausruf von 'partition' ist die Position des Pivotelements korrekt, aber die
    // linke und rechte Unterlisten sind nicht unbedingt richtig sortiert
    int leftPointer = partition(array, lowIndex, highIndex, pivot);
    // 4. Sortierte die linke Unterliste (rekursiv)
    quicksort(array, lowIndex, leftPointer - 1);
    // 5. Sortierte die rechte Unterliste (rekursiv)
    quicksort(array, leftPointer + 1, highIndex);

  }

  private static int partition(int[] array, int lowIndex, int highIndex, int pivot) {
    int leftPointer = lowIndex;
    int rightPointer = highIndex - 1;

    while (leftPointer < rightPointer) {
        // 1. Von links gehen, bis eine Zahl gefunden wird, die größer als der Pivot ist, 
        // oder der linke Zeiger mit dem rechten identisch ist
        while (array[leftPointer] <= pivot && leftPointer < rightPointer) {
            leftPointer++;
        }
        // 2. Von rechts gehen, bis eine Zahl gefunden wird, die kleiner als der Pivot ist, 
        // oder der rechte Zeiger mit dem linken identisch ist.
        while (array[rightPointer] >= pivot && leftPointer < rightPointer) {
        rightPointer--;
        }
        //  3. Die Werte vertauschen, auf die der linke und der rechte Zeiger zeigen
        swap(array, leftPointer, rightPointer);
    }
    // 4. An dieser Stelle muss das Pivotelement mit dem Wert am linken Zeiger vertauscht werden
    // (highIndex ist der Index des Pivotelements)
    if(array[leftPointer] > array[highIndex]) {
      swap(array, leftPointer, highIndex);
    }
    else {
      leftPointer = highIndex;
    }
    
    return leftPointer;
  }

  private static void swap(int[] array, int index1, int index2) {
    int temp = array[index1];
    array[index1] = array[index2];
    array[index2] = temp;
  }

  private static void printArray(int[] numbers) {
    for (int i = 0; i < numbers.length; i++) {
      System.out.println(numbers[i]);
    }
  }
}

FASTQ

Ziele:
  1. FASTQ verstehen
  2. Einfache Qualitätsbewertungsroutined in idiomatischem Rust programmieren.
  3. Grundlagen von GitHub vestehen und anwenden.
Wir beginnen mit einer Einführung in das FASTQ-Format. Falls das Format schon bekannt ist, kann die Einführung übersprungen werden.

FASTQ ist ein Dateiformat für den Austausch von Sequenzierungsdaten, das sowohl die Sequenz als auch eine entsprechende Bewertung der Basenqualität enthält.1 Sein Name leitet sich vom ehrwürdigen FASTA-Format ab, einem textbasierten Format zur Darstellung von DNA- und Aminosäuresequenzen, das ursprünglich 1985 zusammen mit einem Softwarepaket für das Alignment von Nukleinsäuren und Proteinen veröffentlicht wurde Lipman et al. 1985. Das FASTA-Format ist eines der in der Bioinformatik am weitesten verbreiteten Formate überhaupt. Die erste Zeile beginnt mit einem > (Größer-als)-Symbol, direkt gefolgt von einem Namen oder einem eindeutigen Sequenzbezeichner, optional gefolgt von einer Beschreibung der Sequenz. Die folgenden Zeilen enthalten die Sequenz (in der Regel mit 70 oder 80 Zeichen pro Zeile). Eine Datei kann mehrere Sequenzeinträge enthalten, aber jeder muss mit einer >-Zeile beginnen. Die FASTA-Datei für die Homo sapiens Fibrillin 1 (\textit{FBN1}) messenger RNA beginnt zum Beispiel so

>NM_000138.4 Homo sapiens fibrillin 1 (FBN1), mRNA
AGTATTTCTCTCGCGAGAAACCGCTGCGCGGACGATACTTGAAGAGGTGGGGAAAGGAGGGGGCTGCGGG
AGCCGCGGCAGAGACTGTGGGTGCCACAAGCGGACAGGAGCCACAGCTGGGACAGCTGCGAGCGGAGCCG
AGCAGTGGCTGTAGCGGCCACGACTGGGAGCAGCCGCCGCCGCCTCCTCGGGAGTCGGAGCCGCCGCTTC
(weitere 165 Zeilen)

Das FASTQ-Format ist eine Erweiterung des FASTA-Formats, in dem zusätzlich ein numerischer Qualitätswert für jedes Nukleotid in einer Sequenz gespeichert wird. Jeder Read in der Datei wird durch vier Zeilen mit den folgenden Informationen dargestellt (ohne Längenbegrenzung):

@SEQ_ID
GATTTGGGGTTCAAAGCAGTATCGATCAAATAGTAAATCCATTTGTTCAACTCACAGTTT
+
!''*((((***+))%%%++)(%%%%).1***-+*''))**55CCF>>>>>>CCCCCCC65 
FASTQ
Bestandteile eines FASTQ-Files.

Die @-Zeile umfasst ein freies Formatfeld, das in erster Linie für den Titel oder die Kennung der gemeldeten Sequenz verwendet wird. Illumina hat eine typische Namenskonvention für die Titelzeile, die wir weiter unten erläutern werden. Die zweite Zeile enthält die aufgerufenen Basen, d. h. die Sequenz des Read. Es folgt die +-Zeile Obwohl frühe Versionen des FASTQ-Formats in dieser Zeile eine Wiederholung der Titelzeile enthielten, ist es inzwischen Standard, dass die Zeile nur das Pluszeichen (+) enthält, was die Dateigröße erheblich reduziert. Die vierte Zeile schließlich enthält die ASCII-kodierten Phred-Qualitätsbewertungen und muss die gleiche Länge wie die Sequenzzeile haben. Somit wird für jede der in der zweiten Zeile angegebenen Basen eine Qualitätsbewertung in der entsprechenden Position der vierten Zeile angegeben. In einigen früheren Versionen von FASTQ wurden verschiedene Schemata für die Kodierung von Qualitätswerten verwendet, aber die Gemeinschaft hat sich auf das Format Sanger/Illumina 1.8+ (Phred+33) geeinigt, das hier beschrieben wird.

1

Texte von diesem und dem folgenden Kapitel wurden den entsprechenden Kapiteln in Computational Exome and Genome Analysis entnommen und leicht angepasst.

Phred-Score

Der Phred-Score (d.h., Punktzahl) ist definiert als

\[ Q = -10 \log_{10} \]

wobei \(p\) die Wahrscheinlichkeit ist, dass der entsprechende Basisaufruf falsch ist und \(Q\) die Phred-Punktzahl ist (gerundet auf den nächstliegenden ganzzahligen Wert). Der Phred-Score ist also eine einfache Transformation der Fehlerwahrscheinlichkeit, die eine einfache, aber einigermaßen platzsparende Kodierung darstellt (Tabelle 1).

Tabelle 1. Basisqualität und -genauigkeit
QPhred p Genauigkeit
0 1 0%
10 10-1 90%
20 10-2 99%
30 10-3 99.9%
40 10-4 99.99%
50 10-5 99.999%
60 10-6 99.9999%
70 10-7 99.99999%
80 10-8 99.999999%
90 10-9 99.9999999%
93 10-9.3 99.99999995%

Anmerkung: Phred-Basisqualitätswerte und ihre zugehörige Fehlerwahrscheinlichkeit (p) und Basisgenauigkeit. Eine Auswahl von Werten vom niedrigsten (0) bis zum höchsten (93) Phred-Score, der in einer FASTQ-Datei darstellbar ist, wird gezeigt.

ASCII-Kodierung von Phred-Scores

Die auf der letzten Seite beschriebene Transformation wandelt eine Wahrscheinlichkeit in einen ganzzahligen Wert zwischen 0 und 93 um. Die Werte werden in der FASTQ-Datei nicht als ein- oder zweistellige Zahl gespeichert, sondern als einzelnes Zeichen (char>), was wiederum zu einer erheblichen Verringerung der Dateigröße führt. Nichtsdestotrotz können Phred-Qualitäten von 0 bis 93 ein sehr breites Spektrum an Fehlerwahrscheinlichkeiten repräsentieren, das von 1,0 (100% Fehlerwahrscheinlichkeit oder einfach eine falsche Base) bis zu \(10^{-9.3}\) reicht, was einem extrem genauen Base-Call entspricht.

Um die Phred-Bewertungen als Zeichen zu speichern, werden die Scores in ASCII-Zeichen umgewandelt. ASCII (American Standard Code for Information Interchange) ist ein früher Zeichencodierungsstandard für die Darstellung von Zeichen in Computern und anderen Geräten, der erstmals 1963 veröffentlicht wurde. Die ASCII-Codes 0 bis 31 sind nicht druckbar. Der ASCII-Code 007 beispielsweise entspricht einem Steuercode, der ursprünglich gesendet wurde, um bei älteren Systemen eine elektromechanische Glocke zu läuten oder bei einigen Computern einen Systemwarnton abzuspielen. Das erste druckbare Zeichen ohne Leerzeichen ist der ASCII-Code 33, und der letzte druckbare ASCII-Code ist 126. FASTQ-Dateien verwenden also die ASCII-Codes 33-126 zur Codierung der Phred-Qualitäten von 0 bis 93 (Tabelle 2).

Base Quality and ASCII Encoding
ASCII character Decimal value Phred score
! 33 0
" 34 1
# 35 2
$ 36 3
A 65 22
B 66 23
x 120 87
y 121 88
z 122 89
{ 123 90
| 124 91
} 125 92
~ 126 93
Beispiele für die ASCII-Kodierung von Phred-Punkten. Der Phred-Score kann durch Subtraktion von 33 vom Dezimalwert des ASCII-Zeichens ermittelt werden.

Illumina FASTQ-Dateibenennungsschema

Illumina verwendet ein Standardbenennungsschema für FASTQ-Dateien. Es ist nützlich zu verstehen, wie dieses Schema aufgebaut ist. Das allgemeine Schema für solche Dateien lautet

{Probenname}_{Barcode_sequenz}_L{Spurnummer}_R{Readnummer}_{Satz_Nummer}.fastq.gz ,

d.h., english

{sample_name}_{barcode_sequence}_L{lane}_R{read_number}_{set_number}.fastq.gz .

Schauen wir uns die folgende GIAB-Datei (Genome in a bottle) an:

NIST7035_TAAGGCGA_L001_R1_001.fastq.gz

Die Bestandteile dieses Namens sind:

  1. sample_name: NIST7035. Dies ist der Probenname, der im Probenblatt für den Sequenzierungslauf angegeben ist.
  2. barcode_sequence: TAAGGCGA. Dies ist die Nukleotidsequenz des molekularen Barcodes, der zur Markierung der Probe für das Multiplexing verwendet wird.
  3. lane: 001. Die Lane-Nummer (1--8).
  4. Spurnummer (d.h., read number): 1. Die Read-Nummer für Paired-End-Reads. R1 bedeutet Read 1, und für einen Paired-End-Sequenzierungslauf gibt es eine zusätzliche Datei mit R2 (Read 2), deren Name ansonsten genau dem Dateinamen für Read 1 entspricht
  5. set_number: 001. Die maximale Dateigröße von FASTQ-Dateien wird mit der Befehlszeilenoption --fastq-cluster-count des Skripts configureBclToFastq.pl festgelegt, das Teil der Illumina \index{CASAVA} CASAVA-Software-Suite gehört. Wenn mehr Daten vorhanden sind, werden die Daten in separate FASTQ-Dateien mit der entsprechenden Dateigröße aufgeteilt (Um nur eine einzige FASTQ-Datei zu erstellen, kann eine ``0'' angegeben werden). Die verschiedenen Dateien, die derselben Probe/demselben Barcode/derselben Spur entsprechen, werden durch die mit 0 gefüllte dreistellige Set-Nummer unterschieden.

Bestimmte Illumina-Sequenzer verwenden andere FASTQ-Dateibenennungsschemata. Einzelheiten finden Sie in der Illumina-Dokumentation.

Paired-End-Sequenzierung

Beachten Sie, dass bei Paired-End-Läufen die übereinstimmenden FASTQ-Dateien genau die gleiche Anzahl von Reads aufweisen müssen und die Reads in beiden Dateien die gleiche Reihenfolge haben müssen. Dies kann mit den UNIX-Befehlen zcat und wc überprüft werden. Der Befehl cat liest Daten aus Textdateien und gibt deren Inhalt auf der Befehlszeilenschnittstelle aus, und der Befehl \verb+zcat+ tut dasselbe mit gzip-komprimierten Dateien. Der Befehl wc zählt Wörter, Zeilen und Zeichen in Textdateien. Wenn wir die Befehle wie folgt kombinieren, sehen wir, dass jede der beiden heruntergeladenen Dateien die gleiche Anzahl von Zeilen hat.

$ zcat NIST7035_TAAGGCGA_L001_R1_001.fastq.gz | wc -l
80812008
$ zcat NIST7035_TAAGGCGA_L001_R2_001.fastq.gz | wc -l
80812008

Wir können nun die Gesamtzahl der Zeilen durch vier teilen, um die Gesamtzahl der Reads zu erhalten (da jeder Read insgesamt vier Zeilen in der FASTQ-Datei einnimmt). Beachten Sie, dass wir nicht einfach nach Zeilen suchen können, die mit @ beginnen, da das ASCII-Symbol, das einem Phred-Score von 31 entspricht, ebenfalls @ ist und somit auch Qualitätszeilen mit diesem Zeichen beginnen können.

Read-Benenunng

Betrachten Sie die folgende Kennzeichnung, die von einem Read aus dem in Kapitel~\ref{ch:data} beschriebenen Exom-Datensatz NA12878 stammt.

@HWI-D00119:50:H7AP8ADXX:1:1101:2100:2202 1:N:0:TAAGGCGA

Die Syntax der Namenszeilen entspricht dem folgenden Schema:

@{instrument}:{rujn}:{flowcell_ID}:{lane}:{tile}:{x-pos}:{y-pos}\
    {read}:{is_filtered}:{control_number}:{index} 

Die im obigen Etikett gespeicherten Informationen sind in Tabelle 3 zusammengefasst. Der erste Teil dieser Bezeichnung, bis zum Leerzeichen, wird als Lesename oder Bezeichner verwendet.

Illumina-Sequenzkennungen
Element Erläuterung
HWI-D00119 Die eindeutige Gerätebezeichnung
50 Die Lauf-ID (dies ist das 50ste Mal, dass dieses Gerät betrieben wurde)
H7AP8ADXX Flowcell-ID
1 Flowcell-Lane (Spur: 1–8)
1101 Tile-Nummer innerhalb der Lane
2100 X-Koordinate des Clusters innerhalb des Tiles (d.h., der ``Kachel'')
2202 Y-Koordinate des Clusters innerhalb des Tiles
1 Mitglied eines Paares (1 oder 2; 2 kann nur für Paired-End- oder Mate-Pair-Sequenzierung verwendet werden)
N Y: Read hat den "Chastity"-Filter verletzt (solche Reads können herausgefiltert oder in der FASTQ-Datei belassen werden); N: Read hat den Keuschheitsfilter nicht verletzt
0 0, wenn keines der Kontrollbits aktiviert ist, andernfalls ist es eine gerade Zahl. Auf HiSeq X- und NextSeq-Systemen wird die Kontrollspezifikation nicht durchgeführt und diese Zahl ist immer 0.
TAAGGCGA Indexsequenz (Barcode)

Hinweis: Jedes Leseetikett speichert Informationen in einem Standardschema (Das Schema wurde mit CASAVA 1.8 und später verwendet). Wir haben die (optionale) UMI-Feld (Unique Molecular Identifier) weggelassen, da es nicht für die Exom- oder Genomsequenzierung verwendet wird.

S. auch Cook, 2010.

FASTQ-Analyse mit Rust

Ziele:
  1. Einfache Qualitätsbewertungsroutinen in idiomatischem Rust programmieren.
  2. GitHub.
  3. Test-Driven Development.

In diesem Abschnitt wollen wir eine einfache Rust-Version des Tools FastQC schreiben.

GitHub

GitHub ist eine Online-Plattform, die Entwicklern und Teams Werkzeuge zur Versionskontrolle und Zusammenarbeit an Softwareprojekten bietet. Sie basiert auf Git, einem weit verbreiteten Versionskontrollsystem, und erweitert dessen Funktionen um eine benutzerfreundliche Weboberfläche sowie zahlreiche zusätzliche Funktionen.

Falls Sie noch nicht mit GitHub gearbeitet haben, arbeiten Sie diesen Tutorial durch.

Praktikumsteilnehmer werden kleine Teams bilden, und am Entwicklen der Software zusammenarbeiten. Hierbei werden wir main'' und develop'' Branches verwenden. Teilnehmer sollen Feature-Branches kreieren und Pull-Requests erstellen, welche von anderen Teilnehmern zu kommentieren und ggf. anzunehmen sind.

Wir werden das Arbeiten mit GitHub während des Praktikums erläutern und demonstrieren.

Testgetriebene Entwicklung (Test-Driven Development; TDD)

Die TDD ist eine Methode zum Schreiben von Code, bei der ein automatisierter Testfall auf Unit-Ebene geschrieben wird, der fehlschlägt, dann wird gerade genug Code geschrieben, um den Test zu bestehen, dann werden sowohl der Testcode als auch der Produktionscode überarbeitet und dann mit einem anderen neuen Testfall wiederholt.

TDD in Rust

Wir werden das Erstellen von Unit-Tests in Rust während des Praktikums erläutern und demonstrieren. Es folgen Erklärungen zu einem einfachen Beispiel.

Übung 1

Die Definition des Phred-Scores in Bezug auf die Fehlerwahrscheinlichkeit wurde in diesem Abschnitt erklärt. Analog dazu lässt sich die Fehlerwahrscheinlichkeit aus der Phred-Punktzahl wie folgt berechnen.

\[ p = 10^{-Q/10}
\]

Konvertieren Sie die ASCII-Symbole für die Basen des folgenden Reads in Phred-Scores und berechnen Sie den durchschnittlichen Phred-Score dieser Basen. Verwenden Sie dann die Gleichung, um die durchschnittliche Fehlerwahrscheinlichkeit zu berechnen.

Wir werden im Laufe des Arbeitens mit FASTQ-Dateien überlegen (müssen), welche Datenstrukturen am besten geeignet sind, um die erforderlichen Analysen korrekt und effizient durchzuführen. Für diese Übung wollen wir damit anfangen, einige Funktionen zu schreiben, die den Qualitätsstring bewerten. Es folgt ein weiteres Bespiel.

@My-Illu:6:73:941:1973#0/1
GATTTGGGGTTCAAAGCAGTATCGATCAAATAGTAAATCCATTTGTTCAACTCACAGTTT
+
!''*((((***+))%%%++)(%%%%).1***-+*''))**55CCF>>>>>>CCCCCCC65

Der folgende einfache Algorithmus kann verwendet werden, um den durchschnittlichen Qualitätswert zu berechnen.

Algorithm read_qual(qual_string)
    let n = len(qual_string);
    let qual_sum = 0;
    for qual in qual_string {
        let q = calculate_phred(qual)
        qual_sum += q
    } 
    return qual_sum / n

Wir richten die TDD für die function ``calculate_phred'' ein. Wir kreieren eine Datei namens fastq.rs und schreiben die folgende Funktion (die natürlich nur für ein bestimmtes Argument das richtige Ergebnis liefert!).

#![allow(unused)]
fn main() {
pub fn calculate_phred(qual: char) -> usigg {
    42
}
}

Wir richten dann eine Testfunktion ein (in der Regel lohnt es sich, das Testmodul am Ende der jeweiligen Code-Datei unterzubringen)

#![allow(unused)]
fn main() {
#[cfg(test)]

mod test {
    fn test_calculate_phred {
        let qual: char = '&';
        let expected:usign =	5; // Phred-Score für das Zeichen '&'
        let res = calculate_phred(qual);
        assert_eq!(expected, res);
    }

    // Alternativ können viele Ergebnisse auf einmal getestet werden:
    fn test_calculate_phred {
        let tests: Vec<char, usign> = vec![
            ('&', 5),
            ('+', 10)
        ];
        for test in tests {
            let res = calculate_phred(test.0);
            assert_eq!(test.1, res);
        }
    }
    // ggf. andere Testfunktionen
}
}

Wenn wir diesen Test zum ersten Mal durchführen, wird er natürlich einen Fehler melden. Wir arbeiten so lange an der Funktion ``calculate_phred'' bis die korrekt funktioniert. Wir arbeiten ggf. an der Funktion solange weiter, bis sie effizient ist. Dann gehen wir zur nächsten Funktion...Mit jeder neuen Funktion schreiben wir zunächst einen Test und dann die Funktion. Wir lassen jedes Mall alle Tests laufen. Das volle Konzept der TDD umfasst vieles mehr, aber für dieses Praktikum, wollen wir die hier skizzierte, einfache Vorgehensweise für alle Übungen anwenden.

Schreiben Sie eine korrekte Version der Funktion. Testen alle relevanten Argumente (kann man das "schlau" machen?).

Schreiben Sie dann eine Funktion für read_qual und testen Sie diese Funktion.

Übung 2

Schreiben Sie ein kleines Rust-Programm, um die in dem folgenden Datei-Namen enthaltenen Information zu erklären.

@HWI-D00107:50:H6BP8ACWV:5:2204:10131:51624 2:N:0:AGGCAGAA

Übung 3

Schreiben Sie ein kleines Rust-Programm, um die in dem folgenden Read-Namen enthaltenen Information zu erklären.

@Machine42:1:FC7:7:19:4229:1044 1:N:0:TTAGGC

\subsubsection*{Exercise 4} What does the quality character \verb+>+ mean? What is the associated error probability?

Übung 4

Stellen wir uns einen Read zu 1000bb vor, bei dem alle Basen einen Phred-Score von & aufweisen. Berechen Sie die erwartete Anzahl von Fehlern für den Read (unter der Unabhängigkeitsannehma).

FasQC in Rust

In diesem Abschnitt wollen wir üben, wie man eine Rustapplikation konzipiert und die Software-Architektur entwirft (Stukturen, Traits, Module, usw.). Praktikumsteilnehmer sollen sich erstmal jeder für sich eine Applikation entwerfen. Wir werden die Entwürfe zusammen besprechen und Teilnehmer sollen ihre entwürfe entsprechend ggf. korrigieren. Die Programmierarbeit soll in Kleingruppen wie oben beschrieben geleistet werden.

FastQC ist eine Applikation, die unter anderem eine Qualitätsbewertung von FASTQ-Dateien ausführt und die Ergebnisse in tabellarischer Form bzw. graphisch darstellt. Inzwischen gibt es viele neuere Applikationen, aber FastQC wurde früher von sehr vielen Gruppen sehr oft verwendet.

Zur Kontrolle Ihrer Ergebnisse können Sie FastQC hier herunterladen.

Ziel dieses Abschnitts ist es, die folgenden Funktionen zu als Rust-Applikation zu programmieren.

Befehlszeilenargumentenparser

Ihre Applikation soll den Pfad zu einer (Single-End) oder zwei (Paired-End) FASTQ-Dateien also Kommandozeilenargumente einlesen.

Wir empfehlen clap.

Beschreibende Statistiken

Als erstes überprüfen Sie ob die Datei existiert. Falls nicht, geben Sie eine verständliche Fehlermeldung aus.

Geben Sie die Informationen über den Dateinamen aus (s. hierzu letzte Seite)

Sequenzqualität (pro Base)

Geben Sie eine Tabelle aus mit der durchschnittlichen Qualität (Phred-Wert) für jede Position der Reads.

Sequenzqualität (pro Sequenz)

Geben Sie die durchschnittliche Qualität aller Reads aus.

Sequenzidentität (pro Base)

Geben Sie die durchschnittlichen Anteile von A, C, G, T für jede Position der Reads aus.

GC-Gehalt (pro Base)

Der GC-Gehalt definiert sich als prozentualer Anteil der Nukleotidbasen Guanin und Cytosin an der Gesamtheit der vier Basen (Adenin, Thymin, Guanin, Cytosin) der DNA. Sie den Wikipedia-Eintrag. Geben Sie den durchschnittlichen GC-Gehalt für jede Position der Reads aus.

GC-Gehalt (pro Sequenz)

Geben Sie den durchschnittlichen GC-Gehalt aller Reads aus.

Längenverteilung

Geben Sie eine Tabelle mit der Verteilung der Längen der einzelen Read aus (Anzahl und Prozentsatz).

SAM/BAM

Ziele:
  1. SAM/BAM verstehen
  2. Ausgewählte SAM/BAM-Analysen in idiomatischem Rust programmieren.

Das SAM-Format (Sequence Alignment/Map) ist ein tabulatorgetrenntes Textformat, das ein universelles Format für die Speicherung von Alignments von NGS-Reads zu einem Referenzgenom sein soll.1 BAM (Binary Alignment/Map) ist die komprimierte (binäre) Version von SAM. Das SAM/BAM-Format kann Alignments von allen wichtigen Alignern wie Bowtie 2 oder BWA speichern, unterstützt die Indexierung für eine schnelle Suche und Betrachtung mit dem IGV und wird von Software zur Variantenerkennung weitgehend unterstützt.

Es gibt eine umfangreiche Online-Dokumentation des Formats, und wir werden hier nicht versuchen, alle Details zu behandeln. Stattdessen werden wir die verschiedenen Komponenten des Formats anhand von kleinen Beispielen intuitiv erklären. Darüber hinaus zeigen wir, wie man die Eigenschaften der durch SAM/BAM-Dateien dargestellten Alignments analysiert. Zu diesem Zweck werden wir hauptsächlich SAMtools verwenden.

1

Texte von diesem Kapitel wurden dem Kapitel SAM/BAM Format von Peter Robinson and Peter Hansen} in Computational Exome and Genome Analysis entnommen und leicht angepasst.

SAM-Grundlagen

Wir werden das grundlegende SAM-Format anhand eines Beispiels erläutern, bei dem fünf Reads der Länge 10 auf eine Referenzsequenz der Länge 40 abgebildet werden (Abbildung 1).

Lesen Sie die Ausführungen zum SAM-Format und bearbeiten Sie die Übung am Ende der Seite.

SE reads in IGV SE Reads
Abbildung 1 SAM-Datei (Minimalbeispiel). Oben: Alignment. Unten: Entsprechende SAM-Datei.

SAM-Dateien bestehen aus einem optionalen Header-Abschnitt, gefolgt von einem Alignment-Abschnitt. Die Kopfzeilen beginnen mit `@'. Im Alignment-Abschnitt der SAM-Datei stellt jede Zeile ein Alignment für einen Read dar und umfasst mindestens 11 Pflichtfelder (Tabelle 1).

SAM Format:Pflichtfelder
Col Feld Beschreibung Beispiel
1 QNAME Query template NAME read_1
2 FLAG Bitweise Flag 0
3 RNAME Name der Referenzsequenz chrE
4 POS Linke Kartierungsposition (1-basiert) 11
5 MAPQ MAPping Qualität 37
6 CIGAR CIGAR String 10M
7 RNEXT Referenzname des Mate-Reads oder NEXT read *
8 PNEXT Position des Mate-Reads oder NEXT read 0
9 TLEN Beobachtete Template-Länge (LENgth) 0
10 SEQ Segment-SEQuenz ACGCATACTG
11 QUAL Basen-QUALitätsstring DIGAFHHBCA

Jede Zeile im Alignmentsabschnitt einer SAM-Datei umfasst 11 Pflichtfelder.

Das SAM/BAM-Format soll so allgemein wie möglich sein und verwendet das Wort template für DNA-Fragmente (englisch: fragments, inserts). Das SAM-Format verwendet das Wort segment, um sich auf eine zusammenhängende Sequenz oder Teilsequenz zu beziehen; zum Beispiel können die beiden Reads eines Lesepaares als zwei Segmente bezeichnet werden.

Das Feld QNAME speichert den Namen der Abfrage-(query)-Sequenz, in der Regel ein Read.1 POS gibt die Position auf dem Chromosom an, an der der Read ausgerichtet wurde. Das Feld FLAG wird weiter unten erklärt.

Das Feld MAPQ stellt die Mappingqualität, die vom Aligner zugewiesen wurde, und spiegelt das Vertrauen wider, mit dem der Read auf die angegebene Position abgebildet werden konnte. Die CIGAR-Zeichenkette bietet eine kompakte kompakte Darstellung des Alignments, die im Folgenden erläutert wird.

Die Felder RNEXT, PNEXT und TLEN werden nur für Paired-End-Reads verwendet. Daher wird bei Single-End Reads RNEXT auf ``*'' (für not-applicable) gesetzt. PNEXT und TLEN werden auf 0 gesetzt. Schließlich enthält das Feld SEQ die Nukleotidsequenz des Reads und das Feld QUAL zeigt die Basenqualitätswerte für jede Position an.

1

Segmente mit demselben QNAME stammen von der gleichen Vorlage, d. h. Reads eines Read-Paares haben normalerweise den gleichen QNAME.

Paired-End-Sequenzen

In Abbildung 2 werden zwei Paare bestehend aus vier Reads der Länge 10 werden auf eine Referenzsequenz der Länge 40 abgebildet. Beachten Sie, dass Mitglieder desselben Paars haben denselben QNAME und werden auf verschiedene Stränge abgebildet. Read 1 wird dem dem Vorwärtsstrang an Position 2 zugeordnet und bildet ein Paar mit dem Read, der dem Rückwärtsstrang an Position Strang an Position 27 zugeordnet ist. RNEXT wird auf = gesetzt, da der zweite Read des Paares des Paares auf dieselbe Referenz, d. h. chrE, abgebildet wird, und PNEXT wird auf 27 gesetzt, weil der Reverse-Read auf diese Position abgebildet wird (der Reverse-Read ist an den Positionen 27-36 ausgerichtet). Daher wird TLEN auf 35 gesetzt, denn dies ist der Abstand zwischen der äußersten linken und der äußersten rechten der am weitesten rechts gelegenen Base der beiden Reads. Der Eintrag TLEN für den Reverse-Read wird mit einem Minuszeichen dargestellt.

SE reads in IGV SE Reads
Abbildung 2 SAM-Datei (Minimalbeispiel, Paired-End-Sequenzen). Oben: Alignment. Unten: Entsprechende SAM-Datei.

Für Paired-End-Reads werden andere bitweise Flags verwendet als für Single-End-Reads. Die Flags 99 und 147 zeigen an, dass beide Mitglieder des Paares korrekt auf den Vorwärts- und den Rückwärtsstrang gemappt wurden.

CIGAR

Eine CIGAR (Concise Idiosyncratic Gapped Alignment Report) String besteht aus einer Reihe von Operationslängen sowie den Operationen, die beschreiben, wie genau ein Read an die Referenzsequenz angeglichen wurde (Abbildung 1).

CIGAR
Figure 1 Der senkrechte Strich stellt ein Alignment von read1 beginnend an Position 3 der Referenz (ref) dar. Das Alignment besteht aus zwei übereinstimmenden Nukleotiden, gefolgt von einer Deletion von 2 Nukleotiden, drei übereinstimmenden Nukleotiden und einer Insertion von einem Nukleotid und zwei übereinstimmenden Nukleotiden.

Die drei wichtigsten CIGAR-Operationen sind M (Match/Mismatch), I (Insertion) und D (Deletion), aber aber es gibt noch sechs weitere Operationen, von denen einige im Folgenden erläutert werden. Alle CIGAR Operationen sind in Tabelle 1 zusammengefasst. Eine CIGAR-Zeichenkette kann man sich als eine Reihe von Operationslängen plus die CIGAR-Operation, die eine abstrakte und kompakte Darstellung beliebiger Ausrichtungen ermöglicht. Betrachten Sie beispielsweise das folgende Alignment, in dem die Abfragesequenz (Q) an die Referenzsequenz (R) angeglichen wird, beginnend an Position 7 der Referenzsequenz.

AGCATGTTAGATAA--GATAGCTGG R
      || ||||| |||| ||||
------TTGGATAAAGGATA-CTGG Q

Es gibt 8 anfängliche Übereinstimmungen, dann eine Einfügung von 2 Nukleotiden in die Abfrage, dann vier weitere Übereinstimmungen, dann eine Löschung von einer Base in der Abfrage und dann vier weitere Übereinstimmungen. Wir können dies als 8M2I4M1D4M darstellen.

Man beachte, dass die dritte Base der Abfragesequenz (G) sich tatsächlich von der Base der Referenzsequenz (A) unterscheidet, an die sie angeglichen wurde. Nichtsdestotrotz handelt es sich um eine "Alignment match" (Ausrichtungsübereinstimmung) (M), obwohl es keine Sequenzübereinstimmung ist.

CIGAR operations
Operation (Op) Beschreibung
M Match (Sequenzübereinstimmung oder Fehlübereinstimmung, keiner Insertion oder Deletion)
I Insertion (zusätzliche Nicht-Referenzbase)
D Deletion (Referenzbase fehlt in der Lesung)
N skipped region=übersprungener Bereich aus der Referenz
S soft clipping (geclippte Sequenzen noch in SEQ vorhanden)
H hartes Clipping (geclippte Sequenzen nicht in SEQ vorhanden)
P padding (stille Löschung aus gepolsterter Referenz)
= sequence match: Sequenzübereinstimmung
X sequence mismatch=Sequenz-Fehlanpassung

Übung 1

Nehmen Sie eine Referenz- und eine Querysequenz sowie den CIGAR-String, der das Alignment spezifiziert (z.B. von Abb. 1). In unserem Beispiel wären die drei Argumente also

  • Query (read_4): GCATCTGTTG
  • Ref: GCATACTGTTG
  • CIGAR: 4M1D6M

Das Ziel ist es, eine Funktion zu schreiben, welche das entsprechende Alignment ausgibt, also in unserem Beispiel

GCAT-CTGTTG
|||| ||||||
GCATACTGTTG

Übung 2

Probieren Sie Ihre Funktion mit "realistischen" BAM-Dateien -- wir haben eine TODO -- "kleine" BAM-Datei erstellen.

Clipped Reads

Wichtige Mapping-Tools wie BWA versuchen, einen Teil eines Reads zu kartieren, wenn sie den Read nicht in voller Länge auf das Referenzgenom abbilden können. In diesen Fällen kann der nicht zuzuordnende Teil durch einen Prozess namens Clipping aus dem Alignment ausgeschlossen werden.

Mit soft-clipping, das in der CIGAR-Zeichenkette mit einem >S angezeigt wird, werden die abgeschnittenen Sequenzbasen nicht aus der SEQ-Zeichenkette entfernt, aber nicht von Varianten-Callern verwendet und nicht in Viewern wie IGV (Integrativer Genomik-Viewer)angezeigt.

Das Hard-Clipping (H) ähnelt dem Soft-Clipping (S), unterscheidet sich aber dadurch, dass die hart geclippte Teilsequenz nicht im Alignment-Datensatz enthalten ist (Abbildung 1).1

soft clipping
Figure 1 Ein IGV-Screenshot des 5'-Endes eines Alignments von zwei Reads zur Referenz. Der zweite Read wurde soft-clipped; er wurde mit REF=chr4, POS=16034528 und einem CIGAR von 14S87M ausgerichtet. Die ersten 14 Nukleotide wurden also „soft-clipped“. Sie wurden in der SAM-Datei nicht aus der Sequenz entfernt, werden aber in IGV nicht angezeigt.
Ref:   GTTCCTAGGAACAGCACAATTTCTAGATACAATCAT
Read1:    CCTAGGAACAGCACAATTTCTAGATACAATCAT
Read2:     ggtcacatgattgtATTTCTAGATACAATCAT

Das entsprechende Alignment von Read1 und Read2 mit Ref. Die soft-clipped (nicht ausrichtbaren) Basen von Read2 sind in Kleinbuchstaben dargestellt.

1

BWA verwendet das Soft-Clipping für das primäre Alignment, damit die ursprünglichen Rohdaten bei Bedarf aus der BAM-Datei regeneriert werden können. Für die sekundären Alignments ist dies nicht notwendig, daher verwendet BWA Hard Clipping, um Speicherplatz zu sparen.

Mapping-Qualität

Das fünfte Feld einer SAM-Datei gibt die Mapping-Qualität des Read an. Der MAPQ-Wert (MAPping Quality) spiegelt die Wahrscheinlichkeit wider, dass der Read an der falschen Position im Genom ausgerichtet ist. Wie der Wert für die Basenqualität handelt es sich dabei um eine Phred-skalierte posteriore Wahrscheinlichkeit, dass die vom Aligner angegebene Mapping-Position falsch ist

\[ MAPQ = -10\log_{10} P(\mathrm{mapping position wrong) \]

Die MAPQ wird auf die nächste ganze Zahl gerundet. Beispielsweise würde einer von tausend Reads mit einem MAPQ von 30 als falsch ausgerichtet vorhergesagt. Ein MAPQ-Wert von 255 bedeutet, dass die Zuordnungsqualität nicht verfügbar ist.

Eine der größten Herausforderungen für Read-Alignment-Algorithmen ist das Vorhandensein von sich wiederholenden Sequenzen im menschlichen Genom (repeats), so dass ein kurzer Read an zwei oder mehr Positionen gleich gut ausgerichtet sein kann. In diesem Fall weist BWA dem Read eine MAPQ von Null zu und wählt eine der Positionen nach dem Zufallsprinzip aus.

MAPQ
Figure 1 Verteilung der MAPQ-Werte (Mapping-Qualität) für ausgerichtete Reads aus dem Exom von NA12878. BWA-MEM ordnete in diesem Beispiel den meisten Reads einen Mapping-Quality-Score von 60 zu, wobei der zweithäufigste Score Null war, und es ordnete einer viel geringeren Anzahl von Reads Zwischen-Scores zu.

Man sollte sich der Tatsache bewusst sein, dass verschiedene Aligner unterschiedliche Methoden zur Berechnung der Mapping-Qualität verwenden und die von verschiedenen Alignern erzielten MAPQ-Scores im Allgemeinen nicht direkt vergleichbar sind.

Übung 1

Berechnen Sie die durchschnittliche Mapping-Qualität aller Reads in einer BAM-Datei. Berechnen Sie den Anteil der Reads mit einer Mindestqualität von Q30.

Das SAM-Bitflag

Das zweite Feld jedes Datensatzes in einer SAM-Datei stellt ein Bitfeld mit Werten aus 12 Bitflags~(Tabelle 1) dar.

Das SAM-Format verwendet 12 Bitflags, von denen jedes den Wert 1 (Ja, Wahr) oder 0 (Nein, Falsch) haben kann. Die Bitflags können unabhängig voneinander kombiniert werden und werden in SAM-Dateien als entsprechender dezimaler Ganzzahlwert angezeigt.

Tabelle 1: SAM-Format und Bitflags
Bit (hex) Bit (dez) Beschreibung
0x1 1 Template hat mehrere Segmente (mehrere Reads, normalerweise ein Readpaar) Template has multiple segments (multiple reads, usually a read pair)
0x2 2 Jedes Segment der Vorlage ist gemäß dem Aligner richtig ausgerichtet (Each segment of the template is properly aligned according to the aligner)
0x4 4 Segment ist nicht zugeordnet (Segment is unmapped)
0x8 8 Nächstes Segment in der Vorlage ist nicht zugeordnet (Next segment in the template is unmapped)
0x10 16 SEQ ist reverse komplementär (SEQ is reverse complemented)
0x20 32 SEQ des nächsten Segments in der Vorlage ist revers komplementär (SEQ of the next segment in the template is reverse complemented
0x40 64 Erstes Segment in der Vorlage (First segment in the template)
0x80 128 Letztes Segment in der Vorlage (Last segment in the template)
0x100 256 Sekundärausrichtung (Secondary alignment)
0x200 512 Segment besteht die Qualitätskontrollen nicht (Segment does not pass quality controls)
0x400 1024 Segment ist eine PCR oder ein optisches Duplikat (Segment is a PCR or optical duplicate)
0x800 2048 Supplementäre Ausrichtung (Supplementary alignment)

Ein Bitfeld wird verwendet, um eine Reihe boolescher Werte (Ja/Nein) kompakt zu speichern. In unserem Fall möchten wir 12 Ja/Nein-Attribute zu jedem Lesevorgang speichern. Theoretisch könnten wir zwölf separate char-Werte (jeweils ein Byte) speichern, aber wenn wir beachten, dass die 12 Werte als einzelne Bits eines Bitfelds gespeichert werden können, können wir eine erhebliche Menge an Speicherplatz einsparen. Tabelle 1 zeigt die zwölf Bitflags und Abb. 1 zeigt ein Beispiel dafür, wie die Bitflags als dezimale Ganzzahlwerte dargestellt werden.

binary and decimal
Figure 1 Um zu verstehen, wie die Bitfelder dargestellt werden, ist es hilfreich, sich an die Position zu erinnern Die Notation von Zahlen funktioniert. In der bekannten Dezimalschreibweise steht die Zahl 453 für \\(4\times 10^2 + 5\times 10^1+3\times10^0= 400 + 50 + 3 = 453\\). In binärer Schreibweise steht eine Zahl wie 101101 für \\(1\times 2^5+0\times 2^4+1\times2^3+1\times 2^2+0\times 1^1+1\times 2^0 = 32 + 8+4+1= 45\\).

Oft ist es zweckmäßig, die hexadezimale Schreibweise („hex“) zu verwenden, bei der die Basis nicht 10 oder 2, sondern 16 ist; Die Ziffern werden durch 0–9 dargestellt, gefolgt von A, B, C, D, E und F. Um mit SAM-Dateien völlig vertraut zu sein und wenn Sie Programme wie SAMtools verwenden, um damit zu arbeiten, ist es hilfreich zu verstehen, wie man konvertiert zwischen diesen drei Zahlendarstellungen.

Ein Wert von 99 zeigt zum Beispiel folgende Ergebnisse an:

  • der Read mehrere Segmente in der Sequenzierung hat,1 d.h., 0x1= \(1\times 16^0=1\)
  • jedes Segment des Reads konnte richtig gemappt werden (korrektes Mapping beider Reads des Read-Paares), d.h., 0x2=\(2 \times 16^0=2\)
  • der Mate-Read auf den Rückwärtsstrang gemappt ist, d.h., 0x20=\(2\times16^1=32|))
  • der Read, auf den im aktuellen SAM-Datensatz Bezug genommen wird, ist das erste Segment im Tempalte (der erste Read im dem Paar, d.h., 0x40=\(4\times 16^1=64\))

Um zu verstehen, wie die Bitfelder dargestellt werden, ist es hilfreich, sich an die Position zu erinnern Die Notation von Zahlen funktioniert. In der bekannten Dezimalschreibweise steht die Zahl 453 für \(4\times 10^2 + 5\times 10^1+3\times10^0= 400 + 50 + 3 = 453\). In binärer Schreibweise steht eine Zahl wie 101101 für \(1\times 2^5+0\times 2^4+1\times2^3+1\times 2^2+0\times 1^1+1\times 2^0 = 32 + 8+4+1= 45\) - vgl. hierzu Abbildung 1 und Tabelle 1.

binary and decimal
Abbildung 1

Bit-Operationen können verwendet werden, um Lesevorgänge in einer SAM-Datei zu filtern. Das SAM-Format verlangt zum Beispiel, dass für jeden Lesevorgang eine und nur eine der zugehörigen Zeilen folgende Bedingungen erfüllt

FLAG & 0x900 == 0

Diese Zeile wird als primäre Zeile (primary line) des Reads bezeichnet. Der hexadezimale Wert 0x900 ist gleich 0x800 plus 0x100, was bedeutet, dass die Bit-Flags für das supplementäre Alignment (0x800) und das sekundäre Alignment (0x100) gesetzt sind. Erinnern Sie sich, dass & für die bitweise logische UND-Verknüpfung eines Bitpaares steht. Wenn beide Bits 1 sind, ist das Ergebnis der Operation 1, ansonsten 0. Zum Beispiel

      1010       
AND   1100       
     =1000      

Wenn also ein Read entweder als sekundäres Alignment2 oder ein \index{supplementary alignment} supplementary alignment,\footnote{Nach der SAM-Spezifikation ist ein chimäres Alignment ein Alignment eines Reads, das nicht als lineares Alignment dargestellt werden kann. In der Regel bedeutet dies, dass ein Read aus mehreren Segmenten besteht, die sich an verschiedenen Teilen des Genoms ausrichten. Die Segmente haben keine großen Überlappungen. Eines der Segmente wird als das repräsentative Alignment betrachtet, die anderen werden als ergänzend bezeichnet und haben das Flag für ergänzendes Alignment gesetzt. dann erhalten wir FLAG & 0x900 != 0.3 Das SAM-Format besagt, dass eine beliebige Anzahl von Zeilen sekundäre oder ergänzende Ausrichtungen darstellen kann, aber dass nur eine Zeile die primäre (repräsentative) Ausrichtung sein kann.

1

normalerweise bezieht sich dies auf einen Paired-End- oder Mate-Pair-Read

2

Wenn ein Read aufgrund von repetitiven Sequenzen oder aus anderen Gründen auf mehrere Stellen abgebildet wird, wird eine dieser Ausrichtungen als primär markiert, und bei allen anderen wird das Bitflag für sekundäres Alignment gesetzt.

3

Das Ergebnis könnte 0x100, 0x800 oder 0x900 sein.

samtools

SAMtools kann Alignments filtern, um nur die alignierten Reads auszugeben, die bestimmten Kriterien entsprechen, einschließlich der Überlappung mit Regionen, die in einer BED-Datei (-L) angegeben sind, Zugehörigkeit zu bestimmten Lesegruppen (-r, -R), Herkunft aus einer bestimmten Bibliothek oder eine bestimmte Mindestanzahl von CIGAR-Basen haben, die die Abfragesequenz verbrauchen.1

Die Option -f gibt nur Alignments aus, bei denen alle Bits in INT im Feld FLAG gesetzt sind (INT kann als Dezimalzahl oder als hexadezimale Zahl angegeben werden, die mit '0x' beginnt). Dies führt eine UND-Verknüpfung mit allen Bits durch, die in INT gesetzt sind, was bedeutet, dass Sequenzen nur angezeigt werden, wenn mindestens die entsprechenden Bit Positionen in FLAG auf 1 gesetzt sind. Die Option -F gibt keine Alignments aus, bei denen Bits, die in INT gesetzt sind, im Feld FLAG. Die Option -F überspringt daher alle Reads, für die FLAG & INT != 0 ist. Um also nur primäre Reads auszugeben, können wir den folgenden Befehl verwenden.

$ samtools view -F 0x900 NIST7035_aln.bam
1

Die M/D/N/=/X-Operatoren "verbrauchen" Referenzbasen (they are said to "consume reference bases.")

Die mit diesem Befehl ausgegebenen Reads enthielten insgesamt 38 verschiedene Werte für das Bitfeld, von denen keiner die Bits 0x100 oder 0x800 enthielt. Mit dem folgenden Befehl werden alle sekundären Alignment-Reads extrahiert.

$ samtools view -f 0x100 NIST7035_aln.bam

Die Funktion flagstat von SAMtools bietet eine Zusammenfassung der Anzahl der Datensätze, die jedem der Bit-Flags von Tabelle 1 entsprechen. Die Auswertung unseres Beispieldatensatzes zeigt beispielsweise Folgendes.

$ samtools flagstat NIST7035_aln37.bam 
35210329 + 0 in total (QC-passed reads + QC-failed reads)
0 + 0 duplicates
35210007 + 0 mapped (100.00%:-nan%)
35210329 + 0 paired in sequencing
17605154 + 0 read1
17605175 + 0 read2
34991352 + 0 properly paired (99.38%:-nan%)
35209746 + 0 with itself and mate mapped
261 + 0 singletons (0.00%:-nan%)
19085 + 0 with mate mapped to a different chr
9718 + 0 with mate mapped to a different chr (mapQ>=5)

SAM-TAGs: Optionale Felder

Auf die 11 Pflichtfelder können beliebig viele optionale Felder folgen. Alle optionalen Felder folgen dem TAG:TYPE:VALUE-Format, wobei TAG eine zweistellige Zeichenfolge, TYPE einer von sechs Datentypen (Tabelle 1) und VALUE der tatsächliche Wert ist.

SAM-Format: Datentypen für die optionalen Tags
Typ Beschreibung
A Einzelzeichen
Z String
i 32-Bit-Ganzzahl mit Vorzeichen
f Float mit einfacher Genauigkeit (reelle Zahl)
H Hexadezimalzahl-String
B Allgemeines Array

Verschiedene Aligner verwenden unterschiedliche optionale Felder. Leser finden eine vollständige Dokumentation allgemeiner Felder bei SAMtools Website1 und Aligner wie BWA stellen ebenfalls Dokumentation zu den von ihnen verwendeten Feldern bereit. Hier stellen wir einige ausgewählte Bereiche vor, um den Lesern einen Eindruck davon zu vermitteln, wie diese Bereiche in der Praxis eingesetzt werden.

Das NM-Feld

Das vordefinierte NM-Tag nimmt einen ganzzahligen Wert (i) an. Das Feld NM gibt den Bearbeitungsabstand zur Referenz an, einschließlich Mehrdeutigkeit bases\footnote{``N'' bases, jedoch ohne Clipping. Denken Sie daran, dass der CIGAR-String 101M zeigt an, dass ein 101 nt langer Lesevorgang lückenlos mit dem Genom abgeglichen wurde. Allerdings macht es das nicht jede Aussage darüber, ob das Alignment nicht übereinstimmende Positionen (Sequenzinkongruenzen) enthielt. A Der CIGAR-String von 101M und das Tag NM:i:0 stimmen jedoch perfekt überein (Abstand bearbeiten). von Null). Bei einem Lesevorgang mit einer CIGAR-Zeichenfolge von 101M und dem Tag NM:i:1 stimmt eine nicht überein Basis (Distanz von eins bearbeiten).

Das MD-Feld

Das Feld MD liefert zusätzliche, referenzzentrierte Informationen über die Ausrichtung. Es handelt sich um eine Zeichenfolge für nicht übereinstimmende Positionen, die es ermöglicht, ohne Blick auf die Referenz abzuleiten, wo sich Einzelnukleotidvarianten (SNVs) und Deletionen befinden. Die Funktionsweise des Feldes MD lässt sich am einfachsten anhand eines Beispiels erklären. Einer der Reads im NA12878-Exom wurde auf chr1:21989502 mit einer Zuordnungsqualität von Null (es lässt sich genauso gut auf eine andere Position auf Chromosom 1 abbilden) und einer CIGAR-Zeichenfolge von 101M abgebildet. Betrachten Sie das MD-Tag:

MD:Z:2C77A4G4A5C4 

Dies bedeutet, dass es zwei Sequenzübereinstimmungen gibt (beginnend an der Position POS=21989502 auf der Referenz chr1), gefolgt von einer Nichtübereinstimmung mit einem C in der Referenz, 77 Übereinstimmungen, einer Nichtübereinstimmung mit A, 4 weitere Übereinstimmungen, ein nicht übereinstimmendes G, 4 Übereinstimmungen, ein weiteres nicht übereinstimmendes A, 5 Übereinstimmungen, ein nicht übereinstimmendes C und endlich 4 Übereinstimmungen. Vergleichen Sie das Alignment mit der entsprechenden Region von Chromosom 1:

read: TGTGGTGACCTGACCATCCTGGTTTGCCTGGAACTTCAGGAGTGAAGACA
      || |||||||||||||||||||||||||||||||||||||||||||||||
ref:  TGCGGTGACCTGACCATCCTGGTTTGCCTGGAACTTCAGGAGTGAAGACA

read: CTGGACATTTAATGCTAAAACTGGGAAGGTCCCAGAAAAAGTGGGAAAAG
      |||||||||||||||||||||||||||||| |||| |||| ||||| |||
ref:  CTGGACATTTAATGCTAAAACTGGGAAGGTACCAGGAAAAATGGGACAAG

read: T
      |
ref:  T 

Betrachten wir nun ein Beispiel mit einer Löschung. Dies ist ein auf chr1:31504512 abgebildeter Lesevorgang mit einer CIGAR-Zeichenfolge von 11M5D60M (Gesamtlänge des zugeordneten Lesevorgangs 83 Nukleotide) und einem Tag von MD:Z:11^TTTTG6G23G29 . Der CIGAR-String sagt uns, dass der Lesevorgang für die ersten 11 Basen ausgerichtet ist, dann eine Deletion von 5 Nukleotiden aufweist und erneut für 60 Basen ausgerichtet ist. Die CIGAR-Zeichenfolge sagt uns nicht, welche Basen gelöscht wurden, und sie sagt uns auch nicht, ob es sich bei den ausgerichteten Basen um Übereinstimmungen oder Nichtübereinstimmungen handelt. Der MD-String sagt uns, dass die ersten 11 Basen perfekt übereinstimmen, dass die Basen TTTTG gelöscht wurden (dies wird durch das Caret-Zeichen ^ gefolgt von den gelöschten Basen, ^TTTTG, angezeigt), die folgenden 6 übereinstimmende Basen, gefolgt von einem nicht übereinstimmenden G in der Referenz, 23 übereinstimmende Basen, gefolgt erneut von einem nicht übereinstimmenden G in der Referenz, gefolgt von 29 übereinstimmenden Basen.

read: TTGGGCAAGTT.....TTTTTTTTTTTTTTTTTTTTTTTTTGAGACAGAG
      |||||||||||     |||||| ||||||||||||||||||||||| |||
ref:  TTGGGCAAGTTTTTTGTTTTTTGTTTTTTTTTTTTTTTTTTGAGACGGAG

read: TCTCTCTCTGTTGCCCGGGCTGGAGT
      ||||||||||||||||||||||||||
ref:  TCTCTCTCTGTTGCCCGGGCTGGAGT 

Wenn es mehrere benachbarte Nichtübereinstimmungen gibt, dann ist eine 0 gebraucht. Zum Beispiel

Read: CGATACGGGGAC
      |  |||  ||||
Ref:  CACTACTCGGAC

Dies würde die CIGAR 12M (zwölf ausgerichtete Positionen ohne Insertionen oder Deletion) ergeben.

Der MD-String MD:Z:1A0C3T0C4 hinweist darauf hin, dass es zwischen den nicht übereinstimmenden A und C und T und C keine (0) passenden Basen gibt. Falls es sich bei der ersten oder letzten Base um eine Sequenzfehlanpassung handelt, wird dieser ebenfalls eine 0 vorangestellt oder folgt (z. B. MD:Z:0A100 oder MD:Z:100A0).

Wenn es mehrere benachbarte Nichtübereinstimmungen gibt, dann ist eine 0 gebraucht. Zum Beispiel

read: CGATACGGGGAC
      |  |||  ||||
ref:  CACTACTCGGAC

Dies würde die CIGAR 12M (zwölf ausgerichtete Positionen ohne) ergeben Einfügungen oder Löschungen) und die MD-Zeichenfolge MD:Z:1A0C3T0C4, was darauf hinweist, dass es zwischen den nicht übereinstimmenden A und C und T und C keine (0) passenden Basen gibt. Falls es sich bei der ersten oder letzten Base um eine Sequenzfehlanpassung handelt, wird dieser ebenfalls eine 0 vorangestellt oder folgt (z. B., MD:Z:0A100 oder MD:Z:100A0).

Beachten Sie, dass Einfügungen nicht im Feld MD angegeben werden, da es referenzzentriert ist und eine Einfügung keinen Informationsverlust über die Referenz darstellt. Darüber hinaus können die eingefügten Basen zusammen mit dem CIGAR-String eines Lesevorgangs aus dem SEQ-Feld abgeleitet werden. Die CIGAR-Zeichenfolge 30M1I70M entspricht beispielsweise MD:Z:100, wenn alle ausgerichteten Basen (M) Sequenzübereinstimmungen aufweisen. Der Read hat insgesamt 101 Basen, aber nur die 100 Referenzbasen, an denen er ausgerichtet ist, werden im MD-Tag beschrieben.

Das Feld MD muss mit der CIGAR-Zeichenfolge kompatibel sein (Einfügungen ausgenommen).

Dies würde die CIGAR 12M (zwölf ausgerichtete Positionen ohne) ergeben Einfügungen oder Löschungen) und die MD-Zeichenfolge MD:Z:1A0C3T0C4, was darauf hinweist, dass es zwischen den nicht übereinstimmenden A und C und T und C keine (0) passenden Basen gibt. Falls es sich bei der ersten oder letzten Base um eine Sequenzfehlanpassung handelt, wird dieser ebenfalls eine 0 vorangestellt oder folgt (z. B. MD:Z:0A100 oder MD:Z:100A0).

Das RG-Feld

Das RG-Feld gibt die Lesegruppe des Lesevorgangs an, z. B. \verb+RG:Z:rg1+. .

Das AS-Feld

Das Feld AS gibt den vom Aligner generierten Alignment-Score an. Beispielsweise gibt \verb+AS:i:84+ an, dass BWA-MEM dem Lesevorgang einen Alignment-Score von 84 zugewiesen hat.

Für Endbenutzer reservierte Felder

Die Felder X?:?, Y?:? und Z?:? sind für Endbenutzer reserviert. Das bedeutet, dass Ausrichtungsprogramme wie BWA ihre eigenen Felder definieren dürfen, deren Tags mit den Buchstaben X, Y oder Z beginnen. BWA hat eine Reihe von Tags definiert, die mit X beginnen. Beachten Sie, dass die verschiedenen BWA-Programme unterschiedliche Kombinationen von verwenden Tags. Beispielsweise zeigt das Tag XA alternative Ausrichtungen im folgenden Format an: chr,pos,CIGAR,NM; für jede alternative Ausrichtung. Zum Beispiel,

XA:Z:chr1,+13074589,101M,3;chr1,-13152100,101M,3;chr1, \
-12882405,101M,3;chr1,-12827800,101M,4; \
chr1_KI270766v1_alt,-97694,98M3S,3;	

Dieses Tag zeigt, dass der Lesevorgang auch alternativen Orten zugeordnet werden könnte. Der erste alternative Treffer befand sich auf Chromosom 1 an Position 13.074.589, er war ohne Indels ausgerichtet (CIGAR 101M) und die Bearbeitungsentfernung (NM) betrug drei. Eine negative Position zeigt an, dass die alternative Ausrichtung auf dem umgekehrten Strang liegt. Zu den weiteren optionalen Tags von BWA gehört XS, das den suboptimalen Alignment-Score anzeigt.

Übungen

Lassen Sie uns mit den oben genannten Kenntnissen einige Übungen durchführen vertiefen unsere Kenntnisse des BAM-Formats.

Übungen 1

Die erste Übung beschäftigt sich mit der Bitflag. Wir haben gesehen, dass die Bitflags 99 und 147 waren unseren Paired-End-Reads zugewiesen. Was bedeuten sie? Schreiben Sie diese Zahlen in binärer und hexadezimaler Darstellung auf und konsultieren Sie Tabelle 1 in Kapitel 4-05.

Nun schreiben Sie ein kleines Rust-Program, dass einen SAM-Bitstring (in Form einer Ganzzahl) als Kommandozeilenargument nimmt und anzeigt, welche Bits "an" sind.

Übung 2

Bestimmen Sie die CIGAR-Strings, 1-basierte Positionen (POS) und die NM- und MD-Tags für die folgenden Ausrichtungen:

ref:  CCATACT-GAACTGACTAAC
          ||| ||| || ||
read:     ACTAGAA-TGGCT

und

ref:  ATCCCCTCATCC-GCCTGCTCCTTCTCACATG
        |  | ||||| |||||||||||   |||||  
read:   CTACGCATCCGGCCTGCTCCTT---ACATG

Nun schreiben Sie ein kleines Rust-Programm, das ein solches Aligment aus einer Datei einliest und den entsprechenden CIGAR-String auf die Kommandozeile ausgibt.

Übung 3

Bestimmen Sie das Alignment zwischen dem folgenden Read:

read:  CATTCATACTGAA

und dem Referenzgenom:

ref:  AGCATTACTACTAAATTT 

wobei POS 3 beträgt und der CIGAR-String 4M1D1M1I7M lautet.

Nun schreiben und testen Sie eine entsprechende Rust-Funktion mit der folgenden Signatur

fn align_by_cigar, U: Into, V: Into>(
    read: T, 
    genome: U, 
    pos: i32, 
    cigar: V) -> String {
        todo!();
    }

Übung 4

Bestimmen Sie NM für den CIGAR-String 50M und den MD-Eintrag von MD:Z:4A45.

Bestimmen Sie NM für den CIGAR-String 6M1D23M und den MD-Eintrag von MD:Z:1T4^T1C21.

Schreiben und testen Sie eine entsprechende Rustfunktion.

{\it See hints and answers on page~\pageref{page:SAMBAMhints}.}

Erklärungen

Erklärungen zu den Übungen auf der vorherigen Seite.

Übung 2: Erklärung

(a) Dies entspricht dem Beginn der CIGAR-Zeichenfolge 3M1I3M1D5M an POS 5, d. h. der Lesevorgang wird beginnend an Position 5 der Referenz ausgerichtet.

Die CIGAR sagt, dass die ersten 3 Basen des Lesens
Sequenz mit der Referenz ausrichten. Die nächste Basis des Lesevorgangs funktioniert nicht in der Referenz (Einfügung) vorhanden sind. Dann werden 3 Basen an der Referenz ausgerichtet. Der
Die nächste Referenzbasis ist in der Lesesequenz nicht vorhanden (Löschung), dann 5 weitere Basen richten sich nach der Referenz aus. Beachten Sie, dass sich bei Position 14 die Basis befindet Der gelesene Wert unterscheidet sich vom Referenznukleotid, aber es ist immer noch so
zählt als M, da es an dieser Position ausgerichtet ist.

Die Sequenzinkongruenz an dieser Position betrifft sowohl die NM- als auch die MD-Tags:

Der Lese- und Referenzwert hat einen Bearbeitungsabstand von drei (daher NM:i:3), wobei die eingefügte Basis, die gelöschte Basis und die nicht übereinstimmende Basis gezählt werden.

Die referenzzentrierte Beschreibung der Ausrichtung lautet MD:Z:6^C2A2. Ab der Startposition des Alignments (fünfte Basis in der Referenz) stimmen die ersten 6 Referenzbasen mit den Basen des Lesevorgangs überein, dann fehlt ein C der Referenz im Lesevorgang (^C), weitere zwei Referenzbasen fehlen übereinstimmend, gefolgt von einer Nichtübereinstimmung (A in der Referenz) und zwei endgültigen Übereinstimmungen. Insertionen werden nicht im MD-Tag aufgezeichnet.

Projekte

TODO

Einige mögliche Projektideen erwähen. Am besten sollen die Teilnehmer Funktionen von samtools view implementieren, hier ist eine Beschreibung

https://cloud.wikis.utexas.edu/wiki/spaces/CoreNGSTools/pages/54068983/Filtering+with+SAMTools#FilteringwithSAMTools-Filteringbylocationrange

Anhang

Die folgenden Abschnitte enthalten Referenzmaterial für das Praktikum.

mdbook

Dieses Material wurde mithilfe von mdBook erstellt. Kurz:

  1. Installation
cargo install mdbook
  1. Server
mdbook serve --open