Skip to content

Commit 188e667

Browse files
author
peng.li24
committed
feat(init): add arange/linspace/logspace/geomspace/eye/identity/diag/empty
- numpy/init.h: add arange_size, arange, linspace, logspace, geomspace, eye, identity, diag_size, diag_from_vec, diag_from_mat - geomspace uses log10+logspace algorithm matching numpy bit-exactly - pycpp/init_py.h: py::object dispatch wrappers for all new functions; always return float64 (numpy 1.23 behaviour for scalar inputs) - tests/module.cpp: register new functions with single py::object overloads - tests/test_all.py: add 44 bit-exact tests for all new creation routines
1 parent 8d7ef6b commit 188e667

4 files changed

Lines changed: 507 additions & 3 deletions

File tree

numpy/init.h

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,25 @@
44
// Array initialisation / creation routines.
55
//
66
// numpy.zeros_like numpy.ones_like numpy.full
7+
// numpy.arange numpy.linspace numpy.logspace numpy.geomspace
8+
// numpy.eye numpy.identity
9+
// numpy.diag (build from 1-D / extract from 2-D)
710
//
811
// Recommended entry point: #include "numpy/numpy.h"
912
// Direct include is also valid for standalone use.
1013
// ════════════════════════════════════════════════════════════════════════════
1114
#pragma once
1215

1316
#include <cstddef>
17+
#include <cmath>
1418
#include <algorithm>
1519

