Source code for pyfastnoisesimd.helpers

import pyfastnoisesimd.extension as ext
import concurrent.futures as cf
import numpy as np
from enum import Enum

_MIN_CHUNK_SIZE = 8192

[docs]def empty_aligned(shape, dtype=np.float32, n_byte=ext.SIMD_ALIGNMENT): """ Provides an memory-aligned array for use with SIMD accelerated instructions. Should be used to build Adapted from: https://github.com/hgomersall/pyFFTW/blob/master/pyfftw/utils.pxi Args: shape: a sequence (typically a tuple) of array axes. dtype: NumPy data type of the underlying array. Note FastNoiseSIMD supports only `np.float32`. Seg faults may occur if this is changed. n_byte: byte alignment. Should always use the `pyfastnoisesimd.extension.SIMD_ALIGNMENT` value or seg faults may occur. """ dtype = np.dtype(dtype) itemsize = dtype.itemsize if not isinstance(shape, (int, np.integer)): array_length = 1 for each_dimension in shape: array_length *= each_dimension else: array_length = shape # Allocate a new array that will contain the aligned data buffer = np.empty(array_length * itemsize + n_byte, dtype='int8') offset = (n_byte - buffer.ctypes.data) % n_byte aligned = buffer[offset:offset-n_byte].view(dtype).reshape(shape) return aligned
[docs]def full_aligned(shape, fill, dtype=np.float32, n_byte=ext.SIMD_ALIGNMENT): """ As per `empty_aligned`, but returns an array initialized to a constant value. Args: shape: a sequence (typically a tuple) of array axes. fill: the value to fill each array element with. dtype: NumPy data type of the underlying array. Note FastNoiseSIMD supports only `np.float32`. Seg faults may occur if this is changed. n_byte: byte alignment. Should always use the `pyfastnoisesimd.extension.SIMD_ALIGNMENT` value or seg faults may occur. """ aligned = empty_aligned(shape, dtype=dtype, n_byte=n_byte) aligned.fill(fill) return aligned
[docs]def empty_coords(length, dtype=np.float32, n_byte=ext.SIMD_ALIGNMENT): """ """ dtype = np.dtype(dtype) itemsize = dtype.itemsize # We need to expand length to be a multiple of the vector size vect_len = max(ext.SIMD_ALIGNMENT // itemsize, 1) aligned_len = int(vect_len*np.ceil(length/vect_len)) shape = (3, aligned_len) coords = empty_aligned(shape) # Lots of trouble with passing sliced views out, with over-running the # array and seg-faulting on an invalid write. # return coords[:,:length] return coords
[docs]def check_alignment(array): """ Verifies that an array is aligned correctly for the supported SIMD level. Args: array: numpy.ndarray Returns: truth: bool """ return ((ext.SIMD_ALIGNMENT - array.ctypes.data) % ext.SIMD_ALIGNMENT) == 0
def check_coords(array): """ Verifies that an array is aligned correctly for the supported SIMD level. Args: array: numpy.ndarray Returns: truth: bool """ # print(' Array shape: ', array.shape) # if array.base is not None: # print(' Base shape: ', array.base.shape) base = array if array.base is None else array.base # print(' Check alignment: ', check_alignment(array)) # print(' Base alignment error: ', (array.shape[1]*array.dtype.itemsize)%ext.SIMD_ALIGNMENT) return check_alignment(array) \ and (array.shape[1] * array.dtype.itemsize) % ext.SIMD_ALIGNMENT == 0 == 0
[docs]def aligned_chunks(array, n_chunks, axis=0): """ An generator that divides an array into chunks that have memory addresses compatible with SIMD vector length. Args: array: numpy.ndarray the array to chunk n_chunks: int the desired number of chunks, the returned number _may_ be less. axis: int the axis to chunk on, similar to `numpy` axis commanes. Returns chunk: numpy.ndarray start: Tuple[int] the start indices of the chunk, in the `array`. """ block_size = 1 if array.ndim > axis + 1: block_size = np.product(array.shape[axis:]) # print(f'Got blocksize of {block_size}') vect_len = max(ext.SIMD_ALIGNMENT // array.dtype.itemsize, 1) if block_size % vect_len == 0: # Iterate at-will, the underlying blocks have the correct shape slice_size = int(np.ceil(array.shape[axis] / n_chunks)) else: # Round slice_size up to nearest vect_len slice_size = int(vect_len*np.ceil(array.shape[axis] / n_chunks /vect_len)) # print('Slice size: ', slice_size) offset = 0 chunk_index = 0 while(chunk_index < n_chunks): # Dynamic slicing is pretty clumsy, unfortunately: # https://stackoverflow.com/questions/24398708/slicing-a-numpy-array-along-a-dynamically-specified-axis#47859801 # so just use some nested conditions. if axis == 0: if array.ndim == 1: chunk = array[offset:offset+slice_size] else: chunk = array[offset:offset+slice_size, ...] elif axis == 1: if array.ndim == 2: chunk = array[:, offset:offset+slice_size] else: chunk = array[:, offset:offset+slice_size, ...] elif axis == 2: chunk = array[:, :, offset:offset+slice_size] # print(f'Chunk has memory addr: {chunk.ctypes.data}, remain: {chunk.ctypes.data%ext.SIMD_ALIGNMENT}') yield chunk, offset offset += slice_size chunk_index += 1
[docs]def num_virtual_cores(): """ Detects the number of virtual cores on a system without importing ``multiprocessing``. Borrowed from NumExpr 2.6. """ import os, subprocess # Linux, Unix and MacOS if hasattr(os, 'sysconf'): if 'SC_NPROCESSORS_ONLN' in os.sysconf_names: # Linux & Unix: ncpus = os.sysconf('SC_NPROCESSORS_ONLN') if isinstance(ncpus, int) and ncpus > 0: return ncpus else: # OSX: return int(subprocess.check_output(['sysctl', '-n', 'hw.ncpu'])) # Windows if 'NUMBER_OF_PROCESSORS' in os.environ: ncpus = int(os.environ['NUMBER_OF_PROCESSORS']) if ncpus > 0: return ncpus else: return 1 # TODO: need method for ARM7/8 return 1 # Default
[docs]class NoiseType(Enum): """ The class of noise generated. Enums: ``{Value, ValueFractal, Perlin, PerlinFractal, Simplex, SimplexFractal, WhiteNoise, Cellular, Cubic, CubicFractal}`` """ Value = 0 ValueFractal = 1 Perlin = 2 PerlinFractal = 3 Simplex = 4 SimplexFractal = 5 WhiteNoise = 6 Cellular = 7 Cubic = 8 CubicFractal = 9
[docs]class FractalType(Enum): """ Enum: Fractal noise types also have an additional fractal type. Values: ``{FBM, Billow, RigidMulti}``""" FBM = 0 Billow = 1 RigidMulti = 2
[docs]class PerturbType(Enum): """ Enum: The enumerator for the class of Perturbation. Values: ``{NoPeturb, Gradient, GradientFractal, Normalise, Gradient_Normalise, GradientFractal_Normalise}`` """ NoPerturb = 0 Gradient = 1 GradientFractal = 2 Normalise = 3 Gradient_Normalise = 4 GradientFractal_Normalise = 5
[docs]class CellularDistanceFunction(Enum): """ Enum: The distance function for cellular noise. Values: ``{Euclidean, Manhattan, Natural}``""" Euclidean = 0 Manhattan = 1 Natural = 2
[docs]class CellularReturnType(Enum): """ Enum: The functional filter to apply to the distance function to generate the return from cellular noise. Values: ``{CellValue, Distance, Distance2, Distance2Add, Distance2Sub, Distance2Mul, Distance2Div, NoiseLookup, Distance2Cave}`` """ CellValue = 0 Distance = 1 Distance2 = 2 Distance2Add = 3 Distance2Sub = 4 Distance2Mul = 5 Distance2Div = 6 NoiseLookup = 7 Distance2Cave = 8
[docs]class FractalClass(object): """ Holds properties related to noise types that include fractal octaves. Do not instantiate this class separately from `Noise`. """ def __init__(self, fns): self._fns = fns self._octaves = 3 self._lacunarity = 2.0 self._gain = 0.5 self._fractalType = FractalType.FBM @property def fractalType(self) -> FractalType: """ The type of fractal for fractal NoiseTypes. Default: ``FractalType.FBM``""" return self._fractalType @fractalType.setter def fractalType(self, new): if isinstance(new, FractalType): pass elif isinstance(new, int): new = FractalType(int) elif isinstance(new, str): new = FractalType[new] else: raise TypeError('Unparsable type for fractalType: {}'.format(type(new))) self._fractalType = new self._fns.SetFractalType(new.value) @property def octaves(self) -> int: """ Octave count for all fractal noise types, i.e. the number of log-scaled frequency levels of noise to apply. Generally ``3`` is sufficient for small textures/sprites (256x256 pixels), use larger values for larger textures/sprites. Default: ``3`` """ return self._octaves @octaves.setter def octaves(self, new): self._octaves = int(new) self._fns.SetFractalOctaves(int(new)) @property def lacunarity(self) -> float: """ Octave lacunarity for all fractal noise types. Default: ``2.0`` """ return self._lacunarity @lacunarity.setter def lacunarity(self, new): self._lacunarity = float(new) self._fns.SetFractalLacunarity(float(new)) @property def gain(self) -> float: """ Octave gain for all fractal noise types. Reflects the ratio of the underlying noise to that of the fractal. Values > 0.5 up-weight the fractal. Default: ``0.5`` """ return self._gain @gain.setter def gain(self, new): self._gain = float(new) self._fns.SetFractalGain(float(new))
[docs]class CellularClass(object): """ Holds properties related to `NoiseType.Cellular`. Do not instantiate this class separately from ``Noise``. """ def __init__(self, fns): self._fns = fns self._returnType = CellularReturnType.Distance self._distanceFunc = CellularDistanceFunction.Euclidean self._noiseLookupType = NoiseType.Simplex self._lookupFrequency = 0.2 self._jitter = 0.45 self._distanceIndices = (0.0, 1.0) @property def returnType(self): """ The return type for cellular (cubic Voronoi) noise. Default: ``CellularReturnType.Distance`` """ return self._returnType @returnType.setter def returnType(self, new): if isinstance(new, CellularReturnType): pass elif isinstance(new, int): new = CellularReturnType(int) elif isinstance(new, str): new = CellularReturnType[new] else: raise TypeError('Unparsable type for returnType: {}'.format(type(new))) self._returnType = new self._fns.SetCellularReturnType(new.value) @property def distanceFunc(self): return self._distanceFunc @distanceFunc.setter def distanceFunc(self, new): """ The distance function for cellular (cubic Voronoi) noise. Default: ``CellularDistanceFunction.Euclidean`` """ if isinstance(new, CellularDistanceFunction): pass elif isinstance(new, int): new = CellularDistanceFunction(int) elif isinstance(new, str): new = CellularDistanceFunction[new] else: raise TypeError('Unparsable type for distanceFunc: {}'.format(type(new))) self._distanceFunc = new self._fns.SetCellularDistanceFunction(new.value) @property def noiseLookupType(self) -> NoiseType: """ Sets the type of noise used if cellular return type. Default: `NoiseType.Simplex` """ return self._noiseLookupType @noiseLookupType.setter def noiseLookupType(self, new): if isinstance(new, NoiseType): pass elif isinstance(new, int): new = NoiseType(int) elif isinstance(new, str): new = NoiseType[new] else: raise TypeError('Unparsable type for noiseLookupType: {}'.format(type(new))) self._noiseLookupType = new self._fns.SetCellularNoiseLookupType(new.value) @property def lookupFrequency(self): """ Relative frequency on the cellular noise lookup return type. Default: ``0.2`` """ return self._lookupFrequency @lookupFrequency.setter def lookupFrequency(self, new): self._lookupFrequency = float(new) self._fns.SetCellularNoiseLookupFrequency(float(new)) @property def jitter(self): """ The maximum distance a cellular point can move from it's grid position. The value is relative to the cubic cell spacing of ``1.0``. Setting ``jitter > 0.5`` can generate wrapping artifacts. Default: ``0.45`` """ return self._jitter @jitter.setter def jitter(self, new): self._jitter = float(new) self._fns.SetCellularJitter(float(new)) @property def distanceIndices(self) -> tuple: """ Sets the two distance indices used for ``distance2X`` return types Default: ``(0, 1)`` .. note: * index0 should be lower than index1 * Both indices must be ``>= 0`` * index1 must be ``< 4`` """ return self._distanceIndices @distanceIndices.setter def distanceIndices(self, new): if not hasattr(new, '__len__') or len(new) != 2: raise ValueError( 'distanceIndices must be a length 2 array/list/tuple' ) new = list(new) if new[0] < 0: new[0] = 0 if new[1] < 0: new[0] = 0 if new[1] >= 4: new[1] = 3 if new[0] >= new[1]: new[0] = new[1]-1 self._distanceIndices = new return self._fns.SetCellularDistance2Indices(*new)
[docs]class PerturbClass(object): """ Holds properties related to the perturbation applied to noise. Do not instantiate this class separately from ``Noise``. """ def __init__(self, fns): self._fns = fns self._perturbType = PerturbType.NoPerturb self._amp = 1.0 self._frequency = 0.5 self._octaves = 3 self._lacunarity = 2.0 self._gain = 2.0 self._normaliseLength = 1.0 @property def perturbType(self) -> PerturbType: """ The class of perturbation. Default: ``PerturbType.NoPeturb`` """ return self._perturbType @perturbType.setter def perturbType(self, new): if isinstance(new, PerturbType): pass elif isinstance(new, int): new = PerturbType(int) elif isinstance(new, str): new = PerturbType[new] else: raise TypeError('Unparsable type for perturbType: {}'.format(type(new))) self._perturbType = new return self._fns.SetPerturbType(new.value) @property def amp(self) -> float: """ The maximum distance the input position can be perturbed. The reasonable values of ``amp`` before artifacts are apparent increase with decreased ``frequency``. The default value of ``1.0`` is quite high. Default: ``1.0`` """ return self._amp @amp.setter def amp(self, new): self._amp = float(new) return self._fns.SetPerturbAmp(float(new)) @property def frequency(self) -> float: """ The relative frequency for the perturbation gradient. Default: ``0.5`` """ return self._frequency @frequency.setter def frequency(self, new): self._frequency = float(new) return self._fns.SetPerturbFrequency(float(new)) @property def octaves(self) -> int: """ The octave count for fractal perturbation types, i.e. the number of log-scaled frequency levels of noise to apply. Generally ``3`` is sufficient for small textures/sprites (256x256 pixels), use larger values for larger textures/sprites. Default: ``3`` """ return self._octaves @octaves.setter def octaves(self, new): self._octaves = int(new) return self._fns.SetPerturbFractalOctaves(int(new)) @property def lacunarity(self) -> float: """ The octave lacunarity (gap-fill) for fractal perturbation types. Lacunarity increases the fineness of fractals. The appearance of graininess in fractal noise occurs when lacunarity is too high for the given frequency. Default: ``2.0`` """ return self._lacunarity @lacunarity.setter def lacunarity(self, new): self._lacunarity = float(new) return self._fns.SetPerturbFractalLacunarity(float(new)) @property def gain(self) -> float: """ The octave gain for fractal perturbation types. Reflects the ratio of the underlying noise to that of the fractal. Values > 0.5 up-weight the fractal. Default: ``0.5`` """ return self._gain @gain.setter def gain(self, new): self._gain = float(new) return self._fns.SetPerturbFractalGain(float(new)) @property def normaliseLength(self) -> float: """ The length for vectors after perturb normalising Default: ``1.0`` """ return self._normaliseLength @normaliseLength.setter def normaliseLength(self, new): self._normaliseLength = float(new) return self._fns.SetPerturbNormaliseLength(float(new))
def _chunk_noise_grid(fns, chunk, chunkStart, chunkAxis, start=[0,0,0]): """ For use by ``concurrent.futures`` to multi-thread ``Noise.genAsGrid()`` calls. """ dataPtr = chunk.__array_interface__['data'][0] # print( 'pointer: {:X}, start: {}, shape: {}'.format(dataPtr, chunkStart, chunk.shape) ) if chunkAxis == 0: fns.FillNoiseSet(chunk, chunkStart+start[0], start[1], start[2], *chunk.shape) elif chunkAxis == 1: fns.FillNoiseSet(chunk, start[0], chunkStart+start[1], start[2], *chunk.shape) else: fns.FillNoiseSet(chunk, start[0], start[1], chunkStart+start[2], *chunk.shape)
[docs]class Noise(object): """ ``Noise`` encapsulates the C++ SIMD class ``FNSObject`` and enables get/set of all relative properties via Python properties. Args: seed: The random number (int32) that seeds the random-number generator If ``seed == None`` a random integer is generated as the seed. numWorkers: The number of threads used for parallel noise generation. If ``numWorkers == None``, the default applied by `concurrent.futures.ThreadPoolExecutor` is used. """ def __init__(self, seed: int=None, numWorkers: int=None): self._fns = ext.FNS() if numWorkers is not None: self._num_workers = int(numWorkers) else: self._num_workers = num_virtual_cores() self._asyncExecutor = cf.ThreadPoolExecutor(max_workers = self._num_workers) # Sub-classed object handles self.fractal = FractalClass(self._fns) self.cell = CellularClass(self._fns) self.perturb = PerturbClass(self._fns) if bool(seed): self.seed = seed # calls setter else: self.seed = np.random.randint(-2147483648, 2147483647) # Syncronizers for property getters should use the default values as # stated in `FastNoiseSIMD.h` self._noiseType = NoiseType.Simplex self._frequency = 0.01 self._axesScales = (1.0, 1.0, 1.0) @property def numWorkers(self) -> int: """ Sets the maximum number of thread workers that will be used for generating noise. Generally should be the number of physical CPU cores on the machine. Default: Number of virtual cores on machine. """ return self._num_workers @numWorkers.setter def numWorkers(self, N_workers) -> int: N_workers = int(N_workers) if N_workers <= 0: raise ValueError('numWorkers must be greater than 0') self._num_workers = N_workers self._asyncExecutor = cf.ThreadPoolExecutor(max_workers = N_workers) @property def seed(self) -> int: """ The random-number seed used for generation of noise. Default: ``numpy.random.randint()`` """ return self._fns.GetSeed() @seed.setter def seed(self, new): return self._fns.SetSeed(int(np.int32(new))) @property def frequency(self) -> float: """ The frequency of the noise, lower values result in larger noise features. Default: ``0.01`` """ return self._frequency @frequency.setter def frequency(self, new): self._frequency = float(new) return self._fns.SetFrequency(float(new)) @property def noiseType(self) -> NoiseType: """ The class of noise. Default: ``NoiseType.Simplex`` """ return self._noiseType @noiseType.setter def noiseType(self, new): if isinstance(new, NoiseType): pass elif isinstance(new, int): new = NoiseType(int) elif isinstance(new, str): new = NoiseType[new] else: raise TypeError('Unparsable type for noiseType: {}'.format(type(new))) self._noiseType = new return self._fns.SetNoiseType(new.value) @property def axesScales(self) -> tuple: """ Sets the FastNoiseSIMD axes scales, which allows for non-square voxels. Indirectly affects `frequency` by changing the voxel pitch. Default: ``(1.0, 1.0, 1.0)`` """ return self._axesScales @axesScales.setter def axesScales(self, new: tuple): if not hasattr(new, '__len__') or len(new) != 3: raise ValueError( 'axesScales must be a length 3 array/list/tuple' ) self._axesScales = new return self._fns.SetAxesScales(*new)
[docs] def genAsGrid(self, shape=[1,1024,1024], start=[0,0,0]) -> np.ndarray: """ Generates noise according to the set properties along a rectilinear (evenly-spaced) grid. Args: shape: Tuple[int] the shape of the output noise volume. start: Tuple[int] the starting coordinates for generation of the grid. I.e. the coordinates are essentially `start: start + shape` Example:: import numpy as np import pyfastnoisesimd as fns noise = fns.Noise() result = noise.genFromGrid(shape=[256,256,256], start=[0,0,0]) nextResult = noise.genFromGrid(shape=[256,256,256], start=[256,0,0]) """ if isinstance(shape, (int, np.integer)): shape = (shape,) # There is a minimum array size before we bother to turn on futures. size = np.product(shape) result = empty_aligned(shape) # Shape could be 1 or 2D, so we need to expand it with singleton # dimensions for the FillNoiseSet call if len(start) == 1: start = [start[0], 0, 0] elif len(start) == 2: start = [start[0], start[1], 1] else: start = list(start) start_zero = start[0] if self._num_workers <= 1 or size < _MIN_CHUNK_SIZE: # print('Grid single-threaded') if len(shape) == 1: shape = (*shape, 1, 1) elif len(shape) == 2: shape = (*shape, 1) else: shape = shape self._fns.FillNoiseSet(result, *start, *shape) return result # else run in threaded mode. n_chunks = np.minimum(self._num_workers, shape[0]) # print(f'genAsGrid using {n_chunks} chunks') # print('Grid multi-threaded') workers = [] for I, (chunk, consumed) in enumerate(aligned_chunks(result, n_chunks, axis=0)): # print(f'{I}: Got chunk of shape {chunk.shape} with {consumed} consumed') if len(chunk.shape) == 1: chunk_shape = (*chunk.shape, 1, 1) elif len(chunk.shape) == 2: chunk_shape = (*chunk.shape, 1) else: chunk_shape = chunk.shape start[0] = start_zero + consumed # print('len start: ', len(start), ', len shape: ', len(chunk_shape)) peon = self._asyncExecutor.submit(self._fns.FillNoiseSet, chunk, *start, *chunk_shape) workers.append(peon) for peon in workers: peon.result() # For memory management we have to tell NumPy it's ok to free the memory # region when it is dereferenced. # self._fns._OwnSplitArray(noise) return result
[docs] def genFromCoords(self, coords: np.ndarray) -> np.ndarray: """ Generate noise from supplied coordinates, rather than a rectilinear grid. Useful for complicated shapes, such as tesselated surfaces. Args: coords: 3-D coords as generated by ``fns.empty_coords()`` and filled with relevant values by the user. Returns: noise: a shape (N,) array of the generated noise values. Example:: import numpy as np import pyfastnoisesimd as fns numCoords = 256 coords = fns.empty_coords(3,numCoords) # <Set the coordinate values, it is a (3, numCoords) array coords[0,:] = np.linspace(-np.pi, np.pi, numCoords) coords[1,:] = np.linspace(-1.0, 1.0, numCoords) coords[2,:] = np.zeros(numCoords) noise = fns.Noise() result = noise.genFromCoords(coords) """ if not isinstance(coords, np.ndarray): raise TypeError('`coords` must be of type `np.ndarray`, not type: ', type(coords)) if coords.ndim != 2: raise ValueError('`coords` must be a 2D array') shape = coords.shape if shape[0] != 3: raise ValueError('`coords.shape[0]` must equal 3') if not check_alignment(coords): raise ValueError('Memory alignment of `coords` is not valid') if coords.dtype != np.float32: raise ValueError('`coords` must be of dtype `np.float32`') itemsize = coords.dtype.itemsize result = empty_aligned(shape[1]) if self._num_workers <= 1 or shape[1] < _MIN_CHUNK_SIZE: self._fns.NoiseFromCoords(result, coords[0,:], coords[1,:], coords[2,:], shape[1], 0) return result n_chunks = np.minimum(self._num_workers, shape[1] * itemsize / ext.SIMD_ALIGNMENT) workers = [] # for I, ((result_chunk, r_offset), (coord_chunk, offset)) in enumerate(zip( # aligned_chunks(result, self._num_workers, axis=0), # aligned_chunks(coords, self._num_workers, axis=1))): for I, (result_chunk, offset) in enumerate( aligned_chunks(result, self._num_workers, axis=0)): # aligned_size = int(vect_len*np.ceil(result_chunk.size/vect_len)) # print(f' {I}: Got chunk of length {result_chunk.size}, AlignedSize would be: {aligned_size}') # print(' Offset: ', offset, ', offset error: ', offset % 8) # zPtr = (coords[0,:].ctypes.data + offset) % 8 # yPtr = (coords[1,:].ctypes.data + offset) % 8 # xPtr = (coords[2,:].ctypes.data + offset) % 8 # print(f' Pointer alignment: {zPtr, yPtr, xPtr}') # peon = self._asyncExecutor.submit(self._fns.NoiseFromCoords, result, # coords[0,:], coords[1,:], coords[2,:], aligned_size, offset) peon = self._asyncExecutor.submit(self._fns.NoiseFromCoords, result, coords[0,:], coords[1,:], coords[2,:], result_chunk.size, offset) workers.append(peon) for peon in workers: peon.result() return result