| Title: | Domain-Agnostic Causal Simulation Engine with Mixed Variable Types |
| Version: | 0.1.2 |
| Description: | A domain-agnostic simulation engine for structural causal models with heterogeneous variable types. Users define systems of continuous, categorical, and ordinal features, declare a directed acyclic graph (DAG) over them, attach typed generative mechanisms to each node, and simulate synthetic populations. A pre-built mechanism library covers all nine parent-type/child-type combinations. Vine copula mechanisms estimate full joint dependence from observed data via pair-copula constructions (Bedford and Cooke, 2002) <doi:10.1214/aos/1031689014>. Individual-level counterfactual queries are supported through partial propagation, which freezes unaffected random draws and resamples only the downstream subgraph, enabling individual treatment effect (ITE) estimation alongside population-level average treatment effects (ATE). Any pre-fitted predictive model can serve as a generative mechanism through three wrapper functions. Systems serialise to JSON for cross-language portability with the companion Python package. |
| License: | MIT + file LICENSE |
| URL: | https://github.com/danilosarti/lepidoptera |
| BugReports: | https://github.com/danilosarti/lepidoptera/issues |
| Encoding: | UTF-8 |
| Depends: | R (≥ 4.1.0) |
| Imports: | stats |
| Suggests: | testthat (≥ 3.0.0), igraph, rvinecopulib (≥ 0.6.0), jsonlite, yaml, knitr, rmarkdown |
| Config/testthat/edition: | 3 |
| RoxygenNote: | 7.3.3 |
| VignetteBuilder: | knitr |
| NeedsCompilation: | no |
| Packaged: | 2026-10-16 16:05:38 UTC; danilosarti |
| Author: | Danilo Sarti [aut, cre] |
| Maintainer: | Danilo Sarti <danilosarti@rcsi.ie> |
| Repository: | CRAN |
| Date/Publication: | 2026-10-28 11:40:02 UTC |
lepidoptera: Generic Simulation Engine for Systems with Mixed-Type Dependent Features
Description
See SPEC.md at the package root for the full design specification:
data model (features, dependency graph, mechanisms, interventions),
supported type combinations, propagation engine, and the R/Python
mirrored API.
Author(s)
Maintainer: Danilo Sarti danilosarti@rcsi.ie
See Also
Useful links:
Report bugs at https://github.com/danilosarti/lepidoptera/issues
Adjacency list from nodes + edges
Description
Adjacency list from nodes + edges
Usage
.adjacency(nodes, edges)
A mechanism object
Description
A mechanism object
Usage
.new_mechanism(name, fn, parent_types, output_type, spec = NULL)
One reproducible seed per unit, derived from a single base seed
Description
Mirrors the role of Python's numpy.random.SeedSequence(seed).spawn(n):
each unit gets an independent draw stream (here, an independent
set.seed() call) so that regenerating a subset of units, or only a
subset of features for a given unit (see propagate(mode = "partial")
in intervention.R), does not change the random draws used elsewhere.
Usage
.unit_seeds(n, seed)
Add a dependency (edge set + mechanism) to a system
Description
Add a dependency (edge set + mechanism) to a system
Usage
add_dependency(system, parents, child, mechanism)
Arguments
system |
A |
parents |
Character vector of parent feature names (use
|
child |
Character. Name of the dependent feature. Must already
exist (see |
mechanism |
A |
Value
The updated system.
Add a feature to a system
Description
Add a feature to a system
Usage
add_feature(system, spec)
Arguments
system |
A |
spec |
A |
Value
The updated system.
Single categorical/ordinal parent -> categorical/ordinal child via a
transition table: transition[[parent_level]] = list(child_level = prob).
Description
Single categorical/ordinal parent -> categorical/ordinal child via a
transition table: transition[[parent_level]] = list(child_level = prob).
Usage
categorical_transition(transition, output_type = "categorical")
Compare baseline and intervened populations
Description
For each feature in affected_features: mean/sd shift for continuous
features, level-frequency shift for categorical/ordinal features.
Usage
compare(result)
Arguments
result |
A |
Value
A named list, one entry per affected feature.
Copula-based dependency mechanism
Description
Fits a copula on fit_data and returns a mechanism that draws the child
feature conditional on realised parent values. Supports both the built-in
Gaussian copula (base R, no extra packages) and the full family catalogue of
rvinecopulib (package must be installed for non-Gaussian families).
Usage
copula_mechanism(
variables,
family = "gaussian",
marginals = "auto",
nominal_handling = c("regression", "arbitrary_order"),
fit_data = NULL
)
Arguments
variables |
Character vector. |
family |
Copula family.
|
marginals |
|
nominal_handling |
|
fit_data |
A |
Value
A mechanism object (not JSON-serializable; spec = NULL).
Intervention: replace a feature's generative distribution
Description
v1 scope: distribution must be a marginal mechanism (no parents) –
the feature is resampled from distribution independently of its
original parents. Use mechanism_override() for a conditional
replacement.
Usage
do_distribution(target, distribution)
Arguments
target |
Character. Feature name. |
distribution |
A marginal |
Value
An intervention object.
Intervention: fix a feature to a constant value
Description
Classic do-operator: disconnects target from its parents and sets it
to value for every unit.
Usage
do_fix(target, value)
Arguments
target |
Character. Feature name (or vector of names). |
value |
The fixed value (or vector of values, one per target). |
Value
An intervention object.
Downstream transitive closure of a set of nodes
Description
Used to compute affected_features when an intervention targets a
node. See SPEC.md section 5, step 4.
Usage
downstream_closure(edges, targets)
Arguments
edges |
A data.frame with character columns |
targets |
Character vector of feature names. |
Value
Character vector: targets plus all descendant feature names, in
the order they were first discovered by a breadth-first search
starting at targets.
Export a system to JSON or YAML
Description
Export a system to JSON or YAML
Usage
export_system(system, path, format = c("json", "yaml"))
Arguments
system |
A |
path |
Output file path. |
format |
Character. |
Value
Invisibly, path.
Levels of a categorical/ordinal feature
Description
Levels of a categorical/ordinal feature
Usage
feature_levels(spec)
Arguments
spec |
A |
Value
Character vector of levels, or NULL for a continuous feature.
Declare a feature
Description
Creates a feature_spec: name, type, domain, and optional metadata.
See SPEC.md section 3.2.
Usage
feature_spec(name, type, domain, metadata = NULL)
Arguments
name |
Character. Unique identifier within the system. |
type |
Character. One of |
domain |
List. For |
metadata |
Optional named list, never interpreted by the core. |
Value
A feature_spec object (S3 list with class "feature_spec").
Valid feature types
Description
Valid feature types
Usage
feature_types()
Value
A character vector of the allowed FeatureType values:
"continuous", "categorical", "ordinal".
Finalize and validate a system
Description
Runs the invariants in SPEC.md section 3.5:
every feature has exactly one mechanism;
the dependency graph is acyclic;
every edge references existing features;
each mechanism's declared parent types match the actual types of the parents it was attached to;
the mechanism's output type matches the feature's declared type.
Usage
finalize_system(system)
Arguments
system |
A |
Value
The same system, marked finalized, ready for simulate().
Single categorical/ordinal parent -> continuous child: per-level
Normal(mean, sd). group_params[[level]] = list(mean = ..., sd = ...).
Description
Single categorical/ordinal parent -> continuous child: per-level
Normal(mean, sd). group_params[[level]] = list(mean = ..., sd = ...).
Usage
group_effect(group_params)
Import a system from JSON or YAML
Description
Import a system from JSON or YAML
Usage
import_system(path, format = c("json", "yaml"))
Arguments
path |
Input file path. |
format |
Character. |
Value
A system object. Not yet finalized – call
finalize_system() before simulating.
Check whether a dependency graph is acyclic
Description
Check whether a dependency graph is acyclic
Usage
is_acyclic(nodes, edges)
Arguments
nodes |
Character vector of all feature names. |
edges |
A data.frame with character columns |
Value
TRUE if acyclic, FALSE otherwise.
Fit a mechanism directly from observed data
Description
v1 scope: parents must all be continuous columns of fit_data. For
a continuous child, fits OLS + Gaussian residual noise. For a
categorical/ordinal child, uses a k-nearest-neighbors empirical
sampler over standardized parent space.
Usage
learned_mechanism(parents, child, fit_data, uncertainty = "auto", k = 10L)
Arguments
parents |
Character vector of continuous parent feature names. |
child |
Character. Name of the column in |
fit_data |
A |
uncertainty |
Reserved for future use; currently only |
k |
Number of neighbours for the categorical/ordinal branch. |
Value
A mechanism object (not JSON-serializable – spec is
NULL, since it carries fitted parameters as a closure).
Continuous parents -> continuous child: linear combination + Gaussian noise.
Description
Continuous parents -> continuous child: linear combination + Gaussian noise.
Usage
linear_gaussian(coefs, intercept = 0, sd = 1)
List available copula families
Description
Returns "gaussian" always (built-in base-R path). When rvinecopulib
is installed the full parametric family list is prepended, together with
the meta-set names "auto", "all", "parametric", "onepar",
"twopar".
Usage
list_copula_families()
Value
Character vector of family identifiers accepted by
copula_mechanism().
A mechanism whose value is generated by an LLM call
Description
A mechanism whose value is generated by an LLM call
Usage
llm_mechanism(
prompt_template,
model,
parents,
parent_types,
output_type,
levels = NULL,
parser = "auto"
)
Arguments
prompt_template |
A |
model |
Function |
parents |
Character vector of parent feature names. |
parent_types |
Character vector, one |
output_type |
One of |
levels |
Required when |
parser |
|
Value
A mechanism object (not JSON-serializable – spec is
NULL, since it carries a live model function).
Rebuild a mechanism from its serialized spec (io.R helper)
Description
Rebuild a mechanism from its serialized spec (io.R helper)
Usage
mechanism_from_spec(spec)
Look up a pre-built mechanism factory from the library
Description
Pre-built mechanisms cover the type-combination matrix in SPEC.md
section 4. mechanism_library_get(key) returns a factory (a
function), not a ready mechanism – pre-built mechanisms always need
parameters. Call the factory with those parameters to obtain the
actual mechanism object, e.g.
mechanism_library_get("linear_gaussian")(coefs = list(age = 0.1), sd = 1).
Usage
mechanism_library_get(key)
Arguments
key |
Character. One of |
Value
A factory function.
Intervention: override a feature's mechanism entirely
Description
The replacement mechanism must declare the same number and types of
parents that target was originally given in add_dependency() (the
parent set itself is not changed by this intervention) – mismatches
surface as a validation error when the derived system is finalized.
Usage
mechanism_override(target, mechanism)
Arguments
target |
Character. Feature name. |
mechanism |
A |
Value
An intervention object.
Wrap a pre-fitted model as a generative mechanism
Description
Turns any model object the user has already trained (a ranger random
forest, a glm, an xgboost booster, a keras network, ...) into a
standard lepidoptera mechanism. The user provides a single
predict_fn that accepts a named list of parent values and returns one
predicted value.
Usage
model_mechanism(
predict_fn,
parents,
parent_types,
output_type,
levels = NULL,
name = "model_mechanism"
)
Arguments
predict_fn |
A function |
parents |
Character vector of parent feature names (must match the
names present in |
parent_types |
Character vector of |
output_type |
One of |
levels |
Required when |
name |
Optional character label stored in the mechanism object.
Defaults to |
Details
This differs from learned_mechanism() in a fundamental way:
learned_mechanism() fits a model internally on fit_data;
model_mechanism() uses a model that was already fitted outside
lepidoptera. The user is responsible for the predict function, including
reshaping inputs, choosing the right output slot, and optionally adding
stochastic noise.
Quick patterns:
# ranger random forest
rf <- ranger::ranger(outcome ~ age + bp, data = df, probability = TRUE)
mech <- model_mechanism(
predict_fn = function(pv) {
nd <- data.frame(age = as.numeric(pv$age), bp = as.numeric(pv$bp))
pred <- predict(rf, nd)$predictions
sample(colnames(pred), 1, prob = pred[1,])
},
parents = c("age", "bp"),
parent_types = c("continuous", "continuous"),
output_type = "categorical",
levels = c("no_response", "response")
)
# glm (logistic regression)
fit <- glm(response ~ age + dose, data = trial, family = binomial)
mech <- model_mechanism(
predict_fn = function(pv) {
nd <- data.frame(age = as.numeric(pv$age), dose = as.numeric(pv$dose))
p <- predict(fit, nd, type = "response")[[1]]
sample(c("yes","no"), 1, prob = c(p, 1-p))
},
parents = c("age", "dose"),
parent_types = c("continuous", "continuous"),
output_type = "categorical",
levels = c("yes", "no")
)
# lm (linear regression) with residual noise
fit_lm <- lm(sbp ~ age + bmi, data = cohort)
sigma <- summary(fit_lm)$sigma
mech <- model_mechanism(
predict_fn = function(pv) {
nd <- data.frame(age = as.numeric(pv$age), bmi = as.numeric(pv$bmi))
pred <- predict(fit_lm, nd)[[1]]
pred + rnorm(1, 0, sigma) # add residual noise manually
},
parents = c("age", "bmi"),
parent_types = c("continuous", "continuous"),
output_type = "continuous"
)
Value
A mechanism object (not JSON-serializable – spec = NULL).
Continuous parents -> categorical child via multinomial logit.
Description
Continuous parents -> categorical child via multinomial logit.
Usage
multinomial_logit(levels, coefs, intercepts = NULL)
Create an empty system
Description
Create an empty system
Usage
new_system()
Value
A system object with no features, edges, or mechanisms yet.
Continuous parents -> ordinal child via proportional-odds (ordered logit).
Description
Continuous parents -> ordinal child via proportional-odds (ordered logit).
Usage
ordered_logit(levels, coefs, cutpoints)
Plot a system's dependency graph
Description
Requires the optional igraph package
(install.packages("igraph")).
Usage
plot_graph(system)
Arguments
system |
A |
Value
Invisibly, the igraph graph object plotted.
Apply interventions and propagate their effects
Description
Apply interventions and propagate their effects
Usage
propagate(
system,
interventions,
n = NULL,
seed = NULL,
mode = c("full", "partial")
)
Arguments
system |
A finalized |
interventions |
A list of |
n |
Integer. Number of units to simulate for the baseline (required). |
seed |
Optional integer seed, shared between baseline and intervened population for paired comparison. |
mode |
Character. |
Value
A simulation_result: a list with baseline, intervened,
interventions, affected_features, propagation_order.
Register a custom mechanism
Description
Register a custom mechanism
Usage
register_mechanism(name, fn, parent_types, output_type)
Arguments
name |
Character. Key under which the mechanism is registered. |
fn |
Function with signature |
parent_types |
Character vector, one |
output_type |
One of |
Value
The registered mechanism object.
Intervention: shift the value a feature would otherwise take
Description
Preserves the dependency on parents; applies fn to the value that the
original mechanism would have produced.
Usage
shift(target, fn)
Arguments
target |
Character. Feature name. |
fn |
Function applied to the originally generated value. |
Value
An intervention object.
Simulate a population from a system
Description
Simulate a population from a system
Usage
simulate(system, n, seed = NULL)
Arguments
system |
A finalized |
n |
Integer. Number of units to generate. |
seed |
Optional integer seed for reproducibility. |
Value
A data.frame with one row per unit and one column per feature,
typed according to each feature's FeatureType (factor for
categorical/ordinal columns, numeric for continuous ones).
An independent copy of a system (used internally by intervention.R)
Description
An independent copy of a system (used internally by intervention.R)
Usage
system_copy(system)
All (parent, child) edges declared via add_dependency()
Description
All (parent, child) edges declared via add_dependency()
Usage
system_edges(system)
Arguments
system |
A |
Value
A data.frame with character columns parent, child.
Topologically sort a dependency graph
Description
Kahn's algorithm. Deterministic: zero-in-degree nodes are processed in
the order they appear in nodes.
Usage
topological_sort(nodes, edges)
Arguments
nodes |
Character vector of all feature names (including isolated ones with no edges). |
edges |
A data.frame with character columns |
Value
A character vector with features in topological order (parents before children).