Source code for pyrogue._Model

#-----------------------------------------------------------------------------
# Company    : SLAC National Accelerator Laboratory
#-----------------------------------------------------------------------------
#  Description:
#       PyRogue base module - Model Class
#-----------------------------------------------------------------------------
# This file is part of the rogue software platform. It is subject to
# the license terms in the LICENSE.txt file found in the top-level directory
# of this distribution and at:
#    https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html.
# No part of the rogue software platform, including this file, may be
# copied, modified, propagated, or distributed except according to the terms
# contained in the LICENSE.txt file.
#-----------------------------------------------------------------------------

from __future__ import annotations

import struct
from typing import Any

import numpy as np
import rogue.interfaces.memory as rim

[docs] def wordCount(bits: int, wordSize: int) -> int: """Return the number of words needed to represent a bit width. Parameters ---------- bits : int Number of bits to represent. wordSize : int Word size in bits. Returns ------- int Number of words required. """ ret = bits // wordSize if (bits % wordSize != 0 or bits == 0): ret += 1 return ret
[docs] def byteCount(bits: int) -> int: """Return the number of bytes needed to represent a bit width. Equivalent to ``wordCount(bits, 8)``. Parameters ---------- bits : int Number of bits to represent. Returns ------- int Number of bytes required. """ return wordCount(bits, 8)
[docs] def reverseBits(value: int, bitSize: int) -> int: """Return the bit-reversed value of an integer. For example, if ``value = 0b1010`` and ``bitSize = 4``, then the function will return ``0b0101``. Parameters ---------- value : int Value to reverse. bitSize : int Number of bits to reverse. Returns ------- int Bit-reversed value. """ result = 0 for i in range(bitSize): result <<= 1 result |= value & 1 value >>= 1 return result
[docs] def twosComplement(value: int, bitSize: int) -> int: """Compute the two's complement of an integer value. For example, if ``value = 0b1010`` and ``bitSize = 4``, then the function will return ``0b0110``. Parameters ---------- value : int Input value. bitSize : int Bit width of the value. Returns ------- int Two's complement value. """ if (value & (1 << (bitSize - 1))) != 0: # if sign bit is set e.g., 8bit: 128-255 value = value - (1 << bitSize) # compute negative value return value # return positive value as is
class ModelMeta(type): """Metaclass for the Model class. This metaclass is used to create a dictionary of subclasses of the Model class. """ def __init__(cls, *args: Any, **kwargs: Any) -> None: """Initialize metaclass state for model-instance caching.""" super().__init__(*args, **kwargs) cls.subclasses = {} def __call__(cls, *args: Any, **kwargs: Any) -> Any: """Return cached model instances for identical constructor arguments.""" key = cls.__name__ + str(args) + str(kwargs) if key not in cls.subclasses: #print(f'Key: {key}') inst = super().__call__(*args, **kwargs) cls.subclasses[key] = inst return cls.subclasses[key]
[docs] class Model(object, metaclass=ModelMeta): """ Extensible base class which describes how a data type is represented and accessed using the Rogue Variables and Blocks Parameters ---------- bitSize : int Number of bits being represented. binPoint : int, optional (default = 0) Binary point position. Attributes ---------- name: str String representation of the Model type fstring: str Not sure what this is, Where is it used? encoding: str Encoding type for converting between string and byte arrays. i.e. UTF-8 pytype: int Python type class. defaultdisp: str Default display formatting string. May be overriden by the Variable disp parameter. signed: bool Flag indicating if value is signed. Default=False endianness: str Endianness indicator. 'little' or 'big'. Default='little' bitReverse: bool Bit reversal flag. modelId: int Block processing ID. See :ref:`interfaces_memory_constants_ptype` isBigEndian: bool True if endianness = 'big' ndType: np.dtype numpy type value (bool, int32, int64, uint32, uin64, float32, float64) """ fstring = None encoding = None pytype = None defaultdisp = '{}' signed = False endianness = 'little' bitReverse = False modelId = rim.PyFunc def __init__(self, bitSize: int, binPoint: int = 0) -> None: """Initialize base model metadata.""" self.binPoint = binPoint self.bitSize = bitSize self.name = self.__class__.__name__ self.ndType = None @property def isBigEndian(self) -> bool: """Return True if the model is big endian.""" return self.endianness == 'big'
[docs] def toBytes(self, value: Any) -> Any: """ Convert the python value to byte array. Implement this method in a subclass to define the conversion. Parameters ---------- value : object Python value to convert. Returns ------- """ return None
# Called by raw read/write and when bitsize > 64
[docs] def fromBytes(self, ba: bytearray) -> Any: """ Convert a byte array to a python value. Implement this method in a subclass to define the conversion. Parameters ---------- ba : bytearray Byte array to extract value from. Returns ------- Python value. """ return None
[docs] def fromString(self, string: str) -> Any: """ Convert a string to a python value. Implement this method in a subclass to define the conversion. Parameters ---------- string : str String representation of the value. Returns ------- Python value. """ return None
[docs] def minValue(self) -> Any: """Return the minimum value for the Model type. Implement this method in a subclass to define the minimum value. """ return None
[docs] def maxValue(self) -> Any: """Return the maximum value for the Model type. Implement this method in a subclass to define the maximum value. """ return None
[docs] class UInt(Model): """Model class for unsigned integers. Parameters ---------- bitSize : int Number of bits being represented. """ pytype = int defaultdisp = '{:#x}' modelId = rim.UInt def __init__(self, bitSize: int) -> None: """Initialize unsigned-integer model metadata.""" super().__init__(bitSize) self.name = f'{self.__class__.__name__}{self.bitSize}' self.ndType = np.dtype(np.uint32) if bitSize <= 32 else np.dtype(np.uint64) # Called by raw read/write and when bitsize > 64
[docs] def toBytes(self, value: int) -> bytes: """ Parameters ---------- value : int Python value to convert. Returns ------- bytes Byte array representation of the value. """ return value.to_bytes(byteCount(self.bitSize), self.endianness, signed=self.signed)
# Called by raw read/write and when bitsize > 64
[docs] def fromBytes(self, ba: bytes) -> int: """ Parameters ---------- ba : bytes Byte array to extract value from. Returns ------- int Python value. """ return int.from_bytes(ba, self.endianness, signed=self.signed)
[docs] def fromString(self, string: str) -> int: """ Parameters ---------- string : str String representation of the value. Returns ------- int Python value. """ return int(string, 0)
[docs] def minValue(self) -> int: """Return the minimum unisgned int (0). """ return 0
[docs] def maxValue(self) -> int: """Return the maximum unsigned int (2**bitSize-1). """ return (2**self.bitSize)-1
[docs] class UIntReversed(UInt): """Model class for unsigned integers, stored in reverse bit order.""" modelId = rim.PyFunc # Not yet supported bitReverse = True def __init__(self, bitSize: int) -> None: """Initialize reversed-bit unsigned-integer model metadata.""" super().__init__(bitSize) self.ndType = None
[docs] def toBytes(self, value: int) -> bytes: """ Convert a python int value to a byte array. Parameters ---------- value : int Python int value to convert. Returns ------- bytes Byte array representation of the value. """ valueReverse = reverseBits(value, self.bitSize) return valueReverse.to_bytes(byteCount(self.bitSize), self.endianness, signed=self.signed)
[docs] def fromBytes(self, ba: bytes) -> int: """ Convert a byte array to a python int value. Parameters ---------- ba : bytes Byte array to extract value from. Returns ------- int Python value. """ valueReverse = int.from_bytes(ba, self.endianness, signed=self.signed) return reverseBits(valueReverse, self.bitSize)
[docs] class Int(UInt): """Model class for signed integers. Parameters ---------- bitSize : int Number of bits being represented. """ # Override these and inherit everything else from UInt defaultdisp = '{:d}' signed = True modelId = rim.Int def __init__(self, bitSize: int) -> None: """Initialize signed-integer model metadata.""" super().__init__(bitSize) self.ndType = np.dtype(np.int32) if bitSize <= 32 else np.dtype(np.int64) # Called by raw read/write and when bitsize > 64
[docs] def toBytes(self, value: int) -> bytes: """ Convert a python int value to a byte array. Parameters ---------- value : int Python int value to convert. Returns ------- bytes Byte array representation of the value. """ if (value < 0) and (self.bitSize < (byteCount(self.bitSize) * 8)): newValue = value & (2**(self.bitSize)-1) # Strip upper bits ba = newValue.to_bytes(byteCount(self.bitSize), self.endianness, signed=False) else: ba = value.to_bytes(byteCount(self.bitSize), self.endianness, signed=True) return ba
# Called by raw read/write and when bitsize > 64
[docs] def fromBytes(self, ba: bytes) -> int: """ Convert a byte array to a python int value. Parameters ---------- ba : bytes Byte array to extract value from. Returns ------- int Python value. """ if (self.bitSize < (byteCount(self.bitSize)*8)): value = int.from_bytes(ba, self.endianness, signed=False) if value >= 2**(self.bitSize-1): value -= 2**self.bitSize else: value = int.from_bytes(ba, self.endianness, signed=True) return
[docs] def fromString(self, string: str) -> int: """ Convert a string to a python int value. Parameters ---------- string : str String representation of the value. Returns ------- int Python value. """ i = int(string, 0) # perform twos complement if necessary if i>0 and ((i >> self.bitSize) & 0x1 == 1): i = i - (1 << self.bitSize) return i
[docs] def minValue(self) -> int: """Return the minimum signed int (-2**(bitSize-1)). """ return -1 * (2**(self.bitSize-1))
[docs] def maxValue(self) -> int: """Return the maximum signed int (2**(bitSize-1)-1). """ return (2**(self.bitSize-1))-1
[docs] class UIntBE(UInt): """Model class for big endian unsigned integers. Inherits from UInt. """ endianness = 'big'
[docs] class IntBE(Int): """Model class for big endian integers. Inherits from Int. """ endianness = 'big'
[docs] class Bool(Model): """Model class for booleans. Parameters ---------- bitSize : int Number of bits being represented. Must be 1. """ pytype = bool defaultdisp = {False: 'False', True: 'True'} modelId = rim.Bool def __init__(self, bitSize: int) -> None: """Initialize boolean model metadata.""" assert bitSize == 1, f"The bitSize param of Model {self.__class__.__name__} must be 1" super().__init__(bitSize) self.ndType = np.dtype(bool)
[docs] def toBytes(self, value: bool) -> bytes: """ Convert a python bool value to a byte array. Parameters ---------- value : bool Python bool value to convert. Returns ------- bytes Byte array representation of the value. """ return value.to_bytes(byteCount(self.bitSize), self.endianness, signed=self.signed)
[docs] def fromBytes(self, ba: bytes) -> bool: """ Convert a byte array to a python bool value. Parameters ---------- ba : bytes Byte array to extract value from. Returns ------- bool Python value. """ return bool(int.from_bytes(ba, self.endianness, signed=self.signed))
[docs] def fromString(self, string: str) -> bool: """Convert a string to a python bool value. Parameters ---------- string : str String representation of the value. Returns ------- bool Python value. Parameters ---------- string : """ return str.lower(string) == "true"
[docs] def minValue(self) -> int: """Return the minimum bool (0). """ return 0
[docs] def maxValue(self) -> int: """Return the maximum bool (1). """ return 1
[docs] class String(Model): """Model class for strings. Parameters ---------- bitSize : int Number of bits being represented. """ encoding = 'utf-8' defaultdisp = '{}' pytype = str modelId = rim.String def __init__(self, bitSize: int) -> None: """Initialize string model metadata.""" super().__init__(bitSize) self.name = f'{self.__class__.__name__}({self.bitSize//8})'
[docs] def toBytes(self, value: str) -> bytearray: """ Convert a python string value to a byte array. Parameters ---------- value : str Python string value to convert. Returns ------- bytearray Byte array representation of the value. """ ba = bytearray(value, self.encoding) ba.extend(bytearray(1)) return ba
[docs] def fromBytes(self, ba: bytearray) -> str: """ Convert a byte array to a python string value. Parameters ---------- ba : bytearray Byte array to extract value from. Returns ------- str Python value. """ s = ba.rstrip(bytearray(1)) return s.decode(self.encoding)
[docs] def fromString(self, string: str) -> str: """ Convert a string to a python string value. Parameters ---------- string : str String representation of the value. Returns ------- str Python string value. """ return string
[docs] class Float(Model): """Model class for 32-bit floating point numbers. Parameters ---------- bitSize : int Number of bits being represented. Must be 32. """ defaultdisp = '{:f}' pytype = float fstring = 'f' modelId = rim.Float def __init__(self, bitSize: int) -> None: """Initialize 32-bit float model metadata.""" assert bitSize == 32, f"The bitSize param of Model {self.__class__.__name__} must be 32" super().__init__(bitSize) self.name = f'{self.__class__.__name__}{self.bitSize}' self.ndType = np.dtype(np.float32)
[docs] def toBytes(self, value: float) -> bytearray: """ Convert a python float value to a byte array. Parameters ---------- value : float Python float value to convert. Returns ------- bytearray Byte array representation of the value. """ return bytearray(struct.pack(self.fstring, value))
[docs] def fromBytes(self, ba: bytes) -> float: """ Convert a byte array to a python float value. Parameters ---------- ba : bytes Byte array to extract value from. Returns ------- float Python value. """ return struct.unpack(self.fstring, ba)[0]
[docs] def fromString(self, string: str) -> float: """ Convert a string to a python float value. Parameters ---------- string : str String representation of the value. Returns ------- float Python value. """ return float(string)
[docs] def minValue(self) -> float: """Return the minimum 32-bit float (-3.4e38). """ return -3.4e38
[docs] def maxValue(self) -> float: """Return the maximum 32-bit float (3.4e38). """ return 3.4e38
[docs] class Double(Float): """Model class for 64-bit floats. Parameters ---------- bitSize : int Number of bits being represented. Must be 64. """ fstring = 'd' modelId = rim.Double def __init__(self, bitSize: int) -> None: """Initialize 64-bit float model metadata.""" assert bitSize == 64, f"The bitSize param of Model {self.__class__.__name__} must be 64" Model.__init__(self,bitSize) self.name = f'{self.__class__.__name__}{self.bitSize}' self.ndType = np.dtype(np.float64)
[docs] def minValue(self) -> float: """Return the minimum 64-bit float (-1.80e308). """ return -1.80e308
[docs] def maxValue(self) -> float: """Return the maximum 64-bit float (1.80e308). """ return 1.80e308
[docs] class FloatBE(Float): """Model class for 32-bit floats stored as big endian""" endianness = 'big' fstring = '!f'
[docs] class DoubleBE(Double): """Model class for 64-bit floats stored as big endian""" endianness = 'big' fstring = '!d'
[docs] class Fixed(Model): """ Model class for fixed point signed integers Parameters ---------- bitSize : int Specifies the total number of bits, including the binary point bit width. binPoint : int Specifies the bit location of the binary point, where bit zero is the least significant bit. """ pytype = float signed = True modelId = rim.Fixed def __init__(self, bitSize: int, binPoint: int) -> None: """Initialize signed fixed-point model metadata.""" super().__init__(bitSize,binPoint) self.name = f'Fixed_{self.bitSize}_{self.binPoint}' self.ndType = np.dtype(np.float64)
[docs] class UFixed(Model): """ Model class for fixed point unsigned integers Parameters ---------- bitSize : int Specifies the total number of bits, including the binary point bit width. binPoint : int Specifies the bit location of the binary point, where bit zero is the least significant bit. """ pytype = float signed = False modelId = rim.Fixed def __init__(self, bitSize: int, binPoint: int) -> None: """Initialize unsigned fixed-point model metadata.""" super().__init__(bitSize,binPoint) self.name = f'UFixed_{self.bitSize}_{self.binPoint}' self.ndType = np.dtype(np.float64)