--- title: "Building, checking and diagramming a theory" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Building, checking and diagramming a theory} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set(collapse = FALSE, comment = "") # Console colour carries no meaning on a rendered page. pkgdown turns it on for # its own build, and the escape sequences then reach the reader as literal text, # so colour is switched off here for a plain vignette render and a site build # alike. The fixed width keeps printed output inside the documentation column. options(cli.num_colors = 1, cli.hyperlink = FALSE, crayon.enabled = FALSE, width = 80) # Both optional packages are needed to turn the DOT source into an inline # SVG, so the rendering chunks below are skipped when either is absent. has_diagram <- requireNamespace("DiagrammeR", quietly = TRUE) && requireNamespace("DiagrammeRsvg", quietly = TRUE) ``` `theoryforge` treats a scientific theory as a versioned, machine-checkable object. This vignette walks through the core loop of building a theory, checking its rigour and diagramming it (see the [package overview](https://pablobernabeu.github.io/theoryforge/r/) for the R/Python parity guarantee). ```{r setup} library(theoryforge) ``` ## Building a theory A theory is built incrementally with the BUILDING-mode verbs. Each verb appends to the theory and records a provenance entry, and each returns the theory, so the steps compose with the native pipe. ```{r build} theory <- tf_theory("panic-network", "A network theory of panic") |> tf_add_construct("c_arousal", "Physiological arousal", "Bodily activation in response to a stressor.", measurement = "heart rate variability", boundary_conditions = "awake adults") |> tf_add_construct("c_threat", "Perceived threat", "Appraised danger of bodily sensations.", measurement = "self-report appraisal scale", boundary_conditions = "awake adults") |> tf_add_proposition( "p1", "c_arousal", "c_threat", "causes", mechanism = "Activation raises the salience of threat cues." ) |> tf_add_prediction("h1", "Arousal raises threat appraisal by a fixed amount.", "point", derives_from = "p1") isTRUE(tf_validate(theory)) # structural checks: required fields and enums # also checks referential integrity of ids and cross-references isTRUE(tf_validate(theory, full = TRUE)) ``` `tf_validate()` returns `TRUE` invisibly on success and stops with a message listing every problem otherwise, which is why the calls above are wrapped in `isTRUE()` to show the result. With `full = TRUE` it additionally checks referential integrity, that ids are unique and every cross-reference points to a declared id. It does not require any optional dependency. The failure path is the more informative one. Pointing a prediction at a proposition that was never declared leaves the structural pass untouched, since every required field is still present and well formed, so only `full = TRUE` reports the problem. ```{r validate-failure, error = TRUE} broken <- theory broken$predictions[[1]]$derives_from <- "p_missing" tf_validate(broken, full = TRUE) ``` The building vocabulary has two further verbs. `tf_add_assumption()` records an auxiliary assumption, and `tf_set_formal_model()` attaches a formal-model reference. Applied to a copy of the theory, they leave the original untouched. ```{r build-more} extended <- theory |> tf_add_assumption("a1", "Arousal is measured at rest.", added_for = "h1") |> tf_set_formal_model("sem", spec_ref = "panic-sem.lavaan") ``` Every verb appends a provenance entry, recording the step, the action and its detail, so the record of how a theory was built travels with the object. ```{r provenance-entries} do.call(rbind, lapply(extended$provenance, as.data.frame)) ``` A theory can be written to and read back from disk. The format follows the file extension (`.json` for JSON, otherwise YAML). ```{r io} path <- tempfile(fileext = ".yaml") tf_write(theory, path) roundtrip <- tf_read(path) identical(roundtrip$id, theory$id) ``` ## Checking rigour `tf_check()` runs the 12-item rigour checklist defined in the vendored `rigor_checklist.yaml`. ```{r check} report <- tf_check(theory) report$aggregate_score # weighted score, 0-100, rounded to 1 dp report$gate # "pass", "blocked", or "advisory" (draft maturity) report$n_blockers_failed # count of failed blocker items # Per-item detail (checklist order preserved): report$items[[1]]$id # "falsifiability" report$items[[1]]$status # "pass" / "warn" / "fail" report$items[[1]]$score # numeric in [0, 1] ``` Render it as JSON (valid, pretty-printed) or as an HTML fragment. The JSON string is an artefact in its own right, shown here verbatim: ```{r report-json} cat(tf_report(theory, format = "json")) ``` The HTML fragment drops straight into a page and renders as a real table: ```{r report-html, results = "asis"} cat(tf_report(theory, format = "html")) ``` ### Screening for redundant constructs The lexical redundancy screen reports the Jaccard similarity of every pair of construct definitions, sorted by descending similarity. ```{r redundancy} tf_redundancy_check(theory) ``` The lexical screen is deterministic but shallow. When a semantic comparison is wanted, `tf_embedding_redundancy()` takes any embedder that maps a definition to a numeric vector and reports the cosine similarity of each construct pair. It needs no optional dependency, since the embedder is supplied by the caller. A toy bag-of-words embedder stands in for a real language model here. ```{r embedding-redundancy} vocab <- c("bodily", "activation", "appraised", "danger", "salience") embedder <- function(def) { words <- strsplit(tolower(def), "[^a-z]+")[[1]] vapply(vocab, function(w) sum(words == w), numeric(1)) } tf_embedding_redundancy(theory, embedder) ``` ## Diagramming `tf_diagram()` emits deterministic intermediate representations for several diagram types. The digraphs are Graphviz DOT, and the causal DAG uses dagitty syntax. ```{r diagram} cat(tf_diagram(theory, type = "nomological_net")) cat(tf_diagram(theory, type = "causal_dag")) ``` The DOT strings render with any Graphviz tool, and `tf_render_diagram()` does this without leaving R: it returns a DiagrammeR widget for the viewer and for R Markdown, or a standalone SVG string with `as = "svg"`. The packages it uses are optional, in Suggests, so the deterministic core stays dependency-free. Rendered, the nomological net above reads as a figure. ```{r diagram-render, results = "asis", eval = has_diagram} cat( '
', tf_render_diagram(theory, "nomological_net", as = "svg"), '
', sep = "" ) ``` The causal DAG is the one view this does not cover, since it emits dagitty syntax rather than DOT. Paste it into a dagitty tool such as [dagitty.net](https://dagitty.net) instead. `tf_implications()` reads the same subgraph without leaving R, checking it for acyclicity and returning the conditional independencies it entails, which the Developing and testing article works through. ## Simulation `tf_simulate()` treats each construct as a state variable and integrates the signed proposition network as a linear dynamical system. The trajectory is deterministic: ```{r simulate} sim <- tf_simulate(theory, steps = 5) unlist(sim$states) unlist(sim$trajectory[[1]]) # the common initial state unlist(sim$trajectory[[6]]) # after five Euler steps ``` The two states separate immediately: arousal, which receives no incoming coupling, decays under the damping term, while perceived threat is pushed up by the positive coupling from arousal before the decay eventually takes over.