Skip to content

Register forcing#20

Merged
jordanplanders merged 17 commits into
mainfrom
register_forcing
Jun 1, 2026
Merged

Register forcing#20
jordanplanders merged 17 commits into
mainfrom
register_forcing

Conversation

@jordanplanders

Copy link
Copy Markdown
Collaborator

This pull request introduces a comprehensive and flexible framework for registering and applying external forcings to model parameters and state variables in the PBModel class. The changes deprecate the old single-forcing approach in favor of a multi-forcing system, allowing for more complex and physically meaningful model configurations. The update also ensures that forcings are applied at the correct stage of the integration process and are compatible with both fixed-step and adaptive solvers.

The most important changes are:

Forcing Registration and Specification

  • Added the ForcingSpec dataclass in forcing.py to encapsulate the details of a single forcing attachment, including validation of attachment style and timing, and a unified evaluation interface. ([paleobeasts/core/forcing.pyR528-R593](https://github.com/LinkedEarth/PaleoBeasts/pull/20/files#diff-93cb2c0865be2a23c8e2932020d00deb054d30815882f12fdfd1583a52e6ffdaR528-R593))
  • Introduced the register_forcing, get_forcings, and clear_forcings methods on PBModel to allow attaching, querying, and removing multiple forcings on any parameter or state variable, with explicit rules for attachment style and timing. ([paleobeasts/core/pbmodel.pyR252-R481](https://github.com/LinkedEarth/PaleoBeasts/pull/20/files#diff-6703a572d8f91eda9906d398c44d2e623341cf856e29a0c9cd0f18531a381aceR252-R481))

Forcing Application Logic

  • Implemented _build_forced_dydt and _build_post_step helper methods to apply pre-step and post-step forcings, respectively, ensuring correct modification of parameters and/or state variables during integration. ([paleobeasts/core/pbmodel.pyR252-R481](https://github.com/LinkedEarth/PaleoBeasts/pull/20/files#diff-6703a572d8f91eda9906d398c44d2e623341cf856e29a0c9cd0f18531a381aceR252-R481))
  • Updated the integrate method to automatically wrap the system ODEs and post-step callbacks with the registered forcings, and to warn the user if post-step forcings are ignored due to the use of adaptive solvers. ([paleobeasts/core/pbmodel.pyR567-R586](https://github.com/LinkedEarth/PaleoBeasts/pull/20/files#diff-6703a572d8f91eda9906d398c44d2e623341cf856e29a0c9cd0f18531a381aceR567-R586))

API and Internal Refactoring

  • Removed the legacy single-forcing interface and associated arguments from PBModel and GenericBoxModel, updating construction and copying logic to support the new multi-forcing system. ([[1]](https://github.com/LinkedEarth/PaleoBeasts/pull/20/files#diff-6703a572d8f91eda9906d398c44d2e623341cf856e29a0c9cd0f18531a381aceR11-R49), [[2]](https://github.com/LinkedEarth/PaleoBeasts/pull/20/files#diff-6703a572d8f91eda9906d398c44d2e623341cf856e29a0c9cd0f18531a381aceL76-R100), [[3]](https://github.com/LinkedEarth/PaleoBeasts/pull/20/files#diff-6703a572d8f91eda9906d398c44d2e623341cf856e29a0c9cd0f18531a381aceL145-L157), [[4]](https://github.com/LinkedEarth/PaleoBeasts/pull/20/files#diff-3475524326dced6edf8e5077bc6efd000371c5653151688e7fc5f6b4370a57cfL378-R388), [[5]](https://github.com/LinkedEarth/PaleoBeasts/pull/20/files#diff-3475524326dced6edf8e5077bc6efd000371c5653151688e7fc5f6b4370a57cfL405-L422))
  • Ensured that ForcingSpec is exported in the public API. ([paleobeasts/core/forcing.pyR602](https://github.com/LinkedEarth/PaleoBeasts/pull/20/files#diff-93cb2c0865be2a23c8e2932020d00deb054d30815882f12fdfd1583a52e6ffdaR602))

These changes make the model codebase more robust and extensible, supporting advanced use cases such as multiple concurrent forcings and precise control over their application.

Introduce ForcingSpec to represent a registered forcing attachment on a model variable. The dataclass validates attachment_style ("replacement"/"additive") and timing ("pre"/"post"), ensures forcing_object is callable or implements get_forcing, and provides evaluate(t) to obtain the forcing value. Export ForcingSpec via __all__ and add unit tests covering validation, evaluation, and acceptance of objects with get_forcing.
Introduce a forcing registry and public API on PBModel to attach external forcings to named parameters or state variables. Adds import of ForcingSpec and implements register_forcing (with validation of parameter vs state namespace, attachment_style and timing rules, conflict checks, and warnings), get_forcings, and clear_forcings. Copies preserve a shallow registry so ForcingSpec objects are shared but per-instance lists are independent. Adds comprehensive tests in paleobeasts/tests/test_core_pbmodel_register_forcing.py to exercise defaults, error cases, timing/attachment semantics, registry inspection/clearing, copy behavior, and ForcingSpec evaluation wiring.
Refactor forcing handling to support pre-step and post-step forcings and wire them into fixed-step solvers. Removed the old single `forcing` constructor argument and `resolve_forcing`; PBModel now uses an internal `_forcings` registry and builds a wrapped `dydt` that applies pre-step parameter/state replacement or additive forcings. Added `_build_post_step` to produce a post-step hook applied after each fixed-step substep, and emit a warning if post-step forcings are present but an adaptive solver is used. Updated euler, euler_maruyama and rk4 in the solver to accept an optional `post_step` hook and apply it after each step/sub-step. Adjusted `__copy__` to use `object.__new__` and clarified sharing semantics for forcing specs and diagnostics.
Remove the explicit `forcing` argument from BoxModelSpec.make_model/make_boxmodel and GenericBoxModel.__init__, and stop reading a model-level `self.forcing` inside GenericBoxModel.resolve_input and TwoBoxCarbon.source_flux. Inputs now resolve exclusively via their fallback parameter; to drive an input with a time-varying signal callers should register a forcing on that fallback parameter (e.g. `model.register_forcing('param_name', forcing)`).

Files changed:
- paleobeasts/signal_models/box_model.py: drop forcing parameters from factory/constructor methods, update resolve_input to use fallback parameters only and add usage docstring, adjust error messages.
- paleobeasts/signal_models/two_box_carbon.py: remove example and constructor use of forcing, and remove direct use of self.forcing in source_flux.
- paleobeasts/tests/test_signal_models_two_box_carbon.py: update tests to instantiate models without a forcing argument and register forcings via register_forcing where appropriate.

This simplifies the forcing API by centralizing forcings as parameter-level registrations and clarifies how to attach time-varying inputs to models.
Remove the explicit 'forcing' constructor argument and direct uses of self.resolve_forcing from SimplePendulum, DrivenPendulum, and DoublePendulum. Update example instantiations and tests to construct models without the forcing parameter; adjust DrivenPendulum behavior/tests to verify zero amplitude drive instead of testing a Forcing override. Clean up internal drive/dynamics to use stored parameters (A, Omega) directly and remove resolve_forcing fallbacks. Tests in paleobeasts/tests/test_signal_models_pendulum.py were updated to match the simplified API and expectations.
Refactor Stocker2003 signal models to use registered forcings and parameter fallbacks instead of accepting a forcing object via the constructor. Removed the `forcing` argument from the constructors, replaced calls to `resolve_forcing` with `get_param_value('Tn' / 'T_N', ...)`, and added example/test changes to call `model.register_forcing(...)` or pass a numeric Tn parameter. Tests updated accordingly to register the forcing or use the Tn param. This simplifies the forcing API and ensures forcings are managed consistently via the model's registry.
Replace positional forcing argument for EBM0D/EBM1DLat with an S0 parameter and rely on register_forcing for time-varying solar forcing. EBM0D: remove forcing ctor arg, add S0 (default 1365.0), store self.S0 and include 'S0' in param_values, and use get_param_value('S0', ...) for solar incoming. EBM1DLat: remove forcing arg from ctor and examples. Update examples and tests to instantiate models without a forcing arg and call model.register_forcing('S0', forcing) where needed. Small adjustments to doc examples and test expectations to match the new API.
Remove the legacy forcing argument and internal _forcing_vector from Stommel. Forcing is no longer accepted in the constructor or injected directly into the tendency; callers must register forcings via register_forcing (attachment_style/timing) for specific state variables. Update example and tests to instantiate Stommel without a forcing argument and to register forcings where needed. This aligns the model with the shared forcing API and simplifies internal forcing handling.
Remove the explicit 'forcing' argument from Lorenz63 and Lorenz96 constructors and simplify forcing handling. Lorenz96 now uses get_param_value('F', ...) in _forcing_value instead of resolve_forcing; Lorenz63 no longer accepts or resolves forcing (removed _forcing_vector and forcing contributions to the RHS). Update super() calls to match the new signature. Update tests to stop passing pb.core.Forcing objects and to construct models with the new signatures, and adjust usages accordingly. This streamlines parameter handling by relying on the existing parameter resolution API and simplifies the model interfaces.
Decouple Model3 from a constructor forcing argument by adding an optional insolation parameter (default 0.0) and using the model's parameter store to retrieve insolation via get_param_value. The change removes the forcing arg from Model3.__init__, adds insolation to param_values, and switches internal use from self.forcing.get_forcing(...) to the new insolation param. Tests were updated to construct Model3 without a forcing and call register_forcing('insolation', ...) where needed. This enables consistent handling of time-varying parameters and the forcing via the model's register_forcing API.
Remove the deprecated forcing argument and related logic from the Roessler PBModel. The constructor no longer accepts a forcing parameter or forwards it to the base class; the internal _forcing_vector helper and the application of forcing in dydt were removed. Example instantiation and tests were updated to stop passing forcing=None and to reflect the simplified derivative equations.
Introduce seasonal_forcing(t, Af=0.5, Pf=6.0) helper and document its usage. Refactor ENSORechargeOscillator to remove the explicit forcing constructor argument and instead use Af/Pf for internal seasonal forcing; _sst_forcing returns 0 when Af==0 and only requires Pf non-zero when Af != 0. Update tests to remove the external-forcing replacement case and add an analytic check that _sst_forcing(t=Pf/4) == Af.
Replace the old forcing constructor argument with an F parameter (default 0.0). Store F in param_values and self.F, remove passing forcing to the PBModel super, and fetch forcing in the ODE with get_param_value("F", t, state) instead of resolve_forcing. Update tests to stop passing forcing=None and to register a forcing function via register_forcing('F', ...) where needed. This unifies forcing handling as a model parameter and enables time-varying or registered forcings.
Remove the deprecated forcing argument from Daisyworld and unify luminosity handling via parameter lookup. _luminosity now returns get_param_value('L', ...) directly, and the constructor signature/super() call no longer accept a forcing parameter. Updated example usage and tests to register forcings via register_forcing('L', ...) and to omit forcing=None in initializers. This simplifies time-varying/forcings handling by relying on the model's parameter/forcing registration system.
Replace direct constructor forcing arguments (forcing=None / passing a Forcing into init) with explicit register_forcing calls across tests, and remove unnecessary forcing=None from PBModel subclass super() calls. Add a comprehensive set of new integration tests covering pre-step parameter replacement, pre-step state additive behaviour, post-step replacement/additive behaviour, state restoration after dydt calls, and adaptive-solver warnings for post-step forcings. Also adjust a Stommel test to register its forcing instead of passing it to the constructor, and simplify time-axis reframing tests to rely on default model construction.
Handle the case where the first element of temp_side is already <= threshold. Previously warm_idx would be set to -1, causing unintended wrap-around or incorrect indexing. Now the code appends 0.0 to hemi_edges and continues, preventing wrong values or index errors when the threshold is met at the first element.
@jordanplanders jordanplanders merged commit 6fc9713 into main Jun 1, 2026
2 checks passed
@jordanplanders jordanplanders deleted the register_forcing branch June 1, 2026 23:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant