Source: ndarray.mjs

import {
	tester,
	slice,
	copyto,
	array,
	Flatiter,
	ndoffset,
	reshape,
	transpose,
	swapaxes,
	Slice,
	broadcast,
	arange,
	concatenate,
	sort,
	cumsum,
	repeat,
	all,
	argsort,
	clip,
	compress,
	cumprod,
	diagonal,
	amax,
	mean,
	amin,
	nonzero,
	prod,
	ptp,
	put,
	ravel,
	contiguous,
	around,
	searchsorted,
	squeeze,
	std,
	sum,
	take,
	variance,
	array_repr,
	array_str,
	Dtype,
	any,
	dtype_,
	NdoffsetIterator,
	greater_equal,
	where,
	add,
	subtract,
	multiply,
	divide,
	mod,
	asarray,
	ones,
	get,
	scatter,
	set,
	argmax,
	argmin,
} from './core.mjs';

export function toArray(obj) {
	return obj instanceof NDArray ? obj.array() : obj;
}

/**
 * @class
 */
export class NDArray {
	/**
	 * @param {number[]} shape
	 * @param {any[]} data
	 * @param {Dtype} dtype
	 * @param {NDArray} base
	 * @param {number[]} strides
	 * @param {number} offset
	 * @param {number} itemsize always 1
	 */
	constructor(shape, data = null, dtype = null, base = null, strides = null, offset = 0, itemsize = 1) {
		// https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html
		/** @member {number} */
		this.ndim = shape.length;
		/** @member {number} */
		this.size = get_size(shape);

		/** @member {number[]} */
		this.shape = shape;
		/** @member {any[]} */
		this.data = data ?? Array(this.size);
		/** @member {number} */
		this.itemsize = itemsize;
		/** @member {number[]} */
		this.strides = strides ?? get_strides(shape, this.ndim, itemsize);
		/** @member {number} */
		this.offset = offset;

		/** @member {Dtype} */
		this.dtype = dtype_(dtype ?? data.constructor);

		/** @member {NDArray|null} */
		this.base = base?.base ?? base;
	}

	/** @member {number} */
	get length() {
		let { ndim, shape } = this;
		if (ndim != 0) return shape[0];
	}

	/**
	 * @member {Flatiter}
	 * @type {Flatiter}
	 */
	get flat() {
		return new Flatiter(this);
	}

	set flat(value) {
		this.flat.set(':', value);
	}

	/** @member {NDArray} */
	get T() {
		return transpose(this);
	}

	set T(value) {
		this.T.set(value);
	}

	/**
	 * @returns {string}
	 */
	toString() {
		return array_str(this);
	}

	/**
	 * @returns {any|string}
	 */
	valueOf() {
		return this.ndim == 0 ? this.item() : array_repr(this);
	}

	*[Symbol.iterator]() {
		for (let i = 0; i < this.shape[0]; i++) {
			yield this.at(i);
		}
	}

	/**
	 * Returns a view with the given shape, strides, and offset.
	 *
	 * out.base = this.base ?? this
	 * @param {number[]} [shape]
	 * @param {number[]} [strides]
	 * @param {number} [offset] required as a ptr to the first item
	 * @returns {NDArray}
	 */
	as_strided(shape = this.shape, strides = this.strides, offset = this.offset) {
		let { data, dtype, base, itemsize } = this;
		return new NDArray(shape, data, dtype, base ?? this, strides, offset, itemsize);
	}

	/**
	 * Returns `ndoffset(this.shape, this.strides, this.offset)`.
	 *
	 * Useful when iterating `this.data[i]` or `this.item(i)` in a `for (let i of this.keys())` loop.
	 * @returns {NdoffsetIterator}
	 */
	keys() {
		let { shape, strides, offset } = this;
		return ndoffset(shape, strides, offset);
	}

	/**
	 * Returns `this.flat`.
	 * @returns {Flatiter}
	 */
	values() {
		return this.flat;
	}

	/**
	 * Converts NDArray index to .data[] index.
	 * @param {number|number[]|undefined} index
	 * @returns {number}
	 */
	idx(index) {
		let { shape, strides, ndim, size, offset } = this;

		if (index == 0) return offset;

		if (index == undefined) {
			if (size != 1) {
				throw new Error('index cannot be empty if size != 1');
			}
			return offset;
		}

		let indices = get_indices(index, shape, ndim, size);

		for (let i = 0; i < indices.length; i++) {
			let idx = indices[i];
			let dim = shape[i];
			if (idx < 0) idx += dim;
			if (idx < 0 || idx >= dim) {
				throw new Error(`index ${indices[i]} out of bound for dimension ${dim}`);
			}
			offset += idx * strides[i];
		}

		return offset;
	}

	// Array methods https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array

	/**
	 * `.at(0)` is is equivalent to `.get([0])`
	 * @param  {...number|Slice|string|null|number[]|boolean[]|NDArray} indices
	 * @returns {NDArray}
	 */
	at(...indices) {
		return this.get(indices);
	}

	/**
	 *
	 * @param {number|null} [start = null]
	 * @param {number|null} [end = null]
	 * @returns {NDArray}
	 */
	slice(start = null, end = null) {
		return this.at(slice(start, end));
	}

	/**
	 * `.get(indices)` is is equivalent to `.at(...indices)`
	 * @param  {Array<number|Slice|string|null|number[]|boolean[]|NDArray>} indices
	 * @param {number} [axis = 0]
	 * @returns {NDArray}
	 */
	get(indices, axis = 0) {
		if (checkBasicIndexing(indices)) {
			return basicIndexing(this, indices, axis);
		}
		return scatter(this, indices, axis).get();
	}

	/**
	 * `.set(value)` is equivalent to `.set(['...'], value)`, but faster
	 * @param  {Array<number|Slice|string|null|number[]|boolean[]|NDArray>} indices
	 * @param  {any} [value]
	 * @returns {NDArray}
	 */
	set(indices, value) {
		if (arguments.length == 1) {
			copyto(this, indices);
		} else if (checkBasicIndexing(indices)) {
			copyto(basicIndexing(this, indices), value);
		} else {
			scatter(this, indices).set(value);
		}
		return this;
	}

	/**
	 * Returns an element of the array.
	 *
	 * if `index` is an integer, `index` is regarded as a flatten index.
	 *
	 * `index` can be undefined only if `a.size == 1`.
	 *
	 * @param {number|number[]|undefined} index
	 * @returns {any}
	 */
	item(index) {
		return this.data[this.idx(index)];
	}

	/**
	 * Sets an element of the array.
	 *
	 * index can be undefined only if a.size == 1.
	 * @param {number|number[]|undefined} index
	 * @param {any} scalar
	 * @returns {NDArray}
	 */
	itemset(index, scalar) {
		this.data[this.idx(index)] = scalar;
		return this;
	}

	/**
	 * @param {Dtype} dtype
	 * @param {boolean} [copy]
	 * @returns {NDArray}
	 */
	astype(dtype, copy = true) {
		dtype = dtype_(dtype);
		copy ||= this.dtype != dtype;

		if (!copy) return this;

		let { shape, size } = this;
		let data = dtype.new(size, [...this.flat]);
		return new NDArray(shape, data, dtype);
	}

	/**
	 * @returns {any[]|any}
	 */
	array() {
		let { ndim } = this;
		if (ndim == 0) {
			return this.data[this.offset];
		}

		let { shape, size } = this;
		let array;
		if (size == 0) {
			if (ndim == 1) return [];

			let length = 1;
			let axis = 0;
			for (let dim; axis < ndim && (dim = shape[axis]); axis++) {
				length *= dim;
			}
			ndim = axis;
			array = Array.from({ length }, () => []);
		} else {
			array = [...this.flat];
		}

		for (let i = ndim - 1; i > 0; i--) {
			let dim = shape[i];
			let nested = [];
			for (let j = 0; j < array.length; j += dim) {
				nested.push(array.slice(j, j + dim));
			}
			array = nested;
		}
		return array;
	}

	/**
	 * @returns {any[]|any}
	 */
	toarray() {
		return this.array();
	}

	/**
	 * Alias of .toarray()
	 * @returns {any[]|any}
	 */
	tolist() {
		return this.array();
	}

	/**
	 * `.reshape(1, 2)` is equivalent to `.reshape([1, 2])`
	 * @param  {...number|number[]} shape
	 * @returns {NDArray}
	 */
	reshape(...shape) {
		if (shape.length == 1 && typeof shape[0] == 'object') {
			shape = shape[0];
		}
		return reshape(this, shape);
	}

	// non-inplace

	/**
	 * Returns add(this, x, out)
	 * @function
	 * @param {NDArray} x
	 * @param {null|NDArray} [out = null]
	 * @returns {NDArray}
	 * @example
	 * x.add(a, x)
	 * // in-place add
	 */
	add(x, out = null) {
		return add(this, x, out);
	}

	/**
	 * Returns subtract(this, x, out)
	 * @function
	 * @param {NDArray} x
	 * @param {null|NDArray} [out = null]
	 * @returns {NDArray}
	 */
	sub(x, out = null) {
		return subtract(this, x, out);
	}

	/**
	 * Returns multiply(this, x, out)
	 * @function
	 * @param {NDArray} x
	 * @param {null|NDArray} [out = null]
	 * @returns {NDArray}
	 */
	mul(x, out = null) {
		return multiply(this, x, out);
	}

	/**
	 * Returns divide(this, x, out)
	 * @function
	 * @param {NDArray} x
	 * @param {null|NDArray} [out = null]
	 * @returns {NDArray}
	 */
	div(x, out = null) {
		return divide(this, x, out);
	}

