Implementation technical details

The ddpcr package makes extensive use of the S3 class inheritance system in R. The plate object is implemented as an S3 object of class ddpcr_plate with the R list as its base type. S3 objects are used for two main purposes: to override the behaviour of some base R functions when applied to plate objects, and to allow plate objects to inherit characteristics of other plate objects.

A plate object is a list

Every S3 object has a base type upon which it is built. The base type of a plate object is a list, as it allows for an easy way to bundle together the several different R objects describing a plate into one. All information required to analyze a plate is part of the plate object. Every plate object contains the following nine items:

Except for the version and dirty flag, all other seven items in the plate's list have corresponding getter and setter functions for convenience. For example, the name of a dataset can be retrieved with a call to name(plate) and can be modified with a call to name(plate) <- "my data". The only information about a plate object that is not inside the plate's list is the plate type. The plate's type is already stored as the plate object's class in order for S3 to function, so including the plate type in the list would be redundant.

Using S3 for plate objects to override base generic functions

Since the plate object is an S3 object, it can benefit from the use of generic functions. There are three common generic functions that the plate object implements: print(), plot(), and subset(). The print() and plot() generics are very often implemented for S3 classes, and they are especially useful for plate objects. The print() method does not take any extra arguments and is used to print a summary of a plate object in a visually appealing way to the console. It gives an overview of the most important parameters of the plate such as its type and name, as well as the current analysis state of a plate. The plot() method generates a scatterplot of every well in the plate object and can be highly customizable using the many arguments it implements. While the base plot() method uses base R graphics, the plot() method for ddpcr_plate objects uses the 'ggplot2' package. The subset() generic is overridden by a method that can be called to retain a subset of wells from a larger plate. The subset() method uses two additional arguments, wells and samples, to specify which wells to select.

Using S3 for plate objects to support inheritance

Inheritance means that every plate type has a parent plate type from which it inherits all its features, but specific behaviour can be added or modified. In ddpcr, transitive inheritance is implemented, which means that features are inherited from all ancestors rather than only the most immediate one. Multiple inheritance is not supported, meaning that each plate object can only have one parent.

The notion of inheritance is an important part of the ddpcr package, as it allows ddPCR data from different assay types to share many properties. For example, PN/PP assays are first treated using the analysis steps common to all ddPCR experiments, and then gated with an assay-specific step, so PN/PP assays can be thought of as inheriting the analysis from general ddPCR assays. Furthermore, FAM-positive and HEX-positive assays (defined in Section 2.1) are both PN/PP assays that share many similarities, so they can be thought of as inheriting many properties of a PN/PP assay. Another benefit of the ddpcr inheritance is that it allows any user to easily extend the functionality of the package by adding custom ddPCR plate types.

Analysis of different built-in plate types

The most basic plate type is ddpcr_plate, and every plate type inherits from it, either directly or by inheriting from other plate types that are descendants of ddpcr_plate. This is useful because it means that all functionality that is common to all ddPCR experiments is implemented once for ddpcr_plate, and every other plate type automatically inherits all the methods that ddpcr_plate has. When a new plate object is created (with the new_plate() function) without specifying a plate type, the ddpcr_plate type is assumed.

Calling the analyze() function on any plate will result in running the ddPCR data through a series of steps that are defined for the given plate type, and the read droplets will be assigned to one of several clusters associated with the given plate type. The exact set of analysis steps and potential clusters are determined by the plate type. Plates of type ddpcr_plate have four possible droplet clusters: “undefined” for any droplet that has not been assigned a cluster yet, “failed” for droplets in failed wells, “outlier” for outlier droplets, and “empty” for droplets without any template in them. Plates of type ddpcr_plate have 4 analysis steps: “INITIALIZE”, “REMOVE_FAILURES”, “REMOVE_OUTLIERS”, and “REMOVE_EMPTY”. This means that any plate that is created will perform these basic steps of removing failed wells, outlier droplets, and empty droplets. Other plate types inherit the same clusters and steps by default, and can alter them to be more appropriate for each specific type. For example, PN/PP plate types use the same clusters, as well as three more: “rain”, “positive” (PP), and “negative” (PN). PN/PP plate types also use the same list of steps as the base type, with the addition of steps at the end to perform droplet gating.

How the analysis steps work

The most important functionality of the ddpcr package is the ability to programmatically analyze a plate's droplet data. As described above, a plate object contains a list of ordered steps that can be accessed via the steps() command. The value of each element in the list refers to the name of the function that implements that analysis step. For example, the last step in the analysis of a basic plate of type ddpcr_plate is “remove_empty”, which means that the remove_empty() function is automatically called when the analysis pipeline reaches this step.

One of the several properties of a plate object that is stored in the plate's underlying list object is its status. The status of a plate is simply an integer that indicates how many analysis steps have been taken. When a plate is created, the first step of initialization runs automatically, and the plate's status is set to one. To analyze a plate, the analyze() or next_step() functions are used. The next_step() runs the next step by examining the plate's list of steps and its current state to determine what function to call. The analyze() function can be used to run the full analysis pipeline on a plate, as it simply calls next_step() repeatedly until the analysis is completed.

There are several commonalities among all functions that implement the analysis steps: