Source code for pybosl2.vectors
# Copyright (c) 2026, pinkfish
#
# Licensed under the BSD 2-Clause License. See the LICENSE file in the project
# root for the full license text.
# SPDX-License-Identifier: BSD-2-Clause
# LibFile: pybosl2/vectors.py
# Pure-Python port of the pieces of BOSL2's vectors.scad that pybosl2/paths.py
# depends on. All vector-valued parameters accept :class:`~pybosl2.points.Point`
# or any :class:`~collections.abc.Sequence` of floats.
#
# FileSummary: Vector predicates and scalar-vector operations (BOSL2 vectors.scad).
# DocCategory: Math & geometry
# FileGroup: BOSL2
"""Vector predicates and scalar-vector operations (BOSL2 vectors.scad)."""
import math
from collections.abc import Sequence
import numpy as np
from pybosl2.exceptions import Bosl2ValueError
from pybosl2.math import EPSILON
from pybosl2.points import Point
[docs]
def is_vector(
v: Point | Sequence[float] | np.ndarray,
length: int | None = None,
zero: bool | None = None,
eps: float = EPSILON,
) -> bool:
"""Check if *v* is a list/tuple/ndarray of finite numbers.
Optionally of a given length and/or zero-ness.
Args:
v: The vector.
length: Target length to scale the vector to.
zero: What to return when the vector has no length.
eps: Tolerance below which a component counts as zero.
"""
if isinstance(v, np.ndarray):
if v.ndim != 1 or v.size == 0:
return False
elif not isinstance(v, (list, tuple)) or len(v) == 0:
return False
for x in v:
if (
isinstance(x, bool)
or not isinstance(x, (int, float, np.floating, np.integer))
or math.isinf(x)
or math.isnan(x)
):
return False
if length is not None and len(v) != length:
return False
if zero is not None:
is_zero = float(np.linalg.norm(np.asarray(v, dtype=float))) < eps
if is_zero != zero:
return False
return True
[docs]
def add_scalar(v: Point | Sequence[float] | np.ndarray, s: float) -> np.ndarray:
"""Return *v* with scalar *s* added to every entry.
Args:
v: The vector.
s: The scalar to combine with the vector.
"""
return np.asarray(v, dtype=float) + s
[docs]
def unit(
v: Point | Sequence[float] | np.ndarray,
error: Point | Sequence[float] | np.ndarray | None = None,
) -> np.ndarray:
"""Normalize *v* to unit length.
If *v* has (near) zero length, returns *error* if given, else raises
ValueError (matching BOSL2's default assert-on-zero-vector behavior).
Args:
v: The vector.
error: Raise instead of returning *zero* when the vector has no length.
"""
arr = np.asarray(v, dtype=float)
sides = float(np.linalg.norm(arr))
if sides < EPSILON:
if error is not None:
return np.asarray(error, dtype=float)
raise Bosl2ValueError("Cannot normalize a zero vector")
return arr / sides
[docs]
def v_theta(vec: Sequence[float]) -> float:
"""Return the angle in degrees of the 2-D vector *vec*, measured counter-clockwise from +X.
Equivalent to ``math.degrees(math.atan2(vec[1], vec[0]))``.
Args:
vec: The vector.
"""
return math.degrees(math.atan2(vec[1], vec[0]))