rdmlImportSeriesrdmlImportDataRDML7 provides two levels for writing file importers.
The simple form is intended for formats that can be represented as one fluorescence table plus one metadata table:
vendor file
|
v
parser
|
+-- fdata
|
`-- description
|
v
buildRDMLFromFData()
|
v
rdmlType
The complex form is intended for formats that contain several independent fluorescence-series groups, for example amplification and melting data, or several data blocks that should be parsed first and converted to RDML in one central step:
vendor file
|
v
parser
|
+-- rdmlImportSeries (adp)
+-- rdmlImportSeries (mdp)
`-- ...
|
v
rdmlImportData
|
v
buildRDMLImport()
|
v
rdmlType
Both forms keep the vendor-specific parser separate from the nested RDML/S7 schema construction.
This separation is useful because a parser should primarily answer questions such as:
adp), melting
(mdp), or both?The common RDML builder then handles creation of
experimentType, runType,
reactType, dataType, sampleType,
targetType, dyes, references, and fluorescence-point
objects.
After a reader has been written, register it with the minimal format registry:
A format may provide a reader, a writer, or both:
Automatic dispatch uses the file extension. A different registered
format can be selected explicitly with format =.
A reader may return either:
rdmlType; orrdmlImportData intermediate object.When a reader returns rdmlImportData,
readRDML() converts it to rdmlType with the
common importer builder.
Both simple and complex importers ultimately use the same table representation.
fdata is a wide table. Its first column is the
coordinate:
cyc for amplification data;tmp for melting data.All remaining columns are fluorescence series.
For example:
fdata <- data.frame(
cyc = 1:5,
r1 = c(100, 102, 110, 180, 650),
r2 = c(101, 103, 112, 190, 690)
)
fdata
#> cyc r1 r2
#> 1 1 100 101
#> 2 2 102 103
#> 3 3 110 112
#> 4 4 180 190
#> 5 5 650 690The fluorescence column names (r1, r2) are
arbitrary identifiers used to join the fluorescence data to the
description table.
The corresponding description table contains one row per fluorescence series.
description <- data.frame(
fdataName = c("r1", "r2"),
expId = c("exp1", "exp1"),
runId = c("run1", "run1"),
reactId = c("1", "2"),
sample = c("sample1", "sample2"),
sampleType = c("std", "unkn"),
target = c("CD53", "CD53"),
targetDyeId = c("FAM", "FAM"),
stringsAsFactors = FALSE
)
description
#> fdataName expId runId reactId sample sampleType target targetDyeId
#> 1 r1 exp1 run1 1 sample1 std CD53 FAM
#> 2 r2 exp1 run1 2 sample2 unkn CD53 FAMThe required columns are:
fdataName
expId
runId
reactId
target
In practice, importers commonly also provide:
sample
sampleType
targetDyeId
Other supported metadata can be added when present in the source file, for example:
targetType
quantity
cq
meltTemp
meltTemp
The exact optional fields accepted by the installed version of RDML7
are documented by setFData().
The key rule is:
description$fdataName <-> names(fdata)[-1]
Each fluorescence column should have exactly one matching description row.
Use buildRDMLFromFData() when the file naturally
contains one fluorescence series group:
This is the preferred interface for most third-party readers.
Typical examples are:
The parser only needs to produce fdata and
description.
readMyFormat <- function(fileName, ...) {
# Vendor-specific parsing
fdata <- ...
description <- ...
buildRDMLFromFData(
fdata = fdata,
description = description,
fdataType = "adp"
)
}The reader does not need to construct nested S7 objects manually.
It should not need code such as:
# Do not do this in a normal file parser:
experimentType(
run = list(
runType(
react = list(
reactType(
data = list(
dataType(...)
)
)
)
)
)
)That construction belongs to the common RDML builder.
Consider a deliberately small text format with three sections:
# Simple qPCR format
[meta]
experiment=cd53_efficiency
run=run1
target=CD53
dye=FAM
[reactions]
fdataName reactId sample sampleType quantity
r1 1 CD53_1 std 1
r2 2 CD53_0.1 std 0.1
r3 3 CD53_0.01 std 0.01
r4 4 CD53_0.001 std 0.001
[adp]
cyc r1 r2 r3 r4
1 100 100 100 100
2 101 100 101 100
3 103 102 101 101
...
The format deliberately stores quantity as metadata
rather than encoding it only in the sample name.
A reader can parse these sections and then convert them to the common tables.
readSimpleQPCR <- function(fileName, ...) {
lines <- readLines(
fileName,
warn = FALSE,
encoding = "UTF-8"
)
getSection <- function(name) {
marker <- paste0("[", name, "]")
start <- match(marker, lines)
if (is.na(start)) {
stop(
"Missing section ",
marker,
call. = FALSE
)
}
sections <- grep(
"^\\[[^]]+\\]$",
lines
)
nextSection <- sections[
sections > start
]
end <- if (length(nextSection)) {
nextSection[[1L]] - 1L
} else {
length(lines)
}
out <- lines[
seq.int(
start + 1L,
end
)
]
out[
nzchar(trimws(out))
]
}
# Metadata -------------------------------------------------------------
metaParts <- strsplit(
getSection("meta"),
"=",
fixed = TRUE
)
meta <- stats::setNames(
vapply(
metaParts,
function(x) {
paste(
x[-1L],
collapse = "="
)
},
character(1)
),
vapply(
metaParts,
`[[`,
character(1),
1L
)
)
# Reaction metadata ----------------------------------------------------
reactions <- utils::read.delim(
text = paste(
getSection("reactions"),
collapse = "\n"
),
stringsAsFactors = FALSE,
check.names = FALSE
)
# Amplification curves -------------------------------------------------
fdata <- utils::read.delim(
text = paste(
getSection("adp"),
collapse = "\n"
),
stringsAsFactors = FALSE,
check.names = FALSE
)
description <- data.frame(
fdataName = reactions$fdataName,
expId = meta[["experiment"]],
runId = meta[["run"]],
reactId = as.character(
reactions$reactId
),
sample = reactions$sample,
sampleType = reactions$sampleType,
target = meta[["target"]],
targetDyeId = meta[["dye"]],
quantity = as.numeric(
reactions$quantity
),
stringsAsFactors = FALSE
)
buildRDMLFromFData(
fdata = fdata,
description = description,
fdataType = "adp",
publisher = "simpleqpcr"
)
}Register the reader:
It can then be used through the normal RDML7 interface:
x <- readRDML(
"cd53_efficiency.sqpcr"
)
summary(x)
asTable(x)
amp <- getFData(
x,
dpType = "adp",
longTable = TRUE
)The parser knows nothing about the internal structure of
sampleType, quantityType,
targetType, experimentType, or
dataType. It only maps the source file into the standard
fluorescence and description tables.
Use the canonical intermediate representation when a single source file contains more than one independently described fluorescence-series group.
The most common case is:
one source file
|
+-- amplification curves (adp)
|
`-- melting curves (mdp)
Other reasons include:
In this situation it is useful to parse the complete vendor file first and only then construct the RDML hierarchy.
rdmlImportSeriesOne rdmlImportSeries represents one homogeneous
fluorescence-series group.
For amplification data:
ampSeries <- newRDMLImportSeries(
fdataType = "adp",
fdata = ampFdata,
description = ampDescription
)For melting data:
meltSeries <- newRDMLImportSeries(
fdataType = "mdp",
fdata = meltFdata,
description = meltDescription
)Each series has its own:
fdataType
fdata
description
This is important because adp and mdp have
different coordinates and may contain different subsets of
reactions.
rdmlImportDatardmlImportData represents the complete result of parsing
one source file.
parsed <- newRDMLImportData(
series = list(
ampSeries,
meltSeries
),
publisher = "Example vendor",
serialNumber = "12345",
format = "myformat"
)The object groups the fluorescence-series blocks and source-level metadata without yet constructing the nested RDML hierarchy.
This gives the parser a clean boundary:
everything above rdmlImportData = vendor parsing
everything below rdmlImportData = RDML construction
The parsed representation can be converted explicitly:
The builder processes each series with the common fluorescence-data
machinery and returns one rdmlType.
Conceptually:
rdmlImportData
|
+-- series 1 -> setFData(..., fdataType = "adp")
|
+-- series 2 -> setFData(..., fdataType = "mdp")
|
`-- ...
|
v
rdmlType
This means all complex importers use the same RDML construction rules.
A complex reader does not have to call buildRDMLImport()
itself.
It may return rdmlImportData:
readMyComplexFormat <- function(fileName, ...) {
# Parse the source file.
ampFdata <- ...
ampDescription <- ...
meltFdata <- ...
meltDescription <- ...
ampSeries <- newRDMLImportSeries(
fdataType = "adp",
fdata = ampFdata,
description = ampDescription
)
meltSeries <- newRDMLImportSeries(
fdataType = "mdp",
fdata = meltFdata,
description = meltDescription
)
newRDMLImportData(
series = list(
ampSeries,
meltSeries
),
publisher = "Example vendor",
serialNumber = "12345",
format = "mycomplexformat"
)
}Register it normally:
Then:
readRDML() detects that the reader returned
rdmlImportData and runs the common builder before returning
the final rdmlType.
This is the preferred complex-reader pattern because the reader remains a pure parser.
The intermediate representation is useful, but it should not be required for simple formats.
For a single amplification table, this:
is clearer than:
series <- newRDMLImportSeries(
"adp",
fdata,
description
)
parsed <- newRDMLImportData(
series = list(series)
)
buildRDMLImport(parsed)The second form adds objects without adding information.
Use the lowest-level interface that faithfully represents the source format.
| Source format | Recommended form |
|---|---|
| One amplification table | buildRDMLFromFData() |
| One melting table | buildRDMLFromFData() |
| Simple CSV or TSV curve export | buildRDMLFromFData() |
| One table plus straightforward metadata | buildRDMLFromFData() |
| Amplification + melting in one file | canonical importer |
| Several independent fluorescence blocks | canonical importer |
| Need to parse/validate all blocks before building RDML | canonical importer |
| Need source-level importer metadata | canonical importer |
A useful rule is:
If the source can be described by one
fdatatable and onedescriptiontable, usebuildRDMLFromFData(). If it naturally requires a list of such pairs, userdmlImportData.
The simple API is not a separate implementation.
Conceptually:
buildRDMLFromFData()
|
v
newRDMLImportSeries()
|
v
newRDMLImportData()
|
v
buildRDMLImport()
In other words, the simple function is a convenience layer over the same canonical construction path.
This is important because both simple and complex third-party importers get the same behavior for:
A custom reader should:
fdata and
description tables;rdmlType or
rdmlImportData.A custom reader normally should not:
setFData() logic;Format selection is handled by the registry; RDML construction is handled by the common builder.
A useful development loop is:
registerRDMLFormat(
name = "myformat",
extensions = "abc",
reader = readMyFormat
)
x <- readRDML(
"example.abc"
)
summary(x)
tbl <- asTable(x)
tbl
validateRDML(x)
amp <- getFData(
x,
dpType = "adp",
longTable = TRUE
)For a format containing melting data, inspect those separately:
During importer development, verify at least:
adp versus mdp assignment.A small test should verify both dispatch and biological metadata.
test_that("myformat reader imports amplification curves", {
x <- readRDML(
test_path(
"data",
"example.abc"
)
)
expect_true(
S7::S7_inherits(
x,
rdmlType
)
)
tbl <- asTable(x)
expect_true(
all(tbl$adp)
)
expect_setequal(
unique(tbl$target),
"CD53"
)
amp <- getFData(
x,
dpType = "adp"
)
expect_gt(
nrow(amp),
0L
)
})For a complex reader:
RDML7 intentionally provides two importer interfaces.
The simple form:
is the preferred choice for a source represented by one fluorescence table.
The complex form:
series <- newRDMLImportSeries(...)
parsed <- newRDMLImportData(
series = list(...)
)
buildRDMLImport(parsed)is useful when a file contains several independently described fluorescence groups.
The format registry remains independent of both approaches:
This keeps the three responsibilities separate:
registry -> which reader handles this file?
parser -> what data are stored in this vendor format?
RDML builder -> how are those data represented as rdmlType?
That separation is the main reason the importer API exists.