Skip to content

Commit 72c224b

Browse files
authored
Merge pull request #701 from Point72/ac/arrow_nodes_numpy_support
Add support for lists to Arrow <-> csp.Struct conversion nodes
2 parents 83a96fc + 6168414 commit 72c224b

11 files changed

Lines changed: 3278 additions & 144 deletions

cpp/csp/adapters/arrow/ArrowFieldReader.cpp

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -40,20 +40,6 @@ void readColumn( const ArrowArrayT & typed, std::vector<StructPtr> & structs, in
4040
fn( typed, i, structs[i].get() );
4141
}
4242

43-
// Helper: compute nanosecond multiplier for a given arrow::TimeUnit
44-
int64_t timeUnitMultiplier( ::arrow::TimeUnit::type unit )
45-
{
46-
switch( unit )
47-
{
48-
case ::arrow::TimeUnit::SECOND: return csp::NANOS_PER_SECOND;
49-
case ::arrow::TimeUnit::MILLI: return csp::NANOS_PER_MILLISECOND;
50-
case ::arrow::TimeUnit::MICRO: return csp::NANOS_PER_MICROSECOND;
51-
case ::arrow::TimeUnit::NANO: return 1LL;
52-
default:
53-
CSP_THROW( TypeError, "Unexpected arrow TimeUnit: " << static_cast<int>( unit ) );
54-
}
55-
}
56-
5743
// --- Generic lambda-based reader (covers Primitive, HalfFloat, StringLike, Nanos, Date) ---
5844
// ExtractFn signature: ValueT(const ArrowArrayT &, int64_t row)
5945

cpp/csp/adapters/arrow/ArrowFieldReader.h

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
#define _IN_CSP_ADAPTERS_ARROW_ArrowFieldReader_H
1212