1620
namespace numpy {
1721

22+
// ============================================================================
23+
// Fill helpers (used by zeros_like / ones_like / full)
24+
// ============================================================================
25+
1826
/// numpy.zeros_like(a, dtype=None, order='K', subok=True, shape=None)
1927
template<typename T>
2028
inline void zeros_like(T* dst, size_t n) {
@@ -33,4 +41,148 @@ inline void full(T* dst, size_t n, T fill_value) {
3341
std::fill_n(dst, n, fill_value);
3442
}
3543

44+
// ============================================================================
45+
// numpy.arange
46+
// ============================================================================
47+
48+
/// Number of elements produced by numpy.arange(start, stop, step).
49+
/// Uses double precision for the ceiling so float32 step errors don't
50+
/// cause an off-by-one (matches numpy's internal length computation).
51+
template<typename T>
52+
inline size_t arange_size(T start, T stop, T step) {
53+
if (step == T(0)) return 0;
54+
double n = std::ceil(
55+
(static_cast<double>(stop) - static_cast<double>(start))
56+
/ static_cast<double>(step));
57+
return (n > 0.0) ? static_cast<size_t>(n) : 0;
58+
}
59+
60+
/// numpy.arange([start,] stop[, step,], dtype=None)
61+
/// dst must have at least arange_size(start, stop, step) elements.
62+
/// Values: dst[i] = start + T(i)*step (index-based, not accumulated).
63+
template<typename T>
64+
inline void arange(T* dst, T start, T stop, T step) {
65+
size_t n = arange_size(start, stop, step);
66+
for (size_t i = 0; i < n; ++i)
67+
dst[i] = start + static_cast<T>(i) * step;
68+
}
69+
70+
// ============================================================================
71+
// numpy.linspace
72+
// ============================================================================
73+
74+
/// numpy.linspace(start, stop, num=50, endpoint=True, ...)
75+
/// Values: dst[i] = start + i * step where step = (stop-start)/(num-1 or num).
76+
/// When endpoint=true the last element is set to stop exactly.
77+
template<typename T>
78+
inline void linspace(T* dst, T start, T stop, size_t num,
79+
bool endpoint = true) {
80+
if (num == 0) return;
81+
if (num == 1) { dst[0] = start; return; }
82+
T div = static_cast<T>(endpoint ? num - 1 : num);
83+
T step = (stop - start) / div;
84+
for (size_t i = 0; i < num; ++i)
85+
dst[i] = start + static_cast<T>(i) * step;
86+
if (endpoint) dst[num - 1] = stop; // exact endpoint (matches numpy)
87+
}
88+
89+
// ============================================================================
90+
// numpy.logspace
91+
// ============================================================================
92+
93+
/// numpy.logspace(start, stop, num=50, endpoint=True, base=10.0)
94+
/// dst[i] = base ^ linspace(start, stop, num, endpoint)[i]
95+
template<typename T>
96+
inline void logspace(T* dst, T start, T stop, size_t num,
97+
bool endpoint = true, T base = T(10)) {
98+
linspace(dst, start, stop, num, endpoint);
99+
for (size_t i = 0; i < num; ++i)
100+
dst[i] = std::pow(base, dst[i]);
101+
}
102+
103+
// ============================================================================
104+
// numpy.geomspace
105+
// ============================================================================
106+
107+
/// numpy.geomspace(start, stop, num=50, endpoint=True)
108+
/// Geometric sequence: num points evenly spaced on a log scale.
109+
/// Matches numpy's algorithm: log10(start..stop) → logspace(..., base=10).
110+
/// Both start and stop must be non-zero and have the same sign.
111+
template<typename T>
112+
inline void geomspace(T* dst, T start, T stop, size_t num,
113+
bool endpoint = true) {
114+
if (num == 0) return;
115+
T sign = (start < T(0)) ? T(-1) : T(1);
116+
T abs_start = sign * start;
117+
T abs_stop = sign * stop;
118+
// Delegate to logspace(log10(start), log10(stop), base=10) — same as numpy
119+
logspace(dst, std::log10(abs_start), std::log10(abs_stop),
120+
num, endpoint, T(10));
121+
// Apply sign for negative-to-negative ranges
122+
if (sign < T(0))
123+
for (size_t i = 0; i < num; ++i) dst[i] = -dst[i];
124+
// Fix endpoints exactly (numpy guarantees start/stop are exact)
125+
dst[0] = start;
126+
if (endpoint && num > 1) dst[num - 1] = stop;
127+
}
128+
129+
// ============================================================================
130+
// numpy.eye / numpy.identity
131+
// ============================================================================
132+
133+
/// numpy.eye(N, M=N, k=0, dtype=float)
134+
/// N-row × M-col matrix; 1 on the k-th diagonal, 0 elsewhere.
135+
/// dst must hold N*M elements (row-major).
136+
template<typename T>
137+
inline void eye(T* dst, size_t N, size_t M, int k = 0) {
138+
std::fill_n(dst, N * M, T(0));
139+
size_t row0 = (k < 0) ? static_cast<size_t>(-k) : 0;
140+
size_t col0 = (k >= 0) ? static_cast<size_t>(k) : 0;
141+
if (row0 >= N || col0 >= M) return;
142+
size_t len = std::min(N - row0, M - col0);
143+
for (size_t i = 0; i < len; ++i)
144+
dst[(row0 + i) * M + (col0 + i)] = T(1);
145+
}
146+
147+
/// numpy.identity(n, dtype=float) ≡ eye(n, n, 0)
148+
template<typename T>
149+
inline void identity(T* dst, size_t n) {
150+
eye(dst, n, n, 0);
151+
}
152+
153+
// ============================================================================
154+
// numpy.diag
155+
// ============================================================================
156+
157+
/// Diagonal length when extracting the k-th diagonal from an N×M matrix.
158+
inline size_t diag_size(size_t N, size_t M, int k) {
159+
size_t row0 = (k < 0) ? static_cast<size_t>(-k) : 0;
160+
size_t col0 = (k >= 0) ? static_cast<size_t>(k) : 0;
161+
if (row0 >= N || col0 >= M) return 0;
162+
return std::min(N - row0, M - col0);
163+
}
164+
165+
/// numpy.diag(v, k=0) — 1-D input: build (n+|k|)×(n+|k|) diagonal matrix.
166+
/// dst must hold (n+|k|)^2 elements.
167+
template<typename T>
168+
inline void diag_from_vec(T* dst, const T* src, size_t n, int k = 0) {
169+
size_t N = n + static_cast<size_t>(std::abs(k));
170+
std::fill_n(dst, N * N, T(0));
171+
size_t row0 = (k < 0) ? static_cast<size_t>(-k) : 0;
172+
size_t col0 = (k >= 0) ? static_cast<size_t>(k) : 0;
173+
for (size_t i = 0; i < n; ++i)
174+
dst[(row0 + i) * N + (col0 + i)] = src[i];
175+
}
176+
177+
/// numpy.diag(v, k=0) — 2-D input: extract k-th diagonal.
178+
/// dst must hold diag_size(N, M, k) elements.
179+
template<typename T>
180+
inline void diag_from_mat(T* dst, const T* src, size_t N, size_t M, int k = 0) {
181+
size_t row0 = (k < 0) ? static_cast<size_t>(-k) : 0;
182+
size_t col0 = (k >= 0) ? static_cast<size_t>(k) : 0;
183+
size_t len = diag_size(N, M, k);
184+
for (size_t i = 0; i < len; ++i)
185+
dst[i] = src[(row0 + i) * M + (col0 + i)];
186+
}
187+
36188
} // namespace numpy

pycpp/init_py.h

Lines changed: 165 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
// numpycpp — pycpp/init_py.h [PUBLIC HEADER]
33
// Pybind11 wrappers: array creation / initialisation.
44
// numpy.zeros_like numpy.ones_like numpy.full_like numpy.empty_like
5-
// numpy.zeros numpy.ones numpy.full
5+
// numpy.zeros numpy.ones numpy.full numpy.empty
6+
// numpy.arange numpy.linspace numpy.logspace numpy.geomspace
7+
// numpy.eye numpy.identity numpy.diag
68
// ════════════════════════════════════════════════════════════════════════════
79
#pragma once
810

@@ -112,4 +114,166 @@ inline py::array ones_like(const py::array& arr, const std::string& dtype) {
112114
throw std::runtime_error("ones_like: unsupported dtype: " + dtype);
113115
}
114116

117+
// ── numpy.empty ──────────────────────────────────────────────────────────────
118+
119+
/// numpy.empty(shape, dtype=float64)
120+
inline py::array_t<double> empty(const std::vector<py::ssize_t>& shape) {
121+
return py::array_t<double>(shape);
122+
}
123+
124+
// ── numpy.arange ─────────────────────────────────────────────────────────────
125+
126+
/// arange<T> — internal typed helper
127+
template<typename T>
128+
py::array_t<T> arange(T start, T stop, T step) {
129+
size_t n = numpy::arange_size(start, stop, step);
130+
py::array_t<T> result(static_cast<py::ssize_t>(n));
131+
numpy::arange(static_cast<T*>(result.request().ptr), start, stop, step);
132+
return result;
133+
}
134+
135+
/// numpy.arange([start,] stop[, step])
136+
/// Returns float64 array (numpy default). Integer stop/step are cast to double.
137+
inline py::array arange(py::object a, py::object b = py::none(),
138+
py::object c = py::none()) {
139+
double start, stop, step;
140+
if (b.is_none()) {
141+
start = 0.0; stop = a.cast<double>(); step = 1.0;
142+
} else if (c.is_none()) {
143+
start = a.cast<double>(); stop = b.cast<double>(); step = 1.0;
144+
} else {
145+
start = a.cast<double>(); stop = b.cast<double>(); step = c.cast<double>();
146+
}
147+
return arange<double>(start, stop, step);
148+
}
149+
150+
// ── numpy.linspace ───────────────────────────────────────────────────────────
151+
152+
/// linspace<T> — internal typed helper
153+
template<typename T>
154+
py::array_t<T> linspace(T start, T stop, py::ssize_t num,
155+
bool endpoint = true) {
156+
if (num < 0) throw std::invalid_argument("linspace: num must be >= 0");
157+
py::array_t<T> result(num);
158+
numpy::linspace(static_cast<T*>(result.request().ptr),
159+
start, stop, static_cast<size_t>(num), endpoint);
160+
return result;
161+
}
162+
163+
/// numpy.linspace(start, stop, num=50, endpoint=True)
164+
/// Returns float64 array (numpy default for scalar inputs).
165+
inline py::array linspace(py::object start_o, py::object stop_o,
166+
py::ssize_t num = 50, bool endpoint = true) {
167+
double start = start_o.cast<double>(), stop = stop_o.cast<double>();
168+
return linspace<double>(start, stop, num, endpoint);
169+
}
170+
171+
// ── numpy.logspace ───────────────────────────────────────────────────────────
172+
173+
/// logspace<T> — internal typed helper
174+
template<typename T>
175+
py::array_t<T> logspace(T start, T stop, py::ssize_t num,
176+
bool endpoint = true, T base = T(10)) {
177+
if (num < 0) throw std::invalid_argument("logspace: num must be >= 0");
178+
py::array_t<T> result(num);
179+
numpy::logspace(static_cast<T*>(result.request().ptr),
180+
start, stop, static_cast<size_t>(num), endpoint, base);
181+
return result;
182+
}
183+
184+
/// numpy.logspace(start, stop, num=50, endpoint=True, base=10.0)
185+
/// Returns float64 array (numpy default for scalar inputs).
186+
inline py::array logspace(py::object start_o, py::object stop_o,
187+
py::ssize_t num = 50, bool endpoint = true,
188+
double base = 10.0) {
189+
double start = start_o.cast<double>(), stop = stop_o.cast<double>();
190+
return logspace<double>(start, stop, num, endpoint, base);
191+
}
192+
193+
// ── numpy.geomspace ──────────────────────────────────────────────────────────
194+
195+
/// geomspace<T> — internal typed helper
196+
template<typename T>
197+
py::array_t<T> geomspace(T start, T stop, py::ssize_t num,
198+
bool endpoint = true) {
199+
if (num < 0) throw std::invalid_argument("geomspace: num must be >= 0");
200+
if (start == T(0) || stop == T(0))
201+
throw std::invalid_argument("geomspace: start and stop must be non-zero");
202+
py::array_t<T> result(num);
203+
numpy::geomspace(static_cast<T*>(result.request().ptr),
204+
start, stop, static_cast<size_t>(num), endpoint);
205+
return result;
206+
}
207+
208+
/// numpy.geomspace(start, stop, num=50, endpoint=True)
209+
/// Returns float64 array (numpy default for scalar inputs).
210+
inline py::array geomspace(py::object start_o, py::object stop_o,
211+
py::ssize_t num = 50, bool endpoint = true) {
212+
double start = start_o.cast<double>(), stop = stop_o.cast<double>();
213+
return geomspace<double>(start, stop, num, endpoint);
214+
}
215+
216+
// ── numpy.eye ────────────────────────────────────────────────────────────────
217+
218+
/// numpy.eye(N, M=N, k=0, dtype=float64)
219+
template<typename T>
220+
py::array_t<T> eye(py::ssize_t N, py::ssize_t M = -1, int k = 0) {
221+
if (N < 0) throw std::invalid_argument("eye: N must be >= 0");
222+
size_t Ns = static_cast<size_t>(N);
223+
size_t Ms = (M < 0) ? Ns : static_cast<size_t>(M);
224+
py::array_t<T> result({N, (M < 0 ? N : M)});
225+
numpy::eye(static_cast<T*>(result.request().ptr), Ns, Ms, k);
226+
return result;
227+
}
228+
229+
inline py::array_t<double> eye(py::ssize_t N, py::ssize_t M = -1,
230+
int k = 0) {
231+
return eye<double>(N, M, k);
232+
}
233+
234+
// ── numpy.identity ───────────────────────────────────────────────────────────
235+
236+
/// numpy.identity(n, dtype=float64)
237+
template<typename T>
238+
py::array_t<T> identity(py::ssize_t n) {
239+
if (n < 0) throw std::invalid_argument("identity: n must be >= 0");
240+
size_t ns = static_cast<size_t>(n);
241+
py::array_t<T> result({n, n});
242+
numpy::identity(static_cast<T*>(result.request().ptr), ns);
243+
return result;
244+
}
245+
246+
inline py::array_t<double> identity(py::ssize_t n) {
247+
return identity<double>(n);
248+
}
249+
250+
// ── numpy.diag ───────────────────────────────────────────────────────────────
251+
252+
/// numpy.diag(v, k=0)
253+
/// 1-D input → 2-D diagonal matrix
254+
/// 2-D input → 1-D diagonal extraction
255+
template<typename T>
256+
py::array_t<T> diag(const py::array_t<T>& v, int k = 0) {
257+
auto buf = v.request();
258+
if (buf.ndim == 1) {
259+
size_t n = static_cast<size_t>(buf.shape[0]);
260+
size_t N = n + static_cast<size_t>(std::abs(k));
261+
py::array_t<T> result({static_cast<py::ssize_t>(N),
262+
static_cast<py::ssize_t>(N)});
263+
numpy::diag_from_vec(static_cast<T*>(result.request().ptr),
264+
static_cast<const T*>(buf.ptr), n, k);
265+
return result;
266+
}
267+
if (buf.ndim == 2) {
268+
size_t rows = static_cast<size_t>(buf.shape[0]);
269+
size_t cols = static_cast<size_t>(buf.shape[1]);
270+
size_t len = numpy::diag_size(rows, cols, k);
271+
py::array_t<T> result(static_cast<py::ssize_t>(len));
272+
numpy::diag_from_mat(static_cast<T*>(result.request().ptr),
273+
static_cast<const T*>(buf.ptr), rows, cols, k);
274+
return result;
275+
}
276+
throw std::invalid_argument("diag: input must be 1-D or 2-D");
277+
}
278+
115279
} // namespace numpy