	/**
	 * Returns mod(this, x, out)
	 * @function
	 * @param {NDArray} x
	 * @param {null|NDArray} [out = null]
	 * @returns {NDArray}
	 */
	mod(x, out = null) {
		return mod(this, x, out);
	}

	// ndarray.[]

	/**
	 * @param {number|number[]|null} axis
	 * @param {NDArray} out
	 * @param {boolean} keepdims
	 * @param {boolean} initial
	 * @param {boolean} return_scalar
	 * @returns {NDArray<boolean>|boolean}
	 */
	all(axis = null, out = null, keepdims = false, initial = true, return_scalar = true) {
		return all(this, axis, out, keepdims, initial, return_scalar);
	}

	/**
	 * @param {number|number[]|null} axis
	 * @param {NDArray} out
	 * @param {boolean} keepdims
	 * @param {boolean} initial
	 * @param {boolean} return_scalar
	 * @returns {NDArray<boolean>|boolean}
	 */
	any(axis = null, out = null, keepdims = false, initial = false, return_scalar = true) {
		return any(this, axis, out, keepdims, initial, return_scalar);
	}

	/**
	 * @param {number|null} [axis = null]
	 * @param {NDArray|null} [out = null]
	 * @param {boolean} [keepdims = false]
	 * @returns {NDArray}
	 */
	argmax(axis = null, out = null, keepdims = false) {
		return argmax(this, axis, out, keepdims);
	}

	/**
	 * @param {number|null} [axis = null]
	 * @param {NDArray|null} [out = null]
	 * @param {boolean} [keepdims = false]
	 * @returns {NDArray}
	 */
	argmin(axis = null, out = null, keepdims = false) {
		return argmin(this, axis, out, keepdims);
	}

	argpartition(kth, axis = -1, kind = 'introselect', order = null) {
		throw `not implemented`;
	}

	/**
	 * @param {number} axis
	 * @param {function} [key]
	 * @returns {NDArray<number>}
	 */
	argsort(axis = -1, key = null) {
		return argsort(this, axis, key);
	}

	byteswap() {
		throw `not implemented`;
	}

	choose(choices, out = null, mode = 'raise') {
		throw `not implemented`;
	}

	/**
	 * @param {number|NDArray} a_min
	 * @param {number|NDArray} a_max
	 * @param {NDArray} out
	 * @returns {NDArray}
	 */
	clip(a_min, a_max, out = null) {
		return clip(this, a_min, a_max, out);
	}

	/**
	 * @param {boolean[]} condition
	 * @param {number} [axis]
	 * @param {NDArray} [out]
	 * @returns {NDArray}
	 */
	compress(condition, axis = null, out = null) {
		return compress(condition, this, axis, out);
	}

	conj() {
		throw `not implemented`;
	}

	conjugate() {
		throw `not implemented`;
	}

	/**
	 * @returns {NDArray}
	 */
	copy() {
		return array(this);
	}

	/**
	 * @param {number} [axis]
	 * @param {NDArray} [out]
	 * @returns {NDArray}
	 */
	cumprod(axis, out) {
		return cumprod(this, axis, out);
	}

	/**
	 * @param {number} [axis]
	 * @param {NDArray} [out]
	 * @returns {NDArray}
	 */
	cumsum(axis, out) {
		return cumsum(this, axis, out);
	}

	/**
	 * @param {number} [offset]
	 * @param {number} [axis1]
	 * @param {number} [axis2]
	 * @returns {NDArray}
	 */
	diagonal(offset = 0, axis1 = 0, axis2 = 1) {
		return diagonal(this, offset, axis1, axis2);
	}

	dump() {
		throw `not implemented`;
	}

	dumps() {
		throw `not implemented`;
	}

	/**
	 * @param {any} value
	 * @returns {NDArray} this
	 */
	fill(value) {
		let { data, shape, strides, offset } = this;
		for (let idx of ndoffset(shape, strides, offset)) {
			data[idx] = value;
		}
		return this;
	}

	/**
	 * @returns {NDArray}
	 */
	flatten() {
		let { size, flat, dtype } = this;
		return new NDArray([size], [...flat], dtype);
	}

	getfield() {
		throw `not implemented`;
	}

	// item
	// itemset

	/**
	 * @param {number|number[]} [axis]
	 * @param {NDArray} [out]
	 * @param {boolean} [keepdims]
	 * @param {any} [initial]
	 * @param {boolean} [return_scalar]
	 * @returns {NDArray<any>|any}
	 */
	max(axis = null, out = null, keepdims = false, initial = null, return_scalar = true) {
		return amax(this, axis, out, keepdims, initial, return_scalar);
	}

	mean(axis = null, out = null, keepdims = false) {
		return mean(this, axis, out, keepdims);
	}

	/**
	 * @param {number|number[]} [axis]
	 * @param {NDArray} [out]
	 * @param {boolean} [keepdims]
	 * @param {any} [initial]
	 * @param {boolean} [return_scalar]
	 * @returns {NDArray|any}
	 */
	min(axis = null, out = null, keepdims = false, initial = null, return_scalar = true) {
		return amin(this, axis, out, keepdims, initial, return_scalar);
	}

	newbyteorder() {
		throw `not implemented`;
	}

	/**
	 * @returns {NDArray[]}
	 */
	nonzero() {
		return nonzero(this);
	}

	partition(kth, axis = -1, kind = 'introselect', order = null) {
		throw `not implemented`;
	}

	/**
	 * @param {number|number[]} [axis]
	 * @param {NDArray} [out]
	 * @param {boolean} [keepdims]
	 * @param {any} [initial]
	 * @param {boolean} [return_scalar]
	 * @returns {NDArray|any}
	 */
	prod(axis = null, out = null, keepdims = false, initial = 0, return_scalar = true) {
		return prod(this, axis, out, keepdims, initial, return_scalar);
	}

	/**
	 * @param {number|number[]} [axis]
	 * @param {NDArray} [out]
	 * @param {boolean} [keepdims]
	 * @returns {NDArray}
	 */
	ptp(axis = null, out = null, keepdims = false) {
		return ptp(this, axis, out, keepdims);
	}

	/**
	 * @param {number[]} indices
	 * @param {any[]} values
	 * @param {string} [mode]
	 * @returns {NDArray} this
	 */
	put(indices, values, mode = 'raise') {
		put(this, indices, values, mode);
		return this;
	}

	/**
	 * @returns {NDArray}
	 */
	ravel() {
		return ravel(this);
	}

	/**
	 * @param {number|number[]} repeats
	 * @param {number} [axis]
	 * @returns {NDArray}
	 */
	repeat(repeats, axis = null) {
		return repeat(this, repeats, axis);
	}

	// reshape

	/**
	 * inplace resize
	 * @param {number[]} new_shape
	 * @returns {NDArray} this
	 */
	resize(new_shape) {
		if (this.base != null) throw `cannot resize this array: it does not own its data`;
		if (!contiguous(this)) throw `resize only works on single-segment arrays`;
		let new_size = get_size(new_shape);
		if (new_size <= this.size) {
			this.data = [...this.data.slice(this.offset, new_size)];
		} else {
			this.data = [...this.data.slice(this.offset, new_size), ...Array(new_size - this.size).fill(0)];
		}
		this.shape = new_shape;
		this.ndim = new_shape.length;
		this.strides = get_strides(new_shape, this.ndim, this.itemsize);
		this.offset = 0;
		return this;
	}

	/**
	 * @param {number} [decimals]
	 * @param {NDArray} [out]
	 * @returns {NDArray}
	 */
	round(decimals = 0, out = null) {
		return around(this, decimals, out);
	}

	/**
	 * @param {any[]} v
	 * @param {string} [side]
	 * @returns {NDArray}
	 */
	searchsorted(v, side = 'left') {
		return searchsorted(this, v, side);
	}

	setfield() {
		throw `not implemented`;
	}

	setflags() {
		throw `not implemented`;
	}

	/**
	 * @param {number} [axis]
	 * @param {Function} [key]
	 * @returns {NDArray} this
	 */
	sort(axis = -1, key = null) {
		this.set(sort(this, axis, key));
		return this;
	}

	/**
	 * @param {number|number[]} axis
	 * @returns {NDArray}
	 */
	squeeze(axis = null) {
		return squeeze(this, axis);
	}

	/**
	 * @param {number|number[]} [axis]
	 * @param {NDArray} [out]
	 * @param {number} [ddof]
	 * @param {boolean} [keepdims]
	 * @returns {NDArray}
	 */
	std(axis = null, out = null, ddof = 0, keepdims = false) {
		return std(this, axis, out, ddof, keepdims);
	}

	/**
	 * @param {number|number[]} [axis]
	 * @param {NDArray} [out]
	 * @param {boolean} [keepdims]
	 * @param {number} [initial]
	 * @param {boolean} [return_scalar]
	 * @returns {NDArray|any}
	 */
	sum(axis = null, out = null, keepdims = false, initial = 0, return_scalar = true) {
		return sum(this, axis, out, keepdims, initial, return_scalar);
	}

	/**
	 * @param {number} axis1
	 * @param {number} axis2
	 * @returns {NDArray}
	 */
	swapaxes(axis1, axis2) {
		return swapaxes(this, axis1, axis2);
	}

	/**
	 * @param {number[]} indices
	 * @param {number} [axis]
	 * @param {NDArray} [out]
	 * @param {string} [mode]
	 * @returns {NDArray}
	 */
	take(indices, axis = null, out = null, mode = 'raise') {
		return take(this, indices, axis, out, mode);
	}

	tobytes() {
		throw `not implemented`;
	}

	tofile() {
		throw `not implemented`;
	}

	// tolist

	trace() {
		throw `not implemented`;
	}

	/**
	 * @param {number|number[]} [axes]
	 * @returns {NDArray}
	 */
	transpose(axes = null) {
		return transpose(this, axes);
	}

	/**
	 * @param {number|number[]} [axis]
	 * @param {NDArray} [out]
	 * @param {number} [ddof]
	 * @param {boolean} [keepdims]
	 * @returns {NDArray}
	 */
	variance(axis = null, out = null, ddof = 0, keepdims = false) {
		return variance(this, axis, out, ddof, keepdims);
	}
}

import util from 'util';
NDArray.prototype[util?.inspect?.custom] = function () {
	return this.valueOf();
};

/**
 * @param {number[]} shape
 * @param {number} ndim
 * @param {number} itemsize
 * @returns {number[]}
 * @ignore
 */
export function get_strides(shape, ndim, itemsize) {
	if (ndim == 0) return [];

	let strides = Array(ndim);
	strides[ndim - 1] = itemsize;
	for (let i = ndim - 1; i > 0; i--) {
		strides[i - 1] = strides[i] * shape[i];
	}
	return strides;
}

/**
 * @param {number[]} shape
 * @returns {number}
 * @ignore
 */
export function get_size(shape) {
	let size = 1;
	for (let dim of shape) size *= dim;
	return size;
}

/**
 * Converts flatten index to multi-dimensional indices.
 * @param {number|number[]} index
 * @param {number[]} shape
 * @param {number} ndim
 * @param {number} size
 * @returns {number[]}
 * @ignore
 */
function get_indices(index, shape, ndim, size) {
	if (Number.isInteger(index)) {
		let idx = index;
		if (idx < 0) idx += size;
		if (idx < 0 || idx >= size) {
			throw new Error(`index ${index} out of bound for size ${size}`);
		}

		if (ndim == 0) return [0];

		let indices = Array(ndim);
		for (let i = ndim - 1; i >= 0; i--) {
			indices[i] = idx % shape[i];
			idx = (idx / shape[i]) | 0;
		}
		return indices;
	}

	if (index.length != ndim) {
		throw new Error('incorrect number of indices for array');
	}

	return index;
}

/**
 * @param {Array<number|Slice|string|null|number[]|boolean[]|NDArray>} indices
 * @returns {boolean}
 * @ignore
 */
function checkBasicIndexing(indices) {
	for (let index of indices) {
		if (index != null && typeof index == 'object' && index.length != undefined) {
			return false;
		}
	}
	return true;
}

/**
 * @param {NDArray} a
 * @param {Array<number|Slice|string|null>} indices
 * @param {number} [axis = 0]
 * @returns {NDArray}
 * @ignore
 */

function basicIndexing(a, indices, axis = 0) {
	let { ndim, shape, strides, offset } = a;

	let newaxes = 0;
	let ellipsisIndex = -1;
	for (let i = 0; i < indices.length; i++) {
		let index = indices[i];
		if (index == Slice.ellipsis || index == '...') {
			if (ellipsisIndex != -1) {
				throw new Error(`can only have a single ellipsis '...'`);
			}
			ellipsisIndex = i;
		} else if (index == null) {
			newaxes++;
		}
	}

	let skipDims = 0;
	if (ellipsisIndex != -1) {
		skipDims = ndim - axis - (indices.length - newaxes - 1);
	}

	if (axis + (indices.length - newaxes - (ellipsisIndex != -1)) > ndim) {
		throw new Error(`too many indices`);
	}

	let outShape = [];
	let outStrides = [];

	for (let i = 0; i < axis; i++) {
		outShape.push(shape[i]);
		outStrides.push(strides[i]);
	}

	for (let index of indices) {
		if (index == null) {
			outShape.push(1);
			outStrides.push(0);
		} else {
			if (typeof index == 'string') index = slice(index);
			else if (!(index instanceof Slice)) {
				let _index = index;
				if (index < 0) index += shape[axis];
				if (index < 0 || index > shape[axis]) {
					throw new Error(`index ${_index} out of bound for dimension size ${shape[axis]}`);
				}
				offset += strides[axis] * index;
				axis++;
				continue;
			}

			if (index == Slice.ellipsis) {
				for (let i = 0; i < skipDims; i++) {
					outShape.push(shape[axis + i]);
					outStrides.push(strides[axis + i]);
				}
				axis += skipDims;
			} else if (index == Slice.colon) {
				outShape.push(shape[axis]);
				outStrides.push(strides[axis]);
				axis++;
			} else {
				let { start, step, slicelength } = index.indices(shape[axis]);
				offset += strides[axis] * start;
				outShape.push(slicelength);
				outStrides.push(strides[axis] * step);
				axis++;
			}
		}
	}

	for (let i = axis; i < ndim; i++) {
		outShape.push(shape[i]);
		outStrides.push(strides[i]);
	}

	return a.as_strided(outShape, outStrides, offset);
}

