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().Fix: unit tests updated to use time_unit = ...
argument (avoid accidental partial match to
max_time).
Fix: add strict 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().
Change: time metadata is now stored under
ctx$time$unit / ctx$time$origin /
ctx$time$zone (replacing the older
ctx$time_unit field).
Notes: 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).
Set 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.