Core Solver¶
core_solver
¶
Core semi-implicit solver for time-evolving models (ODE + IMEX multiphysics).
This solver advances a :class:op_engine.model_core.ModelCore instance over its
configured time grid. In the updated semantics, ModelCore.time_grid is treated
as output times: the times at which the user wants a stored solution state.
Between consecutive output times, the solver may take either:
- one or more fixed steps bounded by RunConfig.fixed_max_step
(adaptive=False), or
- multiple internal adaptive substeps that land exactly on t_{i+1} (adaptive=True).
Supported methods (keyword method=):
- "euler": Explicit Euler (order 1), adaptive via step-doubling.
- "heun": Explicit Heun / RK2 (order 2), embedded Euler estimator.
- "rk4": Classic explicit Runge--Kutta (order 4).
- "dopri5": Dormand--Prince 5(4), embedded adaptive estimator.
- "imex-euler": IMEX Euler: explicit Euler on F(t,y), implicit Euler on A.
Adaptive via step-doubling (IMEX step-doubling).
- "imex-heun-tr": IMEX Heun-Trapezoidal: Heun on F, trapezoidal/CN on A.
Adaptive via embedded low/high (Euler vs Heun) mapped by the
same implicit operator solve.
- "imex-trbdf2": IMEX TR-BDF2 (order 2), adaptive via step-doubling.
- "imex-ark3": ARS(4,4,3) additive Runge--Kutta with embedded order 2.
- "implicit-euler": One-linearization Euler approximation (order 1).
- "trapezoidal": One-linearization trapezoidal approximation (order 2).
- "bdf2": One-linearization BDF2 approximation (order 2).
- "ros2": L-stable Rosenbrock-W 2(1).
- "sdirk2": L-stable Alexander SDIRK2 with full nonlinear stages.
IMEX structure
We assume a split system: y' = A(t,y) y + F(t,y) where F is provided by rhs_func(t, y), and A is represented by linear operators applied along a single tensor axis. Operators may be: - None (ODE-only / explicit-only behavior), or - provided as tuples (predictor?, L, R), or - provided as factories depending on dt, stage-scale, and context.
Operator application
Operators act along a configured axis (default "state"). All other axes are batched. The solve form is: L @ y_next = R @ x optionally with a preprocessing predictor: x_tilde = predictor @ x
Non-uniform dt
- Explicit methods naturally support non-uniform dt.
- Implicit/IMEX methods require operator factories whenever dt varies across steps (non-uniform output grid or adaptive stepping), because L/R depend on dt.
Performance hygiene
- NumPy paths retain preallocated scratch arrays and in-place operations.
- Immutable namespaces use functional stepping operations.
- Dense implicit solves use Array-API linalg; sparse adapters cache factors.
AdaptiveAdvanceParams(plan, t0, t1, y0, adaptive_cfg, dt_ctrl)
dataclass
¶
Bundle of parameters for adaptive advancement to an output time.
Attributes:
| Name | Type | Description |
|---|---|---|
plan |
RunPlan
|
Resolved run plan. |
t0 |
float
|
Start time. |
t1 |
float
|
End/output time. |
y0 |
NDArray[floating]
|
Initial state at t0. |
adaptive_cfg |
AdaptiveConfig
|
Adaptive stepping configuration. |
dt_ctrl |
DtControllerConfig
|
dt controller configuration. |
AdaptiveConfig(rtol=1e-06, atol=1e-09, dt_init=None, max_reject=25, max_steps=1000000)
dataclass
¶
Configuration for adaptive stepping.
Attributes:
| Name | Type | Description |
|---|---|---|
rtol |
float
|
Relative tolerance. |
atol |
float | Array
|
Absolute tolerance (scalar or array-like). |
dt_init |
float | None
|
Optional initial dt guess; if None, use output dt. |
max_reject |
int
|
Maximum number of rejected attempts per accepted step. |
max_steps |
int
|
Maximum number of internal substeps per output interval. |
__post_init__()
¶
Validate static adaptive-step parameters without coercing arrays.
Raises:
| Type | Description |
|---|---|
ValueError
|
If any static adaptive parameter is invalid. |
Source code in src/op_engine/core_solver.py
340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 | |
AdaptiveStepSchedule(output_times, step_sizes)
dataclass
¶
Accepted step sizes for replaying one adaptive solve.
The schedule records controller decisions, not array values. Replaying it therefore uses the same numerical kernels and active Array-API namespace as a live solve while keeping loop lengths and step sizes static.
Attributes:
| Name | Type | Description |
|---|---|---|
output_times |
tuple[float, ...]
|
Output grid used to create the schedule. |
step_sizes |
tuple[tuple[float, ...], ...]
|
Accepted internal step sizes for each output interval. |
__post_init__()
¶
Normalize and validate the recorded mesh.
Raises:
| Type | Description |
|---|---|
ValueError
|
If times or step sizes do not define a valid mesh. |
Source code in src/op_engine/core_solver.py
397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 | |
CoreSolver(core, operators=None, *, operator_axis='state')
¶
Semi-implicit solver operating on a ModelCore time/state grid.
Initialize CoreSolver.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
core
|
ModelCore
|
ModelCore instance to solve. |
required |
operators
|
CoreOperators | StageOperatorFactory | None
|
Default operator spec (tuple or factory) for implicit stages. |
None
|
operator_axis
|
str | int
|
Axis along which operators act (name or index). |
'state'
|
Source code in src/op_engine/core_solver.py
1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 | |
last_adaptive_schedule
property
¶
Return the schedule recorded or replayed by the latest adaptive run.
last_nonlinear_diagnostics
property
¶
Return nonlinear diagnostics from the latest SDIRK run or replay.
adaptive_explicit_step(rhs_func, *, method, t, dt, y, first_stage=None)
¶
Attempt one explicit step and return its local-error estimate.
This functional boundary does not mutate :class:ModelCore history
and does not extract host scalars. Provider integrations can compose
it with backend-native adaptive controller loops.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
rhs_func
|
RHSFunction
|
Function computing the explicit RHS F(t, y). |
required |
method
|
str
|
Explicit solver method name. |
required |
t
|
Scalar
|
Step start time as a Python float or backend-native scalar. |
required |
dt
|
Scalar
|
Step size as a Python float or backend-native scalar. |
required |
y
|
Array
|
State at the step start. |
required |
first_stage
|
Array | None
|
Optional cached derivative at |
None
|
Returns:
| Type | Description |
|---|---|
ExplicitStepResult
|
Candidate state, local error estimate, controller order, and |
ExplicitStepResult
|
reusable derivative stages. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/op_engine/core_solver.py
2995 2996 2997 2998 2999 3000 3001 3002 3003 3004 3005 3006 3007 3008 3009 3010 3011 3012 3013 3014 3015 3016 3017 3018 3019 3020 3021 3022 3023 3024 3025 3026 3027 3028 3029 3030 3031 3032 3033 3034 3035 3036 3037 | |
fixed_explicit_step(rhs_func, *, method, t, dt, y, first_stage=None)
¶
Take one functional fixed step with an explicit method.
This boundary does not mutate :class:ModelCore history, so callers
can compose it with backend-native loop primitives and apply the
completed trajectory once. It is the supported public step kernel
for external drivers such as jax.lax.scan; the method and state
shape are static, while t, dt, y, and RHS parameters may
be traced. RHS results must preserve the configured state shape and
the namespace of y.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
rhs_func
|
RHSFunction
|
Function computing the explicit RHS F(t, y). |
required |
method
|
str
|
Explicit solver method name. |
required |
t
|
Scalar
|
Step start time as a Python float or backend-native scalar. |
required |
dt
|
Scalar
|
Step size as a Python float or backend-native scalar. |
required |
y
|
Array
|
State at the step start. |
required |
first_stage
|
Array | None
|
Optional derivative at |
None
|
Returns:
| Type | Description |
|---|---|
Array
|
Next state and an FSAL derivative for Dormand--Prince, or |
Array | None
|
|
tuple[Array, Array | None]
|
with one step outside the scan so its structure stays fixed. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/op_engine/core_solver.py
3039 3040 3041 3042 3043 3044 3045 3046 3047 3048 3049 3050 3051 3052 3053 3054 3055 3056 3057 3058 3059 3060 3061 3062 3063 3064 3065 3066 3067 3068 3069 3070 3071 3072 3073 3074 3075 3076 3077 3078 3079 3080 3081 3082 3083 3084 3085 3086 3087 3088 | |
replay_adaptive_schedule(rhs_func, schedule, *, config)
¶
Replay a recorded adaptive mesh through the configured method.
Replay bypasses error norms and accept/reject decisions while invoking
the same high-order step kernels used by a live adaptive solve. With a
JAX state, the static Python schedule can therefore be traced by
jax.jit and differentiated with respect to array-valued model inputs.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
rhs_func
|
RHSFunction
|
Function computing the explicit RHS F(t, y). |
required |
schedule
|
AdaptiveStepSchedule
|
Accepted step mesh recorded by an adaptive run. |
required |
config
|
RunConfig
|
Matching adaptive run configuration. |
required |
Returns:
| Type | Description |
|---|---|
NonlinearIntegrationDiagnostics | None
|
Array-valued nonlinear diagnostics for SDIRK2, otherwise |
Raises:
| Type | Description |
|---|---|
TypeError
|
If schedule has the wrong type or the state changes array ecosystems during replay. |
ValueError
|
If config is not adaptive or the output grid differs. |
Source code in src/op_engine/core_solver.py
4615 4616 4617 4618 4619 4620 4621 4622 4623 4624 4625 4626 4627 4628 4629 4630 4631 4632 4633 4634 4635 4636 4637 4638 4639 4640 4641 4642 4643 4644 4645 4646 4647 4648 4649 4650 4651 4652 4653 4654 4655 4656 4657 4658 4659 4660 4661 4662 4663 4664 4665 4666 4667 4668 4669 4670 4671 4672 4673 4674 4675 4676 4677 4678 4679 4680 4681 4682 4683 4684 4685 4686 4687 4688 4689 4690 4691 4692 4693 4694 4695 4696 4697 4698 4699 4700 4701 | |
run(rhs_func, *, config=None)
¶
Advance the ModelCore state through its time grid.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
rhs_func
|
RHSFunction
|
Function computing the explicit RHS F(t, y). |
required |
config
|
RunConfig | None
|
Optional run configuration. If None, defaults are used. |
None
|
Returns:
| Type | Description |
|---|---|
NonlinearIntegrationDiagnostics | None
|
Array-valued nonlinear diagnostics for SDIRK2, otherwise |
Raises:
| Type | Description |
|---|---|
TypeError
|
If the state changes array ecosystems during a run. |
ValueError
|
If invalid parameters are provided. |
Source code in src/op_engine/core_solver.py
4703 4704 4705 4706 4707 4708 4709 4710 4711 4712 4713 4714 4715 4716 4717 4718 4719 4720 4721 4722 4723 4724 4725 4726 4727 4728 4729 4730 4731 4732 4733 4734 4735 4736 4737 4738 4739 4740 4741 4742 4743 4744 4745 4746 4747 4748 4749 4750 4751 4752 4753 4754 4755 4756 4757 4758 4759 4760 4761 4762 4763 4764 4765 4766 4767 4768 4769 4770 4771 4772 4773 4774 4775 4776 4777 4778 4779 4780 4781 4782 4783 4784 4785 4786 4787 4788 4789 4790 4791 4792 4793 4794 4795 | |
DtControllerConfig(dt_min=0.0, dt_max=float('inf'), safety=0.9, fac_min=0.2, fac_max=5.0)
dataclass
¶
Configuration for adaptive timestep control.
Attributes:
| Name | Type | Description |
|---|---|---|
dt_min |
float
|
Minimum allowed dt. |
dt_max |
float
|
Maximum allowed dt. |
safety |
float
|
Safety factor applied to dt updates. |
fac_min |
float
|
Minimum multiplicative change factor. |
fac_max |
float
|
Maximum multiplicative change factor. |
__post_init__()
¶
Validate static timestep-controller parameters.
Raises:
| Type | Description |
|---|---|
ValueError
|
If any controller parameter is outside its valid range. |
Source code in src/op_engine/core_solver.py
293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 | |
ExplicitStepResult(state, error, controller_order, first_stage, last_stage)
dataclass
¶
Result and reusable stages from one explicit adaptive attempt.
Attributes:
| Name | Type | Description |
|---|---|---|
state |
Array
|
Accepted-order candidate state. |
error |
Array
|
Local error estimate. |
controller_order |
int
|
Order supplied to the existing step-size controller. |
first_stage |
Array
|
Derivative at the attempted step's initial state. |
last_stage |
Array | None
|
Derivative reusable by an FSAL method after acceptance. |
ImexEulerOnceParams(t, y, dt, op_spec, out)
dataclass
¶
Bundle of parameters for one IMEX Euler step (non-doubling).
Attributes:
| Name | Type | Description |
|---|---|---|
t |
float
|
Current time. |
y |
NDArray[floating]
|
Current state. |
dt |
float
|
Step size. |
op_spec |
CoreOperators | StageOperatorFactory | None
|
Operator spec for implicit stage. |
out |
NDArray[floating]
|
Output state array (written in-place). |
ImplicitStageParams(spec, dt, scale, t_stage, y_stage, stage, x, out)
dataclass
¶
Bundle of parameters for one implicit operator application.
Attributes:
| Name | Type | Description |
|---|---|---|
spec |
CoreOperators | StageOperatorFactory | None
|
Operator spec (tuple or factory) or None for identity. |
dt |
float
|
Full-step dt for operator factory context. |
scale |
float
|
Stage scaling factor for dt-dependent operators. |
t_stage |
float
|
Stage time. |
y_stage |
NDArray[floating]
|
Stage state proxy for operator factories. |
stage |
str
|
Stage label (e.g., "be", "tr", "bdf2"). |
x |
NDArray[floating]
|
Input array to map. |
out |
NDArray[floating]
|
Output array (written in-place). |
NonlinearIntegrationConvergenceError(diagnostics)
¶
Bases: RuntimeError
Raised after validating failed array-valued integration diagnostics.
Store diagnostics for the invalid integration or frozen mesh.
Source code in src/op_engine/core_solver.py
545 546 547 548 549 550 551 552 553 554 555 | |
NonlinearIntegrationDiagnostics
¶
Bases: NamedTuple
Array-valued nonlinear diagnostics for one integration or replay.
Every field is in the state array's namespace, so the record can cross a
compiled JAX boundary. stages_per_step maps flattened stage fields back
to attempted steps. Rejected adaptive attempts remain present with
step_accepted=False.
require_converged()
¶
Return valid diagnostics or invalidate a failed compiled replay.
Returns:
| Type | Description |
|---|---|
NonlinearIntegrationDiagnostics
|
This unchanged diagnostic record. |
Raises:
| Type | Description |
|---|---|
NonlinearIntegrationConvergenceError
|
If an accepted or replayed step contains a failed nonlinear stage. |
Source code in src/op_engine/core_solver.py
527 528 529 530 531 532 533 534 535 536 537 538 539 | |
NonlinearMethodConfig(rhs_jacobian, solver=DenseNewtonSolver())
dataclass
¶
Configuration for fully nonlinear integration methods.
The Jacobian acts on the entire flattened RHS state. It is deliberately
separate from :attr:RunConfig.jacobian, whose operators act only along
CoreSolver.operator_axis for linearly implicit methods.
Attributes:
| Name | Type | Description |
|---|---|---|
rhs_jacobian |
FullRhsJacobianFunction
|
Dense full-system Jacobian with shape
|
solver |
NonlinearSolver
|
Backend-neutral nonlinear solver implementation. |
__post_init__()
¶
Validate the method boundary without selecting an array backend.
Raises:
| Type | Description |
|---|---|
TypeError
|
If a callback or nonlinear solver is invalid. |
Source code in src/op_engine/core_solver.py
707 708 709 710 711 712 713 714 715 716 717 718 | |
OperatorLike
¶
Bases: Protocol
Minimal operator interface required by CoreSolver.
Implementations are expected to behave like 2D linear operators suitable for implicit_solve(L, R, rhs2d). Only shape is required for validation.
shape
property
¶
Operator shape.
OperatorSpecs(default=None, tr=None, bdf2=None)
dataclass
¶
Operator specifications for implicit/IMEX methods.
Attributes:
| Name | Type | Description |
|---|---|---|
default |
CoreOperators | StageOperatorFactory | None
|
Default operator spec (tuple or factory) used by IMEX Euler/Heun-TR and as a fallback for TR/BDF2 stages. |
tr |
CoreOperators | StageOperatorFactory | None
|
Operator spec for trapezoidal stage of TR-BDF2 (optional). |
bdf2 |
CoreOperators | StageOperatorFactory | None
|
Operator spec for BDF2 stage of TR-BDF2 (optional). |
PredictorLike
¶
Bases: Protocol
Minimal predictor interface required by CoreSolver.
The predictor is an optional preprocessing operator applied as
rhs2d = predictor @ rhs2d
__matmul__(other)
¶
Apply the predictor to a 2D array.
Source code in src/op_engine/core_solver.py
256 257 258 | |
RunConfig(method='heun', adaptive=False, strict=True, dt_controller=DtControllerConfig(), adaptive_cfg=AdaptiveConfig(), operators=OperatorSpecs(), jacobian=None, nonlinear=None, gamma=None, fixed_max_step=None, replay_loop='unroll', replay_checkpoint=False)
dataclass
¶
Configuration for CoreSolver.run.
Attributes:
| Name | Type | Description |
|---|---|---|
method |
str
|
Method name. |
adaptive |
bool
|
Whether to use adaptive substepping between output times. |
strict |
bool
|
If True, invalid configurations raise; otherwise warnings and method downshifts may occur. |
dt_controller |
DtControllerConfig
|
Parameters for dt controller when adaptive=True. |
adaptive_cfg |
AdaptiveConfig
|
Parameters controlling error tolerances and limits. |
operators |
OperatorSpecs
|
Operator specifications for implicit/IMEX methods. |
jacobian |
JacobianFunction | None
|
Optional Jacobian function for linearly implicit methods. |
nonlinear |
NonlinearMethodConfig | None
|
Full-system Jacobian and backend-neutral nonlinear solver. |
gamma |
float | None
|
Optional TR-BDF2 gamma (if None, uses default). |
fixed_max_step |
float | None
|
Maximum explicit fixed-step size between output times.
|
replay_loop |
Literal['auto', 'scan', 'unroll']
|
Iteration strategy for frozen adaptive replay. |
replay_checkpoint |
bool
|
Rematerialize the scan body during reverse mode. Used only when replay selects an adapter. |
__post_init__()
¶
Normalize the method and validate context-free configuration.
Raises:
| Type | Description |
|---|---|
TypeError
|
If a nested configuration has the wrong type. |
ValueError
|
If the method or TR-BDF2 gamma is invalid. |
Source code in src/op_engine/core_solver.py
758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 | |
RunPlan(method, gamma, op_default, op_tr, op_bdf2, jacobian, nonlinear=None)
dataclass
¶
Resolved execution plan derived from RunConfig.
This is the internal, validated form used by the stepping loops.
Attributes:
| Name | Type | Description |
|---|---|---|
method |
MethodName
|
Final method after any strict=False downshifts. |
gamma |
float | None
|
TR-BDF2 gamma, or None for non-TR-BDF2 methods. |
op_default |
CoreOperators | StageOperatorFactory | None
|
Operator spec for IMEX Euler/Heun-TR. |
op_tr |
CoreOperators | StageOperatorFactory | None
|
TR-stage operator spec for TR-BDF2. |
op_bdf2 |
CoreOperators | StageOperatorFactory | None
|
BDF2-stage operator spec for TR-BDF2. |
jacobian |
JacobianFunction | None
|
Optional Jacobian function for linearly implicit methods. |
nonlinear |
NonlinearMethodConfig | None
|
Configuration for fully nonlinear methods. |
StepIO(t, dt, y, out, err_out=None, history=())
dataclass
¶
Bundle of per-step state for stepping kernels.
Attributes:
| Name | Type | Description |
|---|---|---|
t |
float
|
Current time. |
dt |
float
|
Step size. |
y |
NDArray[floating]
|
Current state array (input). |
out |
NDArray[floating]
|
Output state array (written in-place). |
err_out |
NDArray[floating] | None
|
Error estimate array (written in-place) for adaptive methods. |
history |
tuple[NDArray[floating], ...]
|
Older accepted states for multistep methods, newest first. |
Trbdf2OnceParams(t, y, dt, operators_tr, operators_bdf2, gamma, out)
dataclass
¶
Bundle of parameters for one TR-BDF2 step (non-doubling).
Attributes:
| Name | Type | Description |
|---|---|---|
t |
float
|
Current time. |
y |
NDArray[floating]
|
Current state. |
dt |
float
|
Step size. |
operators_tr |
CoreOperators | StageOperatorFactory | None
|
TR stage operator spec. |
operators_bdf2 |
CoreOperators | StageOperatorFactory | None
|
BDF2 stage operator spec. |
gamma |
float
|
TR-BDF2 gamma. |
out |
NDArray[floating]
|
Output state array (written in-place). |
fixed_step_sizes(t0, t1, max_step)
¶
Partition one output interval into deterministic fixed steps.
The first steps use max_step and the final step absorbs the remainder,
so the partition lands on t1 without changing the stored output grid.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
t0
|
float
|
Output interval start. |
required |
t1
|
float
|
Output interval end. |
required |
max_step
|
float | None
|
Maximum internal step, or |
required |
Returns:
| Type | Description |
|---|---|
tuple[float, ...]
|
Positive internal step sizes that sum to |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the interval or maximum step is invalid, or if the partition would be unreasonably large. |
Source code in src/op_engine/core_solver.py
446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 | |
propose_step_size(step_size, error_norm, order, *, config)
¶
Return the next adaptive step size without host scalar extraction.
Returns:
| Type | Description |
|---|---|
Array
|
Scalar array in |
Array
|
controller bounds. |
Source code in src/op_engine/core_solver.py
923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 | |
scaled_error_norm(error, reference, previous, *, rtol, atol, reduction='rms')
¶
Return a backend-native scaled local-error norm.
Unlike the host controller wrapper used by :class:CoreSolver, this
function never extracts a Python scalar. Provider integrations can
therefore compose it with backend loop primitives such as
jax.lax.while_loop while retaining the core solver's error semantics.
Returns:
| Type | Description |
|---|---|
Array
|
Scalar array in |
Array
|
positive infinity so compiled controllers reject the attempted step. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/op_engine/core_solver.py
878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 | |