tester.onload(() => {
	// console.log(arange(0, 10));
	// console.log(array([0, 'strss', 6.3]));
	// console.log(array([false, 2.3, 3, 4, { ok: 5 }], 'int8'));
	// console.dir(array([false, true]).dtype == _dtype('boolean'));
	// console.log(
	// 	arange(2 * 3 * 4 * 5)
	// 		.reshape(2, 3, 4, 5)
	// 		.at(0, 0, 0, 0, null)
	// );
	// let a = {};
	// a[array(12)] = 55;
	// a[12] = 51;
	// console.log(a);
});

process.env.PRODUCTION ||
	tester
		.add(
			'ndarray.get',
			() => {
				let x = arange(120).reshape(4, 6, 5, 1);
				return x.at(array([0, 2, 1]), slice(), [0, 2, 4], slice());
			},
			() =>
				array([
					[[0], [5], [10], [15], [20], [25]],
					[[62], [67], [72], [77], [82], [87]],
					[[34], [39], [44], [49], [54], [59]],
				])
		)
		.add(
			'ndarray.get',
			() => {
				let x = arange(120).reshape(4, 6, 5, 1);
				return x.at(array([0, 2, 1]), array([0, 2, 1]), slice(), slice());
			},
			() =>
				array([
					[[0], [1], [2], [3], [4]],
					[[70], [71], [72], [73], [74]],
					[[35], [36], [37], [38], [39]],
				])
		)
		.add(
			'ndarray.get',
			() => {
				let x = arange(120).reshape(4, 6, 5, 1);
				return x.at(slice(), array([0, 2, 1]), array([0, 2, 4]), slice());
			},
			() =>
				array([
					[[0], [12], [9]],
					[[30], [42], [39]],
					[[60], [72], [69]],
					[[90], [102], [99]],
				])
		)
		.add(
			'ndarray.get',
			() => {
				let x = arange(120).reshape(4, 6, 5, 1);
				return x.at(slice(), array([0, 2, 1]), slice(), [0, 0, 0]);
			},
			() =>
				array([
					[
						[0, 1, 2, 3, 4],
						[30, 31, 32, 33, 34],
						[60, 61, 62, 63, 64],
						[90, 91, 92, 93, 94],
					],
					[
						[10, 11, 12, 13, 14],
						[40, 41, 42, 43, 44],
						[70, 71, 72, 73, 74],
						[100, 101, 102, 103, 104],
					],
					[
						[5, 6, 7, 8, 9],
						[35, 36, 37, 38, 39],
						[65, 66, 67, 68, 69],
						[95, 96, 97, 98, 99],
					],
				])
		)
		.add(
			'ndarray.get',
			() => {
				let x = arange(120).reshape(4, 6, 5, 1);
				return x.at(slice(), slice(), array([0, 2, 1]), array([0, 0, 0]));
			},
			() =>
				array([
					[
						[0, 2, 1],
						[5, 7, 6],
						[10, 12, 11],
						[15, 17, 16],
						[20, 22, 21],
						[25, 27, 26],
					],
					[
						[30, 32, 31],
						[35, 37, 36],
						[40, 42, 41],
						[45, 47, 46],
						[50, 52, 51],
						[55, 57, 56],
					],
					[
						[60, 62, 61],
						[65, 67, 66],
						[70, 72, 71],
						[75, 77, 76],
						[80, 82, 81],
						[85, 87, 86],
					],
					[
						[90, 92, 91],
						[95, 97, 96],
						[100, 102, 101],
						[105, 107, 106],
						[110, 112, 111],
						[115, 117, 116],
					],
				])
		)
		.add(
			'ndarray.get',
			() => {
				let x = arange(120).reshape(4, 6, 5, 1);
				return x.at(array([0, 2, 1]), slice(), slice(), array([0, 0, 0]));
			},
			() =>
				array([
					[
						[0, 1, 2, 3, 4],
						[5, 6, 7, 8, 9],
						[10, 11, 12, 13, 14],
						[15, 16, 17, 18, 19],
						[20, 21, 22, 23, 24],
						[25, 26, 27, 28, 29],
					],
					[
						[60, 61, 62, 63, 64],
						[65, 66, 67, 68, 69],
						[70, 71, 72, 73, 74],
						[75, 76, 77, 78, 79],
						[80, 81, 82, 83, 84],
						[85, 86, 87, 88, 89],
					],
					[
						[30, 31, 32, 33, 34],
						[35, 36, 37, 38, 39],
						[40, 41, 42, 43, 44],
						[45, 46, 47, 48, 49],
						[50, 51, 52, 53, 54],
						[55, 56, 57, 58, 59],
					],
				])
		)
		.add(
			'ndarray.get',
			() => {
				let x = arange(120).reshape(4, 6, 5, 1);
				return x.at(array([0, 2, 1]), array([0, 2, 1]), slice(), 0);
			},
			() =>
				array([
					[0, 1, 2, 3, 4],
					[70, 71, 72, 73, 74],
					[35, 36, 37, 38, 39],
				])
		)
		.add(
			'ndarray.get',
			() => {
				let x = arange(120).reshape(4, 6, 5, 1);
				return x.at(array([0, 2, 1]), slice(), slice(), 0);
			},
			() =>
				array([
					[
						[0, 1, 2, 3, 4],
						[5, 6, 7, 8, 9],
						[10, 11, 12, 13, 14],
						[15, 16, 17, 18, 19],
						[20, 21, 22, 23, 24],
						[25, 26, 27, 28, 29],
					],
					[
						[60, 61, 62, 63, 64],
						[65, 66, 67, 68, 69],
						[70, 71, 72, 73, 74],
						[75, 76, 77, 78, 79],
						[80, 81, 82, 83, 84],
						[85, 86, 87, 88, 89],
					],
					[
						[30, 31, 32, 33, 34],
						[35, 36, 37, 38, 39],
						[40, 41, 42, 43, 44],
						[45, 46, 47, 48, 49],
						[50, 51, 52, 53, 54],
						[55, 56, 57, 58, 59],
					],
				])
		)
		.add(
			'ndarray.get',
			() => {
				let a = new NDArray([2, 5], [...Array(10).keys()]);
				return a.at(slice(), slice([, , -1]));
			},
			() => [
				[4, 3, 2, 1, 0],
				[9, 8, 7, 6, 5],
			]
		)
		.add(
			'ndarray.get',
			() => {
				let a = new NDArray([2, 5], [...Array(10).keys()]);
				return a.at(slice(-1), 3);
			},
			() => [8]
		)
		.add(
			'ndarray.get',
			() => {
				let a = new NDArray([2, 5], [...Array(10).keys()]);
				return a.at(slice([, , -1]), slice([, , -1]));
			},
			() => [
				[9, 8, 7, 6, 5],
				[4, 3, 2, 1, 0],
			]
		)
		.add(
			'ndarray.get',
			() => {
				let x;
				x = array([
					[3, 0, 0],
					[0, 4, 0],
					[5, 6, 0],
				]).at(slice(), slice(), null);
				return x;
			},
			() =>
				array([
					[[3], [0], [0]],

					[[0], [4], [0]],

					[[5], [6], [0]],
				])
		)
		.add(
			'ndarray.get',
			() => {
				let x;
				x = array([
					[3, 0, 0],
					[0, 4, 0],
					[5, 6, 0],
				]).at(slice('...'), null);
				return x;
			},
			() =>
				array([
					[[3], [0], [0]],

					[[0], [4], [0]],

					[[5], [6], [0]],
				])
		)
		.add(
			'ndarray.get',
			() =>
				arange(2 * 3 * 4)
					.reshape(2, 3, 4)
					.at(null, '...', null),
			() =>
				array([
					[
						[
							[[0], [1], [2], [3]],
							[[4], [5], [6], [7]],
							[[8], [9], [10], [11]],
						],
						[
							[[12], [13], [14], [15]],
							[[16], [17], [18], [19]],
							[[20], [21], [22], [23]],
						],
					],
				])
		)
		.add(
			'ndarray.get',
			() =>
				arange(2 * 3 * 4)
					.reshape(2, 3, 4)
					.at(slice(':'), [0, 2], slice('::2')),
			() =>
				array([
					[
						[0, 2],
						[8, 10],
					],
					[
						[12, 14],
						[20, 22],
					],
				])
		)
		.add(
			'ndarray.get',
			() =>
				arange(2 * 3 * 4)
					.reshape(2, 3, 4)
					.at([1, 0], 0).shape,
			() => [2, 4]
		);

process.env.PRODUCTION ||
	tester.add(
		'ndarray.item',
		() => {
			let a = new NDArray([2, 5], [...Array(10).keys()]);
			return a.at(slice([, , -1]), slice([, , -1])).item(5);
		},
		() => 4
	);

process.env.PRODUCTION ||
	tester.add(
		'ndarray.itemset',
		() => {
			let x = new NDArray(
				[3, 3],
				[
					[2, 2, 6],
					[1, 3, 6],
					[1, 0, 1],
				].flat()
			);
			x.itemset(4, 0);
			x.itemset([2, 2], 9);
			return x;
		},
		() => [
			[2, 2, 6],
			[1, 0, 6],
			[1, 0, 9],
		]
	);

process.env.PRODUCTION ||
	tester
		.add(
			'ndarray.array',
			() => array(1).array(),
			() => 1
		)
		.add(
			'ndarray.array',
			() => array([1]).array(),
			() => [1]
		)
		.add(
			'ndarray.array',
			() => array([]).array(),
			() => []
		)
		.add(
			'ndarray.array',
			() => array([[[]]]).array(),
			() => [[[]]]
		)
		.add(
			'ndarray.array',
			() => array([[], [], []]).array(),
			() => [[], [], []]
		)
		.add(
			'ndarray.array',
			() => ones([2, 3, 0, 2, 1]).array(),
			() => [
				[[], [], []],
				[[], [], []],
			]
		);

