Internal method. An abstract R6 Class encapsulating the data and functionality for an experimental design. This class takes care of data storage and response handling.
Details
Throughout the package, treatment assignment vectors \(w\) use the
\(\{0, 1\}\) encoding: \(1\) indicates a treated subject and \(0\)
a control subject. All public methods that return or accept \(w\)
(e.g. get_w(), draw_ws_according_to_design()) use this
convention. A handful of variance estimators (e.g. InferenceIncidCMH,
InferenceIncidExtendedRobins) recode to a signed \(\{-1,+1\}\)
contrast internally where their formulas require it; that recoding is
local to those classes and does not affect this public convention.
Saving and loading
Design (and its DesignSeqOneByOne subclasses) is the unit of
persistence for a trial. Persist a des_obj with base R's
saveRDS()/readRDS() – there is no dedicated
save_edi_design()/load_edi_design() wrapper, and none is
planned: the audit behind this section found nothing that needs
transformation on load beyond what is documented here. Inference*
objects are disposable, cheaply reconstructed from a Design object
on demand (see each class's $new()), and must never be
saveRDS()'d directly – nothing currently prevents it (they
serialize "successfully" like any R6 object), but the result is a frozen
snapshot a user could easily mistake for something that stays live against
the design, and re-running inference from a reloaded Design is both
cheap and the only tested path.
Worked example (mirrors the round-trip tests in
R/EDI/tests/testthat/test-save-load-design.R):
des_obj = DesignSeqOneByOneBernoulli$new(n = 20, response_type = "continuous")
for (i in 1:10) {
des_obj$add_one_subject_to_experiment_and_assign(data.frame(x1 = rnorm(1)))
des_obj$add_one_subject_response(i, y = rnorm(1))
}
saveRDS(des_obj, "trial.rds", version = 2)
# ...new R session...
des_obj = readRDS("trial.rds")
for (i in 11:20) {
des_obj$add_one_subject_to_experiment_and_assign(data.frame(x1 = rnorm(1)))
des_obj$add_one_subject_response(i, y = rnorm(1))
}
inf_obj = InferenceContinOLS$new(des_obj) # reconstructed fresh, never persisted
inf_obj$compute_estimate()Passing version = 2 to saveRDS() is recommended, matching the
one existing internal precedent for RDS serialization in this package
(SimulationFramework's replication cache); it is not required for a
same-R-version round trip.
Version stamp. Every Design object records the package
version it was constructed under (get_edi_version_created()). This
is stamped once at construction and is not refreshed by
readRDS() – it reflects the version that originally built the
object, not whatever version is currently loaded. The first "resume the
trial" call after a reload (draw_ws_according_to_design() for fixed
designs, add_one_subject_to_experiment_and_assign() for sequential
designs) compares the stamped version's major component against the
currently loaded package's major component and emits a one-time
warning() on a mismatch; minor/patch differences are silent, since
most field additions are additive under this class's
lock_objects = FALSE R6 fields and do not warrant nagging on every
routine upgrade. Objects saved before this field existed self-initialize
it to the currently loaded version the first time it is read, rather than
erroring on the missing field.
RNG/reproducibility caveat. private$seed is consumed only
once, inside maybe_set_seed() at construction time, and is
not re-applied on readRDS(). Continuing to enroll subjects
after a reload therefore draws from whatever the global
.Random.seed happens to be in the new session, not a deterministic
continuation of the original stream. This is almost certainly the right
behavior for a real trial (bit-for-bit-reproducible continuation across a
process restart is not a property a production trial should have), but it
means a same-seed reload-and-continue is not expected to reproduce
the same draws as an uninterrupted run with that seed – do not rely on
that for testing.
Known non-serializable case. A DesignFixedOptimal
constructed with objective = "custom" from a raw
RcppXPtrUtils::cppXPtr() external pointer (rather than a C++ source
string) cannot be safely reloaded: compiled function pointers do not
survive a saveRDS()/readRDS() round trip, and there is no
retained source to recompile from. This is detected on first use after
reload and raises a clear error rather than failing silently; supply
custom_objective as a C++ source string instead of a pre-built
cppXPtr() object if you need this design to survive a save/reload
cycle – that form recompiles itself automatically the first time it is
used post-reload. Every other audited private cache on Design and
its components (all_subject_data_cache, permutations_cache,
lin_centered_covariates, matching/blocking/cluster component state
such as m, xm_structural, boot_pair_rows) was traced
to its originating C++ return type and confirmed to hold only plain
R matrices/vectors/lists, not external pointers or other
non-serializable values.
Methods
Public methods
Design$is_blocking_design()
Check whether this design currently has blocking structure.
The base implementation returns FALSE. Designs that compose
BlockingStructure override this method with the structural check.
Design$is_matching_design()
Check whether this design currently has matching structure.
The base implementation returns FALSE. Designs that compose
MatchingStructure override this method with the structural check.
Design$is_a_kk_matching_capable()
Characterization: is this a KK matching-on-the-fly-capable
design (sequential KK or its fixed binary-match equivalent)? Default
FALSE; overridden to TRUE on
DesignSeqOneByOneKK14 and DesignFixedBinaryMatch.
Design$is_a_cluster_capable()
Characterization: is this a cluster-structured design?
Default FALSE; overridden to TRUE on
DesignFixedCluster and DesignFixedBlockedCluster.
Design$is_a_bernoulli_capable()
Characterization: is this a Bernoulli-randomized design?
Default FALSE; overridden to TRUE on
DesignSeqOneByOneBernoulli and DesignFixedBernoulli.
Design$new()
Initialize an experimental design
Usage
Design$new(
response_type,
prob_T = 0.5,
include_is_missing_as_a_new_feature = FALSE,
n = NULL,
verbose = FALSE,
missingness_method = "impute",
design_formula = ~.,
ordinal_levels = NULL,
seed = NULL
)Arguments
response_type"continuous", "incidence", "proportion", "count", "survival", or "ordinal".
prob_TProbability of treatment assignment.
include_is_missing_as_a_new_featureFlag for missingness indicators.
nThe sample size (if fixed).
verboseFlag for verbosity.
missingness_methodHow to handle missing values in covariates when building the model matrix for inference. One of:
"impute"(default)Missing values are filled in using random-forest imputation (
missRanger, falling back tomissForeston failure). The response vector is included as an auxiliary predictor when available. This preserves all covariates and all subjects but introduces imputed values that influence inference."drop_column"Any covariate column that contains at least one missing value is dropped entirely from the model matrix before inference. No values are invented; the remaining complete columns are used as-is. This is conservative but transparent.
"error"An error is thrown as soon as any missing value is detected in the covariate matrix. Use this when you want to guarantee that inference runs on exactly the data you supplied, with no silent modification.
design_formulaA formula object used to create the design matrix from covariates. Default is
~ ..ordinal_levelsIf the response type is "ordinal", the labels for the levels.
seedInteger seed for reproducibility.
Design$add_one_subject_response()
For CARA designs, add a single subject response.
Arguments
tThe subject index.
yThe exact response value. Supply this XOR both
y_Landy_R– never together, never just one of the two.y_LFor a censored survival response, the lower bound of the event-time interval. Right-censored: the last known event-free time (pair with
y_R = Inf). Left-censored:0, which must be stated explicitly rather than defaulted. Interval-censored: the interval's lower bound. Storage accepts any well-formed left-/interval-censored value; whether a givenInferenceclass can actually consume it depends on that class (most survivalInferenceclasses still only accept exact/right-censored data and will reject construction with a clear error otherwise – see individual class docs).y_RFor a censored survival response, the upper bound of the event-time interval. Right-censored:
Inf. Left-/ interval-censored: the confirmed-by time / interval upper bound.
Design$add_all_subject_responses()
For non-CARA designs, add all subject responses.
Arguments
ysThe exact responses as a numeric vector,
NAfor any subject whose response is censored (supplyy_Ls/y_Rsfor those instead).y_LsThe censored-response lower bounds,
NAfor any subject with an exact response inys. Right-censored: the last known event-free time (pair withy_Rs = Inf). Left-censored:0, stated explicitly. Interval-censored: the interval's lower bound. Storage accepts any well-formed left-/interval-censored value; whether a givenInferenceclass can actually consume it depends on that class (most survivalInferenceclasses still only accept exact/ right-censored data and will reject construction with a clear error otherwise – see individual class docs).y_RsThe censored-response upper bounds,
NAfor any subject with an exact response inys. Right-censored:Inf. Left-/interval-censored: the confirmed-by time / interval upper bound.
Design$has_general_censoring()
Checks if the experiment has any left- or
interval-censored survival responses – i.e. any subject whose
y_R is finite (right-censored subjects have
y_R = Inf, which is excluded). Most survival
Inference classes cannot yet consume this shape of data
(see get_effective_time()/get_effective_dead());
this is the check Inference$initialize() uses to reject
construction cleanly for those classes.
Design$capabilities()
Returns the capabilities this design instance exposes
(see fix_design_hierarchy.md, "Capability Model").
Deliberately instance-level, not a class-registry read (fix_design_hierarchy.md,
TODO-28): is_blocking_design()/is_matching_design() depend on
real construction-time state (e.g. private$m/private$blocking_capable),
not just which components a class composes – DesignFixediBCRD
constructed with an unknown n, for instance, composes
BlockingStructure but is not blocking-capable for that particular
instance. A class-registry-only answer (this function briefly unioned in
get_effective_design_capabilities(), a purely class-level, component-
composition-based check) would silently report "blocking" for every instance
of such a class regardless of its actual construction state – confirmed as a
real, reproducible false positive during this TODO's implementation, not a
hypothetical. get_effective_design_capabilities()/
design_class_registry.R's direct_components still exist and are
correct – they're the right tool for a generator-only query with no
instance in hand (see design_class_generator_supports_batch_w_pregeneration()),
just not for this instance-level method.
Design$supports()
Returns whether this design object supports a capability.
See capabilities().
Design$applicable_inference_class_names()
Returns the sorted character vector of concrete, exported
Inference class names legal for this design object under
default constructor arguments, derived purely from this design's own
normalized metadata (response type, KK-matching capability, blocking,
and both censoring axes) filtered through the registry's
compatibility predicates – the same normalization and predicate
logic InferenceSuite uses for
discovery (see normalize_inference_design_metadata() and
is_inference_class_compatible_with_design_metadata() in
inference_suite.R). No candidate class is constructed to
determine applicability, so this has no side effects and cannot be
influenced by a constructor failure or a missing optional package
(see unavailable_inference_classes_due_to_missing_packages()
for that case, reported separately). A class whose censoring
tolerance depends on non-default constructor arguments (e.g.
InferenceSurvivalCoxPHRegr only tolerates general censoring
with testing_type = "wald") is listed here when its
default configuration is compatible; a construction-time
error for an incompatible non-default argument combination remains
the documented behavior of that class's initialize().
Design$unavailable_inference_classes_due_to_missing_packages()
Companion to applicable_inference_class_names():
returns the subset of otherwise design-compatible Inference
classes that are excluded solely because a registered
required_packages entry is not installed, as a named list
(class name -> character vector of missing package names) – kept
separate from plain design incompatibility so callers can tell "not
applicable to this design" apart from "applicable, but an optional
dependency isn't installed."
Design$incompatible_inference_classes_due_to_design_structure()
Companion to applicable_inference_class_names():
returns the subset of otherwise design-compatible Inference
classes that are excluded because they declared a
design_compatibility_reason predicate (a design-*structure*
requirement, e.g. even treatment allocation or equal block sizes,
beyond what response type/KK/blocking/censoring metadata alone can
express) and this design object fails it, as a named list (class
name -> one-line reason string) – kept separate from plain design
incompatibility and from a missing package for the same reason
unavailable_inference_classes_due_to_missing_packages() is
kept separate: so callers can tell exactly why a class is missing
from applicable_inference_class_names() instead of only
discovering it as a construction-time error.
Design$randomization_family()
Returns this design object's registry-backed randomization
family (see fix_design_hierarchy.md, "Class Metadata"), e.g.
"kk14", "bernoulli", "rerandomization". Replaces
class-identity (inherits()/is()) dispatch at call sites that
need to distinguish design variants (see "Class-Identity Dispatch
Replacement"). Returns NA_character_ if the class is not registered
or is one of the unsplit/timing-root abstract bases.
Design$supports_resampling()
Check if the design supports resampling at all – FALSE
only for the abstract timing-family bases themselves (DesignFixed,
DesignSeqOneByOne, and their custom-extension abstract bases)
instantiated directly; TRUE for every concrete subclass,
including ObservationalDesign. This is the general check
for resampling methods that never need the design's own randomization
mechanism – plain nonparametric bootstrap, Bayesian bootstrap,
m-out-of-n bootstrap, PRW subsampling – which only resample already-observed
units/rows and their fixed, observed assignment, so they remain valid and
available even for a design with no randomization mechanism at all (see
ObservationalDesign's class documentation: "resampling subjects with
their observed, fixed assignment does not require a known randomization
probability"). Contrast with supports_randomization_draw()/
supports_resampling_replay() below, which gate the narrower set of
methods that actually do need to invoke the design's mechanism (a plain
randomization test/CI, or a bootstrap randomization test that re-randomizes
resampled data) and are therefore FALSE for ObservationalDesign
specifically – see fix_design_hierarchy.md, "Observational Design
Migration" for the live bug that split fixes.
Design$supports_randomization_draw()
Check if this design can draw a fresh treatment assignment
from its own randomization mechanism – the eligibility condition for
permutation-style randomization tests/CIs (compute_rand_two_sided_pval()
and friends), which redraw \(w\) directly. FALSE for the abstract
timing-family bases themselves (same as supports_resampling()) and,
unlike supports_resampling(), also FALSE for
ObservationalDesign (no draw mechanism at all – \(w\) is supplied
by the user, so there is nothing to redraw); TRUE for every other
concrete subclass. See supports_resampling()'s documentation for why
this is a narrower, separate capability rather than reusing that one, and
"Observational Design Migration" for the live bug this fixes
(ObservationalDesign previously answered the old, unsplit
supports_resampling() TRUE, silently passing the
randomization-test eligibility assert before failing later and deeper,
inside draw_ws_raw()'s throwing stub).
Design$supports_resampling_replay()
Check if this design's mechanism can be faithfully replayed
against resampled data – the eligibility condition specifically for the
bootstrap randomization test (BRT), which resamples units and then
re-randomizes each resample using the design's own mechanism (see
inference_all_abstract_rand_bootstrap.R's repeated
draw_ws_according_to_design() calls). Not the eligibility
condition for plain nonparametric/Bayesian/m-out-of-n/PRW-subsampling
bootstrap – those never redraw \(w\) at all (they resample already-observed
units and their fixed, observed assignment) and are gated by the broader
supports_resampling() instead, which stays TRUE for
ObservationalDesign. FALSE for the same abstract timing-family
bases as supports_randomization_draw() and for
ObservationalDesign (no randomization mechanism to replay); TRUE
for every other concrete subclass. See supports_randomization_draw()'s
documentation for why this is a separate capability rather than the same flag
reused.
Design$prepare_for_resampling_replay()
Hook invoked by the bootstrap-randomization-test machinery
on a design object whose assignment mechanism is about to be replayed
against resampled data (once per replicate draw site, ahead of
draw_ws_according_to_design(1L)). The base implementation is a
no-op; designs whose replay is a full re-optimization
(DesignFixedOptimal) override it to switch to their
per-replicate solver profile (solver_args$brt_*). Idempotent.
Design$warm_all_subject_data_cache()
Warm the per-subject assignment-data cache, when this design uses covariates. This is an internal optimization hook for randomization inference; it keeps cache mutation inside the Design object instead of exposing its private environment to callers.
Design$get_effective_time()
Get the effective response time per subject: the exact
value y where recorded, or the lower bound y_L
for a censored subject. This reconstructs "the one informative
number" every response type other than left-/interval-censored
survival data has always had, for code that needs a single
numeric value per subject rather than the y/y_L/
y_R triple directly.
Design$get_effective_dead()
Get the effective event indicator per subject: 1
for an exact response, 0 for a censored one. This
reconstructs today's dead semantics for right-censored
survival data (and is trivially all-1 for every other
response type, which never has censoring). It is only valid
for exact/right-censored data – a left- or interval-censored
subject also returns 0 here, which is not meaningful
right-censoring status, so callers must confirm (e.g. via
any_censoring() plus their own censoring-shape checks)
that no such rows are present before relying on this value.
Design$get_edi_version_created()
Get the EDI package version this object was created under.
Stamped once, at construction time, from
utils::packageVersion("EDI"); never re-stamped on
readRDS() reload, so it reflects the version that originally
built the object rather than whatever version is currently loaded.
Objects saved before this field existed self-initialize it to the
currently loaded version the first time it is read (there is
no way to recover the true original version for those objects),
rather than erroring on the missing field.
Examples
if (FALSE) { # \dontrun{
# Design is abstract and cannot be instantiated directly; construct a
# concrete subclass instead, e.g.:
seq_des = DesignSeqOneByOneBernoulli$new(n = 6, response_type = 'continuous')
seq_des$add_one_subject_to_experiment_and_assign(data.frame(x1 = rnorm(1)))
} # }
