# CyclicCRS


Cyclic Cubic Regression Spline (periodic natural cubic spline).


Usage

``` python
CyclicCRS(k=10)
```


A cyclic (periodic) cubic regression spline is the natural extension of [CRS](CRS.md#whittaker.CRS) to covariates that wrap around, such as time of day, day of year, or wind direction, where the value and slope at the end of the range must match the value and slope at the start. Equivalent to mgcv's `bs="cc"` basis. The spline is periodic over the range of the training data: `f(x_min) = f(x_max)`, `f'(x_min) = f'(x_max)`, and `f''(x_min) = f''(x_max)`. Values outside the training range are mapped into the periodic domain via modular arithmetic, so predictions remain well-defined for any `x`. Choose [CyclicCRS](CyclicCRS.md#whittaker.CyclicCRS) (rather than plain [CRS](CRS.md#whittaker.CRS)) whenever the covariate is inherently circular; using a non-cyclic basis on such data would otherwise produce an artificial discontinuity at the wrap-around point.

The cyclic constraint absorbs one degree of freedom, so k knots produce k-1 basis functions. The penalty null space is 1-dimensional (constant functions only as linear functions are no longer unpenalized under periodicity).


## Parameters


`k: int = ``10`  
Number of knots placed around the periodic domain. Must be at least `4`, which yields at least `3` basis functions (`n_basis = k - 1`) after the cyclic constraint removes one degree of freedom. As with [CRS](CRS.md#whittaker.CRS), larger `k` gives more local flexibility, with overall smoothness controlled by the penalty and `lambda` rather than by `k` alone. The default is `10`.


## Notes

Knots are placed at evenly-spaced quantiles of the training data (falling back to an evenly-spaced grid when quantile ties would produce duplicates), exactly as in [CRS](CRS.md#whittaker.CRS), but the second-derivative relationship between the coefficient vector and the knot second derivatives is built on a *circular* tridiagonal system: `_build_cyclic_Q()` and `_build_cyclic_R()` construct the periodic analogues of [CRS](CRS.md#whittaker.CRS)'s `Q` and `R` matrices, wrapping the sub/super-diagonal entries around modulo `m = k - 1` (the number of free coefficients once `f(x_min) = f(x_max)` is imposed). Concretely, the periodic constraint identifies knot `t_{k-1}` with `t_0`, so `\boldsymbol{\beta}` has only `m = k - 1` free entries, and second derivatives satisfy `\mathbf{R}_c \mathbf{d} = \mathbf{Q}_c^\top \boldsymbol{\beta}` with `Q_c`, `R_c` both `m \times m` and *circulant-tridiagonal* (each row's neighbors wrap around index `m`). Unlike [CRS](CRS.md#whittaker.CRS), there are no natural boundary conditions -- periodicity itself replaces them -- so the penalty null space drops from dimension 2 (constant and linear) to dimension 1 (constant only): a periodic function cannot be a nonzero linear function of `x`, since a nonzero slope is incompatible with `f(x_min) = f(x_max)`.

The penalty is the same quadratic form as in [CRS](CRS.md#whittaker.CRS), computed from the circular matrices,

 \mathbf{S} = \mathbf{Q}\_c \mathbf{R}\_c^{-1} \mathbf{Q}\_c^\top, 

an `m \times m` (`m = k - 1`) positive semi-definite matrix of rank `m - 1`, whose one-dimensional null space is the constant sequence. At evaluation time, `basis_matrix()` maps any `x` into the periodic domain `[x_min, x_max)` via `knots[0] + (x - knots[0]) % period` before evaluating the same piecewise-cubic construction as [CRS](CRS.md#whittaker.CRS), so extrapolation is not linear (as in [CRS](CRS.md#whittaker.CRS)) but exactly periodic.


## Examples


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

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


    ((100, 9), (9, 9))


## Attributes

| Name | Description |
|----|----|
| [knots](#knots) | Knot locations used by the fitted basis. |
| [n_basis](#n_basis) | Total number of basis functions. |

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


### knots


Knot locations used by the fitted basis.


`knots: NDArray`


The `k` locations, placed at evenly-spaced quantiles of the training data (or an evenly-spaced grid as a fallback), spanning one full period `[x_min, x_max]`. The last knot is identified with the first under the periodicity constraint, leaving `k - 1` free coefficients.


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


### n_basis


Total number of basis functions.


`n_basis: int`


Equal to `k - 1`: the periodicity constraint `f(x_min) = f(x_max)` identifies the last knot with the first, so only `k - 1` of the `k` knot values are free coefficients.


## Methods

| Name | Description |
|----|----|
| [basis_matrix()](#basis_matrix) | Evaluate the cyclic CRS basis at `x`. |
| [fit()](#fit) | Fit the cyclic CRS 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 - 1) x (k - 1)` cyclic penalty matrix `S = Q_c R_c^{-1} Q_c'`. |

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


### basis_matrix()


Evaluate the cyclic CRS 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, so the returned basis values are exactly periodic with period `x_max - x_min`.


#### Parameters


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


#### Returns


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


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


### fit()


Fit the cyclic CRS to training data `x`.


Usage

``` python
fit(x)
```


Places `k` knots at evenly-spaced quantiles of `x` (spanning one full period), then pre-computes the circular second-derivative operator and penalty matrix.


#### Parameters


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


#### Returns


`CyclicCRS`  
Returns `self` for method chaining.


#### Raises


`ValueError`  
If `n < k`, 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 fitted basis at the free knots (excluding the duplicated wrap-around knot) and averages each column, giving a `(1, k - 1)` row `C` such that `C @ beta == 0` constrains the smooth to have mean zero over the training data.


#### Returns


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


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


### null_space_dimension()


Return the dimension of the penalty null space.


Usage

``` python
null_space_dimension()
```


Always `1` for a cyclic CRS: only constant functions have zero penalty, since a nonzero linear term would violate the periodicity constraint `f(x_min) = f(x_max)`.


#### Returns


`int`  
Always `1`.


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


### penalty_matrix()


Return the `(k - 1) x (k - 1)` cyclic penalty matrix `S = Q_c R_c^{-1} Q_c'`.


Usage

``` python
penalty_matrix()
```


`S` is positive semi-definite with rank `k - 2`. Its one-dimensional null space is spanned by the constant function (linear functions are no longer unpenalized under periodicity).


#### Returns


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