Last active
June 2, 2025 00:06
-
-
Save stellarpower/f85adf9286024b4820a3ddefe594c028 to your computer and use it in GitHub Desktop.
Interfacting with NumPy using Numo and PyCall. Allows passing Numo array to e.g. SciPy function; TODO: functionality in other direction.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
%w| fiddle numo/narray pycall pp |.each{|g| require g } | |
NumPy, CTypes = PyCall.import_module("numpy"), PyCall.import_module("ctypes") | |
class Array | |
def multiply | |
inject(:*) | |
end | |
end | |
class Object | |
def toNumo | |
if Numo::NumPy.isArray self | |
Numo::NumPy.fromNumPy self | |
else | |
Numo::NArray.cast self | |
end | |
end | |
end | |
module Numo | |
# Contains hashes etc. to map from Numo native classes to Python DTypes. | |
# Python's dtype is separated into "kind" (single character) and byte length. | |
# First we map from a Numo class to the equivalent character; then the byte length is defined on Numo classes. | |
module NumPy | |
# FIXME: hardcoded; not all supported either | |
DTypeKindMap = { | |
SComplex => 'c', | |
DComplex => 'c', | |
DFloat => 'f', | |
SFloat => 'f', | |
Int8 => 'i', | |
Int16 => 'i', | |
Int32 => 'i', | |
Int64 => 'i', | |
UInt8 => 'u', | |
UInt16 => 'u', | |
UInt32 => 'u', | |
UInt64 => 'u', | |
} | |
# The dictionary contains all types; there are some duplicates, and we need to make an explicit cast to list ot iterate using PyCall. | |
AllDTypes = (PyCall::List.new ::NumPy.sctypeDict.values).uniq.map {|dType| | |
::NumPy::dtype.new dType | |
} | |
# Extract the fields we want, then fill out as a hash to hash to array | |
# Kind character => length in bytes => numpy.dtype instance. | |
TypeMap = AllDTypes.map {|dType| | |
[dType.kind, dType.itemsize, dType] | |
}.group_by(&:first).transform_values {|properties| | |
properties.to_h { |kind, byteLength, pythonClass| [byteLength, pythonClass] } | |
} | |
# Now can look up e.g. Numo::DFloat to get numpy.float64 | |
module_function | |
def isArray(object) | |
# __id__ is not the same between different instances. | |
# We will have to falll back to string comparison, and hope that any modules do not creep in. | |
object.__class__.__name__ =~ /ndarray/ | |
rescue | |
false | |
end | |
# Reverse lookup from the above hash. | |
# Filter DTypeKindMap on the values with the kind character, then the key's element size in bytes. | |
def classFromNumpyDType(numpyDType) | |
numpyDType = ::NumPy::dtype.new numpyDType | |
DTypeKindMap.to_a.select{|numoClass, kind| | |
kind == numpyDType.kind && numoClass::ELEMENT_BYTE_SIZE == numpyDType.itemsize | |
}[0][0] | |
end | |
def fromNumPy(numPyArray) | |
raise "This does not appear to be a NumPy array" unless self.isArray(numPyArray) | |
# Seems the values we want are split; to get the raw void*, we need to use ctypes.data | |
# Meanwhile .data is a Python memoryview (like a span), where it is harder to get the actual address | |
# Make a raw pointer | |
pointer = Fiddle::Pointer[ numPyArray.ctypes.data ] | |
# and convert to a span; within in Ruby this operates like a string.... | |
span = pointer[ 0, numPyArray.data.nbytes ] | |
# ... so we can read it in when using a raw pointer will blow up becuase Numo does chek the bounds. | |
numoClass = classFromNumpyDType(numPyArray.dtype) | |
# And finally numo will itself take acopy with from_string, so not ideal | |
# But the python object at least has its own lifecycle so can be freed | |
numoClass.from_binary span, numPyArray.shape.to_a | |
end | |
end | |
class NArray | |
# Just to reduce typing | |
def elementSize | |
self.class::ELEMENT_BYTE_SIZE | |
end | |
def numPyType | |
kind = NumPy::DTypeKindMap[self.class] | |
NumPy::TypeMap[kind][elementSize] | |
end | |
# Take the address anbd contruct an ndarray within the python side wrapping the buffer in a non-owning fashion. | |
def toNumPy | |
address = Fiddle::Pointer[to_string] | |
pythonPointer = (CTypes.c_char * elementSize * shape.multiply).from_address address.to_i | |
# Appears this is non-owning | |
inPython = ::NumPy.ndarray.new(shape: shape, dtype: numPyType, buffer: pythonPointer) | |
end | |
end # class NArray | |
end # module Numo | |
################################################## | |
# Basic Tests | |
if __FILE__ == $0 | |
testArray = Numo::DComplex[ 0...15 ].reshape 3, 5 | |
inPython = testArray.toNumPy | |
pp NumPy.mean testArray # => nil | |
pp NumPy.mean inPython # => np.float64(7.5) | |
Numo::NumPy.fromNumPy(inPython) | |
end | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment