Skip to contents

Beyond the Manual

The TDA User’s Manual stops at chapter 8, and development did not. The spatial commands, the file readers for formats that postdate the manual, and TDA’s online help are all in the program and in none of its documentation. This is what is there.

What happens inside

There is no reimplementation. TDA’s C, context-threaded and compiled into the package, runs unchanged; a wrapper writes the command file TDA would have been given, hands it over in memory, and reads the answer back. Every object therefore carries the run it came from.

Box 1One fit, and the run underneath it

d <- tda_rrdat()
fit <- tda_rate(Surv(TFP, DES) ~ EDU, d)
cat(fit$run$commands, sep = "\n")
rdataframe;
edef(
    ts = Tstart,
    tf = Tfin,
    org = Org,
    des = Des,
);
rate(
    mfmt = 24.16,
    tfmt = 24.16,
    xa (0,1) = EDU,
) = 2;

The answer arrives on two channels. The console is what TDA printed – tda_output() shows all of it, and tda_diagnostics() the lines that are errors, warnings or notes, which TDA otherwise leaves in the middle of its output for the reader to find. The exports are the doubles behind the numbers TDA printed: coefficients, covariance matrices, tables, and a tap on every number the console formatter was handed. A wrapper takes its numbers from the exports and uses the text only to find its way around, so coef() holds the double TDA computed, not a number re-read from a print format.

Box 2Printed, and computed

grep("EDU", fit$run$output, value = TRUE)
[1] "  5 EDU       3   8  24.16 c5"                                                                                
[2] "  2  1   0   1  A EDU            0.0150660299026325       0.0211574987658471       0.7120893669601657  0.5236"
sprintf("%.17g", coef(fit)[["EDU"]])
[1] "0.015066029902632524"
names(fit$run$exports)
 [1] "episodes.table"    "episodes.excluded" "rate.logLik.null"  "ml.vcov"          
 [5] "ml.logLik"         "ml.gradient"       "ml.change.f"       "ml.change.p"      
 [9] "rate.est"          "rate.est.names"    "ml.logLik.pair"    "print.values"     
[13] "print.labels"     

tda_output_audit() is the check that this holds: it classifies every line TDA printed as boilerplate, an echo of the input, a setting, a diagnostic, or a number that is in the object or in the exports, and reports whatever is none of those. The package’s tests run it over every example in this manual and over the 634 command files of TDA’s own regression suite.

Box 3Where every line went

attr(tda_output_audit(fit), "summary")
cls
      blank boilerplate        echo    settings      stored 
         20          13           9          11          18 

Spatial data

TDA holds points, lines and polygons in a spatial structure of its own, and has about twenty commands over it. None of them appears in the manual.

tda_spatial() builds the structure from a data frame. type = 1 is points; type = 3 groups consecutive rows sharing an id into one polygon each.

Box 4A spatial structure, and what is in it

set.seed(7)
pts <- data.frame(id = 1:12, x = round(runif(12), 3), y = round(runif(12), 3))
s <- tda_spatial(pts)
tda_sd_info(s)
Call: tda_sd_info(s)

Cases: 12 

 points lines polygons xmin  ymin  xmax  ymax
     12     0        0 0.07 0.009 0.989 0.997

tda_sd_voronoi() is the Voronoi diagram. What sdvd writes is itself a spatial data file – points for the vertices, lines for the edges – so the result plots with the same machinery the input does.

Box 5The Voronoi diagram of those points

v <- tda_sd_voronoi(s, opt = 2)
plot(v, xlim = c(-0.2, 1.2), ylim = c(-0.2, 1.2))

tda_sd_select() keeps what falls inside a rectangle and tda_sd_clip() cuts to it. Both want the rectangle given; TDA will not guess one.

Box 6Selecting and clipping to a region

tda_sd_select(s, rec = "0,0,0.5,0.5")$table
8 records, lengths 2-5
[1]  1 1 1 2 1 
[2]  0.398 0.096 
[3]  2 1 1 3 1 
[4]  0.116 0.453 
[5]  3 1 1 4 1 
[6]  0.070 0.085 
[7]   4  1  1 10  1 
[8]  0.459 0.295 
tda_sd_clip(s, c(0, 0, 0.5, 0.5))$table
8 records, lengths 2-5
[1]  1 1 1 2 1 
[2]  0.398 0.096 
[3]  2 1 1 3 1 
[4]  0.116 0.453 
[5]  3 1 1 4 1 
[6]  0.070 0.085 
[7]   4  1  1 10  1 
[8]  0.459 0.295 

Polygons are the same structure with type = 3. tda_sd_data() gives the vertices back as an ordinary data frame, which is the form most things in R want.

Box 7Two square polygons