1313
#include <csp/engine/Struct.h>
14+
#include <csp/core/Exception.h>
15+
#include <csp/core/Time.h>
1416
#include <arrow/record_batch.h>
1517
#include <arrow/type.h>
1618
#include <functional>
@@ -22,6 +24,20 @@
2224
namespace csp::adapters::arrow
2325
{
2426

27+
// Helper: compute nanosecond multiplier for a given arrow::TimeUnit
28+
inline int64_t timeUnitMultiplier( ::arrow::TimeUnit::type unit )
29+
{
30+
switch( unit )
31+
{
32+
case ::arrow::TimeUnit::SECOND: return csp::NANOS_PER_SECOND;
33+
case ::arrow::TimeUnit::MILLI: return csp::NANOS_PER_MILLISECOND;
34+
case ::arrow::TimeUnit::MICRO: return csp::NANOS_PER_MICROSECOND;
35+
case ::arrow::TimeUnit::NANO: return 1LL;
36+
default:
37+
CSP_THROW( TypeError, "Unexpected arrow TimeUnit: " << static_cast<int>( unit ) );
38+
}
39+
}
40+
2541
class FieldReader
2642
{
2743
public:

cpp/csp/adapters/arrow/RecordBatchToStruct.cpp

Lines changed: 8 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,25 +10,6 @@
1010
namespace csp::adapters::arrow
1111
{
1212

13-
namespace
14-
{
15-
16-
// Helper to resolve column name -> struct field name mapping
17-
// If fieldMap is null, column name = field name (identity mapping)
18-
std::string resolveFieldName( const DictionaryPtr & fieldMap, const std::string & columnName )
19-
{
20-
if( !fieldMap )
21-
return columnName;
22-
23-
std::string fieldName;
24-
if( fieldMap -> tryGet<std::string>( columnName, fieldName ) )
25-
return fieldName;
26-
27-
return columnName;
28-
}
29-
30-
} // anonymous namespace
31-
3213
RecordBatchToStructConverter::RecordBatchToStructConverter(
3314
const std::shared_ptr<::arrow::Schema> & schema,
3415
const std::shared_ptr<StructMeta> & structMeta,
@@ -51,12 +32,16 @@ RecordBatchToStructConverter::RecordBatchToStructConverter(
5132
if( customColumnNames.count( arrowField -> name() ) )
5233
continue;
5334

54-
// Skip columns that don't have a matching struct field
55-
std::string fieldName = resolveFieldName( fieldMap, arrowField -> name() );
56-
auto structField = structMeta -> field( fieldName );
57-
if( !structField )
35+
// Look up field name from the mapping; skip columns not in the map
36+
std::string fieldName;
37+
if( !fieldMap -> tryGet<std::string>( arrowField -> name(), fieldName ) )
5838
continue;
5939

40+
auto structField = structMeta -> field( fieldName );
41+
CSP_TRUE_OR_THROW_RUNTIME( structField != nullptr,
42+
"Struct field '" << fieldName << "' (mapped from column '" << arrowField -> name()
43+
<< "') not found on struct type '" << structMeta -> name() << "'" );
44+
6045
m_scalarReaders.push_back( { createFieldReader( arrowField, structField ), i } );
6146
}
6247

cpp/csp/adapters/arrow/StructToRecordBatch.cpp

Lines changed: 15 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -17,42 +17,22 @@ StructToRecordBatchConverter::StructToRecordBatchConverter(
1717
{
1818
std::vector<std::shared_ptr<::arrow::Field>> arrowFields;
1919

20-
if( fieldMap )
20+
for( auto it = fieldMap -> begin(); it != fieldMap -> end(); ++it )
2121
{
22-
// When fieldMap is provided, only include fields listed in it
23-
for( auto it = fieldMap -> begin(); it != fieldMap -> end(); ++it )
24-
{
25-
auto fieldName = it.key();
26-
auto colName = it.value<std::string>();
27-
auto structField = structMeta -> field( fieldName );
28-
if( !structField )
29-
continue;
30-
31-
// Skip DIALECT_GENERIC fields (handled by custom writers)
32-
if( structField -> type() -> type() == CspType::Type::DIALECT_GENERIC )
33-
continue;
34-
35-
auto created = createFieldWriter( colName, structField );
36-
for( auto & dt : created.writer -> dataTypes() )
37-
arrowFields.push_back( std::make_shared<::arrow::Field>( colName, dt ) );
38-
m_writers.push_back( std::move( created.writer ) );
39-
}
40-
}
41-
else
42-
{
43-
// No fieldMap: include all non-DIALECT_GENERIC fields using fieldNames() for stable insertion order
44-
// (fields() is sorted by type/size for memory layout optimization, not insertion order)
45-
for( auto & fieldName : structMeta -> fieldNames() )
46-
{
47-
auto structField = structMeta -> field( fieldName );
48-
if( !structField || structField -> type() -> type() == CspType::Type::DIALECT_GENERIC )
49-
continue;
50-
51-
auto created = createFieldWriter( fieldName, structField );
52-
for( auto & dt : created.writer -> dataTypes() )
53-
arrowFields.push_back( std::make_shared<::arrow::Field>( fieldName, dt ) );
54-
m_writers.push_back( std::move( created.writer ) );
55-
}
22+
auto fieldName = it.key();
23+
auto colName = it.value<std::string>();
24+
auto structField = structMeta -> field( fieldName );
25+
CSP_TRUE_OR_THROW_RUNTIME( structField != nullptr,
26+
"Struct field '" << fieldName << "' not found on struct type '" << structMeta -> name() << "'" );
27+
28+
// Skip DIALECT_GENERIC fields (handled by custom writers)
29+
if( structField -> type() -> type() == CspType::Type::DIALECT_GENERIC )
30+
continue;
31+
32+
auto created = createFieldWriter( colName, structField );
33+
for( auto & dt : created.writer -> dataTypes() )
34+
arrowFields.push_back( std::make_shared<::arrow::Field>( colName, dt ) );
35+
m_writers.push_back( std::move( created.writer ) );
5636
}
5737

5838
// Append custom writers and their columns to schema

cpp/csp/python/NumpyConversions.cpp

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,5 +202,66 @@ void validateNumpyTypeVsCspType( const CspTypePtr & type, char numpy_type_char )
202202
}
203203
}
204204

205+
DialectGenericType numpyReshape( DialectGenericType flatData, const std::vector<int64_t> & dims )
206+
{
207+
// PyArray_Newshape accepts a PyArray_Dims struct directly, avoiding a numpy array allocation for dims.
208+
// Stack-allocate the dims for typical 2-8 dimensional arrays.
209+
npy_intp dimsBuf[8];
210+
npy_intp ndims = static_cast<npy_intp>( dims.size() );
211+
npy_intp * dimsPtr;
212+
213+
std::vector<npy_intp> heapDims;
214+
if( ndims <= 8 )
215+
{
216+
for( npy_intp i = 0; i < ndims; ++i )
217+
dimsBuf[i] = static_cast<npy_intp>( dims[i] );
218+
dimsPtr = dimsBuf;
219+
}
220+
else
221+
{
222+
heapDims.resize( ndims );
223+
for( npy_intp i = 0; i < ndims; ++i )
224+
heapDims[i] = static_cast<npy_intp>( dims[i] );
225+
dimsPtr = heapDims.data();
226+
}
227+
228+
PyArray_Dims newshape{ dimsPtr, static_cast<int>( ndims ) };
229+
auto * flatPyArr = reinterpret_cast<PyArrayObject *>( toPythonBorrowed( flatData ) );
230+
PyObjectPtr reshaped{ PyObjectPtr::own(
231+
reinterpret_cast<PyObject *>( PyArray_Newshape( flatPyArr, &newshape, NPY_CORDER ) ) ) };
232+
if( !reshaped.get() )
233+
CSP_THROW( PythonPassthrough, "" );
234+
235+
return fromPython<DialectGenericType>( reshaped.get() );
236+
}
237+
238+
std::vector<int64_t> numpyShape( DialectGenericType ndarray )
239+
{
240+
auto * pyArr = reinterpret_cast<PyArrayObject *>( toPythonBorrowed( ndarray ) );
241+
int ndim = PyArray_NDIM( pyArr );
242+
npy_intp * shape = PyArray_SHAPE( pyArr );
243+
std::vector<int64_t> result( ndim );
244+
for( int i = 0; i < ndim; ++i )
245+
result[i] = shape[i];
246+
return result;
247+
}
248+
249+
int npyTypeFromPyType( DialectGenericType pyTypeObj )
250+
{
251+
auto & cspType = pyTypeAsCspType( toPythonBorrowed( pyTypeObj ) );
252+
253+
return csp::PartialSwitchCspType<csp::CspType::Type::DOUBLE, csp::CspType::Type::INT64,
254+
csp::CspType::Type::BOOL, csp::CspType::Type::STRING,
255+
csp::CspType::Type::DATETIME, csp::CspType::Type::TIMEDELTA,
256+
csp::CspType::Type::DATE>::invoke(
257+
cspType.get(),
258+
[]( auto tag ) -> int
259+
{
260+
using CValueType = typename decltype( tag )::type;
261+
return NPY_TYPE<CValueType>::value;
262+
}
263+
);
264+
}
265+
205266

206267
}

cpp/csp/python/NumpyConversions.h

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ template<> struct NPY_TYPE<int64_t> { static const int value = NPY_LONGLONG; };
2424
template<> struct NPY_TYPE<uint64_t> { static const int value = NPY_ULONGLONG; };
2525
template<> struct NPY_TYPE<double> { static const int value = NPY_DOUBLE; };
2626
template<> struct NPY_TYPE<std::string> { static const int value = NPY_UNICODE; };
27+
template<> struct NPY_TYPE<DateTime> { static const int value = NPY_DATETIME; };
28+
template<> struct NPY_TYPE<TimeDelta> { static const int value = NPY_TIMEDELTA; };
29+
template<> struct NPY_TYPE<Date> { static const int value = NPY_DATETIME; };
2730

2831
template<typename T> struct is_native { static constexpr bool value = false; };
2932
template<> struct is_native<bool> { static constexpr bool value = true; };
@@ -93,7 +96,7 @@ inline PyObject * as_nparray( const csp::TimeSeriesProvider * ts, const TickBuff
9396
{
9497
int32_t len = startIndex - endIndex + 1;
9598
if( len <= 0 || !ts -> valid() || ( !tickBuffer && endIndex != 0 ) )
96-
return empty_array( NPY_TYPE<DateTime>::value );
99+
return empty_array( NPY_OBJECT );
97100

98101
DateTime * values = getValues( tickBuffer, lastValue, startIndex, endIndex, &len, tailPadding );
99102
npy_intp dims[]{ ( npy_intp ) len };
@@ -104,6 +107,7 @@ inline PyObject * as_nparray( const csp::TimeSeriesProvider * ts, const TickBuff
104107
auto date_type = PyPtr<PyObject>::own( PyUnicode_FromString( "<M8[ns]" ) );
105108
PyArray_DescrConverter( date_type.get(), &datetime_descr );
106109
}
110+
107111
// PyArray_NewFromDescr steals a reference
108112
Py_INCREF( datetime_descr );
109113
auto * array = PyArray_NewFromDescr( &PyArray_Type, datetime_descr, 1, dims, nullptr, values, 0, nullptr );
@@ -116,7 +120,7 @@ inline PyObject * as_nparray( const csp::TimeSeriesProvider * ts, const TickBuff
116120
{
117121
int32_t len = startIndex - endIndex + 1;
118122
if( len <= 0 || !ts -> valid()|| ( !tickBuffer && endIndex != 0 ) )
119-
return empty_array( NPY_TYPE<TimeDelta>::value );
123+
return empty_array( NPY_OBJECT );
120124

121125
TimeDelta * values = getValues( tickBuffer, lastValue, startIndex, endIndex, &len, tailPadding );
122126
npy_intp dims[]{ ( npy_intp ) len };
@@ -127,6 +131,7 @@ inline PyObject * as_nparray( const csp::TimeSeriesProvider * ts, const TickBuff
127131
auto timedelta_type = PyPtr<PyObject>::own( PyUnicode_FromString( "<m8[ns]" ) );
128132
PyArray_DescrConverter( timedelta_type.get(), &timedelta_descr );
129133
}
134+
130135
Py_INCREF( timedelta_descr );
131136
auto * array = PyArray_NewFromDescr( &PyArray_Type, timedelta_descr, 1, dims, nullptr, values, 0, nullptr );
132137
PyArray_ENABLEFLAGS( ( PyArrayObject * ) array, NPY_ARRAY_OWNDATA);
@@ -232,14 +237,23 @@ PyObject * valuesAtIndexToNumpy( ValueType valueType, const csp::TimeSeriesProvi
232237
autogen::TimeIndexPolicy startPolicy, autogen::TimeIndexPolicy endPolicy,
233238
DateTime startDt = DateTime::NONE(), DateTime endDt = DateTime::NONE() );
234239

235-
int64_t scalingFromNumpyDtUnit( NPY_DATETIMEUNIT base );
236-
NPY_DATETIMEUNIT datetimeUnitFromDescr( PyArray_Descr* descr );
240+
CSPIMPL_EXPORT int64_t scalingFromNumpyDtUnit( NPY_DATETIMEUNIT base );
241+
CSPIMPL_EXPORT NPY_DATETIMEUNIT datetimeUnitFromDescr( PyArray_Descr* descr );
237242

238243
// for getting strings from elems of numpy arrays of strings
239244
void stringFromNumpyStr( void* data, std::string& out, char numpy_type, int elem_size_bytes );
240245

241246
void validateNumpyTypeVsCspType( const CspTypePtr & type, char numpy_type_char );
242247

248+
// Reshape a flat 1D numpy array into an NDArray using the given dimensions.
249+
CSPIMPL_EXPORT DialectGenericType numpyReshape( DialectGenericType flatData, const std::vector<int64_t> & dims );
250+
251+
// Extract shape from a numpy NDArray as a vector of dimension sizes.
252+
CSPIMPL_EXPORT std::vector<int64_t> numpyShape( DialectGenericType ndarray );
253+
254+
// Convert a Python type object (passed as DialectGenericType) to an NPY type constant.
255+
CSPIMPL_EXPORT int npyTypeFromPyType( DialectGenericType pyTypeObj );
256+
243257
}
244258

245259
#endif

0 commit comments

Comments
 (0)