NumPy Compliance & Compatibility
NumSharp's compatibility target is NumPy 2.x, with the implementation and differential-test oracle currently pinned to NumPy 2.4.2. The target is both API compatibility and behavioral compatibility: dtype promotion, shape, strides, views, broadcasting, exceptions, special values, and output bytes all matter.
NumSharp is not claiming that every NumPy API is implemented or that the existence of a method proves exact behavior. This page separates three questions that used to be conflated:
- Is the public API present? The compiled assembly answers this through the generated NumPy API Coverage & Support dashboard.
- Does the behavior match? Unit tests and committed NumPy-oracle cases answer this per operation, dtype, layout, and edge case. See Unit Tests & Oracle.
- Does the operation need an optional backend? Most operations do not. Matrix factorizations do; the distinction is documented below.
The authoritative NumPy implementation is the pinned clone at refs/numpy/. Documentation and
tests are derived from that source and from running NumPy 2.4.2, rather than from memory or from
older NumSharp behavior.
Current API Snapshot
The checked-in coverage artifact reflects the compiled NumSharp assembly against NumPy 2.4.2.
Its default scope is top-level np.*, public ndarray members, and the np.random, np.linalg,
and np.fft callables.
| Surface | Available | Partial | Missing | Total |
|---|---|---|---|---|
np.* |
312 | 1 | 77 | 390 |
ndarray.* |
66 | 1 | 3 | 70 |
np.random.* |
51 | 0 | 0 | 51 |
np.linalg.* |
31 | 0 | 0 | 31 |
np.fft.* |
18 | 0 | 0 | 18 |
| Default scope | 478 | 2 | 80 | 560 |
That is 85.4% public-API availability, not an 85.4% behavioral-parity score. A row is "available" when a compiled public mapping exists; exact dtype, signature, layout, exception, and numeric parity still require tests. NumSharp-only extensions are catalogued separately and do not inflate the denominator.
The dashboard is generated by coverage/generate_coverage.py; it is the exhaustive source for
the current 80 missing APIs and the two reviewed partial mappings. This page summarizes capability
boundaries instead of maintaining a second hand-written function inventory.
NumPy 2.x Type Promotion (NEP 50)
NumPy 1.x sometimes inspected a Python scalar's value when choosing a dtype. The historical NEP 50 example is:
# NumPy 1.x behavior
np.result_type(np.int8, 1) # int8
np.result_type(np.int8, 255) # int16: the value does not fit in int8
NumPy 2.x treats Python int, float, and complex values as weakly typed. Precision is chosen
from the strongly typed array or NumPy scalar, subject to kind promotion and an in-range check.
For example, NumPy 2.4.2 and NumSharp both keep this operation at uint8:
var a = np.array(new byte[] { 1, 2, 3 });
var b = a + 255;
// b.dtype == np.uint8
// b is [0, 1, 2] because uint8 arithmetic wraps
The corresponding NumPy array operation does not emit an overflow warning. NumPy's scalar
expression np.uint8(1) + 255 does emit a RuntimeWarning; that is a different scalar execution
path.
NumSharp's binary-operator and result_type paths implement NEP 50 promotion. C# primitive
scalar arguments are the analogue of weak Python literals; an NDArray or explicit dtype remains
strongly typed. Reductions also apply NumPy's accumulator rules, such as sum(int32) -> int64,
while width-preserving operations such as abs(int32) remain int32.
Promotion is still verified per operation. The repository contains dedicated NEP 50 matrices and NumPy-generated oracle cases; the statement above does not imply that an untested API is correct merely because it calls a shared promotion helper.
NumPy 2.x API Additions and C# Spellings
The additions that the previous version of this page listed as missing are implemented:
| NumPy 2.x API | NumSharp mapping |
|---|---|
np.concat |
Public np.concat overloads |
np.permute_dims |
Public np.permute_dims API |
np.isdtype |
Public overloads for NPTypeCode, Type, and NDArray |
np.unique_values |
Returns an NDArray |
np.unique_counts |
Returns values and counts |
np.unique_inverse |
Returns values and inverse indices |
np.unique_all |
Returns values, indices, inverse, and counts |
ndarray.mT |
View that swaps the last two axes |
ndarray.device |
Returns "cpu" |
ndarray.to_device |
Accepts only the CPU device |
Some reviewed mappings deliberately use an established NumSharp or C# spelling. np.acos,
np.asin, and np.atan map to np.arccos, np.arcsin, and np.arctan; np.pow maps to
np.power; and NumSharp's np.round_ maps to current NumPy's np.round. The coverage dashboard
labels these as aliases instead of pretending that their names are exact.
Removed NumPy 1.x aliases are not counted as current NumPy 2.4.2 requirements. Ported code should
prefer the current names such as prod, any, and all.
Dtypes
NumSharp's array engine has 15 core dtypes. Twelve map directly to real NumPy dtypes, one maps
to complex128, and two are .NET-specific extensions.
NPTypeCode |
C# storage type | NumPy analogue | Important boundary |
|---|---|---|---|
Boolean |
bool |
bool_ |
Boolean is not in NumPy's numeric type hierarchy |
SByte |
sbyte |
int8 |
Signed 8-bit integer |
Byte |
byte |
uint8 |
Unsigned 8-bit integer |
Int16 |
short |
int16 |
|
UInt16 |
ushort |
uint16 |
|
Int32 |
int |
int32 |
|
UInt32 |
uint |
uint32 |
|
Int64 |
long |
int64 |
|
UInt64 |
ulong |
uint64 |
|
Half |
System.Half |
float16 |
Scalar arithmetic where the BCL has no vector arithmetic |
Single |
float |
float32 |
SIMD-capable on supported operations |
Double |
double |
float64 |
SIMD-capable on supported operations |
Complex |
System.Numerics.Complex |
complex128 |
No native complex64 storage dtype |
Char |
char |
no general analogue | Treated as an unsigned 16-bit value; .npy maps it to <U1 |
Decimal |
decimal |
none | .NET extension; cannot be written as a NumPy .npy dtype |
This table describes the dtype matrix used by numerical operations. It does not promise that every
operation accepts all 15 dtypes; support is tested and documented per operation. NPTypeCode.String
remains as a legacy compatibility code used by older string-conversion helpers, but it is outside
that 15-dtype operation matrix and does not provide a NumPy string-array dtype.
NumPy dtype families not represented natively
complex64has no NumSharp storage dtype. A.npyc8array can be read, but its values widen toSystem.Numerics.Complex; writes usec16.datetime64andtimedelta64, including their unit metadata, are not implemented.- Object, structured, subarray, and void dtypes are not implemented.
- Byte strings, fixed-width Unicode strings wider than
<U1, and NumPy 2.xStringDTypeare not implemented. float128/longdoubleandcomplex256/clongdoublehave no corresponding .NET storage type.
See Dtypes for scalar conversion, promotion, special-value, and casting details.
Memory Layout, Strides, and Views
NumSharp is not C-order-only. Shape records dimensions, element strides, a base offset, and
cached C-contiguous, F-contiguous, ownership, alignment, writeability, and broadcast flags.
- Fresh arrays default to C order, and APIs that accept
order: 'F'can allocate Fortran-order arrays.np.asfortranarrayis implemented. - Transposes, negative-stride slices, stepped slices, and other non-contiguous views retain their strides and offset instead of being silently densified.
- Slicing returns a view that shares the underlying buffer. Call
.copy()when independent storage is required. - Broadcast views use zero strides and are read-only, matching NumPy's protection against writing one physical element through many logical coordinates.
- Order-aware creation and conversion paths resolve the relevant
C,F,A, orKmode to physical C or F storage. Arbitrary non-contiguous stride patterns remain views; they are not a third allocation order.
Kernel paths distinguish contiguous, strided, broadcast, and offset views. Correctness tests cover C- and F-contiguous arrays, transposes, positive and negative strides, sliced offsets, and broadcast reads. See Broadcasting and Buffering & Memory.
Python Array API Features
NumSharp uses the Array API features that NumPy exposes in its main namespace as compatibility targets, but it does not claim independent certification as a Python Array API implementation. The C# public surface cannot be substituted for a Python namespace object without an interop layer.
The formerly missing Array API additions isdtype, concat, permute_dims, the four unique_*
functions, mT, device, and to_device are present. NumSharp is CPU-only: device is "cpu",
creation functions that expose a device argument accept only null or "cpu", and to_device
cannot transfer to a GPU.
The generated NumPy coverage dashboard is the current inventory. The old hand-maintained claim of "74% Array API coverage" and its 133-function denominator have been removed because they mixed a particular specification revision with NumPy API aliases and became stale.
Random Number Generation
NumSharp implements both NumPy random APIs:
The legacy
np.random/RandomStatesurface uses MT19937, including NumPy-compatible state, array seeding, cached Gaussian state, and legacy distribution algorithms (randint's masked per-dtype sampler,shuffle/permutation/choicewith the legacy messages and the Clongdtype, modelled as int64 — see below). An unseededRandomState()draws OS entropy throughSeedSequence, as NumPy's does.Every legacy distribution is a line-by-line port of NumPy's frozen
legacy-distributions.c(and the shareddistributions.csamplers it calls: Poisson PTRS, BTPE binomial, HRUA hypergeometric,random_loggam, …), soRandomState(seed).<method>(...)is byte-identical to NumPy for every sampler, includinggammawithshape < 1,binomial,negative_binomial,f,pareto,standard_cauchy,multinomial(which sharesbinomial's setup cache, as NumPy's does) anddirichlet. Parameters are validated with NumPy's constraints and messages (ValueError: scale < 0,-0.0rejected where NumPy tests the sign bit, NaN accepted where NumPy accepts it) before anything is drawn.multivariate_normalfollows NumPy's SVD algorithm and is byte-identical whenNumSharp.Interop.OpenBLASsupplies LAPACK'sgesdd; without it a managed Jacobi SVD stands in and a singular vector's sign can differ.The legacy integers are C
longin NumPy, and NumSharp models ONE width for all of them: a 64-bitlong, NumPy's Linux/macOS (LP64) build, which it matches exactly. That coversrandint's default dtype,random_integers,permutation(n),choice's indices andmultinomial's counts (all int64), the discrete samplers, and the integer parameters (multinomial'sn,binomial'sn, the counts ofhypergeometric). NumPy's Windows build (32-bitlong) returns the same values as int32 wherever both builds accept the input, but it rejects bounds past2**31(randint(0, 2**40),random_integers(0, 2**31),choice(2**40)), raises formultinomial(2**31, …), and differs where that width overflows inside a sampler (zipfneara = 1,poissonabove2**31,tomaxint's range,negative_binomialwith an infinite mean).permutation(n)also reproduces NumPy's arange length arithmetic at the int64 edge: a length whose double rounds to2**63gives an empty range, as on NumPy's x86 builds.RandomState(bit_generator)runs the legacy samplers on any bit generator (np.random.RandomState(new PCG64(42))), with NumPy'sstr()(RandomState(PCG64)),seed()refused on a non-MT19937 engine (TypeError), and both state forms:get_state()returns the legacy MT19937 tuple,get_state(legacy: false)the dict (NumPyRandom.State: the bit generator's state plus the cached Gaussian), andset_stateaccepts either. Consuming the cached Gaussian zeroes it, as NumPy'slegacy_gaussdoes.get_bit_generator()andset_bit_generator(bitgen)read and hot-swap thenp.randomsingleton's engine (the swap discards the cached Gaussian), and after a swap the module'snp.random.seed(x)re-seeds the new engine as NumPy's module function does (engine.state = type(engine)(x).state, the cached Gaussian kept) — only that singleton: any otherRandomStateover a non-MT19937 engine refuses to re-seed, NumPy's method behaviour.Seedrecords the last legacy seed as auint, NumPy's[0, 2**32 - 1].tomaxint,ranfandsample(each also taking the size as oneShape, as NumPy'srandom_sample(size)does) complete the legacy surface. Two NumPy inputs never return (zipf(a)fora >= 1025,vonmisesonce4*kappa^2overflows); NumSharp returns the distribution's limit there instead of hanging.Sized legacy draws are fast without changing a bit.
- Read-ahead uniforms. They come through a buffer bulk-filled by the bit generator's vectorized
FillDouble, which never draws past what NumPy's per-value calls would. - Setup hoisted. Per-parameter setup that NumPy recomputes for every value runs once per call: Marsaglia-Tsang's
1/sqrt(9b),exp(-lam)and the PTRS constants,log(1-p),2^(a-1), the von Mises envelope, and HRUA'ssqrtand fourloggams. - Memoized sub-terms. Deterministic sub-terms are memoized: the binomial inversion's CDF walk, BTPE's Step50 and Step52 bounds, HRUA's per-candidate
loggamsums, zipf's acceptance power and the small-sample urn walk's ratios.multinomialalso memoizes the per-category binomial setups that NumPy's single-entry cache thrashes on. - Two-phase Gaussian fills. The polar Gaussian fills (
standard_normal,normal,lognormal,standard_cauchy,wald) run in two phases, so theirlog/sqrtchains overlap.
Every memoized value is the same expression over the same inputs, so the streams stay byte-identical. Measured against NumPy 2.4.2 (NPY/NS, one pinned P-core, turbo off, min over repeated sweeps):
- Most samplers: 1.5–5.6×. HRUA hypergeometric 4–5.6×, zipf 2–2.7×, poisson 1.6–2.3×,
lognormal~2×, the gamma family, chi-square, F and t with shapes ≥ 1 at 1.5–1.7×. - BTPE-sized
binomial, andmultinomialwith such categories: ~1.4–1.5×. - CRT-bound samplers: 1.2–1.5×. This covers
weibull,power,vonmisesand gamma with shape < 1 (Johnk's algorithm), plus what builds on that gamma:betawith a shape ≤ 1,standard_twithdf < 2,negative_binomialwithn < 1anddirichletwithalpha < 1. Their remaining work is the same MSVCpow/log/exp/coscall NumPy makes per value, and a bit-identical faster one does not exist.
- Read-ahead uniforms. They come through a buffer bulk-filled by the bit generator's vectorized
np.random.default_rng(seed)returns aGeneratorbacked byPCG64andSeedSequence, with the modern ziggurat and bounded-integer algorithms used by NumPy 2.4.2.The
Generatorhas NumPy's whole distribution surface, on NumPy's modern algorithms rather than the legacy ones. That coversbeta,binomial,chisquare,dirichlet,f,geometric,gumbel,hypergeometric,laplace,logistic,lognormal,logseries,multinomial,multivariate_hypergeometric,multivariate_normal,negative_binomial,noncentral_chisquare,noncentral_f,pareto,poisson,power,rayleigh,standard_cauchy,standard_t,triangular,vonmises,wald,weibullandzipf. The modern details are reproduced:- Ziggurat normals and exponentials under the gamma family.
- beta's tiny-shape and log-space paths.
- The HRUA hypergeometric with its
logfactorialtable, and an int64 zipf with its acceptance window. - vonmises' wrapped-normal fallback above
kappa = 1e6. - noncentral chi-square's NaN short-circuit.
- Int64 integer outputs.
Every sampler matches
default_rng(seed)bit for bit. The oracle checks 100 branch-reaching parameter sets, 222 constraint corners (NumPy's messages and check order), 66 streams of 20000 draws and 185 byte-parity corpus cases.multinomialhas NumPy's vector path: counts broadcast against the probabilities' leading axes, withsizerequired to equal the broadcast shape. Array parameters pass NumPy's'safe'casting gate, so a float or uint64 count, or complex probabilities, raise NumPy'sTypeError. Two documented exceptions:paretoandpower. They call the C runtime'sexpm1. NumSharp reproduces it exactly outside[-ln 2, ln 1.5], but inside that band Windows' ucrtbase uses a closed approximation. Values there can differ by up to 2 ulp forpareto.powerraises the result to1/a, which scales the difference: measured ≤1 ulp ata = 2.5and 9 ulp ata = 0.3.multivariate_normal. It is byte-identical withNumSharp.Interop.OpenBLAS, which supplies NumPy's owngesdd/syevd/potrf. Without it, managed factorizations are exact for diagonal covariances and agree to rounding forcholesky. Forsvd/eighthey may pick the other sign of an eigenvector: still a valid sample, but not NumPy's.
The sized draws use the legacy sampler's techniques (read-ahead draws, per-fill setups, memoized deterministic sub-terms) without changing a bit. Two are specific to the modern algorithms: HRUA's setup is hoisted and each candidate's
logfactorialsum memoized, and the urn walk keeps its 32-bit read-ahead in registers. Measured against NumPy 2.4.2 (NPY/NS, one pinned P-core, turbo off, min over repeated sweeps):- 1.5–3.1×.
zipf2.3–3.1×,standard_cauchy2.3–2.5×,multinomial2.1–2.3×, HRUAhypergeometric2.1–2.2×,geometricinversion 2–2.3×,multivariate_normal2–2.3×,standard_t2×,lognormal1.9×,noncentral_fandnoncentral_chisquare(df > 1) 1.8–1.9×,dirichlet(alpha >= 0.1) up to 1.8×, andchisquare,f,betawith shapes above 1 and small-meanbinomialat 1.5–1.7×. - 1.1–1.5×. BTPE
binomial1.4–1.5×, the urn-walkhypergeometric1.35–1.4×,logseries,poisson, thegeometricsearch,triangular,wald,laplace,gumbel,logisticandrayleigh,multivariate_hypergeometric1–1.3×. - About 1× (the CRT ceiling).
pareto,power,weibull,vonmises, and Johnk's gamma/beta (a shape below 1), with what builds on them:dirichletwith tiny alphas,noncentral_chisquarewithdf <= 1,negative_binomial. They measure 0.96–1.2×. Each value's cost is thepow/log/exp/cos/expm1call NumPy makes too. multinomial's broadcast path when every position brings a new(count, probability)pair: about 0.9×. This is the same per-position BTPE setup NumPy runs. When pairs recur, it measures 2.2×.
Every sampler that takes parameters accepts them as arrays, on both
RandomStateandGenerator, broadcast the way NumPy broadcasts them. That coversbeta,binomial,chisquare,exponential,f,gamma,geometric,gumbel,hypergeometric,laplace,logistic,lognormal,logseries,negative_binomial,noncentral_chisquare,noncentral_f,normal,pareto,poisson,power,rayleigh,standard_gamma,standard_t,triangular,uniform,vonmises,wald,weibullandzipf:rs.normal(locs, 2.0),g.poisson(column, new Shape(k, k)). The flow is NumPy'scont/disc:- Each parameter converts under NumPy's
'safe'rule, so a complex parameter or a float count raises NumPy'sTypeError. When every parameter is 0-d the call takes the scalar path, so a 0-d array behaves exactly like a number. Anullstands for Python'sNone: the default where NumPy has one (loc,scale,lam, …), otherwise NumPy'sNonehandling. - Constraints are checked over the whole array before anything is drawn, in NumPy's order and with its
array messages:
lam value too largefor a NaN mean,-0.0rejected wherever NumPy tests the sign bit. - The output shape is
sizewhen given, else the parameters' broadcast shape. NumPy'sshape mismatchandOutput size (1,) is not compatible with broadcast dimensions of inputs (3,).errors are reproduced;defaultstands forsize=NoneandShape.Scalarforsize=(). A givensizeis allocated before the parameters are broadcast against it, as NumPy'snp.empty(size)is. So a size that is too big to allocate and also incompatible reportsarray is too big, not the shape mismatch. - One value is drawn per output position, in C order, with that position's parameters, so the values and the stream position after the call are byte-identical to NumPy. The oracle replays 834 legacy and 851 Generator array-parameter cases, 507 of them NumPy's constraint and broadcast errors. A mutation sweep over the read-ahead decisions, the constraint scans and the per-run setup sharing is caught in full.
- The legacy samplers keep mtrand's own rules where they differ from the Generator's.
uniformaccepts reversed bounds (only the range must be finite),hypergeometricchecksngood + nbad < nsamplefirst and has no10^9caps,negative_binomialhas no Poisson-mean bound, andbinomial'snis a Clong.laplace,gumbelandlogisticrun the modern samplers on the MT19937 stream, as mtrand does. - Legacy integer results model NumPy's LP64
long, as the scalar samplers do. NumPy's Windows build (32-bitlong) truncates or rejects some of those inputs, and its legacy HRUAhypergeometricnever returns for a population past2**31(hypergeometric(2e9, 1.5e9, 12)). - NumPy's legacy samplers never return for two inputs:
zipfwitha >= 1025andvonmiseswithkappa >= 2**511, where4*kappa*kappaoverflows. NumSharp returns the distribution's limit there without drawing, as its scalar samplers do.
Measured against NumPy 2.4.2 (NPY/NS, one pinned P-core, turbo off, best of 7, 1K / 100K / 1M values):
- Parameters that repeat along the walk: the scalar samplers' speed. This is a column against a row, or
a parameter stretched over a larger
size. It is drawn run by run, each run of equal parameters like a scalar fill: NumPy's per-call statements are computed once per run, and a long run also gets HRUA's, zipf's and the urn walk's memos. Most samplers measure 1.5–5.3× (legacy HRUAhypergeometric5.3×,uniform3.6–4.4×,zipf2.9×,binomial2.6–2.8×,normal2.4–3.6×). The Generator'spoisson,f,waldandnegative_binomialmeasure 1.1–1.4×, and the CRT-boundvonmises,powerandpareto1.06–1.35×, as their scalar fills do. - One transform per value: 1.3–4.4×. This covers
normal,lognormal,exponential,uniform,laplace,logistic,gumbel,rayleigh,triangular,waldandstandard_t, with any parameter layout. - A new parameter at every position: 0.88–1.8×, per-call parity. NumPy recomputes each sampler's
per-call setup for every value (
sqrt/log/exp, and PTRS's, BTPE's or HRUA's constants), and so does NumSharp. The fill's advantages (shared setups, memos) do not apply. The Generator's cells sit at the low end because NumPy's PCG64 call path is cheap:hypergeometric0.88–1.1×,poisson0.91–1×, andbeta,zipf,logseries,vonmisesandpower0.95–1.2×. Legacybinomialmeasures 0.91–1×. Its setups go through RandomState's single-entry binomial cache, kept exactly as NumPy's because what one call leaves in it is visible to the next. The Generator'sbinomialkeeps a per-fill memo of recurring keys and measures 1.2–2.4×.
- Each parameter converts under NumPy's
All five NumPy bit generators exist —
PCG64,PCG64DXSM,Philox,SFC64andMT19937— each aBitGeneratorwith NumPy's surface:random_raw,state(typedPCG64.State,Philox.State, …),seed_seq,lockandspawn. NumPy's spellingnp.random.Generator(np.random.Philox(seed))works verbatim —np.randomcarries a factory for every class NumPy's module exposes (MT19937,PCG64,PCG64DXSM,Philox,SFC64,SeedSequence,Generator), each mirroring every constructor overload — as doesnew Generator(new Philox(seed)), and either reproduces NumPy's stream byte for byte.PCG64,PCG64DXSMandPhiloxhaveadvance(delta)(anyBigInteger, wrapped like NumPy'swrap_int) andjumped(n);Philoxalso takes an explicitkey/counter(new Philox(key: 5), NumPy's integer and array forms and error texts included). As in NumPy,new MT19937(42)seeds throughSeedSequence; the legacy stream ofRandomState(42)ismt._legacy_seeding(42), andMT19937.jumped(n)advances a copy byn * 2**128draws with NumPy's jump polynomial.SeedSequencehas NumPy's whole surface, with NumPy's types:spawn(n)(childspawn_keys — Python ints, soBigIntegers — andn_children_spawned, auint32_t),pool_size(aPy_ssize_t:long),poolandgenerate_state(n_words, dtype)(uint32/uint64NDArrays over a 64-bit count, with NumPy's allocation errors),state,entropy(the 128-bit OS entropy of an unseeded sequence, to log for reproducibility) and NumPy's repr. Entropy and spawn keys coerce exactly as NumPy's_coerce_to_uint32_arraydoes: nested sequences flatten, a"0x…"string is hex and a string that starts with a digit is decimal ("012"is 12 — there is no octal reading), a string key is a sequence of one-character strings.BitGenerator.spawn(n)andGenerator.spawn(n)derive independent children from it — the recommended way to hand streams to parallel workers. AnyISeedSequenceis accepted as a bit generator's seed (aSeedlessSeedSequenceis refused as NumPy refuses it:seedless SeedSequences cannot generate state); every engine names its parameterseedas NumPy does, and a null one — anullliteral or a typed null array alike — is NumPy'sseed=None(fresh OS entropy).default_rngaccepts every NumPy seed form: an integer (up to anyBigInteger), a sequence or integer array, aSeedSequence, aBitGenerator(wrapped), aGenerator(passed through) or a legacyRandomState(its engine is wrapped, asdefault_rng(RandomState)does). Every overload names its parameterseed, NumPy's only parameter, and a null argument of any type isdefault_rng(None).Generator.integersand the legacyrandinttake NumPy's array bounds (g.integers(lows, highs),rs.randint(0, highs, dtype: np.uint8)) through NumPy's own broadcast path: each bound is checked against the dtype and against the other in NumPy's order and words (low is out of bounds for uint64,high <= 0,low >= high,cannot convert float NaN to integer) before anything is drawn, the draws continue one buffered stream across positions, and NumPy's two quirks carry over — asizesmaller than the bounds' broadcast takes the first positions, and 64-bit float bounds in a non-C layout come out in NumPy's scrambled order. Bounds pastlong/ulongareBigIntegers, NumPy's Python ints:g.integers(0, BigInteger.Pow(2, 64), dtype: np.uint64)is the full-range idiom, and anything beyond a dtype raises NumPy's out-of-bounds message.Every
GeneratorandRandomStatedraw holds the bit generator'slock, so one generator (or several over the same bit generator) can be shared between threads: each call consumes a contiguous piece of the one stream.The whole random surface is oracle-checked, overload by overload. Every public member — each overload of
RandomState,Generator, the five bit generators and theirStateclasses,SeedSequence,SeedlessSeedSequenceanddefault_rng, 493 in all (four NumSharp-only members exempt) — is replayed against NumPy 2.4.2 by its exact C# signature: every legacy sampler on the legacy MT19937 and onRandomState(engine)for each engine, everyGeneratormember on all five engines, under 10 fixed seeds, comparing NumPy's result AND the generator's full state after the call (so drawing one value too many or too few fails, not only a wrong value). CI gates prove the coverage itself — every overload, every parameter (omitted, non-default, null, every accepteddtype/methodvalue, NumPy's parameter names in NumPy's order), every engine, every seed — and a nightly job replays the same families under 10 fresh seeds. About 26,600 cases; the few intended differences (the modernpareto/powerwithin a few ULP where the Windows CRT's in-bandexpm1differs, and states NumPy would store and then read outside an array) are listed in the fuzz README. A replay that invokes overloads by reflection cannot tell whether NumPy's CALL compiles in C# and binds the right overload, so 1,793 of NumPy's spellings were also compiled verbatim and compared: every legacy sampler whose parameters all default takessize:alone or any keyword subset, anullseed binds everywhere, and NumPy'ssize=Noneis spelledsize: default(aShapeis a struct, sosize: nullcannot compile).
The familiar legacy example matches NumPy:
np.random.seed(42);
var a = np.random.rand(5);
// [0.37454012, 0.95071431, 0.73199394, 0.59865848, 0.15601864]
The oracle suite separates portable byte-parity cases from distributions whose transforms depend on the platform math library. MT19937/PCG64 bit streams and purely integer/exactly-rounded paths are hard-gated across hosts. Host-libm cases are byte-pinned on the matching Windows AMD64 host and are inconclusive elsewhere. Therefore, "same seed" is a tested stream guarantee for the corresponding NumPy API, but it is not a blanket promise that every transformed distribution has identical last bits on every operating system.
.npy and .npz Interoperability
The .npy implementation is a port of NumPy 2.4.2's format code and is tested against files written
by real NumPy. For supported dtypes and layouts, NumSharp's writer produces the same header and data
bytes as NumPy's writer.
- Format versions 1.0, 2.0, and 3.0 are read and written. Automatic writes choose the oldest format that can represent the header.
- C- and F-order files are supported. A strictly F-contiguous array is written with
fortran_order: Trueand loads as F-contiguous. - Little- and big-endian numeric files load. Big-endian data is converted to native byte order because NumSharp has no byte-swapped dtype metadata.
bool, all eight integer widths,float16,float32,float64,complex128, and<U1/Charhave round-trip mappings.complex64loads by widening tocomplex128.- Decimal cannot be saved because NumPy has no decimal dtype. Object/pickle, structured, subarray, datetime/timedelta, byte-string, void, and unsupported extended-precision descriptors are rejected with a capability-specific error.
np.load(..., mmap_mode: ...)supports read-only, read-write, and copy-on-write mapping for a.npyfile path. Streams and byte arrays cannot be memory-mapped.
np.savez and np.savez_compressed write uncompressed and deflated ZIP archives. np.load_npz
opens members lazily and caches loaded arrays; the returned NpzFile owns resources and should be
disposed. np.load detects .npy versus .npz from magic bytes and returns either NDArray or
NpzFile, while np.load_npy and np.load_npz provide typed entry points.
allow_pickle defaults to false. NumSharp cannot execute Python pickle data even when
allow_pickle: true; the flag changes validation/error behavior but does not add an object dtype or
an unpickler.
Linear Algebra and Optional OpenBLAS
NumSharp's numerical kernels in NumSharp.Core are implemented in managed C# and do not require a
third-party native library. Core does contain a Windows-only P/Invoke to VirtualAlloc/VirtualFree
for large zero-initialized buffer allocation; that is a memory-management fast path, not a numerical
backend. Matrix products such as dot, matmul, inner, vdot, vecdot, matvec, and vecmat
have managed implementations and remain available without an optional package.
The separate NumSharp.Interop.OpenBLAS package installs an IBlasBackend. It accelerates and, for
supported floating/complex routes, can reproduce NumPy's BLAS accumulation order. Referencing the
package is the normal opt-in; removing or disabling the backend leaves managed matrix products
available.
Matrix factorizations are different. Core does not carry managed LU, QR, SVD, or eigensolver kernels. These public APIs validate inputs and then require a backend implementation:
choleskydetandslogdeteig,eigvals,eigh, andeigvalshinvandsolvelstsqandqrsvdandsvdvals
The OpenBLAS package implements those operations through LAPACK. Without a serving backend they
throw OpenBlasMissingBackendException; they are not null-returning stubs. APIs built from an
SVD or solve, such as pinv, matrix_rank, negative matrix_power, and spectral/nuclear matrix
norms, inherit that backend requirement.
Pure compositions such as non-negative matrix_power, multi_dot, ordinary matrix products, and
non-SVD norm orders work in Core. See OpenBLAS interoperability for supported
dtypes, delivery, parity, and configuration details.
FFT
The complete 18-callable np.fft inventory is implemented in Core: complex, real, Hermitian, 1-D,
2-D, and N-D transforms, frequency helpers, and shift helpers. Compute runs through a managed port
of NumPy 2.4.2's vendored pocketfft engine; it does not require OpenBLAS.
The double/complex128 path preserves pocketfft's operation order and is covered by byte-parity
oracles across contiguous, transposed, strided, negative-stride, and broadcast-read inputs. NumSharp
evaluates pocketfft the way NumPy's x86-64 wheels do: each product rounded separately, with twiddle
factors from the platform's math library. On x86-64 it therefore matches the NumPy running beside it,
checked live on Windows and Linux. NumPy's own arm64 wheels are the exception. They are compiled with
the compiler's default floating-point contraction for a baseline that has fused multiply-add, so
pocketfft's twiddle and butterfly multiply-adds round once there. Most bins of an arm64 NumPy transform
sit an ULP or so from NumSharp's, which is the literal evaluation. Because the twiddles come from the
platform's math library, whose last bits differ between operating systems, a large transform's exact
bits can also differ between hosts. NumSharp's are no exception.
NumSharp also has a single-precision pocketfft engine, but it has no complex64 storage dtype:
float16/float32 transform results are therefore exposed as complex128, where NumPy exposes
complex64. The FFT oracle treats that as a documented dtype divergence while still checking the
values. As elsewhere, public API availability does not replace the per-dtype oracle evidence.
Execution Engine and SIMD
SIMD is not future roadmap work. Core emits runtime-specialized IL kernels and selects V128, V256,
or V512 paths when the operation, dtype, hardware, and layout permit it. Contiguous numeric arrays
take the fast paths; strided and broadcast operands use general iterator paths. Half, Complex,
and Decimal use scalar arithmetic where the .NET BCL has no suitable vector operation.
The optional OpenBLAS package is the only native numerical backend. The Windows virtual-memory calls described above only allocate and release buffers. Installing OpenBLAS does not replace the tensor engine; it fills the engine's settable backend seam, and unsupported operand combinations fall back to Core for matrix products.
Known Gaps
The generated dashboard is the exhaustive API list. The important capability-level gaps are:
- no native
complex64, datetime/timedelta, object, structured, or general string dtype; - no GPU device or DLPack import (
np.from_dlpackis missing); - missing floating-point handling APIs: error-state controls such as
geterrandseterr, plusnan_to_num; - missing histogram, window, and several numerical utility functions, including
histogram*,gradient,interp,hypot,nextafter, andsignbit; - partial mappings for NumPy's top-level
np.shapehelper and Python buffer-stylendarray.data; - matrix factorizations require the optional OpenBLAS/LAPACK backend.
When exact parity matters, search the coverage dashboard for API presence and the test dashboard for oracle depth. Do not infer support from this summary alone.
Reporting a Compatibility Difference
A useful report includes:
- The NumPy 2.4.2 code and observed output or exception.
- The equivalent NumSharp code and observed output or exception.
- Input dtype, shape, strides/layout, and whether the input is a view or broadcast array.
- Raw floating-point or complex bytes when the difference is below display precision.
The preferred fix is a test generated from real NumPy output, followed by an implementation that matches NumPy's source structure and handles contiguous, strided, broadcast, sliced-offset, empty, scalar, and dtype edge cases.
References
- NumPy API Coverage & Support - generated compiled-API inventory
- Unit Tests & Oracle - generated correctness-evidence inventory
- Dtypes - NumSharp dtype and casting details
- NumPy 2.0 migration guide
- NEP 50: Promotion rules for Python scalars
- NEP 52: Python API cleanup for NumPy 2.0
- NEP 56: Array API compatible functions in the main namespace
- NEP 1: A simple file format for NumPy arrays
- NumPy source v2.4.2