Array utilities¶
Array functions shared across the codebase.
Each function below is a standalone operation on plain NumPy arrays. This module acts a a place to collect common conventions, patterns, and conversions so that they can be used consistently across the code.
Flow angle and coordinate conversions¶
Paired functions convert between the native ember data representation and and other conventions, each the inverse of its partner. There are utilities for conversion between flow angles and velocity components, between Cartesian and polar coordinates, and between polar and pseudo-Cartesian coordinates.
|
Resolve velocity magnitude into polar components. |
|
Convert velocity components to velocity magnitude and flow angles. |
|
Convert Cartesian coordinates and velocities to polar form. |
|
Convert polar coordinates and velocities to Cartesian form. |
|
Convert (x, r, θ) coordinates to pseudo-Cartesian (x, r, rθ). |
Vector and matrix operations¶
These functions are intended for use batches of vectors or matrices stacked
along leading axes, with the last one or two axes holding the vector or matrix
components. Some are thin wrappers around numpy.einsum, but for
operations on the hot path of a solver loop they are implemented in Fortran for
better performance.
|
Dot product of two arrays along the last axis. |
|
Calculate the norm of a vector array along the last axis. |
|
Matrix-matrix multiplication over trailing dimensions. |
|
Matrix-vector multiplication using einsum over trailing dimensions. |
Grid construction¶
The below functions are used to build coordinate arrays for the solver. For
simple duct geometries, they allow convenient construction of uniform or
clustered xrt arrays. The resample() is
useful for coarsening or refining the distribution of an existing grid.
|
Create 3D coordinate rectangular meshgrid. |
|
Create 3D coordinate meshgrid from ranges and shape. |
Generate cosine-clustered points from 0 to 1, dense at both ends. |
|
|
Generate geometrically spaced points from 0 to 1 with expansion ratio and max spacing. |
|
Generate geometrically spaced points from 0 to 1, dense at both ends. |
|
Resampled a vector with specified factor, optionally preserving critical points. |
Array allocation and buffers¶
Many of these functions are are thin wrappers around their numpy
namesakes that fix the dtype and memory layout that weember standardises on:
Fortran order and single-precision float32. Calling these rather than the
bare NumPy functions signals intent and keeps that convention in one place –
use them for any new array creation. There are also functions for memory
management: allocation, or buffer creation and reuse.
|
Zero-filled array in standard layout and dtype. |
|
Copy array data into standard layout and dtype. |
|
Uninitialised array in standard layout and dtype. |
|
Constant-filled array in standard layout and dtype. |
|
Allocate output array if not provided, otherwise reuse existing array. |
|
Broadcast |
|
Carve one or more zero-copy Fortran-order views from a buffer of any shape. |
Resident and peak-resident memory of this process, in bytes. |
Miscellaneous geometry¶
Assorted utilities for working with coordinates and bounding boxes.
|
Calculate per-component min and max values. |
|
Calculate bounding box vertices from Cartesian coordinates. |
|
Apply permutation and flipping to array while preserving coordinate dimension. |
|
Unwrap meridional coordinates onto conformal distance along a curve. |
- util.angles_to_components(V, Alpha, Beta)[source]¶
Resolve velocity magnitude into polar components.
Uses numerically stable trigonometry for all angles including 90 degrees.
Flow angle conventions:
Alpha = 0°: No swirl (Vt = 0)
Beta = 0°: Pure axial flow (Vr = 0)
Beta = ±90°: Pure radial flow (Vx = 0)
- Parameters:
V_rel (Array) – Relative velocity magnitude [m/s]
Alpha (Array) – Relative yaw angle (tangential flow direction) [degrees]
Beta (Array) – Pitch angle (radial flow direction) [degrees]
- Returns:
Vx (Array) – Axial velocity component [m/s]
Vr (Array) – Radial velocity component [m/s]
Vt (Array) – Tangential velocity component [m/s]
- util.components_to_angles(Vx, Vr, Vt)[source]¶
Convert velocity components to velocity magnitude and flow angles.
This is the inverse of
angles_to_components(), obeying the conventions:Alpha = 0°: No swirl (Vt_rel = 0)
Beta = 0°: Pure axial flow (Vr = 0)
Beta = ±90°: Pure radial flow (Vx = 0)
For velocity components with very small magnitudes, angles may be numerically unstable. Zero velocity returns (0, 0, 0).
- Parameters:
Vx (Array) – Axial velocity component [m/s]
Vr (Array) – Radial velocity component [m/s]
Vt (Array) – Tangential velocity component [m/s]
- Returns:
V (Array) – Velocity magnitude [m/s]
Alpha (Array) – Yaw angle (tangential flow direction) [degrees]
Beta (Array) – Pitch angle (radial flow direction) [degrees]
- util.zeros(shape, dtype=<class 'numpy.float32'>)[source]¶
Zero-filled array in standard layout and dtype.
Equivalent to
numpy.zeros, but defaulting todtype=float32and alwaysorder="F".
- util.array(x, dtype=<class 'numpy.float32'>)[source]¶
Copy array data into standard layout and dtype.
Equivalent to
numpy.array, but defaulting todtype=float32and alwaysorder="F".- Parameters:
x (array_like) – Data to copy into the new array.
dtype (numpy dtype, optional) – Data type for the new array. Default is
np.float32.
- Returns:
Fortran-ordered array of the given dtype, holding a copy of
x.- Return type:
Array
- util.empty(shape)[source]¶
Uninitialised array in standard layout and dtype.
Equivalent to
numpy.empty, but alwaysdtype=float32andorder="F". As withnumpy.empty, contents are arbitrary until written – use this only where every element will be set before being read.
- util.full(shape, fill_value)[source]¶
Constant-filled array in standard layout and dtype.
Equivalent to
numpy.full, but alwaysdtype=float32andorder="F".
- util.allocate_or_reuse(out, shape, dtype=<class 'numpy.float32'>)[source]¶
Allocate output array if not provided, otherwise reuse existing array.
Helper function for functions that accept an optional out parameter. If
out is None, allocates a new F-contiguous zero array with the specified shape and dtype. Otherwise returns the provided array.- Parameters:
out (Array or None) – Pre-allocated output array, or None to allocate new array.
shape (tuple) – Shape of array to allocate if out is None.
dtype (numpy dtype, optional) – Data type for new array. Default is np.float32.
- Returns:
Either the provided out array or a newly allocated zero array.
- Return type:
Array
Examples
>>> def my_function(x, out=None): ... out = allocate_or_reuse(out, x.shape) ... # ... compute results into out ... ... return out
- util.bcast_if_needed(a, shape)[source]¶
Broadcast
atoshapeonly if it doesn’t already have it.A shape-tuple comparison is far cheaper than
numpy.broadcast_to()itself, so this is a near-zero-cost no-op on the common path whereais alreadyshape– e.g. a hot-path setter that documents its inputs as “must broadcast to X” for callers that need it, but whose actual callers already pass exactlyX-shaped arrays.- Parameters:
a (array_like) – Candidate array; may already have
shapeor be broadcastable to it.shape (tuple) – Target shape.
- Returns:
aunchanged ifa.shape == shape, otherwisenp.broadcast_to(a, shape).- Return type:
Array
- util.carve_view(buf, *shapes)[source]¶
Carve one or more zero-copy Fortran-order views from a buffer of any shape.
Reinterprets successive spans of a Fortran-contiguous buf as arrays of the requested shapes, packing them end-to-end (view
kstarts where viewk-1ends) so every returned view aliases distinct storage and all may be held live simultaneously. Used to borrow differently-shaped scratch arrays out of one oversized solver buffer without allocating.The offsets are computed internally, so a caller carving several coexisting slots cannot accidentally overlap or gap them.
buf.reshape(-1, order="F")on such a buffer is itself a free view, so repeated calls stay zero-copy and there is no need to hoist a shared flat view at the call site.- Parameters:
buf (Array) – Fortran-contiguous source buffer to reinterpret. Must be large enough to hold the concatenation of all shapes (
sum(prod(shape))elements).*shapes (tuple) – One shape per view, in packing order. Passing a single shape returns a single view; passing several returns a list, one per shape.
- Returns:
A single F-order view when one shape is given, else a list of them, each aliasing a disjoint span of buf.
- Return type:
Array or list of Array
- Raises:
ValueError – If the packed views would not fit within buf.
- util.rss_bytes()[source]¶
Resident and peak-resident memory of this process, in bytes.
Reads
VmRSS/VmHWMfrom/proc/self/status, so the peak is the kernel’s own high-water mark: it covers transients that came and went between two calls, which samplingVmRSSalone would miss. Where/procis unavailable (non-Linux) both come back as0, so callers on a debug-logging path never have to branch.
- util.meshgrid3(xv, rv, tv)[source]¶
Create 3D coordinate rectangular meshgrid.
This function combines the common pattern of creating a 3D meshgrid and stacking the results into a single coordinate array. It preserves the input dtype unless the inputs have mixed dtypes.
- Parameters:
xv (array_like) – X-coordinate vector.
rv (array_like) – R-coordinate vector.
tv (array_like) – Theta-coordinate vector.
- Returns:
Coordinate array with [x, r, t] components.
- Return type:
Array, shape (len(xv), len(rv), len(tv), 3)
Examples
>>> xv = np.linspace(0, 1, 3) >>> rv = np.linspace(1, 2, 4) >>> tv = np.linspace(0, np.pi, 5) >>> xrt = meshgrid3(xv, rv, tv) >>> xrt.shape (3, 4, 5, 3)
- util.linmesh3(x, r, t, shape)[source]¶
Create 3D coordinate meshgrid from ranges and shape.
This function creates linearly spaced vectors from coordinate ranges and then generates a 3D meshgrid using
meshgrid3(). It combines the common pattern of creating linspace vectors and then meshing them.- Parameters:
- Returns:
Coordinate array with (x, r, t) components, dtype=float32.
- Return type:
Examples
>>> xrt = linmesh3([0, 1], [1, 2], [0, np.pi], (3, 4, 5)) >>> xrt.shape (3, 4, 5, 3) >>> xrt.dtype dtype('float32')
- util.pol_to_pseudocart(xrt, inplace=False)[source]¶
Convert (x, r, θ) coordinates to pseudo-Cartesian (x, r, rθ).
In the pseudo-Cartesian system, the theta coordinate is multiplied by the radial coordinate to give rθ, which behaves like a Cartesian coordinate for distance calculations in cylindrical coordinates.
- Parameters:
xrt (array_like) – Input coordinates with shape (…, 3) where the last dimension contains [x, r, θ] coordinates.
inplace (bool, optional) – If True, modify the input array in-place. If False (default), return a copy with the conversion applied.
- Returns:
Coordinates with shape (…, 3) containing [x, r, rθ]. If inplace=True, returns the modified input array. If inplace=False, returns a new array.
- Return type:
Array
- util.extent(*args)[source]¶
Calculate per-component min and max values.
- Parameters:
*args (array_like) – Arrays to compute the extent of, must have same number of dimensions, and same trailing dimension if ndim > 1.
- Returns:
Extent array where extent[0, …] contains minimum values and extent[1, …] contains maximum values.
- Return type:
Array, shape (2, …)
- util.unwrap_meridional(xr_curve, xr_query)[source]¶
Unwrap meridional coordinates onto conformal distance along a curve.
Returns the conformal (or “blade-to-blade”) meridional coordinate
\[m' = \int \frac{\mathrm{d}m}{r}, \qquad \mathrm{d}m = \sqrt{\mathrm{d}x^2 + \mathrm{d}r^2},\]integrated along xr_curve from its first point. Paired with \(\theta\) in radians, \(m'\) spans a conformal plane: angles and aspect ratios are preserved, so an aerofoil section drawn on \((m', \theta)\) axes keeps its shape at any radius.
The coordinate is a property of the curve, not of any grid: every query point is referred to one datum by one integration, so points from different blocks — or from different cuts of the same machine — come back on a common scale with nothing to match up afterwards. Where the curve is the same one passed to
ember.cut.structured_meridional(), the coordinate and the surface it describes are exactly consistent.Query points are projected onto the curve and their perpendicular offset discarded, so points lying near it rather than exactly on it — the triangulated output of
ember.cut.unstructured(), say, or a blade surface — are handled without special-casing. A point beyond either end clamps to that end.- Parameters:
- Returns:
Conformal distance \(m'\) [-] at each query point, zero at the first point of the curve.
- Return type:
Array, shape (…)
- Raises:
ValueError – If the curve has fewer than two points, or reaches the axis.
Examples
>>> curve = np.array([[0.0, 2.0], [1.0, 2.0]]) # cylindrical, r = 2 >>> float(unwrap_meridional(curve, np.array([1.0, 2.0]))) # m' = m / r 0.5
- util.cart_to_pol(xyz, Vxyz, perm=(0, 1, 2), signs=(1, 1, 1))[source]¶
Convert Cartesian coordinates and velocities to polar form.
Inverts the ember \((x, r, \theta)\) convention described at Coordinate system, i.e. the map implemented by
pol_to_cart():\[y = r \cos\theta, \qquad z = -r \sin\theta\]This function takes Cartesian data stacked on the last axis, and returns polar data of the same shape, e.g. for setting a block’s raw coordinates and velocities
xrtandVxrtafter reading a Cartesian solution.permandsignsarguments, when supplied, reorient the input before conversion, for source data that doesn’t already follow our Cartesian axis order or sense.perm=(1, 0, 2)swaps \(x\) and \(y\) values at every point.signs[i] = -1negates the (already permuted) componenti. A mesh authored with \(z\) pointing the opposite way to our convention should be read withsigns=(1, 1, -1), for example.- Parameters:
xyz (array_like, shape (..., 3)) – Cartesian coordinates (x, y, z) with components on last axis
Vxyz (array_like, shape (..., 3)) – Cartesian velocity components (Vx, Vy, Vz) with components on last axis
perm (tuple of int, optional) – Coordinate permutation (0, 1, 2) -> reordered indices. Default: (0, 1, 2)
signs (tuple of int, optional) – Coordinate signs (-1 or 1 for each axis). Default: (1, 1, 1)
- Returns:
xrt (Array, shape (…, 3)) – Polar coordinates (x, r, t) with components on last axis
Vxrt (Array, shape (…, 3)) – Polar velocity components (Vx, Vr, Vt) with components on last axis
- util.pol_to_cart(xrt, Vxrt, perm=(0, 1, 2), signs=(1, 1, 1))[source]¶
Convert polar coordinates and velocities to Cartesian form.
The reverse of
cart_to_pol(). Implements the ember \((x, r, \theta)\) convention described at Coordinate system:\[y = r \cos\theta, \qquad z = -r \sin\theta\]The inputs are typically read from
xrtandVxrt; the returned arrays are Cartesian data of the same shape.permandsignsreorient the output after conversion, for a target convention that isn’t ember’s own axis order or sense.perm[i]says which converted component supplies output componenti, and thensigns[i] = -1negates the (already permuted) componenti.Note that reorientation is applied on the way in by
cart_to_pol()but on the way out here, so the two are not simply symmetric for generalpermandsigns.ember.grid.Grid.align_cart_unstr()detects the correct orientation to align with a destination grid automatically when given Cartesian data.- Parameters:
xrt (array_like, shape (..., 3)) – Polar coordinates (x, r, t) with components on last axis
Vxrt (array_like, shape (..., 3)) – Polar velocity components (Vx, Vr, Vt) with components on last axis
perm (tuple of int, optional) – Coordinate permutation (0, 1, 2) -> reordered indices. Default: (0, 1, 2)
signs (tuple of int, optional) – Coordinate signs (-1 or 1 for each axis). Default: (1, 1, 1)
- Returns:
xyz (Array, shape (…, 3)) – Cartesian coordinates (x, y, z) with components on last axis
Vxyz (Array, shape (…, 3)) – Cartesian velocity components (Vx, Vy, Vz) with components on last axis
- util.matmat(A, B)[source]¶
Matrix-matrix multiplication over trailing dimensions.
Performs matrix multiplication on stacks of matrices where the matrices are stored in the trailing dimensions. This is optimized for arrays with matrix data in the last two dimensions and arbitrary leading dimensions.
- Parameters:
- Returns:
Result of matrix multiplication A @ B for each corresponding pair of matrices in the trailing dimensions. Uses f32 precision and Fortran ordering for optimal performance.
- Return type:
Array, shape (…, m, n)
Examples
>>> # Stack of 2x2 matrices >>> A = np.random.randn(10, 5, 2, 2).astype(np.float32, order='F') >>> B = np.random.randn(10, 5, 2, 2).astype(np.float32, order='F') >>> C = matmat(A, B) # Shape: (10, 5, 2, 2)
>>> # Single matrix multiplication >>> A = np.eye(3, dtype=np.float32, order='F') >>> B = np.ones((3, 3), dtype=np.float32, order='F') >>> C = matmat(A, B) # C = B
- util.rotation_matrices(chi)[source]¶
Build a paired 2x2 rotation matrix and its inverse from a meridional-plane angle.
Both
RevolutionPatch’s interface frame andresolve_to_interface()rotate a velocity pair \((V_x, V_r)\) by the same convention, so this is the one place that convention is written down:\[\begin{split}V_n &= \cos\chi\, V_x + \sin\chi\, V_r \\ V_s &= -\sin\chi\, V_x + \cos\chi\, V_r\end{split}\]rot_fromis the transpose ofrot_to– the rotation is orthogonal – so it undoes exactly this and nothing has to be re-derived to invert it.- Parameters:
chi (float or Array) – Angle [rad] of the frame axis from \(+x\), any shape.
- Returns:
rot_to, rot_from –
rot_toturns \((V_x, V_r)\) into \((V_n, V_s)\);rot_fromturns \((V_n, V_s)\) back into \((V_x, V_r)\).- Return type:
Array, shape
chi.shape + (2, 2)
- util.matvec(A, b, out=None)[source]¶
Matrix-vector multiplication using einsum over trailing dimensions.
- Parameters:
- Returns:
Result of matrix-vector multiplication A @ b for each corresponding matrix and vector in the trailing dimensions. Uses f32 precision and Fortran ordering for optimal performance.
- Return type:
Array, shape (…, n)
Notes
When
outis given,Aandbare checked against two specific hot-path shapes – the per-grid-point 5x5 Jacobian contractions the solver runs every iteration – and dispatched to a specialised Fortran kernel if they match. Any other shape combination falls back tonumpy.matmul(..., out=...), which is still correct but without the specialised kernel. Either wayoutis updated in place and returned by identity.The two fast-path shapes, with
b.shape[-1]fixed at 5 in every case:Single batch axis:
b.shape == (N, 5)andA.shape == (N, 5, 5)– one matrix per vector, no broadcasting.Three-dimensional grid with one broadcast:
b.shape === (ni, nj, nk, 5), andA.shapeone of:(ni, nj, 1, 5, 5)– one matrix per \((i,j)\), shared across \(k\)(1, 1, nk, 5, 5)– one matrix per \(k\), shared across \((i,j)\)(ni, 1, 1, 5, 5)– one matrix per \(i\), shared across \((j,k)\)
A parallel set of kernels handles the same three broadcast patterns for
b.shape[-1] == 2instead of 5. Any other broadcast pattern within the 3-grid-axis case, including a fullAshape(ni, nj, nk, n, m)with no broadcast axis at all, falls back tonumpy.matmul.
Examples
>>> # Stack of 3x3 matrices with 3-element vectors >>> A = np.random.randn(10, 5, 3, 3).astype(np.float32, order='F') >>> b = np.random.randn(10, 5, 3).astype(np.float32, order='F') >>> y = matvec(A, b) # Shape: (10, 5, 3)
>>> # Single matrix-vector multiplication >>> A = np.eye(3, dtype=np.float32, order='F') >>> b = np.array([1, 2, 3], dtype=np.float32, order='F') >>> y = matvec(A, b) # y = b
>>> # Write into a preallocated buffer instead of allocating >>> A = np.random.randn(4, 5, 5).astype(np.float32, order='F') >>> b = np.random.randn(4, 5).astype(np.float32, order='F') >>> out = np.empty((4, 5), dtype=np.float32, order='F') >>> matvec(A, b, out=out) is out True
- util.resample(factor, vector, i_crit=None)[source]¶
Resampled a vector with specified factor, optionally preserving critical points.
Creates a new vector by resampling with a given factor. The new length equals len(vector) * factor (approximately). Critical indices are preserved, and for upsampling, fractional indices are linearly interpolated.
- Parameters:
factor (float) – Resampling factor. Values > 1 increase resolution, < 1 decrease resolution. Must be > 0.
vector (array_like) – Input vector to resample.
i_crit (array_like, optional) – Critical indices that must be preserved in the output. Must be sorted and within [0, len(vector)-1]. If None, uses [0, len(vector)-1].
- Returns:
resampled_values (Array) – Resampled vector with length approximately len(vector) * factor, with values at critical indices preserved and interpolated elsewhere.
crit_mapping (dict) – Dictionary mapping old critical indices to their positions in the new vector. Format: {old_index: new_index}
Examples
>>> # Increase resolution by factor of 2 with linear interpolation >>> x = np.array([0, 1, 4, 9, 16]) >>> x_resampled, mapping = resample(2.0, x) >>> len(x_resampled) # approximately 10 >>> mapping[0], mapping[4] # endpoints preserved at new positions
>>> # Decrease resolution preserving critical points >>> x_resampled, mapping = resample(0.5, x, [0, 2, 4]) >>> len(x_resampled) # approximately 2-3 >>> mapping # {0: 0, 2: 1, 4: 2} - critical indices mapped to new positions
- util.apply_perm_flip(array, perm, flip=())[source]¶
Apply permutation and flipping to array while preserving coordinate dimension.
This function applies a permutation and optional flipping to the spatial dimensions of an array, while preserving the last dimension (typically coordinates). This is commonly used in patch operations for coordinate transformations.
- Parameters:
- Returns:
Transformed array with permutation and flipping applied to spatial dimensions, last dimension preserved.
- Return type:
Array
Examples
>>> coords = np.random.randn(5, 4, 3).astype(np.float32, order='F') >>> # Swap i and j dimensions >>> transformed = apply_perm_flip(coords, perm=(1, 0, 2)) >>> transformed.shape == (4, 5, 3) # i,j swapped, coordinates preserved True
>>> # Swap dimensions and flip along first dimension >>> transformed = apply_perm_flip(coords, perm=(1, 0, 2), flip=(0,))
- util.cosine_cluster(n)[source]¶
Generate cosine-clustered points from 0 to 1, dense at both ends.
Produces
npoints on the unit interval following a half-cosine distribution, which clusters nodes near both endpoints (zeta = 0andzeta = 1) and spaces them most coarsely in the middle:\[\zeta_k = \tfrac{1}{2}\left(1 - \cos\frac{\pi k}{n - 1}\right), \quad k = 0, \ldots, n - 1.\]The result is symmetric about 0.5 with exact endpoints 0 and 1.
- Parameters:
n (int) – Number of points to generate (must be >= 2).
- Returns:
Cosine-clustered vector from 0 to 1, dtype=float32.
- Return type:
Array, shape (n,)
Examples
>>> z = cosine_cluster(5) >>> z[0], z[-1] # exact endpoints (0.0, 1.0) >>> np.allclose(z + z[::-1], 1.0) # symmetric True
- util.cluster(ni, ER, dmax)[source]¶
Generate geometrically spaced points from 0 to 1 with expansion ratio and max spacing.
Creates a vector from 0 to 1 with geometrically spaced points, where the spacing expands at ratio ER but is capped to maximum size dmax. The algorithm creates an initial geometric dx vector, then iteratively adjusts the scaling to achieve unit total length while respecting dmax after capping.
- Parameters:
- Returns:
Vector from 0 to 1 with clustered spacing, dtype=float32
- Return type:
Examples
>>> # Basic clustering with expansion ratio 1.2 >>> x = cluster(10, 1.2, 0.5) >>> x[0], x[-1] # Should be (0.0, 1.0)
>>> # Uniform spacing when ER=1.0 >>> x = cluster(5, 1.0, 1.0) >>> np.allclose(x, np.linspace(0, 1, 5)) True
- util.cluster_symmetric(n, ER, dmax=1.0)[source]¶
Generate geometrically spaced points from 0 to 1, dense at both ends.
Where
cluster()expands away from a single end, this mirrors a half-widthcluster()vector about the centreline, so the spacing grows at expansion ratioERaway from both endpoints and is coarsest in the middle. Unlikecosine_cluster(), which is also symmetric, the growth rate is controlled rather than fixed by the distribution.- Parameters:
- Returns:
Vector from 0 to 1 with spacing clustered at both ends, dtype=float32.
- Return type:
Array, shape (n,)
Examples
>>> z = cluster_symmetric(9, 1.2, 1.0) >>> z[0], z[-1] # exact endpoints (0.0, 1.0) >>> np.allclose(z + z[::-1], 1.0) # symmetric about 0.5 True
>>> # Uniform spacing when ER=1.0 >>> np.allclose(cluster_symmetric(5, 1.0, 1.0), np.linspace(0, 1, 5)) True