Core Concepts

This page explains the three building blocks that every ClimateCritters workflow uses: CCModel, Forcing, and CCOutput.

CCModel

CCModel is the abstract base class for all signal models. You never instantiate it directly — instead you use one of the concrete subclasses (e.g. Lorenz63, EBM, Stommel).

Every subclass shares the same integration interface:

output = model.integrate(
    t_span=(t0, tf),   # integration window
    y0=[...],          # initial conditions (one value per state variable)
    method='RK45',     # solver: 'RK45', 'euler', 'rk4', 'euler_maruyama'
    dt=None,           # required for 'euler', 'rk4', 'euler_maruyama'
)

Internally, each model implements dydt(t, state) — the right-hand side of the ODE — and optionally populate_diagnostics_from_history() for quantities computed after the full trajectory is available.

Parameter handling

Model parameters (e.g. sigma, C, D) can be:

Type Example Resolved as
Constant sigma=10.0 Fixed value at every timestep
Callable (t) sigma=lambda t: 10.0 + 0.1*t Called with current time
Callable (t, state) sigma=lambda t, x: ... Called with time and state vector
Callable (t, state, model) sigma=lambda t, x, m: ... Also receives the model instance
cc.Forcing sigma=cc.Forcing(...) Evaluated via get_forcing(t)

The first argument of any callable must be named t or time. See contracts/signal_model_contract.md for the full specification.


Forcing

Forcing wraps any time-dependent input signal and provides a uniform get_forcing(t) interface to the model. It accepts a callable, a data array, a CSV file, or a compiled ForcingSequence:

# Callable
f = cc.Forcing(lambda t: 1360.0 + 5.0 * np.sin(2 * np.pi * t / 11.0))

# Data array
f = cc.Forcing(data=my_array, time=my_time, interpolation='cubic')

# Bundled CSV dataset
f = cc.Forcing.from_csv(dataset='vieira_tsi')

Composable scenarios

For signals that change character over time, build a ForcingSequence from Hold, Ramp, and Harmonic segments using +, then call .compile():

scenario = (
    cc.forcing.Hold(duration=100, value=1360.0)
    + cc.forcing.Ramp(duration=50, y0=1360.0, yf=1380.0, shape='linear')
    + cc.forcing.Hold(duration=100, value=1380.0)
)
f = scenario.compile()   # → Forcing, ready to register

Two signals can be superposed with +:

combined = cc.Forcing(orbital_func) + cc.Forcing(noise_func)

See the Forcing page for the full API.


CCOutput

integrate() always returns a CCOutput object. It holds:

Attribute Contents
output.time Solver time axis (1-D array)
output.state_variables Structured NumPy array, one field per state variable
output.diagnostic_variables Dict of derived quantities (e.g. 'energy', 'ice_line_lat')
output.solution Raw solver solution (scipy or custom)

Accessing variables

x = output.state_variables['x']            # state variable by name
energy = output.diagnostic_variables['energy']  # diagnostic by name

Reframing the time axis

To slice off a spin-up period or resample to a regular grid:

t_out = np.linspace(100, 500, 401)
output.reframe_time_axis(t_out)
# output.time and output.state_variables are now on t_out

Exporting to Pyleoclim

ts = output.to_pyleo(var_names='x')          # returns a pyleo.Series
mts = output.to_pyleo(var_names=['x', 'y'])  # returns a pyleo.MultipleSeries

Once in Pyleoclim, the full analysis toolkit is available: ts.spectral(), ts.wavelet(), ts.dashboard(), ts.interp(), etc.


Putting it together

import numpy as np
import climatecritters as cc
from climatecritters.model_critters import EBM

# composable forcing: spin-up then a solar ramp
forcing = cc.Forcing.from_sequence([
    cc.forcing.Hold(duration=500, value=1360.0),
    cc.forcing.Ramp(duration=200, y0=1360.0, yf=1390.0, shape='linear'),
])

# time-varying albedo
model = EBM(forcing=forcing, albedo=lambda t, x: 0.3 + 0.01 * np.sin(t))

output = model.integrate(t_span=(0, 700), y0=[288.0], method='RK45')

# discard spin-up, export to pyleo
t_analysis = np.linspace(500, 700, 201)
output.reframe_time_axis(t_analysis)
output.to_pyleo(var_names='T').plot()