Skip to content

function_solve_VIE_2

Solve the scalar Volterra integral equation of the second kind

\[y(t) = g(t) + \int_0^t K(t-s)\,y(s)\,ds\]

with callable kernel and right-hand side, on an arbitrary mesh.

Parameters:

Name Type Description Default
kernel callable

kernel(u) returns \(K(u)\) for scalar \(u > 0\): a scalar for scalar equations, or a square \((d, d)\) matrix for \(d\)-dimensional vector and matrix-valued equations.

required
g callable

g(t) returns the forcing term \(g(t)\). Defaults to zero. Return a scalar for scalar equations, a \((d,)\) array for vector equations, or a \((d, m)\) array to solve \(m\) right-hand sides simultaneously (the matrix-valued case). A 2-D return signals the matrix case; matrix-valued problems require a \((d, d)\) matrix kernel.

None
mesh_breakpoints array_like

Strictly increasing 1-D array starting at 0. Defines the integration intervals directly.

required
coll_divs int, list of int

Collocation node positions: nodes lie at coll_choices[k] / coll_divs in each interval. Unlike the array- based solvers, coll_divs does not sub-divide intervals. Defaults to coll_divs=2, coll_choices=[0, 1, 2] when neither these nor coll_nodes are given. Mutually exclusive with coll_nodes.

None
coll_choices int, list of int

Collocation node positions: nodes lie at coll_choices[k] / coll_divs in each interval. Unlike the array- based solvers, coll_divs does not sub-divide intervals. Defaults to coll_divs=2, coll_choices=[0, 1, 2] when neither these nor coll_nodes are given. Mutually exclusive with coll_nodes.

None
coll_nodes array_like

Collocation node positions given directly as floats in \([0, 1]\), for node sets that are not rational multiples of \(1/c\) (e.g. Gauss-Legendre or Radau IIA points; see gauss_legendre_nodes, radau_iia_nodes, lobatto_nodes). Mutually exclusive with coll_divs/coll_choices. Convergence of the chosen node set is the caller's responsibility.

None
kernel_singularity None, float, list of float, or callable

Declare the singularity structure of the kernel.

  • None: kernel is smooth everywhere.
  • float or list of float: convolution-style singularity locations (in \(u = t - s\)); e.g. 0.0 for \(K(u) \sim u^{-\alpha}\).
  • callable f(t) -> list[float]: returns the singular \(s\)-locations for collocation point \(t\). Forward-compatible with non-convolution \(K(s, t)\).
None
return_function bool

If True, also return a callable solution wrapper.

False
show_warnings bool

Currently unused; reserved for the graded-mesh / mesh-uniformity hint.

True

Returns:

Name Type Description
soln_values ndarray

Solution values at collocation nodes, where M is the number of intervals and p = len(coll_choices). Shape (M, p) for scalar equations, (M, p, d) for vector equations, and (M, p, d, m) for matrix-valued equations.

(soln_values, y_callable) : tuple

When return_function=True. y_callable(t) evaluates the piecewise polynomial at any time t, returning a scalar / (d,) / (d, m) value for scalar t (with a leading time axis for array t).

Notes

See the module docstring for the mesh-convention difference from the array-based solvers.

Matrix-valued problems share the kernel weight tensor across all \(m\) right-hand sides (it is built once), so they are substantially cheaper than \(m\) separate calls.

