Tabular Model File
models.RmdThe models we use in this package are written in a very loose tabular form: the model is a collection of TSV files (tab-separated-values). Tabs are the best separator in our opinion (as compared to commas or semi-colon separated files), because it can be entered with a text editor into the file, by hand, but is quite hard to enter as the content of a field in a spreadsheet program. So, TABs usually don’t appear coincidntally in the files, but can be typed on purpose. Furthermore:
- TSV files render nicely on sites such as GitHub
- TSV files are text files and thus work well with git
- TSV files do not require a library to read
- Column-names can contain spaces, which is very human friendly
We interpret the files partially using functions such as
grep (in R), tolower, with settings such as
ignore.case where possible, so that various different
spellings are acceptable, e.g. “stdv”, “sd”, or “standard deviation”
when describing a distribution.
When reporting data values, we use parenthesised (concise) error
notation, e.g.: 1.23(1) which is intrepreted as
1.23 with an uncertainty of 0.01, more
examples:
| string | value | standard-error |
|---|---|---|
1.23(1) |
1.23 | 0.01 |
1.23(1)e2 |
123 | 1 |
-1.234(1)e-6 |
-1.234e-6 | 1e-9 |
1.23;0.01 |
1.23 | 0.01 |
1.23±0.01 |
1.23 | 0.01 |
The last two rows represent a fall-back notation with redundand zeros, which you can use if you really don’t like parenthesised location, the parser automatically tries to find some separator if there are no parentheses in the string:
x <- parse_concise(
c(
"1.23(1)","1.23(1)e2","-1.234(1)e-6", # concise notation
"1.23;0.01", "1.23±0.01" # fall-back
)
)
print(class(x))
#> [1] "errors"
print(as.data.frame(x)) # this looks better as a data-frame
#> x
#> 1 1.23(1)
#> 2 123(1)
#> 3 -1.234(1)e-6
#> 4 1.23(1)
#> 5 1.23(1)Components of Models
All quantities in the model have a unit. If your model is initially formulated using concentrations and reaction rates (kinetic laws), then you can still simulate it using the Gillespie solver. We convert all quantities with concentrations in the unit to particle counts automatically. So, feel free to formulate the model using concentrations.
The role of each file is determined from the file’s name:
- Constant.tsv, list of constants that are not subjetc to optimization, calibration, or any other investigation
- Input.tsv, known parameters that can change between different simulations
- Parameter.tsv, possibly unknown parameters (e.g. ranges), subject to fitting/sampling/etc.
- Expression.tsv
-
Compound.tsv, list of reacting compounds,
e.g.
Ca - Reaction.tsv, list of reactions
-
Output.tsv, algebraic functions that express a
measureable value (or very close to measureable),
- e.g. the total amount of something:
A + AB + AC, the total amount of bound and freeA
- e.g. the total amount of something:
- Experiment.tsv, list of experiments
The first column in each TSV table contains some kind of unique id, the id of each experiment (row) in the experiment table (Experiment.tsv) is also used as the name of the TSV file that contains the data for this experiment.
Data
The TSV files correspond pretty directly to R data.frame
objects, the import function model_from_tsv returns a list
of data frames, named like the file. The model’s name is taken from the
directory (dirname) the models are stored in. This name is
stored in the comment attribute.
There are two main cases we want to distinguish in the same way that fungi and plants are distinct:
- Experimental data that corresponds fairly well to an output function
- there is an output function that can be compared to the data, perhaps up to some scaling constant
- the data column is labelled exactly like the output function is
named, e.g.:
AMPA_OUTPUT
- Experimental data that has a very complex relationship to the model
- raw data, where several columns together can be used to make a comparison with something in the model
One-to-One Correspondence
The first case, with a one-to-one correspondence between output
functions and data columns can be automatically parsed using the
experiments function. This function reads several of the
tables and determines the initial state, the input, the time-line for
any given experiment and creates a data-matrix (data), with
standard-errors using the errors package. Here, you can
still create your own custom likelihood function that performs some kind
of complex normalization to evaluate a simulation. It is not difficult
to have a control experiment that contains a value we need to normalize
with. The user supplied likelihood may perform such normalizations.
m <- model_from_tsv(uqsa_example("AKAP79"))
print(m$Reaction[,c(2,3,4)]) # an example table, the reactions
#> reactants arw products
#> r51 Rii_C <=> RiiP_C
#> r14 RiiP + C <=> RiiP_C
#> r12 RiiP_C + cAMP <=> RiiP_C_cAMP
#> r43 cAMP + RiiP <=> RiiP_cAMP
#> r23 RiiP_cAMP + C <=> RiiP_C_cAMP
#> r78 cAMP + Rii <=> Rii_cAMP
#> r56 Rii_C + cAMP <=> Rii_C_cAMP
#> r76 Rii_cAMP + C <=> Rii_C_cAMP
#> r62 Rii_C_cAMP <=> RiiP_C_cAMP
#> r58 Rii + C <=> Rii_C
#> r44 RiiP + CaN <=> RiiP_CaN
#> r33 CaN + RiiP_cAMP <=> RiiP_cAMP_CaN
#> r48 RiiP_CaN <=> Rii + CaN
#> r37 RiiP_cAMP_CaN <=> CaN + Rii_cAMP
#> r1 C + AKAR4 <=> AKAR4_C
#> r2 AKAR4_C <=> AKAR4p + C
print(head(m$Experiment)) # the list of experiments
#> cAMP CaN b_AKAP t0 type event
#> EX11____0nM__TRUE___TRUE 0.0 1.5 1 -30 time series
#> EX12____0nM__TRUE__FALSE 0.0 1.5 0 -30 time series
#> EX13____0nM_FALSE__FALSE 0.0 0.0 0 -30 time series
#> EX21__100nM__TRUE___TRUE 0.1 1.5 1 -30 time series
#> EX22__100nM__TRUE__FALSE 0.1 1.5 0 -30 time series
#> EX23__100nM_FALSE__FALSE 0.1 0.0 0 -30 time series
#> comment
#> EX11____0nM__TRUE___TRUE Figure 3 in https://doi.org/10.7554/eLife.68164
#> EX12____0nM__TRUE__FALSE Figure 3 in https://doi.org/10.7554/eLife.68164
#> EX13____0nM_FALSE__FALSE Figure 3 in https://doi.org/10.7554/eLife.68164
#> EX21__100nM__TRUE___TRUE Figure 3 in https://doi.org/10.7554/eLife.68164
#> EX22__100nM__TRUE__FALSE Figure 3 in https://doi.org/10.7554/eLife.68164
#> EX23__100nM_FALSE__FALSE Figure 3 in https://doi.org/10.7554/eLife.68164This is how the output function corresponds to the data-label, AKAR4pOUT:
print(m$Output)
#> formula unit
#> AKAR4pOUT (AKAR4p*5)*71.67+100 µM
rn <- rownames(m$Experiment)
print(head(m[[rn[18]]])) # experiment 18, data found by name
#> time AKAR4pOUT
#> E0301T001 -15 9.941(71)E1
#> E0301T002 -10 1.006(4)E2
#> E0301T003 -5 1.0194(92)E2
#> E0301T004 0 <NA>
#> E0301T005 5 1.0120(28)E2
#> E0301T006 10 1.0204(56)E2So, the data-table (called like the row in the experiments table),
has a column that corresponds to the output function. This is how we
link the two together and know that the measured values have to somehow
correspond to the output function AKAR4pOUT.
x <- experiments(m)
print(x[[18]]$data[,seq(8),drop=FALSE]) # a sub-set of the data-matrix
#> Errors: 0.71 0.40 0.92 NA 0.28 ...
#> [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8]
#> AKAR4pOUT 99.41 100.6 101.94 NA 101.2 102.04 102.35 103.8The default likelihood function will take the squared difference
between the simulated values of AKAR4pOUT (the function)
and the measured values in that column.
Very Indirect Data
In the second big case of very cryptic data, you should not
rely on the data matrix returned by the
experiments(m) function and instead use the
measurements field, which is a data frame, exactly as it
was written in the TSV file, of just use the data data frame in
m:
print(head(m[[rn[18]]],12)) # these two should be the same
#> time AKAR4pOUT
#> E0301T001 -15 9.941(71)E1
#> E0301T002 -10 1.006(4)E2
#> E0301T003 -5 1.0194(92)E2
#> E0301T004 0 <NA>
#> E0301T005 5 1.0120(28)E2
#> E0301T006 10 1.0204(56)E2
#> E0301T007 15 1.0235(43)E2
#> E0301T008 20 1.038(4)E2
#> E0301T009 25 1.055(11)E2
#> E0301T010 30 1.06(1)E2
#> E0301T011 35 1.106(5)E2
#> E0301T012 40 1.110(13)E2
print(head(x[[18]]$measurement,12)) # (a 12 row subset)
#> time AKAR4pOUT
#> E0301T001 -15(0) 99.4(7)
#> E0301T002 -10(0) 100.6(4)
#> E0301T003 -5(0) 101.9(9)
#> E0301T004 0(0) NA(NA)
#> E0301T005 5(0) 101.2(3)
#> E0301T006 10(0) 102.0(6)
#> E0301T007 15(0) 102.4(4)
#> E0301T008 20(0) 103.8(4)
#> E0301T009 25(0) 106(1)
#> E0301T010 30(0) 106(1)
#> E0301T011 35(0) 110.6(5)
#> E0301T012 40(0) 111(1)The only difference is that all numbers were parsed by
parse_concise to resolve all parenthesised standard-errors
(in x), in m the values are raw (either stings
if they contain parentheses, or numbers if R could successfully coerce
them.
So, m can be used directly, if you have a very complex
data-case and thus a very complex likelihood.