Eine Laborplattform, deren Zahlen einer Prüfung standhalten müssen
Eine interne Fachanwendung für Hochdurchsatz-Experimente in der Prozessentwicklung: Rohdaten einlesen, Auswertung im Browser, am Ende die fertigen Arbeitslisten für den Pipettierroboter und die Berichte dazu. Läuft vollständig in der Infrastruktur des Kunden.
Ausgangslage
In der Prozessentwicklung laufen Experimente im Hochdurchsatz: viele parallele Läufe, aus denen Messgeräte Rohdaten als Textdateien auswerfen, dazu Planungstabellen in Excel und externe Analytikergebnisse. Aus all dem muss eine Entscheidung werden, welche Fraktionen weiterverwendet werden, und daraus eine Anweisung, die ein Pipettierroboter abarbeiten kann.
In Excel geht das, aber es skaliert nicht und es ist schwer zu belegen. In einem regulierten Umfeld reicht es nicht, dass eine Zahl stimmt. Es muss nachvollziehbar sein, wie sie entstanden ist.
Ziel
Experimente reproduzierbar erfassen, die Auswertung in der Anwendung rechnen, die Arbeitslisten für den Roboter automatisch erzeugen und alles so ablegen, dass es später prüfbar bleibt.
Die Rahmenbedingungen gibt das regulierte Umfeld vor. Die Anwendung ist eine Eigenentwicklung und unterliegt damit einer dokumentierten Verifizierung. Das heißt: jede Berechnung, die ein Experimentergebnis beeinflusst, muss überprüft und die Überprüfung nachweisbar sein. Dazu Audit-Trail, versionierte Deployments und ein Änderungsprozess, an dem eine Freigabe hängt. Betrieb ausschließlich im Haus des Kunden.
Lösung
Ein Dashboard, das den ganzen Lebenszyklus eines Experiments abdeckt. Nutzer laden Planungstabelle und Rohdaten hoch; die Anwendung prüft die Struktur und ordnet die Messwerte den einzelnen Fraktionen zu. In der Übersicht liegen Chromatogramm und Plattenkarte übereinander, und man sieht, wo welche Fraktion sitzt und wie die Kurve verläuft.
Danach folgt die eigentliche Arbeit: Fraktionen nach frei definierbaren Kriterien zu Pools zusammenfassen oder gezielt einzelne herauspicken. Die Zuordnung steht direkt auf der Plattenkarte.
Aus der Auswahl erzeugt die App die Transferanweisung für den Roboter, dazu Transfersheets und einen konsolidierten Bericht als PDF. Am Ende steht etwas Physisches: der Roboter arbeitet die Liste ab.
Ergebnis
„Die Implementierung durch Stefan Linner hat unsere Möglichkeiten zur Analyse biotechnologischer Hochdurchsatzdaten deutlich verbessert. Unsere Labormitarbeiter können direkt nach den Experimenten ohne zeitraubende Formatierungsarbeiten in einer visuell ansprechenden Benutzeroberfläche arbeiten. Dank Stefans umfangreichen Kenntnissen in R und R-Shiny konnten wir auch komplexe Prozesse und Spezialfälle effizient automatisieren. Sein schnelles Verständnis unserer Geschäftsprozesse ermöglicht es ihm, ohne lange Einarbeitungszeit innovative Beiträge zu leisten.“
Wo es knifflig wurde
Runden nach Laborvorschrift statt nach R-Default
Die Rundung folgt einer schriftlichen Laborvorschrift. R macht von sich aus etwas anderes:
round() rundet halbe Werte zur geraden Zahl statt kaufmännisch auf und schleppt
zusätzlich Fließkommafehler aus Zwischenergebnissen mit, sodass das Resultat von der Vorschrift
abweichen kann. Der unangenehme Teil: es gibt keine einzelne richtige Implementierung. Für
bestimmte Grenzfälle liefern zwei plausible Varianten unterschiedliche, jeweils begründbare
Ergebnisse. Solche Fälle mussten anhand der verbindlichen Vorschrift entschieden und dokumentiert
werden, statt still im Code zu verschwinden.
Wenn zwei Rechenwege dieselbe Zahl liefern müssen
Die Ausbeute wird in R gerechnet und zusätzlich als Excel-Formel in die exportierte Arbeitsmappe geschrieben, damit die Datei sich selbst gegenrechnet. Eine Prüfspalte behauptet, dass beide Werte gleich sind. Der ursprüngliche Vergleich prüfte auf exakte Gleichheit und meldete regelmäßig Abweichungen, die keine waren: R und Excel unterscheiden sich in den letzten Bits. Die klassische Falle beim Vergleich von Floats, hier verschärft dadurch, dass zwei absichtlich unabhängige Rechenwege dasselbe Ergebnis liefern sollen.
Rohdaten aus Messgeräten sind kein sauberes Format
Die Geräte schreiben tabgetrennten Text. Läuft ein Messwert über den Messbereich, steht in der
Zelle statt einer Zahl das Wort „Overflow“, bei deutsch eingestellten Geräten
„Ueberlauf“. Eine einzige solche Zelle macht die ganze Spalte zu Text, und Dezimaltrenner
können Kommas sein. Ein beherztes as.numeric() verwandelt „Overflow“ still in einen
fehlenden Wert und zerstört damit eine Unterscheidung, die fachlich zählt: gesättigt ist nicht
dasselbe wie nicht gemessen. Dazu muss die Berechnung physikalisch unmögliche Eingaben abfangen, etwa
negative Messwerte.
Technische Umsetzung
Jedes Feature ist ein Modul mit fester Konvention: UI, Server und Datenlogik getrennt, die reine Rechenlogik liegt immer in der Helper-Datei. Nur so lassen sich die Berechnungen, die ins Ergebnis eingehen, überhaupt testen, ohne die App zu starten. Der Modulbaum wird beim Start automatisch eingelesen, ein neues Modul muss nirgends registriert werden.
Alles Persistente liegt in einer relationalen Datenbank, angebunden über einen Connection Pool, damit sich gleichzeitige Sessions nicht blockieren. Alle Schreibvorgänge laufen über eine zentrale Schicht, die zusätzlich einen Audit-Eintrag anlegt und nur die Zeilen anfasst, die sich wirklich geändert haben. Zugriffsrechte kommen aus der zentralen Gruppenzugehörigkeit und werden serverseitig geprüft.
Die Rundungsfunktionen stecken in einem eigenen kleinen R-Paket, das im Image mit installiert wird. Das klingt nach Overkill für zwei Funktionen, ist aber der Punkt: sie sind versioniert, dokumentiert und getestet, statt als Hilfsfunktion irgendwo im Code zu leben.
Berichte und Transfersheets entstehen über R Markdown. Die Templates liegen statisch im Image und werden zur Laufzeit in ein temporäres Verzeichnis kopiert und dort gerendert, damit das Template selbst unangetastet bleibt und parallele Läufe sich nicht in die Quere kommen.
Betrieb und Weiterentwicklung
Das Deployment besteht aus drei Images: ein Base-Image mit den gepinnten Paketen, das App-Image darauf und ein Test-Image auf derselben Basis. Bei jedem Push baut die Pipeline zuerst das Test-Image, führt die Suite darin aus und bricht ab, sobald ein Test fehlschlägt. Erst danach wird das App-Image gebaut, mit dem Commit getaggt und ausgerollt. Produktion und Entwicklungsumgebung sind getrennt; der Deploy nach Produktion ist ein eigener Schritt und hängt an einem abgenommenen Verifizierungsdokument.
Die Tests sind Integrationstests und laufen gegen eine Datenbank. Sie decken die Pfade ab, die Ergebnisse beeinflussen: das Einlesen der Planungsdateien und Rohdaten, Speichern und Laden von Experimenten und Pool-Entscheidungen, die Datenbankschicht samt Audit-Log und die Datenaufbereitung für die Auswertung.