Source code in src/voles/_callable_solvers.py
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
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
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
@_escalate_complex_warning
def function_solve_VIE_2(*, kernel, g=None, mesh_breakpoints,
                          coll_divs: int = None, coll_choices: list[int] = None,
                          coll_nodes=None,
                          kernel_singularity=None,
                          return_function: bool = False,
                          show_warnings: bool = True,
                          _smooth_gl_order: int = 6):
    r"""Solve the scalar Volterra integral equation of the second kind

    $$y(t) = g(t) + \int_0^t K(t-s)\,y(s)\,ds$$

    with callable kernel and right-hand side, on an arbitrary mesh.

    Parameters
    ----------
    kernel : callable
        ``kernel(u)`` returns $K(u)$ for scalar $u > 0$: a scalar for scalar
        equations, or a square $(d, d)$ matrix for $d$-dimensional vector and
        matrix-valued equations.
    g : callable, optional
        ``g(t)`` returns the forcing term $g(t)$. Defaults to zero. Return a
        scalar for scalar equations, a $(d,)$ array for vector equations, or a
        $(d, m)$ array to solve $m$ right-hand sides simultaneously (the
        matrix-valued case). A 2-D return signals the matrix case; matrix-valued
        problems require a $(d, d)$ matrix kernel.
    mesh_breakpoints : array_like
        Strictly increasing 1-D array starting at 0. Defines the integration
        intervals directly.
    coll_divs, coll_choices : int, list of int, optional
        Collocation node positions: nodes lie at
        ``coll_choices[k] / coll_divs`` in each interval. Unlike the array-
        based solvers, ``coll_divs`` does *not* sub-divide intervals. Defaults
        to ``coll_divs=2, coll_choices=[0, 1, 2]`` when neither these nor
        ``coll_nodes`` are given. Mutually exclusive with ``coll_nodes``.
    coll_nodes : array_like, optional
        Collocation node positions given directly as floats in $[0, 1]$, for
        node sets that are not rational multiples of $1/c$ (e.g. Gauss-Legendre
        or Radau IIA points; see ``gauss_legendre_nodes``, ``radau_iia_nodes``,
        ``lobatto_nodes``). Mutually exclusive with
        ``coll_divs``/``coll_choices``. Convergence of the chosen node set is
        the caller's responsibility.
    kernel_singularity : None, float, list of float, or callable
        Declare the singularity structure of the kernel.

        - ``None``: kernel is smooth everywhere.
        - ``float`` or list of float: convolution-style singularity locations
          (in $u = t - s$); e.g. ``0.0`` for $K(u) \sim u^{-\alpha}$.
        - callable ``f(t) -> list[float]``: returns the singular $s$-locations
          for collocation point $t$. Forward-compatible with non-convolution
          $K(s, t)$.
    return_function : bool, optional
        If True, also return a callable solution wrapper.
    show_warnings : bool, optional
        Currently unused; reserved for the graded-mesh / mesh-uniformity hint.

    Returns
    -------
    soln_values : ndarray
        Solution values at collocation nodes, where M is the number of
        intervals and p = ``len(coll_choices)``. Shape ``(M, p)`` for scalar
        equations, ``(M, p, d)`` for vector equations, and ``(M, p, d, m)`` for
        matrix-valued equations.
    (soln_values, y_callable) : tuple
        When ``return_function=True``. ``y_callable(t)`` evaluates the
        piecewise polynomial at any time t, returning a scalar / ``(d,)`` /
        ``(d, m)`` value for scalar t (with a leading time axis for array t).

    Notes
    -----
    See the module docstring for the mesh-convention difference from the
    array-based solvers.

    Matrix-valued problems share the kernel weight tensor across all $m$
    right-hand sides (it is built once), so they are substantially cheaper than
    $m$ separate calls.
    """
    mesh_breakpoints = np.asarray(mesh_breakpoints, dtype=float)
    if mesh_breakpoints.ndim != 1 or len(mesh_breakpoints) < 2:
        raise ValueError("mesh_breakpoints must be 1-D with at least two entries")
    if not np.all(np.diff(mesh_breakpoints) > 0):
        raise ValueError("mesh_breakpoints must be strictly increasing")
    if mesh_breakpoints[0] != 0.0:
        raise ValueError("mesh_breakpoints[0] must be 0")

    node_pos, _, _ = _resolve_node_pos(
        coll_nodes, coll_divs, coll_choices,
        default_divs=2, default_choices=[0, 1, 2],
        exclude_zero=False, fname="function_solve_VIE_2")

    if not callable(kernel):
        raise TypeError("kernel must be callable")
    if g is not None and not callable(g):
        raise TypeError("g must be callable or None")

    # Complex dispatch: block-decompose to a real problem of doubled dimension.
    if _samples_indicate_complex([kernel, g], mesh_breakpoints, None):
        sample_u = float(np.diff(mesh_breakpoints)[0]) * 0.5
        d_orig = _detect_complex_d_orig(kernel, sample_u)
        result = function_solve_VIE_2(
            kernel=_block_wrap_kernel(kernel, d_orig),
            g=_block_wrap_g(g, d_orig),
            mesh_breakpoints=mesh_breakpoints,
            coll_nodes=node_pos,
            kernel_singularity=kernel_singularity,
            return_function=return_function, show_warnings=show_warnings,
            _smooth_gl_order=_smooth_gl_order)
        if return_function:
            y_real, y_func_real = result
            return (_recombine_complex_y(y_real, d_orig),
                    _ComplexSolutionFunction(y_func_real, d_orig))
        return _recombine_complex_y(result, d_orig)

    M = len(mesh_breakpoints) - 1
    p = len(node_pos)
    widths = np.diff(mesh_breakpoints)

    # Detect scalar vs vector kernel by sampling at a non-singular point.
    sample_u = float(widths[0]) * 0.5
    is_vector, d = _detect_kernel_shape(kernel, sample_u)

    _maybe_warn_mesh_uniform_with_singularity(
        mesh_breakpoints, kernel_singularity, show_warnings)

    from . import _dlang as _dlang_module
    max_p = _dlang_module.function_solve_max_p_d()
    if p > max_p:
        raise ValueError(
            f"number of collocation nodes p = {p} exceeds the maximum compiled "
            f"into the D extension ({max_p}). Use fewer collocation nodes.")

    if not is_vector:
        W = _build_W_scalar(kernel, mesh_breakpoints, node_pos,
                            kernel_singularity, _smooth_gl_order)
        g_arr = np.zeros((M, p), dtype=np.float64)
        if g is not None:
            for n in range(M):
                t_n = mesh_breakpoints[n]
                h_n = widths[n]
                for i in range(p):
                    g_arr[n, i] = float(g(t_n + node_pos[i] * h_n))
        y = _dlang_module.function_solve_vie2_d(W, g_arr)

        if return_function:
            polys = _build_polynomials(y, mesh_breakpoints, node_pos)
            y_func = _SolutionFunction(polys, mesh_breakpoints, d=0)
            return y, y_func
        return y

    # ----- Vector / matrix path -----
    max_d = _dlang_module.function_solve_max_d_d()
    if d > max_d:
        raise ValueError(
            f"kernel dimension d = {d} exceeds the maximum compiled into the "
            f"D extension ({max_d}).")

    # The weight tensor depends only on the kernel, so it is built once and
    # shared across all right-hand sides in the matrix case.
    W = _build_W_vector(kernel, mesh_breakpoints, node_pos,
                        kernel_singularity, _smooth_gl_order, d)

    sample_t = float(mesh_breakpoints[0] + node_pos[0] * widths[0])
    m = _detect_g_matrix_cols(g, d, sample_t)

    if m is not None:
        # Matrix-valued problem: m simultaneous right-hand sides sharing W.
        G = _sample_g_at_coll_matrix(g, mesh_breakpoints, node_pos, widths,
                                     M, p, d, m)

        def _col_solve(j):
            g_arr_j = np.ascontiguousarray(G[:, :, :, j])
            return _dlang_module.function_solve_vie2_vec_d(W, g_arr_j)

        with ThreadPoolExecutor(max_workers=m) as ex:
            cols = list(ex.map(_col_solve, range(m)))
        y = np.stack(cols, axis=3)  # (M, p, d, m)

        if return_function:
            polys = _build_polynomials_matrix(y, mesh_breakpoints, node_pos,
                                              d, m)
            return y, _SolutionFunction(polys, mesh_breakpoints, d=d, m=m)
        return y

    g_arr = _sample_g_at_coll_vec(g, mesh_breakpoints, node_pos, widths, M, p, d)
    y = _dlang_module.function_solve_vie2_vec_d(W, g_arr)

    if return_function:
        polys = _build_polynomials_vector(y, mesh_breakpoints, node_pos, d)
        y_func = _SolutionFunction(polys, mesh_breakpoints, d=d)
        return y, y_func
    return y