process.env.PRODUCTION ||
	tester
		.add(
			'ndarray.set',
			() => {
				let a = new NDArray([2, 5], [...Array(10).keys()]);
				a.at(slice(), slice(1, -1)).set(10);
				return a;
			},
			() => [
				[0, 10, 10, 10, 4],
				[5, 10, 10, 10, 9],
			]
		)
		.add(
			'ndarray.set',
			() => {
				let a = arange(2 * 3 * 4).reshape(2, 3, 4);
				a.set([greater_equal(a, 10)], -1);
				return a;
			},
			() =>
				array([
					[
						[0, 1, 2, 3],
						[4, 5, 6, 7],
						[8, 9, -1, -1],
					],
					[
						[-1, -1, -1, -1],
						[-1, -1, -1, -1],
						[-1, -1, -1, -1],
					],
				])
		)
		.add(
			'ndarray.set',
			() => {
				let a = arange(2 * 3 * 4).reshape(2, 3, 4);
				a.set([':', [true, false, true]], [-1, -2, -3, -4]);
				return a;
			},
			() =>
				array([
					[
						[-1, -2, -3, -4],
						[4, 5, 6, 7],
						[-1, -2, -3, -4],
					],
					[
						[-1, -2, -3, -4],
						[16, 17, 18, 19],
						[-1, -2, -3, -4],
					],
				])
		)
		.add(
			'ndarray.set',
			() => {
				let a = arange(2 * 3 * 4).reshape(2, 3, 4);
				a.set(
					[
						':',
						[
							[true, false, true, false],
							[false, false, true, true],
							[false, true, true, true],
						],
					],
					[1, 2, 3, 4, 5, 6, 7]
				);
				return a;
			},
			() =>
				array([
					[
						[1, 1, 2, 3],
						[4, 5, 3, 4],
						[8, 5, 6, 7],
					],

					[
						[1, 13, 2, 15],
						[16, 17, 3, 4],
						[20, 5, 6, 7],
					],
				])
		)
		.add(
			'ndarray.set',
			() => {
				let a = arange(2 * 3 * 4).reshape(2, 3, 4);
				a.set(
					[
						':',
						[
							[true, false, true, false],
							[false, false, true, true],
							[false, true, true, true],
						],
					],
					-1
				);
				return a;
			},
			() =>
				array([
					[
						[-1, 1, -1, 3],
						[4, 5, -1, -1],
						[8, -1, -1, -1],
					],

					[
						[-1, 13, -1, 15],
						[16, 17, -1, -1],
						[20, -1, -1, -1],
					],
				])
		);

process.env.PRODUCTION ||
	tester.add(
		'ndarray.flatten',
		() =>
			array([
				[1, 2],
				[3, 4],
			]).flatten(),
		() => array([1, 2, 3, 4])
	);

process.env.PRODUCTION ||
	tester
		.add(
			'ndarray.flat.set',
			() => {
				let x = array([
					[1, 2, 3],
					[4, 5, 6],
				]);
				x.flat = 3;
				return x;
			},
			() =>
				array([
					[3, 3, 3],
					[3, 3, 3],
				])
		)
		.add(
			'ndarray.flat.set',
			() => {
				let x = array([
					[1, 2, 3],
					[4, 5, 6],
				]);
				x.flat = [7, 8, 9, 10];
				return x;
			},
			() =>
				array([
					[7, 8, 9],
					[10, 7, 8],
				])
		);

process.env.PRODUCTION ||
	tester
		.add(
			'ndarray.copy',
			() => {
				let x = array([[1, 2, 3]]);
				return x.copy() == x;
			},
			() => false
		)
		.add(
			'ndarray.copy',
			() => {
				let x = array([[1, 2, 3]]);
				let y = x.copy();
				y.itemset(0, -1);
				return [x, y];
			},
			() => [array([[1, 2, 3]]), array([[-1, 2, 3]])]
		);

process.env.PRODUCTION ||
	tester
		.add(
			'ndarray.resize',
			() => {
				let a = array([
					[0, 1],
					[2, 3],
				]);
				a.resize([2, 1]);
				return a;
			},
			() => array([[0], [1]])
		)
		.add(
			'ndarray.resize',
			() => {
				let a = array([
					[0, 1],
					[2, 3],
				]);
				a.resize([2, 3]);
				return a;
			},
			() =>
				array([
					[0, 1, 2],
					[3, 0, 0],
				])
		)
		.add(
			'ndarray.resize',
			() => {
				let a = array([
					[0, 1],
					[2, 3],
				]);
				let c = a;
				a.resize([1, 1]);
				return [a, c];
			},
			() => [array([[0]]), array([[0]])]
		);