Skip to content

Instantly share code, notes, and snippets.

@stellarpower
Last active June 2, 2025 00:06
Show Gist options
  • Save stellarpower/f85adf9286024b4820a3ddefe594c028 to your computer and use it in GitHub Desktop.
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.
#!/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