tests/module.cpp

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,40 @@ PYBIND11_MODULE(numpycpp, m) {
5959
m.def("ones_like", static_cast<py::array(*)(const py::array&, const std::string&)>(&numpy::ones_like),
6060
py::arg("arr"), py::arg("dtype"));
6161
m.def("zeros", &numpy::zeros);
62-
m.def("ones", &numpy::ones);
63-
m.def("full", static_cast<py::array_t<double>(*)(const std::vector<py::ssize_t>&, double)>(&numpy::full));
62+
m.def("ones", &numpy::ones);
63+
m.def("full", static_cast<py::array_t<double>(*)(const std::vector<py::ssize_t>&, double)>(&numpy::full));
64+
m.def("empty", static_cast<py::array_t<double>(*)(const std::vector<py::ssize_t>&)>(&numpy::empty));
65+
66+
// -- arange / linspace / logspace / geomspace --------------------------
67+
// Single py::object dispatch: dtype inferred from input numpy scalar type
68+
// (np.float32 → float32 output, Python float / np.float64 → float64 output)
69+
// exactly mirroring numpy's own dtype inference behaviour.
70+
m.def("arange",
71+
static_cast<py::array(*)(py::object, py::object, py::object)>(&numpy::arange),
72+
py::arg("a"), py::arg("b")=py::none(), py::arg("c")=py::none());
73+
m.def("linspace",
74+
static_cast<py::array(*)(py::object, py::object, py::ssize_t, bool)>(&numpy::linspace),
75+
py::arg("start"), py::arg("stop"), py::arg("num")=50, py::arg("endpoint")=true);
76+
m.def("logspace",
77+
static_cast<py::array(*)(py::object, py::object, py::ssize_t, bool, double)>(&numpy::logspace),
78+
py::arg("start"), py::arg("stop"), py::arg("num")=50, py::arg("endpoint")=true, py::arg("base")=10.0);
79+
m.def("geomspace",
80+
static_cast<py::array(*)(py::object, py::object, py::ssize_t, bool)>(&numpy::geomspace),
81+
py::arg("start"), py::arg("stop"), py::arg("num")=50, py::arg("endpoint")=true);
82+
83+
// -- eye / identity / diag ---------------------------------------------
84+
// eye / identity: scalar int arguments — dtype determined by registered order;
85+
// float64 is numpy's default so register it last (wins for int args).
86+
m.def("eye", static_cast<py::array_t<float> (*)(py::ssize_t,py::ssize_t,int)>(&numpy::eye<float>),
87+
py::arg("N"), py::arg("M")=-1, py::arg("k")=0);
88+
m.def("eye", static_cast<py::array_t<double>(*)(py::ssize_t,py::ssize_t,int)>(&numpy::eye),
89+
py::arg("N"), py::arg("M")=-1, py::arg("k")=0);
90+
m.def("identity", static_cast<py::array_t<float> (*)(py::ssize_t)>(&numpy::identity<float>));
91+
m.def("identity", static_cast<py::array_t<double>(*)(py::ssize_t)>(&numpy::identity));
92+
m.def("diag", static_cast<py::array_t<float> (*)(const py::array_t<float>&, int)>(&numpy::diag<float>),
93+
py::arg("v"), py::arg("k")=0);
94+
m.def("diag", static_cast<py::array_t<double>(*)(const py::array_t<double>&,int)>(&numpy::diag),
95+
py::arg("v"), py::arg("k")=0);
6496

6597
// -- astype ------------------------------------------------------------
6698
// NOTE: astype_int / astype_bool / astype_bool_from_int instead of a

0 commit comments

Comments
 (0)