# CyclicPSpline


Cyclic P-Spline (periodic B-spline basis with circular difference penalty).


Usage

``` python
CyclicPSpline(
    k=10,
    degree=3,
    m=2,
)
```


A cyclic P-spline is the periodic counterpart of [PSpline](PSpline.md#whittaker.PSpline): a B-spline basis whose knots and coefficients wrap around a circle, combined with a *circular* finite-difference penalty that also wraps around, so smoothness is enforced across the boundary rather than just within it. Equivalent to mgcv's `bs="cp"` basis. The B-spline basis is constructed to be periodic over the training data range, so `f(x_min) = f(x_max)` and, because the penalty ties coefficients across the wrap point, the fit avoids any artificial kink at the seam. Values outside the training range are mapped into the periodic domain via modular arithmetic. Prefer [CyclicPSpline](CyclicPSpline.md#whittaker.CyclicPSpline) over [CyclicCRS](CyclicCRS.md#whittaker.CyclicCRS) for the same reasons [PSpline](PSpline.md#whittaker.PSpline) is often preferred over [CRS](CRS.md#whittaker.CRS): cheaper construction, equally-spaced (rather than quantile) knots, and a sparse banded penalty that scales well to larger `k`; prefer [CyclicCRS](CyclicCRS.md#whittaker.CyclicCRS) when knot-based interpretability or an integrated-derivative penalty is wanted.

The circular difference penalty penalizes the m-th differences of adjacent coefficients with wrap-around at the boundaries. The penalty null space is 1-dimensional (constant functions only).


## Parameters


`k: int = ``10`  
Number of periodic B-spline basis functions. Must satisfy `k >= degree + 1`. Larger `k` gives more local flexibility around the cycle; overall smoothness is governed by the penalty and `lambda`. The default is `10`.

`degree: int = ``3`  
Polynomial degree of each B-spline piece. `degree=3` (cubic) is conventional; lower degrees give coarser, more locally-supported bases. The default is `3` (cubic).

`m: int = ``2`  
Order of the circular difference penalty applied to adjacent coefficients, with wrap-around so that the coefficients at the end of the cycle are treated as adjacent to those at the start. `m=2` (the default) penalizes circular second differences, the periodic analogue of curvature. The default is `2`.


## Notes

`_periodic_bspline_knots()` builds a periodic knot vector by taking `k` equally-spaced knots spanning one period `[x_min, x_max)` and extending them periodically by `degree` knots on each side, so that the B-spline basis functions near the boundary have support that wraps smoothly around the cycle. `basis_matrix()` evaluates the (non-periodic) B-spline design on this extended knot vector and then folds the trailing `degree` "wrapped" columns back onto the leading `degree` columns (`B[:, :degree] += B_full[:, k:]`), so the returned matrix has exactly `k` periodic basis functions.

The penalty replaces the ordinary finite-difference matrix used by [PSpline](PSpline.md#whittaker.PSpline) with a *circular* difference matrix `\mathbf{D}_m` (built by `_cyclic_diff_matrix()` via repeated left-multiplication by the circular first-difference operator `D_1[i, i] = -1`, `D_1[i, (i+1) \bmod k] = 1`), giving a square `k \times k` penalty

 \mathbf{S} = \mathbf{D}\_m^\top \mathbf{D}\_m, 

positive semi-definite with rank `k - 1`. Its one-dimensional null space is the constant coefficient sequence: because the difference operator wraps around, no nonconstant polynomial sequence in the coefficient index can have zero circular difference (unlike the non-cyclic [PSpline](PSpline.md#whittaker.PSpline), where degree-`< m` polynomial sequences are all unpenalized). As with [PSpline](PSpline.md#whittaker.PSpline), the penalty is banded/sparse (with corner entries from the wrap-around) and cheap to factorize even for large `k`.


## Examples


``` python
import numpy as np
from whittaker.smooths import CyclicPSpline

x = np.linspace(0, 2 * np.pi, 100)
basis = CyclicPSpline(k=10).fit(x)
B = basis.basis_matrix(x)
S = basis.penalty_matrix()
B.shape, S.shape
```


    ((100, 10), (10, 10))


## Attributes

| Name | Description |
|----|----|
| [n_basis](#n_basis) | Total number of periodic basis functions. |

------------------------------------------------------------------------


### n_basis


Total number of periodic basis functions.


`n_basis: int`


Equal to `k`: unlike [CyclicCRS](CyclicCRS.md#whittaker.CyclicCRS), the periodic B-spline construction folds the extra "wrapped" columns back into the leading `degree` columns during basis construction, so no coefficients are dropped and the basis dimension is exactly `k`.


## Methods

| Name | Description |
|----|----|
| [basis_matrix()](#basis_matrix) | Evaluate the periodic B-spline basis at `x`. |
| [fit()](#fit) | Fit the cyclic P-spline to training data `x`. |
| [identifiability_constraints()](#identifiability_constraints) | Return the sum-to-zero constraint row for the intercept. |
| [null_space_dimension()](#null_space_dimension) | Return the dimension of the penalty null space. |
| [penalty_matrix()](#penalty_matrix) | Return the `k x k` circular penalty matrix `S = D_m' D_m`. |

------------------------------------------------------------------------


### basis_matrix()


Evaluate the periodic B-spline basis at `x`.


Usage

``` python
basis_matrix(x)
```


Any `x` outside the training range is first mapped into the periodic domain `[x_min, x_max)` via modular arithmetic. The trailing `degree` columns of the raw B-spline design (from the periodic knot extension) are folded back onto the leading `degree` columns so the result has exactly `k` periodic basis functions.


#### Parameters


`x: NDArray`  
1-D evaluation points, shape `(n,)` or `(n, 1)`.


#### Returns


`NDArray`  
Design matrix of shape `(n, k)`.


------------------------------------------------------------------------


### fit()


Fit the cyclic P-spline to training data `x`.


Usage

``` python
fit(x)
```


Determines the period from the data range, builds the periodic knot vector, and pre-computes the circular difference penalty matrix.


#### Parameters


`x: NDArray`  
1-D training covariate, shape `(n,)` or `(n, 1)`. The period is inferred as `x.max() - x.min()`.


#### Returns


`CyclicPSpline`  
Returns `self` for method chaining.


#### Raises


`ValueError`  
If `n < k`, if all values of `x` are identical, or if `x` is not 1-D / `(n, 1)`.


------------------------------------------------------------------------


### identifiability_constraints()


Return the sum-to-zero constraint row for the intercept.


Usage

``` python
identifiability_constraints()
```


Evaluates the basis on a fine equally-spaced grid spanning one period and averages each column, giving a `(1, k)` row `C` such that `C @ beta == 0` constrains the smooth to have mean zero over the training range.


#### Returns


`NDArray`  
Shape `(1, k)` constraint matrix.


------------------------------------------------------------------------


### null_space_dimension()


Return the dimension of the penalty null space.


Usage

``` python
null_space_dimension()
```


Always `1` for a cyclic P-spline: only the constant coefficient sequence has zero circular difference.


#### Returns


`int`  
Always `1`.


------------------------------------------------------------------------


### penalty_matrix()


Return the `k x k` circular penalty matrix `S = D_m' D_m`.


Usage

``` python
penalty_matrix()
```


`S` is positive semi-definite with rank `k - 1`. Its one-dimensional null space is spanned by the constant coefficient sequence; unlike the non-cyclic [PSpline](PSpline.md#whittaker.PSpline), no nonconstant polynomial sequence in the coefficient index is unpenalized because the difference operator wraps around the boundary.


#### Returns


`NDArray`  
Shape `(k, k)`.