poly <- data.frame(id = c(1, 1, 1, 1, 2, 2, 2, 2),
                   x  = c(0, 2, 2, 0, 3, 5, 5, 3),
                   y  = c(0, 0, 2, 2, 0, 0, 2, 2))
sp <- tda_spatial(poly, type = 3)
tda_sd_data(sp)$table
  V1 V2 V3
1  1  0  0
2  1  2  0
3  1  2  2
4  1  0  2
5  2  3  0
6  2  5  0
7  2  5  2
8  2  3  2

Reading a shapefile

tda_read_shapefile() reads geometry and attaches the .dbf attribute table as $attributes. The manual documents SPSS and Stata files and nothing else; shapefiles, E00, DCW, GSHHS, .dbf and BIFF .xls are all readable and all undocumented. The file is the North Carolina SIDS shapefile that the sf package ships, read here by TDA itself; these boxes run when sf is installed.

Box 8North Carolina counties, read through TDA

nc <- tda_read_shapefile(system.file("shape/nc.shp", package = "sf"))
tda_sd_info(nc)
Call: tda_sd_info(nc)

Cases: 108 

 points lines polygons      xmin     ymin      xmax     ymax
      0     0      108 -84.32385 33.88199 -75.45698 36.58965
head(nc$attributes[, c("AREA", "PERIMETER", "NAME", "FIPS")])
   AREA PERIMETER        NAME  FIPS
1 0.114     1.442        Ashe 37009
2 0.061     1.231   Alleghany 37005
3 0.143     1.630       Surry 37171
4 0.070     2.968   Currituck 37053
5 0.153     2.206 Northampton 37131
6 0.097     1.670    Hertford 37091

The geometry is TDA’s, so TDA’s plotting draws it.

Box 9The same counties, drawn by TDA

plot(nc)

tda_sd_enclosing() gives the bounding box or the convex hull of everything in the structure, and tda_sd_data() the vertices.

Box 10The bounding box, and the vertices

tda_sd_enclosing(nc)$table
5 records, lengths 2-3
[1]  1 3 4 
[2]  -84.32385  33.88199 
[3]  -75.45698  33.88199 
[4]  -75.45698  36.58965 
[5]  -84.32385  36.58965 
head(tda_sd_data(nc)$table)
  V1        V2       V3
1  1 -81.47276 36.23436
2  1 -81.54084 36.27251
3  1 -81.56198 36.27359
4  1 -81.63306 36.34069
5  1 -81.74107 36.39178
6  1 -81.69828 36.47178

Some of these commands compare one object type against another in the same structure. A set of polygons on its own has no lines or points for them to find, and saying so is the right answer rather than an error.

Box 11Nothing to report, correctly

tda_sd_neighbours(nc)$text
[1] "Creating a network from lines. Current memory: 407281 bytes."
[2] "Number of lines: 0"                                          
[3] "Number of segments: 0"                                       
[4] "Tolerance: 0.0001"                                           
[5] "Maximal number of segments: 0"                               
[6] "Number of nodes: 0"                                          
[7] "Number of edges: 0"                                          
[8] "0 records written to: out.txt"                               

TDA’s help

The program carries its help text for every command, and tda_help() reads it. This is the fastest way to find what a command’s options are actually called, since the R wrappers deliberately do not expose all of them by name.

Box 12tda_help

cat(head(tda_help("ploth"), 20), sep = "\n")
ploth: The ploth command can be used to plot a histogram. Syntax is:

    ploth(
        x=...,      intervals, required
        w=...,      variable for weights
        dscal=...,  scaling factor, def. 1.0
        s=...,      s=0 (default) classes are left closed
                    s=1 classes are left open
        ns=...,     ns=1, don't plot vertical lines, def. ns=0
        lt=...,     line type, def. 1 (solid line)
        lw=...,     line width, def. 0.2 (mm)
        gs=...,     grey scale value, def. 1 (white)
        nc=...,     nc=1 if no clipping, def. 0
        df=...,     print table to output file
        fmt=...,    print format for output file, def. 10.4
    ) = variable;

    There must be exactly one variable name on the right-hand side. Also
    one has to use the x parameter to specify a set of intervals (classes)
    for the histogram. The syntax is

        x=x1,x2,...,xn,   or   x=x1(d)x2,

    x1 must be not greater than the minimum of the values of the variable,
    and xn must not smaller than the maximum of the values of the variable.
    The command requires a valid setup for plots and uses the currently
    defined coordinate system. The heights of the histogram are calculated
    in such a way that the areas equal the frequencies. A corresponding
    frequency table can be requested with the df parameter. The output file
    will then contain n - 1 lines with the following entries:

    1) begin of class
    2) end of class
    3) absolute frequency
    4) relative frequency
    5) height of histogram

