Skip to contents

Why use the CLI?

Sometimes you want the fmrigds pipeline without opening an R session for each run. The CLI gives you the same lazy plan grammar that you use in R, but in a form that fits scripts, shell pipelines, and scheduled jobs.

The common path is short: inspect a source, save a plan, preview a small block, then execute it. When you need more control, the same CLI also accepts metadata tables, model formulas, reducer options, post-hoc options, and saved plan JSON.

At the terminal, the workflow looks like this:

fmrigds probe --input group.csv
fmrigds plan --input group.csv --reduce fixed --save-plan plan.json
fmrigds preview --load-plan plan.json --n 3
fmrigds run --load-plan plan.json --out results.h5

The chunks below call the same CLI parser from inside R so the examples stay runnable in the vignette.

What are we driving?

We’ll use a small long-table input with ROI labels, four subjects, and two contrasts. The extra time column will become contrast metadata later.

read.csv(stats_path, stringsAsFactors = FALSE)
#>    sample subject contrast time beta  var
#> 1   ROI_1  sub-01 baseline    0 0.50 0.04
#> 2   ROI_1  sub-01     task    1 0.80 0.04
#> 3   ROI_1  sub-02 baseline    0 0.40 0.05
#> 4   ROI_1  sub-02     task    1 0.90 0.05
#> 5   ROI_1  sub-03 baseline    0 0.45 0.05
#> 6   ROI_1  sub-03     task    1 1.05 0.05
#> 7   ROI_1  sub-04 baseline    0 0.52 0.05
#> 8   ROI_1  sub-04     task    1 1.12 0.05
#> 9   ROI_2  sub-01 baseline    0 0.60 0.03
#> 10  ROI_2  sub-01     task    1 0.70 0.03
#> 11  ROI_2  sub-02 baseline    0 0.55 0.02
#> 12  ROI_2  sub-02     task    1 0.95 0.02
#> 13  ROI_2  sub-03 baseline    0 0.58 0.02
#> 14  ROI_2  sub-03     task    1 1.10 0.02
#> 15  ROI_2  sub-04 baseline    0 0.62 0.02
#> 16  ROI_2  sub-04     task    1 1.18 0.02

How do you get a result quickly?

If you already know the source and the reducer you want, run is the shortest path from input table to output file.

cli_show(
  "run",
  "--input", stats_path,
  "--subset", "contrast=task",
  "--reduce", "fixed",
  "--out", quick_out
)
#> $ fmrigds 'run' '--input' '/tmp/Rtmp17Odxi/file1cec36df0018.csv' '--subset' 'contrast=task' '--reduce' 'fixed' '--out' '/tmp/Rtmp17Odxi/file1cec1ed7fd8f.csv'
#> GDS
#>   dims: 2 x 1 x 1 [sample x subject x contrast]
#>   space: sample_labels n=2
#>   subjects: 1 (meta)
#>   contrasts: 1 (task)
#>   assays: beta[location], var[variance], beta_g[?], var_g[?], se_g[?], z_g[?], p_g[?], Q[?], I2[?], n_eff[n_eff], z[z], p[p]
head(read.csv(quick_out, stringsAsFactors = FALSE))
#>   sample subject contrast      beta         var    beta_g       var_g
#> 1  ROI_1    meta     task 0.9576471 0.011764706 0.9576471 0.011764706
#> 2  ROI_2    meta     task 1.0081818 0.005454545 1.0081818 0.005454545
#>         se_g      z_g          p_g        Q        I2 n_eff        z
#> 1 0.10846523  8.82907 1.055500e-18 1.385529 0.0000000     4  8.82907
#> 2 0.07385489 13.65085 1.995474e-42 5.232727 0.4266852     4 13.65085
#>              p
#> 1 1.055500e-18
#> 2 1.995474e-42

That is the “easy path”: subset to the contrast you want, reduce across subjects, and write a tidy result. For one-off jobs or shell scripts, that is often enough.

How do you inspect a source before you commit to a model?

probe tells you what the CLI sees before anything is computed: the adapter, the dimensions, the assays, and whether the source has enough structure for the next step.

probe <- cli_json(
  "probe",
  "--input", stats_path,
  "--contrast-data-cols", "time",
  "--json"
)

data.frame(
  adapter = probe$adapter,
  samples = probe$dims[1],
  subjects = probe$dims[2],
  contrasts = probe$dims[3],
  assays = paste(probe$assays, collapse = ", "),
  has_contrast_data = probe$has_contrast_data,
  stringsAsFactors = FALSE
)
#>   adapter samples subjects contrasts    assays has_contrast_data
#> 1 tabular       2        4         2 beta, var              TRUE

Here --contrast-data-cols time promotes the time column into contrast-level metadata. That matters when you move from a simple group summary to a model that needs contrast annotations.

How do you separate planning from execution?

plan is useful when you want to describe the workflow once, save it, review it, and execute it later.

