Version 2.1.0 hardens the decision/action, model-clock, parameter, run-identity, and callback contracts. It also adds a bounded grouped-decision API for one coordinated policy consultation across existing decision points, without changing the independent realization of their selected actions.
trajectory_table() renames action_taken to selected_action. There is
no legacy alias. Update code that reads the old column name. The corrected name
is deliberate: it records the policy's selection before pending-action
resolution, not proof that the action was staged or realized. Populated and
empty results also retain run_id and entity_id as their leading columns and
expose compact grouped_decision_point_id and group_activation_id fields.
Consult entity event history for actions that actually realized.DecisionPoint() preserves the released v2.0 positional contract.
observation_fn and label remain arguments seven and eight;
on_pending_action is appended after them. Fully positional v2.0 calls and
named 2.1 calls therefore retain their intended meanings.
Rejected Entity updates no longer leave phantom events. Entity$update()
now validates and constructs the complete candidate event, state, and history
before committing events, last_j, last_time, current, or hist. A
malformed or invalid patch leaves all five Entity-owned fields unchanged.
Loaded models now have one matching clock. A full schema's
schema$time_spec and its bundle's time_spec must be semantically equal in
unit, origin instant, origin class, and zone. A genuinely variables-only
schema remains a 2.1 compatibility input, uses the bundle clock, and emits a
targeted migration warning; a malformed full schema or clock mismatch errors.
Cohort run identity now reaches callbacks and trajectory records. The
batch-local run_id assigned in batch$index is carried into the matching
run name, SimContext, and every TrajectoryRecord, including supported
parallel backends. Stable entity/draw/simulation coordinates remain the
cross-call replay identity.
Decision callback errors now fail fast with context. Errors thrown by a
decision condition, policy$propose_action(), grouped
policy$propose_plan(), or an action handler are no longer converted into a
veto, no-action result, or realized no-effect action. Intentional condition
FALSE and policy/handler NULL remain supported. A condition or policy runs
after the triggering event was atomically committed, so that trigger is not
rolled back on failure; a failing action handler stops before its action event
or state effect is committed.
Cohort parameter contexts are no longer nested or renumbered.
run_cohort(param_draws = ) and bundle$sample_params(D) now use one
unambiguous list<ParamContext> boundary. Core validates and sorts the
collection once by its positive, unique draw_id, preserves each context's
direct params and provenance fields in callbacks, uses the actual ids in
run indexing and deterministic seeds, and returns the canonical collection.
Bare parameter payload lists at this Core boundary now fail early. When no
draw source is supplied, Core constructs typed 1:D contexts from
bundle$params or an empty list.
Cohort and lower-level draw seeds are no longer overwritten by a loaded
Engine. run_cohort() now resolves its effective runtime settings once and
owns coordinate-specific seeding; Engine$run_draw() preserves RNG state
established by its caller. A private handoff prevents Engine$run() from
applying the stored RuntimeContext seed a second time, while direct
Engine$run() seeding is unchanged. Seeded results that previously collapsed
distinct cohort or streaming replicates will change under the corrected
ownership contract.
ParamContext(draw_id = ) no longer truncates invalid ids. Positive
whole-valued doubles such as 5.0 retain the documented convenience and are
stored as integers; fractional, non-positive, non-finite, and out-of-range
values now error.
A scheduled action could be silently discarded before it fired. Refreshing all
processes -- the default when a bundle does not supply refresh_rules() -- replaced
the whole proposal set, destroying any action proposed in an earlier step that had
not yet been realized. An action scheduled meaningfully into the future would
therefore never happen. Pending actions are now untouched by refresh, under either
refresh strategy.
A realized action could repeat indefinitely. When refresh_rules() returned a
selective list of process ids, the action that had just been realized stayed in the
proposal set and was selected again at the same instant, on every subsequent step.
The engine now retires an action as soon as it is realized. This was not something a
model could work around: the engine identified pending actions by an internal name
the model was never given.
Grouped decisions coordinate one policy consultation across existing
leaves. GroupedDecisionPoint() declarations live in
schema$decision_groups and reference canonical DecisionPoint() ids. After
one triggering transition, Core evaluates member conditions in declared
order and calls policy$propose_plan() once with the non-empty eligible set;
an empty set skips policy. The returned DecisionPlan() must name every and
only eligible member exactly once, using an ActionEvent or explicit NULL
for each. Core validates and preflights the complete plan before modifying any
member pending slot. This all-or-none boundary covers plan acceptance and
staging only: selected actions subsequently arbitrate and realize
independently, and separate ordinary/group activations are not one global
transaction.
Grouped trajectory rows carry activation identity without a synthetic
parent row. Eligible leaves, including explicit NULL selections, share a
static grouped_decision_point_id and deterministic run-local
group_activation_id; opted-in veto rows use the same identity, including
zero-eligible activations. Optional plan metadata is opaque audit information
retained on raw grouped records only and excluded from trajectory_table().
propose_events() may declare last_event. When declared, it receives the event
that was just realized, including an ActionEvent's params, metadata, and
decision_point_id. This makes it possible for a parameterized action to influence a
future event process without a state variable used purely to carry the value.
last_event is NULL on the first call, which also distinguishes initial proposal
generation from a mid-run refresh -- previously indistinguishable. Callbacks that do
not declare the argument are called exactly as before.
DecisionPoint(on_pending_action = ) declares what happens when a policy proposes
an action while that decision point's previous action is still pending: "warn"
(the default; supersede and warn), "replace" (supersede silently), "keep"
(discard the new proposal), or "error". A decision point re-proposing after its own
action has fired is not a conflict and never warns.
Engine$run() reports stopped_by, one of "stop", "max_time",
"max_events", or "no_proposals". Previously a run that exhausted its event budget
ended silently and was indistinguishable from normal completion.
ActionEvent$decision_point_id is enforced as policy provenance, not a
routing override. During ordinary or grouped policy dispatch, Core fills a
missing id from the owning decision point, accepts an exact match, and errors
on a mismatch. Manually constructed actions outside dispatch may still carry
any valid self-described id.
Process ids beginning with . are reserved for internal use and are rejected from
both propose_events() and refresh_rules() with an explanatory error.
A realized action no longer carries a process_id. A process_id identifies a
model process, and an action is not one; actions are identified by
decision_point_id, which ActionEvent() already carries. is.null(event$process_id)
therefore distinguishes a policy action from a model event, and a model process may
safely share a name with a decision point.
load_model() rejects duplicated DecisionPoint ids. The engine keys pending
actions by decision point id, so duplicates would silently collapse into one slot.
Several pending actions can now coexist and all will be realized. Each decision
point holds at most one pending action, but distinct decision points hold their own.
Scheduling one action earlier than another orders them; it does not cancel the later
one. Where two decision points represent alternative responses, make them mutually
exclusive with condition rather than relying on time_next ordering. Tutorial 03
has been updated accordingly.
This is a major release. The core engine is unchanged; v2.0.0 layers a formalized
decision/policy/action architecture on top of it and replaces the old catch-all
ctx argument with explicit typed context objects.
DecisionPoint(): declares a named checkpoint in the event timeline where
a policy can propose an action. Declared on the schema, not buried in transition
logic. Supports trigger (which event types fire it), allowed_actions,
action_handlers (per-action state-change functions), an optional condition
predicate, and audit flag.ActionEvent: an action proposed by a policy enters the normal event
timeline and is realized by the same transition() / stop() path as any
other event. Actions do not mutate state directly.TrajectoryRecord: logged at every decision point firing. Records the time,
decision point id, what state the policy observed, what it proposed, what was
realized, and state before/after. Captures the full decision audit trail.trajectory_table(): convenience helper to flatten a list of
TrajectoryRecord objects into a tidy data frame.load_model(): validated assembly function. Accepts schema, bundle,
policy, trajectory, runtime, and param_source; validates that all
components are mutually consistent and returns a configured Engine. This is
now the recommended entry point for models with policies or runtime config.ctxctx is removed as a first-class interface. Bundle callbacks that declared ctx
as a formal now receive a hard error on engine construction. Replace with the
following typed objects (all optional in callback signatures):
SimContext: per-run metadata (run_id, time_spec, model_id,
scenario_id, horizon).ParamContext: one parameter realization (draw_id, params named list,
optional provenance). Constructed by ParamContext().RuntimeContext: reproducibility and backend settings (seed,
replicate_id, backend, n_workers). Constructed by RuntimeContext().EnvironmentContext: external signals for ABM/RL hooks (signals,
step_fn, reset_fn, info). Reserved for future use.sample_params(n) bundle hook: when present, run_cohort() calls it to
draw n ParamContext objects and runs every entity under every draw, fully
crossing entities × parameter draws × stochastic replicates.run_cohort() param_draws argument: alternatively, pass a pre-built list
of ParamContext objects directly.batch$param_draws: drawn contexts are returned alongside results for
reproducibility.refresh_rules(entity, last_event, changes): bundle hook controlling which
processes re-propose after each event. Returns "ALL" (default) or a character
vector of process_ids. entity is the full post-transition state;
changes is only the delta from the last transition() call.derive() / derived variables: schema variables can declare a f function
of (entity, j, t) computed on read rather than stored. Supports time-aware
lookups via snapshot_at_time().schema_validate() type-implied bounds: default, min, and max values are
now cross-checked against the range implied by the declared type (e.g.,
nonnegative_integer must have default ≥ 0; probability must be in [0, 1]).set_schema() gains time_spec and decision_points arguments: assemble
a complete schema including clock spec and decision points in one call.ModelProvider / PackageProvider / FileProvider / MLflowProvider:
unexported. Engine$new(provider=) removed. Engine$new(bundle=) and
load_model() are the only Engine construction paths..Rd files to inline roxygen2 comments.
man/ and NAMESPACE are now generated artifacts; do not edit by hand.Engine$new(bundle = ...) shortcut. New bundle parameter to the Engine constructor accepts a ModelBundle directly, bypassing the ModelProvider machinery for in-memory / inline models. Equivalent to writing your own one-method provider, but without the boilerplate. The provider = ... path is unchanged and remains the right choice for packaged or pluggable models. Supplying both bundle and provider is an error.tutorials/01_core_engine_scaffold.Rmd) updated to use Engine$new(bundle = toy_bundle) directly; ModelProvider is now a forward-pointer aside rather than a required first-encounter concept. Vignette title renamed to "Engine and ModelBundle scaffold".man/Engine.Rd updated with a Constructor section documenting the two construction paths.id_string type (no deprecation alias). Use nonempty_string or a custom validate function for identifier columns; the supported type list is now 14 entries.percent type: numeric in [0, 100], honors optional min / max overrides.set_schema() rewrite with hybrid vars syntax. Each vars entry is now either a type-name string (e.g. "count") or a full list spec (e.g. list(type = "positive_numeric", max = 20)); both shapes can be mixed in one call. New overwrite = FALSE argument errors on collision when extending an existing schema (set overwrite = TRUE to replace). New remove = argument drops named entries (errors if absent). Removed the previous replace = and add = arguments.min / max or a custom validate function to tighten.set_schema() shape, the percent type, and the removal of id_string.logical, binary, integer, count, nonnegative_integer, positive_integer, numeric, nonnegative_numeric, positive_numeric, probability, categorical, ordinal, string, nonempty_string, id_string.default and coerce fields are now optional in schema specifications; fluxCore applies appropriate defaults (e.g., as.numeric for numeric types, NA_real_ for numeric defaults).set_schema() helper: simplified schema creation via named character vector of type mappings; reduces boilerplate for common use cases..validate_schema() now handles automatic defaults; stricter type checking and validation rules for new types.schema_spec.md with complete type reference; tutorial examples showcase both manual and helper-function workflows.default_model_bundle() and default_entity_schema() from runtime code.PackageProvider now requires explicit registry input; no hidden fallback bundle is injected.new_entity() wrapper and standardized on Entity$new(); wrapper input-normalization behavior is now handled directly in Entity$initialize().tests/testthat/helper_fixtures.R).refresh_rules() engine boundary validation:"ALL" (scalar), or"ALL"), valid targeted
refresh, and malformed return structures.refresh_rules behavior and strict
return contract.time_spec runtime contract introduced in
1.5.1 (single model declaration via bundle$time_spec, no runtime override).bundle$time_spec; runtime context attempts to override time metadata now
error.time_unit wiring from cohort/engine run APIs and aligned
manual documentation with actual signatures.Finalized ecosystem harmonization for the 1.5.0 coordinated release.
Documentation quality pass: fixed check() example/codoc/Rd usage issues and completed missing manual Rd coverage for exported APIs.
run_cohort() hardening: unnamed entity lists are now auto-named for stable run indexing in examples and batch execution.
Licensing update: switched package license to LGPL-3.
flux_ prefixes from exported time/schema helpers. New names are set_time_unit(), time_spec(), time_to_model(), time_from_model(), schema_validate(), schema_assert_vars(), schema_var_info(), schema_assert_types(), and schema_assert_levels()..Rd pages were renamed/updated to match the new helper names.var() to avoid masking stats::var(). Use declare_variable() instead.var() to declare_variable() to avoid masking stats::var() when attaching fluxCore.Entity$meta for bundle/runtime bookkeeping (e.g., refresh cadence clocks) without polluting the validated state schema.fluxCore.time_to_model() now explicitly rejects time-only inputs (e.g., difftime, hms). Calendar inputs must be Date or POSIXct (date+time).schema_validate(), schema_assert_vars(), schema_var_info(), schema_assert_types(), schema_assert_levels().time_unit = ... argument (avoid accidental partial match to max_time).max_time validation to prevent silent mis-specified calls.Fix: test-time-spec.R string literals now use fixed matching (no invalid escapes).
Add time-axis utilities for deterministic mapping between calendar time (Date/POSIXct) and numeric model time: time_spec(), time_to_model(), time_from_model(), and set_time_unit().
ctx$time$unit / ctx$time$origin / ctx$time$zone (replacing the older ctx$time_unit field).months and years are fixed approximations (30.4375 and 365.25 days). origin is a mapping reference, not model baseline.derive(fn = "count", target = declare_variable(...)) now uses snapshot_at_time() (was incorrectly calling snapshot_at() with a time value).derive(fn = 'count', target = declare_variable(...)) now counts non-missing values in-window (avoids counting schema-default init placeholders such as NA).run_cohort(backend = 'cluster') to reduce parallel-backend drift risk.batch.R (remove stray parenthesis) affecting installation.id argument in Entity$initialize() (package parse/collate error).Fix: run_cohort() run index ordering is now entity_id -> param_draw_id -> sim_id (contractual invariant).
Fix run-index ordering unit test to use a minimal bundle that always proposes a single no-op event (avoids Engine error when no proposals are available).
time_unit in the ordering test to avoid warnings.run_cohort() now guarantees that runs[[i]] corresponds to index[i, ] (run_index alignment invariant). This removes the need for downstream reordering hacks and is critical for correct entity-level grouping.type and levels (used by downstream summary code).No functional changes. Version bump to align with fluxForecast 1.0.2.
Clarified documentation around active_followup: it is a regular state variable and does not automatically stop the Engine.
run_cohort() context handling: ctx may be a single list (recycled) or a per-parameter-draw list-of-ctx (length = n_param_draws).print.flux_state() implemented to match the declared S3 method and remove the NAMESPACE warning.Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.