See also: atab
ploth: The ploth command can be used to plot a histogram. Syntax is:

    ploth(
        x=...,      intervals, required
        w=...,      variable for weights
        dscal=...,  scaling factor, def. 1.0
        s=...,      s=0 (default) classes are left closed
                    s=1 classes are left open
        ns=...,     ns=1, don't plot vertical lines, def. ns=0
        lt=...,     line type, def. 1 (solid line)
        lw=...,     line width, def. 0.2 (mm)
        gs=...,     grey scale value, def. 1 (white)
        nc=...,     nc=1 if no clipping, def. 0
        df=...,     print table to output file
        fmt=...,    print format for output file, def. 10.4
    ) = variable;

    There must be exactly one variable name on the right-hand side. Also
    one has to use the x parameter to specify a set of intervals (classes)
    for the histogram. The syntax is

Running TDA directly

tda_run() takes command-file text and runs it, which is the escape hatch for anything without a wrapper. tda_block() builds one command from named arguments so the syntax does not have to be written by hand.

Box 13A command file, from R

d <- data.frame(X = c(1, 2, 3, 4), Y = c(2, 4, 6, 9))
res <- tda_run(c(tda_nvar(d), "dstat;"), data = d)
cat(grep("^ *(Variable|X|Y)", res$output, value = TRUE), sep = "\n")
Variable    Minimum    Maximum       Mean   Std.Dev.    Sum of values
X            1.0000     4.0000     2.5000     1.2910          10.0000
Y            2.0000     9.0000     5.2500     2.9861          21.0000

Every wrapper returns its $run, so the command file a call generated can always be read back. This is worth knowing when a result looks wrong: the question is usually what TDA was asked, not what it computed.

Box 14What a wrapper actually sent

fit <- tda_lsreg(Y ~ X, d)
cat(fit$run$commands, sep = "\n")
rdataframe;
lsreg(mfmt=24.16, tfmt=24.16) = Y,X;

The expression language

TDA’s operators are available without a data set through tda_evalf(), and its distribution functions are the ones the manual lists in 5.2.5 – nd, ndf, td, chd, fd, mr, poisson, negbin. They agree with R’s, which is checked by a differential fuzzer on every run of the test suite, so either side gives the same numbers.

Box 15The same quantity, twice

sprintf("%.17g", c(tda_evalf("nd(x)", options = list(x = 1.96))$value,
                   pnorm(1.96)))
[1] "0.97500210485082472" "0.97500210485177963"
sprintf("%.17g", c(tda_evalf("mr(x)", options = list(x = 0.5))$value,
                   dnorm(0.5) / (1 - pnorm(0.5))))
[1] "1.141077770368061"  "1.1410777703680643"

Character data

TDA holds fixed-width string variables, and a character or factor column reaches it: tda_nvar() declares it as str(n,m) and tda_strings() brings it back. The four string operators – strlen, strsp, strv, strvp – work on it.

Box 16Strings through TDA

d2 <- data.frame(ID = 1:3, Name = c("Ashe", "Alleghany", "Surry"),
                 Area = c(0.114, 0.061, 0.143))
res <- tda_run(c(tda_nvar(d2), "pdata() = out.txt;"), data = d2)
tda_strings(res)
$Name
[1] "Ashe"      "Alleghany" "Surry"    

Graphs

Chapter 7 of the manual covers relational data, and the 31 tda_g_* commands implement it: components, cliques, cut points, spanning trees, cycles, flows, shortest paths, and the social-network measures of 7.6. They are documented but easy to miss, so one example, on a small graph.

Box 17A graph, and some of what TDA asks of it

e <- data.frame(from = c(1, 1, 2, 3, 4, 5, 5),
                to   = c(2, 3, 3, 4, 5, 6, 7))
g <- tda_graph(e, directed = FALSE)
tda_g_degrees(g)
  index node in_degree out_degree loops degree
1     1    1         2          2     0      2
2     2    2         2          2     0      2
3     3    3         3          3     0      3
4     4    4         2          2     0      2
5     5    5         3          3     0      3
6     6    6         1          1     0      1
7     7    7         1          1     0      1
tda_g_components(g)
Call: tda_g_components(g)

Cases: 7 

 component n i node_i
         1 7 1      1
         1 7 2      2
         1 7 3      3
         1 7 4      4
         1 7 5      5
         1 7 6      6
         1 7 7      7
tda_g_cutpoints(g)
Call: tda_g_cutpoints(g)

Cases: 7 

 V1 V2 V3 V4 V5 V6
  1  7  3  3  4  5

Box 18The same graph, drawn by TDA

plot(g)