---
title: "Common Numerical Operations in R with numops"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Common Numerical Operations in R with numops}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>"
)
```

numops provides dependency-free helpers for common numerical operations on
vectors, matrices, and arrays. This vignette shows how to clamp, wrap,
interpolate, remap, divide, and normalize numeric data with consistent input
validation and shape preservation.

```{r}
library(numops)
```

## Why use numops?

Many numerical tasks in R require short but easily repeated expressions. For
example, clamping a value to an interval requires nested `pmin()` and `pmax()`
calls, while row normalization requires combining `rowSums()` and `sweep()`.
numops gives these operations concise names and consistent behavior.

The package has no runtime dependencies. Functions that combine inputs use
strict scalar recycling, and operations on matrices and arrays preserve their
dimensions and dimnames.

## Quick reference

| Problem | Function |
|---|---|
| Restrict values to an interval | `clamp()` |
| Restrict probabilities to `[0, 1]` | `clamp01()` |
| Test whether values are within bounds | `in_range()` |
| Wrap periodic values | `wrap()` |
| Interpolate between endpoints | `lerp()` |
| Find a relative position between endpoints | `inv_lerp()` |
| Map values between intervals | `remap()` |
| Calculate a midpoint | `midpoint()` |
| Divide with a fallback for zero | `divide_or()` |
| Calculate Euclidean length | `l2_norm()` |
| Normalize vectors or array slices | `normalize_l2()` |
| Calculate length-preserving differences | `adjacent_difference()` |

## Clamp and wrap values in R

### Clamp values to an interval

`clamp()` restricts each value to the closed interval `[lower, upper]`. Its
element-wise formula is

```text
min(max(x, lower), upper)
```

```{r}
x <- c(-3, -1, 0.5, 4, 8)

clamp(x, lower = -1, upper = 4)
```

Use `clamp01()` when the interval is `[0, 1]`, as is common for probabilities
and proportions.

```{r}
clamp01(c(-0.2, 0.35, 1.4, NA))
```

Missing values in `x` remain missing. Bounds may be infinite, but the lower
bound cannot exceed the upper bound.

### Test whether values are within a range

`in_range()` performs the inclusive test
`x >= lower & x <= upper`.

```{r}
in_range(1:7, lower = 3, upper = 5)
```

### Wrap angles and periodic values

`wrap()` maps values to the half-open interval `[lower, upper)`. A value at the
upper boundary wraps to the lower boundary.

```{r}
wrap(c(-10, 0, 360, 370), lower = 0, upper = 360)
```

This is useful for angles, clock times, phases, and other periodic quantities.

## Linear interpolation and range remapping in R

### Interpolate between endpoints

`lerp()` implements linear interpolation:

```text
a + t * (b - a)
```

At `t = 0`, the result is `a`; at `t = 1`, it is `b`. Values of `t` outside
`[0, 1]` extrapolate beyond the endpoints.

```{r}
lerp(a = 10, b = 20, t = c(0, 0.25, 0.5, 1, 1.5))
```

`inv_lerp()` performs the inverse calculation. It returns the position of `x`
relative to `a` and `b`:

```text
(x - a) / (b - a)
```

```{r}
inv_lerp(a = 10, b = 20, x = c(5, 10, 15, 20, 25))
```

Results below zero or above one indicate that `x` lies outside the endpoints.

### Remap one interval to another

`remap()` combines inverse interpolation and interpolation. It maps `x` from
the interval `from` to the interval `to`:

```text
to[1] + (x - from[1]) / (from[2] - from[1]) * (to[2] - to[1])
```

```{r}
remap(
  c(0, 25, 50, 75, 100),
  from = c(0, 100),
  to = c(-1, 1)
)
```

Values outside `from` are extrapolated. Call `clamp()` separately when the
output must remain inside the destination interval.

### Calculate midpoints safely

`midpoint()` calculates the value halfway between corresponding endpoints. It
uses equivalent formulas chosen to avoid unnecessary overflow for large finite
values.

```{r}
midpoint(c(0, 10), c(10, 30))
midpoint(-.Machine$double.xmax, .Machine$double.xmax)
```

## Handle division by zero in R

`divide_or()` evaluates `x / y` except where `y` is zero. At those positions,
it returns `default`.

```{r}
divide_or(
  x = c(12, 8, 5),
  y = c(3, 0, 2),
  default = NA_real_
)
```

Only a zero denominator triggers the fallback. A missing denominator produces
a missing result according to ordinary R division.

## Normalize vectors, matrices, and arrays

### Calculate the L2 norm of a vector

For values $x_1, \ldots, x_n$, the Euclidean or L2 norm is

$$
\lVert x \rVert_2 = \sqrt{\sum_{i = 1}^{n} x_i^2}.
$$

```{r}
l2_norm(c(3, 4))
```

The calculation is scaled internally to avoid unnecessary overflow and
underflow.

```{r}
l2_norm(c(1e308, 1e308))
```

### Normalize a vector to unit length

`normalize_l2()` divides a vector by its L2 norm.

```{r}
unit_vector <- normalize_l2(c(3, 4))