plan_info <- cli_json(
  "plan",
  "--input", stats_path,
  "--contrast-data-cols", "time",
  "--subset", "contrast=task",
  "--reduce", "fixed",
  "--posthoc", "fdr:bh",
  "--save-plan", plan_path,
  "--json"
)

knitr::kable(node_table(plan_info$nodes), row.names = FALSE)
op summary
subset_axis subset
reduce reduce:fixed
posthoc posthoc:fdr:bh

This is the point where the CLI stops being a thin wrapper around compute() and becomes a real planning tool. You can keep the saved JSON under version control, review the steps, and reuse the same plan across runs.

How do you preview a saved plan?

preview runs a small sample block through the current plan. That is useful when you want to check the output contract without writing the full result.

preview_out <- cli_json(
  "preview",
  "--load-plan", plan_path,
  "--n", "1",
  "--show-assay", "beta,q",
  "--json"
)

preview_out$data
#>   sample subject contrast   beta          q
#> 1  ROI_1    meta     task 0.9576 1.0555e-18

The preview already includes the derived and post-hoc assays from the saved plan. In practice, this is the fastest way to confirm that a workflow is producing the fields you expect before you launch a longer batch run.

How do you reach more complex workflows?

The CLI is not limited to fixed-effects summaries. You can also attach subject-level covariates, specify formulas, and hand reducer-specific options to more complex models.

cli_show(
  "preview",
  "--input", stats_path,
  "--subset", "contrast=task",
  "--col-data", cov_path,
  "--reduce", "meta:fe_reg",
  "--formula", "~ age",
  "--show-assay", "coef:age",
  "--n", "2"
)
#> $ fmrigds 'preview' '--input' '/tmp/Rtmp17Odxi/file1cec36df0018.csv' '--subset' 'contrast=task' '--col-data' '/tmp/Rtmp17Odxi/file1cec57b20e9d.csv' '--reduce' 'meta:fe_reg' '--formula' '~ age' '--show-assay' 'coef:age' '--n' '2'
#> GDS
#>   dims: 2 x 1 x 1 [sample x subject x contrast]
#>   space: sample_labels n=2
#>   subjects: 1 (meta)
#>   contrasts: 1 (task)
#>   assays: Q[?], df_res[?], coef:(Intercept)[?], coef:age[?], se_coef:(Intercept)[?], se_coef:age[?], z_coef:(Intercept)[?], z_coef:age[?], p_coef:(Intercept)[?], p_coef:age[?]
#>   design[1]: (Intercept), age
#>  sample subject contrast   coef:age
#>   ROI_1    meta     task 0.02776596
#>   ROI_2    meta     task 0.03831522

The same pattern extends to other advanced switches:

  • --source-option key=value passes adapter-specific arguments through gds().
  • --reduce-option key=value passes reducer options through reduce().
  • --posthoc-option key=value passes method-specific options through posthoc().
  • --write-option key=value controls exporters when you write to disk.
  • --load-plan and --save-plan let you mix shell-driven execution with plan-first workflows.

How do you discover available methods?

list exposes the registries that the CLI can target. You can use it to see which reducers and post-hoc methods are available in the current installation.

reducers <- cli_json("list", "reducers", "--json")

head(
  data.frame(
    name = reducers$name,
    requires = vapply(reducers$requires, paste, collapse = ", ", character(1)),
    provides = vapply(reducers$provides, paste, collapse = ", ", character(1)),
    stringsAsFactors = FALSE
  ),
  6
)
#>                name  requires
#> 1    combine:fisher         p
#> 2 combine:lancaster         p
#> 3  combine:stouffer         z
#> 4            lmm:ri      beta
#> 5   lmm:ri_knownvar beta, var
#> 6     lmm:ri_slope1      beta
#>                                                                                                                                                                                                                provides
#> 1                                                                                                                                                                                                         p_g, chi2, df
#> 2                                                                                                                                                                                                         p_g, chi2, df
#> 3                                                                                                                                                                                                              z_g, p_g
#> 4                                                                                                             coef, se_coef, t_coef, p_coef, df_coef, sigma2, vc_intercept, vc_resid, df_res, logLik, converged, lambda
#> 5                                                                                                  coef, se_coef, t_coef, p_coef, df_coef, sigma2, vc_intercept, vc_resid, sampling_var_mean, df_res, logLik, converged
#> 6 coef, se_coef, t_coef, p_coef, df_coef, sigma2, vc_intercept, vc_slope, vc_cov_intercept_slope, vc_resid, df_res, logLik, converged, lambda_intercept, lambda_slope, lambda_cov_intercept_slope, corr_intercept_slope

If you are scripting around the CLI, this registry view is a good way to make your wrapper validate method names before a larger run.

Where should you go next?

Once you are comfortable with the CLI surface, the R vignettes fill in the modeling details:

The CLI is best when you want a reproducible command surface. When you need custom transforms or deeply programmatic branching, save a plan from the CLI and continue in R with load_plan(), explain_plan(), and compute().