unit_vector
l2_norm(unit_vector)
```

### Normalize matrix rows and columns

The `margin` argument identifies the dimensions that index separate slices.
For a matrix, `margin = 1` operates on rows and `margin = 2` operates on
columns.

```{r}
x <- matrix(
  c(3, 4, 0, 1, 2, 2),
  nrow = 2,
  byrow = TRUE,
  dimnames = list(c("a", "b"), c("x", "y", "z"))
)

l2_norm(x, margin = 1)
normalize_l2(x, margin = 1)
```

Column normalization uses the same interface.

```{r}
normalize_l2(x, margin = 2)
```

The approach extends to higher-dimensional arrays by supplying one or more
dimensions in `margin`.

### Handle zero-length slices

A zero vector cannot be scaled to unit length. The `zero` argument controls
the result:

- `"keep"` leaves the zero slice unchanged.
- `"na"` replaces the slice with missing values.
- `"error"` stops the calculation.

```{r}
normalize_l2(c(0, 0), zero = "keep")
normalize_l2(c(0, 0), zero = "na")
```

## Calculate adjacent differences

`adjacent_difference()` keeps the first value and then records the change from
each preceding value:

```text
result[1] = x[1]
result[i] = x[i] - x[i - 1]
```

```{r}
values <- c(10, 13, 12, 18)

changes <- adjacent_difference(values)
changes
cumsum(changes)
```

Unlike `diff()`, the result has the same length as the input. Applying
`cumsum()` reconstructs the original values when ordinary arithmetic is
reversible.

## Recycling and output shape

Functions with multiple numeric inputs use strict scalar recycling. Every
input must have length one or a shared length. Length-one inputs are recycled;
other length combinations produce an error.

```{r}
lerp(a = 0, b = c(10, 20, 30), t = 0.5)
```

Names, dimensions, and dimnames come from the first input already having the
shared length.

```{r}
x <- matrix(
  1:4,
  nrow = 2,
  dimnames = list(c("first", "second"), c("a", "b"))
)

clamp(x, lower = 2, upper = 3)
```

This rule avoids the partial recycling that base R permits for some length
combinations.

## A complete data-preparation example

Consider three sensor readings containing temperature, relative humidity, and
wind direction. The variables use different units and need separate numerical
transformations before they can be combined as features.

```{r}
sensors <- data.frame(
  temperature = c(18, 22, 30),
  humidity = c(45, 0, 75),
  wind_direction = c(-10, 360, 450)
)

sensors
```

Map temperatures from 0--40 degrees to `[0, 1]`, convert percentage humidity
to a proportion, and wrap wind directions to `[0, 360)`.

```{r}
temperature_score <- remap(
  sensors$temperature,
  from = c(0, 40),
  to = c(0, 1)
)

humidity_score <- clamp01(
  divide_or(sensors$humidity, 100, default = NA_real_)
)

wind_direction <- wrap(
  sensors$wind_direction,
  lower = 0,
  upper = 360
)

transformed <- data.frame(
  temperature_score,
  humidity_score,
  wind_direction
)

transformed
```

The two unitless scores can then be combined and normalized by row.

```{r}
features <- as.matrix(
  transformed[c("temperature_score", "humidity_score")]
)

normalize_l2(features, margin = 1)
```

This workflow makes each numerical step explicit: remapping changes units,
clamping enforces valid proportions, wrapping handles periodic values, and L2
normalization scales feature vectors to unit length.

## Summary

numops supplies a compact vocabulary for recurring numerical tasks in R. The
functions are dependency-free, work across vectors, matrices, and arrays, and
share consistent rules for validation, recycling, missing values, and output
shape. See the individual function help pages for complete edge-case behavior.
