Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision

Target

Select target project
  • anirudh.jonnalagadda/pystencils
  • hyteg/pystencils
  • jbadwaik/pystencils
  • jngrad/pystencils
  • itischler/pystencils
  • ob28imeq/pystencils
  • hoenig/pystencils
  • Bindgen/pystencils
  • hammer/pystencils
  • da15siwa/pystencils
  • holzer/pystencils
  • alexander.reinauer/pystencils
  • ec93ujoh/pystencils
  • Harke/pystencils
  • seitz/pystencils
  • pycodegen/pystencils
16 results
Select Git revision
Show changes
Showing
with 1191 additions and 657 deletions
class PsTypeError(Exception):
"""Indicates an error relating to incorrect usage of a pystencils type."""
"""
Caching of Instances
^^^^^^^^^^^^^^^^^^^^
To handle and compare types more efficiently, the pystencils type system customizes class
instantiation to cache and reuse existing instances of types.
This means, for example, if a 32-bit const unsigned integer type gets created in two places
in the program, the resulting objects are exactly the same:
>>> from pystencils.types import PsUnsignedIntegerType
>>> t1 = PsUnsignedIntegerType(32, const=True)
>>> t2 = PsUnsignedIntegerType(32, const=True)
>>> t1 is t2
True
This mechanism is implemented by the metaclass `PsTypeMeta`. It is not perfect, however;
some parts of Python that bypass the regular object creation sequence, such as `pickle` and
`copy.copy`, may create additional instances of types.
.. autoclass:: pystencils.types.meta.PsTypeMeta
:members:
Extending the Type System
^^^^^^^^^^^^^^^^^^^^^^^^^
When extending the type system's class hierarchy, new classes need to implement at least the internal
method `__args__`. This method, when called on a type object, must return a hashable sequence of arguments
-- not including the const-qualifier --
that can be used to recreate that exact type. It is used internally to compute hashes and compare equality
of types, as well as for const-conversion.
.. autofunction:: pystencils.types.PsType.__args__
"""
from __future__ import annotations
from warnings import warn
from abc import ABCMeta, abstractmethod
from typing import TypeVar, Any, cast
import numpy as np
class PsTypeMeta(ABCMeta):
"""Metaclass for the `PsType` hierarchy.
`PsTypeMeta` holds an internal cache of all created instances of `PsType` and overrides object creation
such that whenever a type gets instantiated more than once with the same argument list,
instead of creating a new object, the existing object is returned.
"""
_instances: dict[Any, PsType] = dict()
def __call__(cls: PsTypeMeta, *args: Any, **kwargs: Any) -> Any:
assert issubclass(cls, PsType)
kwarg_tuples = tuple(sorted(kwargs.items(), key=lambda t: t[0]))
try:
key = (cls, args, kwarg_tuples)
if key in cls._instances:
return cls._instances[key]
except TypeError:
key = None
obj = super().__call__(*args, **kwargs)
canonical_key = (cls, obj.__args__(), (("const", obj.const),))
if canonical_key in cls._instances:
obj = cls._instances[canonical_key]
else:
cls._instances[canonical_key] = obj
if key is not None:
cls._instances[key] = obj
return obj
class PsType(metaclass=PsTypeMeta):
"""Base class for all pystencils types.
Args:
const: Const-qualification of this type
"""
# -------------------------------------------------------------------------------------------
# Arguments, Equality and Hashing
# -------------------------------------------------------------------------------------------
@abstractmethod
def __args__(self) -> tuple[Any, ...]:
"""Return the arguments used to create this instance, in canonical order, excluding the const-qualifier.
The tuple returned by this method must be hashable and for each instantiable subclass
``MyType`` of ``PsType``, the following must hold::
t = MyType(< arguments >)
assert MyType(*t.__args__(), const=t.const) == t
"""
def __eq__(self, other: object) -> bool:
if self is other:
return True
if type(self) is not type(other):
return False
other = cast(PsType, other)
return self.const == other.const and self.__args__() == other.__args__()
def __hash__(self) -> int:
return hash((type(self), self.const, self.__args__()))
# -------------------------------------------------------------------------------------------
# Constructor and properties
# -------------------------------------------------------------------------------------------
def __init__(self, const: bool = False):
self._const = const
self._requalified: PsType | None = None
@property
def const(self) -> bool:
return self._const
# -------------------------------------------------------------------------------------------
# Optional Info
# -------------------------------------------------------------------------------------------
@property
def required_headers(self) -> set[str]:
"""The set of header files required when this type occurs in generated code."""
return set()
@property
def itemsize(self) -> int | None:
"""If this type has a valid in-memory size, return that size in bytes."""
return None
@property
def numpy_dtype(self) -> np.dtype | None:
"""A np.dtype object representing this data type.
Available both for backward compatibility and for interaction with the numpy-based runtime system.
"""
return None
# -------------------------------------------------------------------------------------------
# String Conversion
# -------------------------------------------------------------------------------------------
def _const_string(self) -> str:
return "const " if self._const else ""
@abstractmethod
def c_string(self) -> str:
pass
@property
def c_name(self) -> str:
"""Returns the C name of this type without const-qualifiers."""
warn(
"`c_name` is deprecated and will be removed in a future version of pystencils. "
"Use `c_string()` instead.",
DeprecationWarning,
)
return deconstify(self).c_string()
def __str__(self) -> str:
return self.c_string()
T = TypeVar("T", bound=PsType)
def constify(t: T) -> T:
"""Adds the const qualifier to a given type."""
if not t.const:
if t._requalified is None:
t._requalified = type(t)(*t.__args__(), const=True) # type: ignore
return cast(T, t._requalified)
else:
return t
def deconstify(t: T) -> T:
"""Removes the const qualifier from a given type."""
if t.const:
if t._requalified is None:
t._requalified = type(t)(*t.__args__(), const=False) # type: ignore
return cast(T, t._requalified)
else:
return t
import numpy as np
from .types import (
PsType,
PsPointerType,
PsStructType,
PsNumericType,
PsUnsignedIntegerType,
PsSignedIntegerType,
PsIeeeFloatType,
PsBoolType,
)
UserTypeSpec = str | type | np.dtype | PsType
"""Valid arguments for `create_type`."""
def create_type(type_spec: UserTypeSpec) -> PsType:
"""Create a pystencils type object from a variety of specifications.
This function converts several possible representations of data types to an instance of `PsType`.
The ``type_spec`` argument can be any of the following:
- Strings (`str`): will be parsed as common C types, throwing an exception if that fails.
Custom types must be created explicitly using `PsCustomType`.
- Python builtin data types (instances of `type`): Attempts to interpret Python numeric types like so:
- `int` becomes a signed 64-bit integer
- `float` becomes a double-precision IEEE-754 float
- No others are supported at the moment
- Supported Numpy scalar data types (see https://numpy.org/doc/stable/reference/arrays.scalars.html)
are converted to pystencils scalar data types
- Instances of `numpy.dtype`: Attempt to interpret scalar types like above, and structured types as structs.
- Instances of `PsType` will be returned as they are
Args:
type_spec: The data type, in one of the above formats
"""
from .parsing import parse_type_string, interpret_python_type, interpret_numpy_dtype
if isinstance(type_spec, PsType):
return type_spec
if isinstance(type_spec, str):
return parse_type_string(type_spec)
if isinstance(type_spec, type):
return interpret_python_type(type_spec)
if isinstance(type_spec, np.dtype):
return interpret_numpy_dtype(type_spec)
raise ValueError(f"{type_spec} is not a valid type specification.")
def create_numeric_type(type_spec: UserTypeSpec) -> PsNumericType:
"""Like `create_type`, but only for numeric types."""
dtype = create_type(type_spec)
if not isinstance(dtype, PsNumericType):
raise ValueError(
f"Given type {type_spec} does not translate to a numeric type."
)
return dtype
def interpret_python_type(t: type) -> PsType:
if t is int:
return PsSignedIntegerType(64)
if t is float:
return PsIeeeFloatType(64)
if t is bool:
return PsBoolType()
if t is np.uint8:
return PsUnsignedIntegerType(8)
if t is np.uint16:
return PsUnsignedIntegerType(16)
if t is np.uint32:
return PsUnsignedIntegerType(32)
if t is np.uint64:
return PsUnsignedIntegerType(64)
if t is np.int8:
return PsSignedIntegerType(8)
if t is np.int16:
return PsSignedIntegerType(16)
if t is np.int32:
return PsSignedIntegerType(32)
if t is np.int64:
return PsSignedIntegerType(64)
if t is np.float16:
return PsIeeeFloatType(16)
if t is np.float32:
return PsIeeeFloatType(32)
if t is np.float64:
return PsIeeeFloatType(64)
if t is np.bool_:
return PsBoolType()
raise ValueError(f"Could not interpret Python data type {t} as a pystencils type.")
def interpret_numpy_dtype(t: np.dtype) -> PsType:
if t.fields is not None:
# it's a struct
if not t.isalignedstruct:
raise ValueError("pystencils currently only accepts aligned structured data types.")
members = []
for fname, fspec in t.fields.items():
members.append(PsStructType.Member(fname, interpret_numpy_dtype(fspec[0])))
return PsStructType(members)
else:
try:
return interpret_python_type(t.type)
except ValueError:
raise ValueError(
f"Could not interpret numpy dtype object {t} as a pystencils type."
)
def parse_type_string(s: str) -> PsType:
tokens = s.rsplit("*", 1)
match tokens:
case [base]: # input contained no '*', is no pointer
match base.split(): # split at whitespace to find `const` qualifiers (C typenames cannot contain spaces)
case [typename]:
return parse_type_name(typename, False)
case ["const", typename] | [typename, "const"]:
return parse_type_name(typename, True)
case _:
raise ValueError(f"Could not parse token '{base}' as C type.")
case [base, suffix]: # input was "base * suffix"
base_type = parse_type_string(base)
match suffix.split():
case []:
return PsPointerType(base_type, restrict=False, const=False)
case ["const"]:
return PsPointerType(base_type, restrict=False, const=True)
case ["restrict"]:
return PsPointerType(base_type, restrict=True, const=False)
case ["const", "restrict"] | ["restrict", "const"]:
return PsPointerType(base_type, restrict=True, const=True)
case _:
raise ValueError(f"Could not parse token '{s}' as C type.")
case _:
raise ValueError(f"Could not parse token '{s}' as C type.")
def parse_type_name(typename: str, const: bool):
match typename:
case "bool":
return PsBoolType(const=const)
case "int" | "int64" | "int64_t":
return PsSignedIntegerType(64, const=const)
case "int32" | "int32_t":
return PsSignedIntegerType(32, const=const)
case "int16" | "int16_t":
return PsSignedIntegerType(16, const=const)
case "int8" | "int8_t":
return PsSignedIntegerType(8, const=const)
case "uint64" | "uint64_t":
return PsUnsignedIntegerType(64, const=const)
case "uint32" | "uint32_t":
return PsUnsignedIntegerType(32, const=const)
case "uint16" | "uint16_t":
return PsUnsignedIntegerType(16, const=const)
case "uint8" | "uint8_t":
return PsUnsignedIntegerType(8, const=const)
case "half" | "float16":
return PsIeeeFloatType(16, const=const)
case "float" | "float32":
return PsIeeeFloatType(32, const=const)
case "double" | "float64":
return PsIeeeFloatType(64, const=const)
case _:
raise ValueError(f"Could not parse token '{typename}' as C type.")
"""Quick access to the pystencils data type system."""
from __future__ import annotations
from .types import (
PsCustomType,
PsScalarType,
PsBoolType,
PsPointerType,
PsArrayType,
PsIntegerType,
PsUnsignedIntegerType,
PsSignedIntegerType,
PsIeeeFloatType,
)
Custom = PsCustomType
"""Alias of `PsCustomType`"""
Scalar = PsScalarType
"""Alias of `PsScalarType`"""
Ptr = PsPointerType
"""Alias of `PsPointerType`"""
Arr = PsArrayType
"""Alias of `PsArrayType`"""
Bool = PsBoolType
"""Alias of `PsBoolType`"""
AnyInt = PsIntegerType
"""Alias of `PsIntegerType`"""
UInt = PsUnsignedIntegerType
"""Alias of `PsUnsignedIntegerType`"""
Int = PsSignedIntegerType
"""Alias of `PsSignedIntegerType`"""
SInt = PsSignedIntegerType
"""Alias of `PsSignedIntegerType`"""
Fp = PsIeeeFloatType
"""Alias of `PsIeeeFloatType`"""
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import final, Any, Sequence, SupportsIndex
from dataclasses import dataclass
import numpy as np
from .exception import PsTypeError
from .meta import PsType, deconstify
class PsCustomType(PsType):
"""Class to model custom types by their names.
Args:
name: Name of the custom type.
"""
__match_args__ = ("name",)
def __init__(self, name: str, const: bool = False):
super().__init__(const)
self._name = name
def __args__(self) -> tuple[Any, ...]:
"""
>>> t = PsCustomType("std::vector< int >")
>>> t == PsCustomType(*t.__args__())
True
"""
return (self._name,)
@property
def name(self) -> str:
return self._name
def c_string(self) -> str:
return f"{self._const_string()}{self._name}"
def __repr__(self) -> str:
return f"CustomType( {self.name}, const={self.const} )"
class PsDereferencableType(PsType, ABC):
"""Base class for subscriptable types.
`PsDereferencableType` represents any type that may be dereferenced and may
occur as the base of a subscript, that is, before the C ``[]`` operator.
Args:
base_type: The base type, which is the type of the object obtained by dereferencing.
const: Const-qualification
"""
__match_args__ = ("base_type",)
def __init__(self, base_type: PsType, const: bool = False):
super().__init__(const)
self._base_type = base_type
@property
def base_type(self) -> PsType:
return self._base_type
@final
class PsPointerType(PsDereferencableType):
"""A C pointer with arbitrary base type.
`PsPointerType` models C pointer types to arbitrary data, with support for ``restrict``-qualified pointers.
"""
__match_args__ = ("base_type",)
def __init__(self, base_type: PsType, restrict: bool = False, const: bool = False):
super().__init__(base_type, const)
self._restrict = restrict
def __args__(self) -> tuple[Any, ...]:
"""
>>> t = PsPointerType(PsBoolType())
>>> t == PsPointerType(*t.__args__())
True
"""
return (self._base_type, self._restrict)
@property
def restrict(self) -> bool:
return self._restrict
def c_string(self) -> str:
base_str = self._base_type.c_string()
restrict_str = " RESTRICT" if self._restrict else ""
const_str = " const" if self.const else ""
return f"{base_str} *{restrict_str}{const_str}"
def __repr__(self) -> str:
return f"PsPointerType( {repr(self.base_type)}, const={self.const}, restrict={self.restrict} )"
class PsArrayType(PsDereferencableType):
"""Multidimensional array of fixed shape.
The element type of an array is never const; only the array itself can be.
If ``element_type`` is const, its constness will be removed.
"""
def __init__(
self,
element_type: PsType,
shape: SupportsIndex | Sequence[SupportsIndex],
const: bool = False,
):
from operator import index
if isinstance(shape, SupportsIndex):
shape = (index(shape),)
else:
shape = tuple(index(s) for s in shape)
if not shape or any(s <= 0 for s in shape):
raise ValueError(f"Invalid array shape: {shape}")
if isinstance(element_type, PsArrayType):
raise ValueError("Element type of array cannot be another array.")
element_type = deconstify(element_type)
self._shape = shape
super().__init__(element_type, const)
def __args__(self) -> tuple[Any, ...]:
"""
>>> t = PsArrayType(PsBoolType(), (13, 42))
>>> t == PsArrayType(*t.__args__())
True
"""
return (self._base_type, self._shape)
@property
def shape(self) -> tuple[int, ...]:
"""Shape of this array"""
return self._shape
@property
def dim(self) -> int:
"""Dimensionality of this array"""
return len(self._shape)
def c_string(self) -> str:
arr_brackets = "".join(f"[{s}]" for s in self._shape)
const = self._const_string()
return const + self._base_type.c_string() + arr_brackets
def __repr__(self) -> str:
return f"PsArrayType(element_type={repr(self._base_type)}, shape={self._shape}, const={self._const})"
class PsStructType(PsType):
"""Named or anonymous structured data type.
A struct type is defined by its sequence of members.
The struct may optionally have a name, although the code generator currently does not support named structs
and treats them the same way as anonymous structs.
Struct member types cannot be ``const``; if a ``const`` member type is passed, its constness will be removed.
"""
@dataclass(frozen=True)
class Member:
name: str
dtype: PsType
def __post_init__(self):
# Need to use object.__setattr__ because instances are frozen
object.__setattr__(self, "dtype", deconstify(self.dtype))
@staticmethod
def _canonical_members(members: Sequence[PsStructType.Member | tuple[str, PsType]]):
return tuple(
(PsStructType.Member(m[0], m[1]) if isinstance(m, tuple) else m)
for m in members
)
def __init__(
self,
members: Sequence[PsStructType.Member | tuple[str, PsType]],
name: str | None = None,
const: bool = False,
):
super().__init__(const=const)
self._name = name
self._members = self._canonical_members(members)
names: set[str] = set()
for member in self._members:
if member.name in names:
raise ValueError(f"Duplicate struct member name: {member.name}")
names.add(member.name)
def __args__(self) -> tuple[Any, ...]:
"""
>>> t = PsStructType([("idx", PsSignedIntegerType(32)), ("val", PsBoolType())], "sname")
>>> t == PsStructType(*t.__args__())
True
"""
return (self._members, self._name)
@property
def members(self) -> tuple[PsStructType.Member, ...]:
return self._members
def find_member(self, member_name: str) -> PsStructType.Member | None:
"""Find a member by name"""
for m in self._members:
if m.name == member_name:
return m
return None
def get_member(self, member_name: str) -> PsStructType.Member:
m = self.find_member(member_name)
if m is None:
raise KeyError(f"No struct member with name {member_name}")
return m
@property
def name(self) -> str:
if self._name is None:
raise PsTypeError("Cannot retrieve name from anonymous struct type")
return self._name
@property
def anonymous(self) -> bool:
return self._name is None
@property
def numpy_dtype(self) -> np.dtype:
members = [(m.name, m.dtype.numpy_dtype) for m in self._members]
return np.dtype(members, align=True)
@property
def itemsize(self) -> int:
return self.numpy_dtype.itemsize
def c_string(self) -> str:
if self._name is None:
raise PsTypeError("Cannot retrieve C string for anonymous struct type")
return self._name
def __str__(self) -> str:
if self._name is None:
return "<anonymous>"
else:
return self._name
def __repr__(self) -> str:
members = ", ".join(f"{m.dtype} {m.name}" for m in self._members)
name = "<anonymous>" if self.anonymous else f"name={self._name}"
return f"PsStructType( [{members}], {name}, const={self.const} )"
class PsNumericType(PsType, ABC):
"""Numeric data type, i.e. any type that may occur inside arithmetic-logical expressions.
**Constants**
Every numeric type has to act as a factory for compile-time constants of that type.
The `PsConstant` class relies on `create_constant` to instantiate constants
of a given numeric type. The object returned by `create_constant` must implement the
necessary arithmetic operations, and its arithmetic behaviour must match the given type.
`create_constant` should fail whenever its input cannot safely be interpreted as the given
type.
"""
@abstractmethod
def create_constant(self, value: Any) -> Any:
"""
Create the internal representation of a constant with this type.
Raises:
PsTypeError: If the given value cannot be interpreted in this type.
"""
@abstractmethod
def is_int(self) -> bool:
pass
@abstractmethod
def is_sint(self) -> bool:
pass
@abstractmethod
def is_uint(self) -> bool:
pass
@abstractmethod
def is_float(self) -> bool:
pass
@abstractmethod
def is_bool(self) -> bool:
pass
class PsScalarType(PsNumericType, ABC):
"""Scalar numeric type."""
@abstractmethod
def create_literal(self, value: Any) -> str:
"""Create a C numerical literal for a constant of this type.
Raises:
PsTypeError: If the given value's type is not the numeric type's compiler-internal representation.
"""
@property
@abstractmethod
def width(self) -> int:
"""Return this type's width in bits."""
def is_int(self) -> bool:
return isinstance(self, PsIntegerType)
def is_sint(self) -> bool:
return isinstance(self, PsIntegerType) and self.signed
def is_uint(self) -> bool:
return isinstance(self, PsIntegerType) and not self.signed
def is_float(self) -> bool:
return isinstance(self, PsIeeeFloatType)
def is_bool(self) -> bool:
return isinstance(self, PsBoolType)
class PsVectorType(PsNumericType):
"""Packed vector of numeric type.
The packed vector's element type will always be made non-const.
Args:
element_type: Underlying scalar data type
num_entries: Number of entries in the vector
"""
def __init__(
self, scalar_type: PsScalarType, vector_entries: int, const: bool = False
):
super().__init__(const)
self._vector_entries = vector_entries
self._scalar_type = deconstify(scalar_type)
def __args__(self) -> tuple[Any, ...]:
"""
>>> t = PsVectorType(PsBoolType(), 8)
>>> t == PsVectorType(*t.__args__())
True
"""
return (self._scalar_type, self._vector_entries)
@property
def scalar_type(self) -> PsScalarType:
return self._scalar_type
@property
def vector_entries(self) -> int:
return self._vector_entries
@property
def width(self) -> int:
return self._scalar_type.width * self._vector_entries
def is_int(self) -> bool:
return self._scalar_type.is_int()
def is_sint(self) -> bool:
return self._scalar_type.is_sint()
def is_uint(self) -> bool:
return self._scalar_type.is_uint()
def is_float(self) -> bool:
return self._scalar_type.is_float()
def is_bool(self) -> bool:
return self._scalar_type.is_bool()
@property
def itemsize(self) -> int | None:
if self._scalar_type.itemsize is None:
return None
else:
return self._vector_entries * self._scalar_type.itemsize
@property
def numpy_dtype(self):
return np.dtype((self._scalar_type.numpy_dtype, (self._vector_entries,)))
def create_constant(self, value: Any) -> Any:
if isinstance(value, np.ndarray):
if value.shape != (self._vector_entries,):
raise PsTypeError(
f"Cannot create constant of vector type {self} from array of shape {value.shape}"
)
return np.array([self._scalar_type.create_constant(v) for v in value])
element = self._scalar_type.create_constant(value)
return np.array(
[element] * self._vector_entries, dtype=self.scalar_type.numpy_dtype
)
def c_string(self) -> str:
raise PsTypeError("Cannot retrieve C type string for generic vector types.")
def __str__(self) -> str:
return f"{self._scalar_type}<{self._vector_entries}>"
def __repr__(self) -> str:
return (
f"PsVectorType( scalar_type={repr(self._scalar_type)}, "
f"vector_width={self._vector_entries}, const={self.const} )"
)
class PsBoolType(PsScalarType):
"""Boolean type."""
NUMPY_TYPE = np.bool_
def __init__(self, const: bool = False):
super().__init__(const)
def __args__(self) -> tuple[Any, ...]:
"""
>>> t = PsBoolType()
>>> t == PsBoolType(*t.__args__())
True
"""
return ()
@property
def width(self) -> int:
return 8
@property
def itemsize(self) -> int:
return self.width // 8
@property
def numpy_dtype(self) -> np.dtype | None:
return np.dtype(PsBoolType.NUMPY_TYPE)
def create_literal(self, value: Any) -> str:
if not isinstance(value, self.NUMPY_TYPE):
raise PsTypeError(
f"Given value {value} is not of required type {self.NUMPY_TYPE}"
)
if value == np.True_:
return "true"
elif value == np.False_:
return "false"
else:
raise PsTypeError(f"Cannot create boolean literal from {value}")
def create_constant(self, value: Any) -> Any:
if value in (1, True, np.True_):
return np.True_
elif value in (0, False, np.False_):
return np.False_
else:
raise PsTypeError(f"Cannot create boolean constant from value {value}")
def c_string(self) -> str:
return "bool"
class PsIntegerType(PsScalarType, ABC):
"""Signed and unsigned integer types.
`PsIntegerType` cannot be instantiated on its own, but only through `PsSignedIntegerType`
and `PsUnsignedIntegerType`. This distinction is meant mostly to help in pattern matching.
"""
__match_args__ = ("width",)
SUPPORTED_WIDTHS = (8, 16, 32, 64)
NUMPY_TYPES: dict[int, type] = dict()
def __init__(self, width: int, signed: bool = True, const: bool = False):
if width not in self.SUPPORTED_WIDTHS:
raise ValueError(
f"Invalid integer width; must be one of {self.SUPPORTED_WIDTHS}."
)
super().__init__(const)
self._width = width
self._signed = signed
@property
def width(self) -> int:
return self._width
@property
def signed(self) -> bool:
return self._signed
@property
def itemsize(self) -> int:
return self.width // 8
@property
def numpy_dtype(self) -> np.dtype | None:
return np.dtype(self.NUMPY_TYPES[self._width])
def create_literal(self, value: Any) -> str:
np_dtype = self.NUMPY_TYPES[self._width]
if not isinstance(value, np_dtype):
raise PsTypeError(f"Given value {value} is not of required type {np_dtype}")
unsigned_suffix = "" if self.signed else "u"
match self.width:
case w if w < 32:
# Plain integer literals get at least type `int`, which is 32 bit in all relevant cases
# So we need to explicitly cast to smaller types
return f"(({deconstify(self).c_string()}) {value}{unsigned_suffix})"
case 32:
# No suffix here - becomes `int`, which is 32 bit
return f"{value}{unsigned_suffix}"
case 64:
# LL suffix: `long long` is the only type guaranteed to be 64 bit wide
return f"{value}{unsigned_suffix}LL"
case _:
assert False, "unreachable code"
def create_constant(self, value: Any) -> Any:
np_type = self.NUMPY_TYPES[self._width]
if isinstance(value, (int, np.integer)):
iinfo = np.iinfo(np_type) # type: ignore
if value < iinfo.min or value > iinfo.max:
raise PsTypeError(
f"Could not interpret {value} as {self}: Value is out of bounds."
)
return np_type(value)
raise PsTypeError(f"Could not interpret {value} as {repr(self)}")
def _str_without_const(self) -> str:
prefix = "" if self._signed else "u"
return f"{prefix}int{self._width}"
def c_string(self) -> str:
return f"{self._const_string()}{self._str_without_const()}_t"
def __str__(self) -> str:
return f"{self._const_string()}{self._str_without_const()}"
def __repr__(self) -> str:
return f"PsIntegerType( width={self.width}, signed={self.signed}, const={self.const} )"
@final
class PsSignedIntegerType(PsIntegerType):
"""Signed integer types."""
__match_args__ = ("width",)
NUMPY_TYPES = {
8: np.int8,
16: np.int16,
32: np.int32,
64: np.int64,
}
def __init__(self, width: int, const: bool = False):
super().__init__(width, True, const)
def __args__(self) -> tuple[Any, ...]:
"""
>>> t = PsSignedIntegerType(32)
>>> t == PsSignedIntegerType(*t.__args__())
True
"""
return (self._width,)
@final
class PsUnsignedIntegerType(PsIntegerType):
"""Unsigned integer types."""
__match_args__ = ("width",)
NUMPY_TYPES = {
8: np.uint8,
16: np.uint16,
32: np.uint32,
64: np.uint64,
}
def __init__(self, width: int, const: bool = False):
super().__init__(width, False, const)
def __args__(self) -> tuple[Any, ...]:
"""
>>> t = PsUnsignedIntegerType(32)
>>> t == PsUnsignedIntegerType(*t.__args__())
True
"""
return (self._width,)
@final
class PsIeeeFloatType(PsScalarType):
"""IEEE-754 floating point data types"""
__match_args__ = ("width",)
SUPPORTED_WIDTHS = (16, 32, 64)
NUMPY_TYPES = {
16: np.float16,
32: np.float32,
64: np.float64,
}
def __init__(self, width: int, const: bool = False):
if width not in self.SUPPORTED_WIDTHS:
raise ValueError(
f"Invalid floating-point width {width}; must be one of {self.SUPPORTED_WIDTHS}."
)
super().__init__(const)
self._width = width
def __args__(self) -> tuple[Any, ...]:
"""
>>> t = PsIeeeFloatType(32)
>>> t == PsIeeeFloatType(*t.__args__())
True
"""
return (self._width,)
@property
def width(self) -> int:
return self._width
@property
def itemsize(self) -> int:
return self.width // 8
@property
def numpy_dtype(self) -> np.dtype | None:
return np.dtype(self.NUMPY_TYPES[self._width])
@property
def required_headers(self) -> set[str]:
if self._width == 16:
return {'"half_precision.h"'}
else:
return set()
def create_literal(self, value: Any) -> str:
np_dtype = self.NUMPY_TYPES[self._width]
if not isinstance(value, np_dtype):
raise PsTypeError(f"Given value {value} is not of required type {np_dtype}")
match self.width:
case 16:
return f"((half) {value})" # see include/half_precision.h
case 32:
return f"{value}f"
case 64:
return str(value)
case _:
assert False, "unreachable code"
def create_constant(self, value: Any) -> Any:
np_type = self.NUMPY_TYPES[self._width]
if isinstance(value, (int, float, np.integer, np.floating)):
finfo = np.finfo(np_type) # type: ignore
if value < finfo.min or value > finfo.max:
raise PsTypeError(
f"Could not interpret {value} as {self}: Value is out of bounds."
)
return np_type(value)
raise PsTypeError(f"Could not interpret {value} as {repr(self)}")
def c_string(self) -> str:
match self._width:
case 16:
return f"{self._const_string()}half"
case 32:
return f"{self._const_string()}float"
case 64:
return f"{self._const_string()}double"
case _:
assert False, "unreachable code"
def __str__(self) -> str:
return f"{self._const_string()}float{self._width}"
def __repr__(self) -> str:
return f"PsIeeeFloatType( width={self.width}, const={self.const} )"
from .sympyextensions import TypedSymbol as _TypedSymbol
from .types import create_type as _create_type
from warnings import warn
warn(
"Importing `TypedSymbol` and `create_type` from `pystencils.typing` is deprecated. "
"Import from `pystencils` instead."
)
TypedSymbol = _TypedSymbol
create_type = _create_type
import numpy as np
import sympy as sp
from sympy.logic.boolalg import Boolean
from pystencils.typing.types import AbstractType, BasicType
from pystencils.typing.typed_sympy import TypedSymbol
class CastFunc(sp.Function):
"""
CastFunc is used in order to introduce static casts. They are especially useful as a way to signal what type
a certain node should have, if it is impossible to add a type to a node, e.g. a sp.Number.
"""
is_Atom = True
def __new__(cls, *args, **kwargs):
if len(args) != 2:
pass
expr, dtype, *other_args = args
# If we have two consecutive casts, throw the inner one away.
# This optimisation is only available for simple casts. Thus the == is intended here!
if expr.__class__ == CastFunc:
expr = expr.args[0]
if not isinstance(dtype, AbstractType):
dtype = BasicType(dtype)
# to work in conditions of sp.Piecewise cast_func has to be of type Boolean as well
# however, a cast_function should only be a boolean if its argument is a boolean, otherwise this leads
# to problems when for example comparing cast_func's for equality
#
# lhs = bitwise_and(a, cast_func(1, 'int'))
# rhs = cast_func(0, 'int')
# print( sp.Ne(lhs, rhs) ) # would give true if all cast_funcs are booleans
# -> thus a separate class boolean_cast_func is introduced
if isinstance(expr, Boolean) and (not isinstance(expr, TypedSymbol) or expr.dtype == BasicType('bool')):
cls = BooleanCastFunc
return sp.Function.__new__(cls, expr, dtype, *other_args, **kwargs)
@property
def canonical(self):
if hasattr(self.args[0], 'canonical'):
return self.args[0].canonical
else:
raise NotImplementedError()
@property
def is_commutative(self):
return self.args[0].is_commutative
@property
def dtype(self):
return self.args[1]
@property
def expr(self):
return self.args[0]
@property
def is_integer(self):
"""
Uses Numpy type hierarchy to determine :func:`sympy.Expr.is_integer` predicate
For reference: Numpy type hierarchy https://docs.scipy.org/doc/numpy-1.13.0/reference/arrays.scalars.html
"""
if hasattr(self.dtype, 'numpy_dtype'):
return np.issubdtype(self.dtype.numpy_dtype, np.integer) or super().is_integer
else:
return super().is_integer
@property
def is_negative(self):
"""
See :func:`.TypedSymbol.is_integer`
"""
if hasattr(self.dtype, 'numpy_dtype'):
if np.issubdtype(self.dtype.numpy_dtype, np.unsignedinteger):
return False
return super().is_negative
@property
def is_nonnegative(self):
"""
See :func:`.TypedSymbol.is_integer`
"""
if self.is_negative is False:
return True
else:
return super().is_nonnegative
@property
def is_real(self):
"""
See :func:`.TypedSymbol.is_integer`
"""
if hasattr(self.dtype, 'numpy_dtype'):
return np.issubdtype(self.dtype.numpy_dtype, np.integer) or np.issubdtype(self.dtype.numpy_dtype,
np.floating) or super().is_real
else:
return super().is_real
class BooleanCastFunc(CastFunc, Boolean):
# TODO: documentation
pass
class VectorMemoryAccess(CastFunc):
"""
Special memory access for vectorized kernel.
Arguments: read/write expression, type, aligned, non-temporal, mask (or none), stride
"""
nargs = (6,)
class ReinterpretCastFunc(CastFunc):
"""
Reinterpret cast is necessary for the StructType
"""
pass
class PointerArithmeticFunc(sp.Function, Boolean):
# TODO: documentation, or deprecate!
@property
def canonical(self):
if hasattr(self.args[0], 'canonical'):
return self.args[0].canonical
else:
raise NotImplementedError()
from typing import Union
import numpy as np
import sympy as sp
from sympy.core.cache import cacheit
from pystencils.typing.types import BasicType, create_type, PointerType
def assumptions_from_dtype(dtype: Union[BasicType, np.dtype]):
"""Derives SymPy assumptions from :class:`BasicType` or a Numpy dtype
Args:
dtype (BasicType, np.dtype): a Numpy data type
Returns:
A dict of SymPy assumptions
"""
if hasattr(dtype, 'numpy_dtype'):
dtype = dtype.numpy_dtype
assumptions = dict()
try:
if np.issubdtype(dtype, np.integer):
assumptions.update({'integer': True})
if np.issubdtype(dtype, np.unsignedinteger):
assumptions.update({'negative': False})
if np.issubdtype(dtype, np.integer) or \
np.issubdtype(dtype, np.floating):
assumptions.update({'real': True})
except Exception: # TODO this is dirty
pass
return assumptions
class TypedSymbol(sp.Symbol):
def __new__(cls, *args, **kwds):
obj = TypedSymbol.__xnew_cached_(cls, *args, **kwds)
return obj
def __new_stage2__(cls, name, dtype, **kwargs): # TODO does not match signature of sp.Symbol???
# TODO: also Symbol should be allowed ---> see sympy Variable
assumptions = assumptions_from_dtype(dtype)
assumptions.update(kwargs)
obj = super(TypedSymbol, cls).__xnew__(cls, name, **assumptions)
try:
obj.numpy_dtype = create_type(dtype)
except (TypeError, ValueError):
# on error keep the string
obj.numpy_dtype = dtype
return obj
__xnew__ = staticmethod(__new_stage2__)
__xnew_cached_ = staticmethod(cacheit(__new_stage2__))
@property
def dtype(self):
return self.numpy_dtype
def _hashable_content(self):
return super()._hashable_content(), hash(self.numpy_dtype)
def __getnewargs__(self):
return self.name, self.dtype
def __getnewargs_ex__(self):
return (self.name, self.dtype), self.assumptions0
@property
def canonical(self):
return self
@property
def reversed(self):
return self
@property
def headers(self):
headers = []
try:
if np.issubdtype(self.dtype.numpy_dtype, np.complexfloating):
headers.append('"cuda_complex.hpp"')
except Exception:
pass
try:
if np.issubdtype(self.dtype.base_type.numpy_dtype, np.complexfloating):
headers.append('"cuda_complex.hpp"')
except Exception:
pass
return headers
SHAPE_DTYPE = BasicType('int64', const=True)
STRIDE_DTYPE = BasicType('int64', const=True)
class FieldStrideSymbol(TypedSymbol):
"""Sympy symbol representing the stride value of a field in a specific coordinate."""
def __new__(cls, *args, **kwds):
obj = FieldStrideSymbol.__xnew_cached_(cls, *args, **kwds)
return obj
def __new_stage2__(cls, field_name, coordinate):
name = f"_stride_{field_name}_{coordinate}"
obj = super(FieldStrideSymbol, cls).__xnew__(cls, name, STRIDE_DTYPE, positive=True)
obj.field_name = field_name
obj.coordinate = coordinate
return obj
def __getnewargs__(self):
return self.field_name, self.coordinate
def __getnewargs_ex__(self):
return (self.field_name, self.coordinate), {}
__xnew__ = staticmethod(__new_stage2__)
__xnew_cached_ = staticmethod(cacheit(__new_stage2__))
def _hashable_content(self):
return super()._hashable_content(), self.coordinate, self.field_name
class FieldShapeSymbol(TypedSymbol):
"""Sympy symbol representing the shape value of a sequence of fields. In a kernel iterating over multiple fields
there is only one set of `FieldShapeSymbol`s since all the fields have to be of equal size."""
def __new__(cls, *args, **kwds):
obj = FieldShapeSymbol.__xnew_cached_(cls, *args, **kwds)
return obj
def __new_stage2__(cls, field_names, coordinate):
names = "_".join([field_name for field_name in field_names])
name = f"_size_{names}_{coordinate}"
obj = super(FieldShapeSymbol, cls).__xnew__(cls, name, SHAPE_DTYPE, positive=True)
obj.field_names = tuple(field_names)
obj.coordinate = coordinate
return obj
def __getnewargs__(self):
return self.field_names, self.coordinate
def __getnewargs_ex__(self):
return (self.field_names, self.coordinate), {}
__xnew__ = staticmethod(__new_stage2__)
__xnew_cached_ = staticmethod(cacheit(__new_stage2__))
def _hashable_content(self):
return super()._hashable_content(), self.coordinate, self.field_names
class FieldPointerSymbol(TypedSymbol):
"""Sympy symbol representing the pointer to the beginning of the field data."""
def __new__(cls, *args, **kwds):
obj = FieldPointerSymbol.__xnew_cached_(cls, *args, **kwds)
return obj
def __new_stage2__(cls, field_name, field_dtype, const):
from pystencils.typing.utilities import get_base_type
name = f"_data_{field_name}"
dtype = PointerType(get_base_type(field_dtype), const=const, restrict=True)
obj = super(FieldPointerSymbol, cls).__xnew__(cls, name, dtype)
obj.field_name = field_name
return obj
def __getnewargs__(self):
return self.field_name, self.dtype, self.dtype.const
def __getnewargs_ex__(self):
return (self.field_name, self.dtype, self.dtype.const), {}
def _hashable_content(self):
return super()._hashable_content(), self.field_name
__xnew__ = staticmethod(__new_stage2__)
__xnew_cached_ = staticmethod(cacheit(__new_stage2__))
from abc import abstractmethod
from typing import Union
import numpy as np
import sympy as sp
def is_supported_type(dtype: np.dtype):
scalar = dtype.type
c = np.issctype(dtype)
subclass = issubclass(scalar, np.floating) or issubclass(scalar, np.integer) or issubclass(scalar, np.bool_)
additional_checks = dtype.fields is None and dtype.hasobject is False and dtype.subdtype is None
return c and subclass and additional_checks
def numpy_name_to_c(name: str) -> str:
"""
Converts a np.dtype.name into a C type
Args:
name: np.dtype.name string
Returns:
type as a C string
"""
if name == 'float64':
return 'double'
elif name == 'float32':
return 'float'
elif name == 'float16' or name == 'half':
return 'half'
elif name.startswith('int'):
width = int(name[len("int"):])
return f"int{width}_t"
elif name.startswith('uint'):
width = int(name[len("uint"):])
return f"uint{width}_t"
elif name == 'bool':
return 'bool'
else:
raise NotImplementedError(f"Can't map numpy to C name for {name}")
class AbstractType(sp.Atom):
# TODO: Is it necessary to ineherit from sp.Atom?
def __new__(cls, *args, **kwargs):
return sp.Basic.__new__(cls)
def _sympystr(self, *args, **kwargs):
return str(self)
@property
@abstractmethod
def base_type(self) -> Union[None, 'BasicType']:
"""
Returns: Returns BasicType of a Vector or Pointer type, None otherwise
"""
pass
@property
@abstractmethod
def item_size(self) -> int:
"""
Returns: Number of items.
E.g. width * item_size(basic_type) in vector's case, or simple numpy itemsize in Struct's case.
"""
pass
class BasicType(AbstractType):
"""
BasicType is defined with a const qualifier and a np.dtype.
"""
def __init__(self, dtype: Union[type, 'BasicType', str], const: bool = False):
if isinstance(dtype, BasicType):
self.numpy_dtype = dtype.numpy_dtype
self.const = dtype.const
else:
self.numpy_dtype = np.dtype(dtype)
self.const = const
assert is_supported_type(self.numpy_dtype), f'Type {self.numpy_dtype} is currently not supported!'
def __getnewargs__(self):
return self.numpy_dtype, self.const
def __getnewargs_ex__(self):
return (self.numpy_dtype, self.const), {}
@property
def base_type(self):
return None
@property
def item_size(self): # TODO: Do we want self.numpy_type.itemsize????
return 1
def is_float(self):
return issubclass(self.numpy_dtype.type, np.floating)
def is_half(self):
return issubclass(self.numpy_dtype.type, np.half)
def is_int(self):
return issubclass(self.numpy_dtype.type, np.integer)
def is_uint(self):
return issubclass(self.numpy_dtype.type, np.unsignedinteger)
def is_sint(self):
return issubclass(self.numpy_dtype.type, np.signedinteger)
def is_bool(self):
return issubclass(self.numpy_dtype.type, np.bool_)
def dtype_eq(self, other):
if not isinstance(other, BasicType):
return False
else:
return self.numpy_dtype == other.numpy_dtype
@property
def c_name(self) -> str:
return numpy_name_to_c(self.numpy_dtype.name)
def __str__(self):
return f'{self.c_name}{" const" if self.const else ""}'
def __repr__(self):
return f'BasicType( {str(self)} )'
def _repr_html_(self):
return f'BasicType( {str(self)} )'
def __eq__(self, other):
return self.dtype_eq(other) and self.const == other.const
def __hash__(self):
return hash(str(self))
class VectorType(AbstractType):
"""
VectorType consists of a BasicType and a width.
"""
instruction_set = None
def __init__(self, base_type: BasicType, width: int):
self._base_type = base_type
self.width = width
@property
def base_type(self):
return self._base_type
@property
def item_size(self):
return self.width * self.base_type.item_size
def __eq__(self, other):
if not isinstance(other, VectorType):
return False
else:
return (self.base_type, self.width) == (other.base_type, other.width)
def __str__(self):
if self.instruction_set is None:
return f"{self.base_type}[{self.width}]"
else:
# TODO VectorizationRevamp: this seems super weird. the instruction_set should know how to print a type out!
# TODO VectorizationRevamp: this is error prone. base_type could be cons=True. Use dtype instead
if self.base_type == create_type("int64") or self.base_type == create_type("int32"):
return self.instruction_set['int']
elif self.base_type == create_type("float64"):
return self.instruction_set['double']
elif self.base_type == create_type("float32"):
return self.instruction_set['float']
elif self.base_type == create_type("bool"):
return self.instruction_set['bool']
else:
raise NotImplementedError()
def __hash__(self):
return hash((self.base_type, self.width))
def __getnewargs__(self):
return self._base_type, self.width
def __getnewargs_ex__(self):
return (self._base_type, self.width), {}
class PointerType(AbstractType):
def __init__(self, base_type: BasicType, const: bool = False, restrict: bool = True, double_pointer: bool = False):
self._base_type = base_type
self.const = const
self.restrict = restrict
self.double_pointer = double_pointer
def __getnewargs__(self):
return self.base_type, self.const, self.restrict, self.double_pointer
def __getnewargs_ex__(self):
return (self.base_type, self.const, self.restrict, self.double_pointer), {}
@property
def alias(self):
return not self.restrict
@property
def base_type(self):
return self._base_type
@property
def item_size(self):
if self.double_pointer:
raise NotImplementedError("The item_size for double_pointer is not implemented")
else:
return self.base_type.item_size
def __eq__(self, other):
if not isinstance(other, PointerType):
return False
else:
own = (self.base_type, self.const, self.restrict, self.double_pointer)
return own == (other.base_type, other.const, other.restrict, other.double_pointer)
def __str__(self):
restrict_str = "RESTRICT" if self.restrict else ""
const_str = "const" if self.const else ""
if self.double_pointer:
return f'{str(self.base_type)} ** {restrict_str} {const_str}'
else:
return f'{str(self.base_type)} * {restrict_str} {const_str}'
def __repr__(self):
return str(self)
def _repr_html_(self):
return str(self)
def __hash__(self):
return hash((self._base_type, self.const, self.restrict, self.double_pointer))
class StructType(AbstractType):
"""
A list of types (with C offsets).
It is implemented with uint8_t and casts to the correct datatype.
"""
def __init__(self, numpy_type, const=False):
self.const = const
self._dtype = np.dtype(numpy_type)
def __getnewargs__(self):
return self.numpy_dtype, self.const
def __getnewargs_ex__(self):
return (self.numpy_dtype, self.const), {}
@property
def base_type(self):
return None
@property
def numpy_dtype(self):
return self._dtype
@property
def item_size(self):
return self.numpy_dtype.itemsize
def get_element_offset(self, element_name):
return self.numpy_dtype.fields[element_name][1]
def get_element_type(self, element_name):
np_element_type = self.numpy_dtype.fields[element_name][0]
return BasicType(np_element_type, self.const)
def has_element(self, element_name):
return element_name in self.numpy_dtype.fields
def __eq__(self, other):
if not isinstance(other, StructType):
return False
else:
return (self.numpy_dtype, self.const) == (other.numpy_dtype, other.const)
def __str__(self):
# structs are handled byte-wise
result = "uint8_t"
if self.const:
result += " const"
return result
def __repr__(self):
return str(self)
def _repr_html_(self):
return str(self)
def __hash__(self):
return hash((self.numpy_dtype, self.const))
def create_type(specification: Union[type, AbstractType, str]) -> AbstractType:
# TODO: Deprecated Use the constructor of BasicType or StructType instead
"""Creates a subclass of Type according to a string or an object of subclass Type.
Args:
specification: Type object, or a string
Returns:
Type object, or a new Type object parsed from the string
"""
if isinstance(specification, AbstractType):
return specification
else:
numpy_dtype = np.dtype(specification)
if numpy_dtype.fields is None:
return BasicType(numpy_dtype, const=False)
else:
return StructType(numpy_dtype, const=False)
...@@ -12,6 +12,7 @@ import sympy as sp ...@@ -12,6 +12,7 @@ import sympy as sp
class DotDict(dict): class DotDict(dict):
"""Normal dict with additional dot access for all keys""" """Normal dict with additional dot access for all keys"""
__getattr__ = dict.get __getattr__ = dict.get
__setattr__ = dict.__setitem__ __setattr__ = dict.__setitem__
__delattr__ = dict.__delitem__ __delattr__ = dict.__delitem__
...@@ -82,8 +83,7 @@ def boolean_array_bounding_box(boolean_array): ...@@ -82,8 +83,7 @@ def boolean_array_bounding_box(boolean_array):
>>> a = np.zeros((4, 4), dtype=bool) >>> a = np.zeros((4, 4), dtype=bool)
>>> a[1:-1, 1:-1] = True >>> a[1:-1, 1:-1] = True
>>> boolean_array_bounding_box(a) >>> assert boolean_array_bounding_box(a) == [(1, 3), (1, 3)]
[(1, 3), (1, 3)]
""" """
dim = boolean_array.ndim dim = boolean_array.ndim
shape = boolean_array.shape shape = boolean_array.shape
...@@ -106,7 +106,7 @@ def binary_numbers(n): ...@@ -106,7 +106,7 @@ def binary_numbers(n):
result = list() result = list()
for i in range(1 << n): for i in range(1 << n):
binary_number = bin(i)[2:] binary_number = bin(i)[2:]
binary_number = '0' * (n - len(binary_number)) + binary_number binary_number = "0" * (n - len(binary_number)) + binary_number
result.append((list(map(int, binary_number)))) result.append((list(map(int, binary_number))))
return result return result
...@@ -130,6 +130,7 @@ class LinearEquationSystem: ...@@ -130,6 +130,7 @@ class LinearEquationSystem:
{x: 7/2, y: 1/2} {x: 7/2, y: 1/2}
""" """
def __init__(self, unknowns): def __init__(self, unknowns):
size = len(unknowns) size = len(unknowns)
self._matrix = sp.zeros(size, size + 1) self._matrix = sp.zeros(size, size + 1)
...@@ -146,7 +147,7 @@ class LinearEquationSystem: ...@@ -146,7 +147,7 @@ class LinearEquationSystem:
def add_equation(self, linear_equation): def add_equation(self, linear_equation):
"""Add a linear equation as sympy expression. Implicit "-0" is assumed. Equation has to be linear and contain """Add a linear equation as sympy expression. Implicit "-0" is assumed. Equation has to be linear and contain
only unknowns passed to the constructor otherwise a ValueError is raised. """ only unknowns passed to the constructor otherwise a ValueError is raised."""
self._resize_if_necessary() self._resize_if_necessary()
linear_equation = linear_equation.expand() linear_equation = linear_equation.expand()
zero_row_idx = self.next_zero_row zero_row_idx = self.next_zero_row
...@@ -163,7 +164,7 @@ class LinearEquationSystem: ...@@ -163,7 +164,7 @@ class LinearEquationSystem:
self._reduced = False self._reduced = False
def add_equations(self, linear_equations): def add_equations(self, linear_equations):
"""Add a sequence of equations. For details see `add_equation`. """ """Add a sequence of equations. For details see `add_equation`."""
self._resize_if_necessary(len(linear_equations)) self._resize_if_necessary(len(linear_equations))
for eq in linear_equations: for eq in linear_equations:
self.add_equation(eq) self.add_equation(eq)
...@@ -202,21 +203,21 @@ class LinearEquationSystem: ...@@ -202,21 +203,21 @@ class LinearEquationSystem:
non_zero_rows = self.next_zero_row non_zero_rows = self.next_zero_row
num_unknowns = len(self.unknowns) num_unknowns = len(self.unknowns)
if non_zero_rows == 0: if non_zero_rows == 0:
return 'multiple' return "multiple"
*row_begin, left, right = self._matrix.row(non_zero_rows - 1) *row_begin, left, right = self._matrix.row(non_zero_rows - 1)
if non_zero_rows > num_unknowns: if non_zero_rows > num_unknowns:
return 'none' return "none"
elif non_zero_rows == num_unknowns: elif non_zero_rows == num_unknowns:
if left == 0 and right != 0: if left == 0 and right != 0:
return 'none' return "none"
else: else:
return 'single' return "single"
elif non_zero_rows < num_unknowns: elif non_zero_rows < num_unknowns:
if right != 0 and left == 0 and all(e == 0 for e in row_begin): if right != 0 and left == 0 and all(e == 0 for e in row_begin):
return 'none' return "none"
else: else:
return 'multiple' return "multiple"
def solution(self): def solution(self):
"""Solves the system. Under- and overdetermined systems are supported. """Solves the system. Under- and overdetermined systems are supported.
...@@ -225,8 +226,9 @@ class LinearEquationSystem: ...@@ -225,8 +226,9 @@ class LinearEquationSystem:
def _resize_if_necessary(self, new_rows=1): def _resize_if_necessary(self, new_rows=1):
if self.next_zero_row + new_rows > self._matrix.shape[0]: if self.next_zero_row + new_rows > self._matrix.shape[0]:
self._matrix = self._matrix.row_insert(self._matrix.shape[0] + 1, self._matrix = self._matrix.row_insert(
sp.zeros(new_rows, self._matrix.shape[1])) self._matrix.shape[0] + 1, sp.zeros(new_rows, self._matrix.shape[1])
)
def _update_next_zero_row(self): def _update_next_zero_row(self):
result = self._matrix.shape[0] result = self._matrix.shape[0]
...@@ -250,3 +252,22 @@ class ContextVar: ...@@ -250,3 +252,22 @@ class ContextVar:
def get(self): def get(self):
return self.stack[-1] return self.stack[-1]
def c_intdiv(num, denom):
"""C-style integer division"""
if isinstance(num, np.ndarray) or isinstance(denom, np.ndarray):
rtype = np.result_type(num, denom)
if not np.issubdtype(rtype, np.integer):
raise TypeError(
"Invalid numpy argument types to c_intdiv: Must be integers."
)
return (num / denom).astype(rtype)
else:
return int(num / denom)
def c_rem(num, denom):
"""C-style integer remainder"""
div = c_intdiv(num, denom)
return num - div * denom
File moved
import sympy import pystencils.sympyextensions.astnodes
import pystencils.astnodes
from pystencils.backends.cbackend import CBackend from pystencils.backends.cbackend import CBackend
from pystencils.typing import TypedSymbol from pystencils.typing import TypedSymbol
class BogusDeclaration(pystencils.astnodes.Node): class BogusDeclaration(pystencils.sympyextensions.astnodes.Node):
"""Base class for all AST nodes.""" """Base class for all AST nodes."""
def __init__(self, parent=None): def __init__(self, parent=None):
...@@ -45,7 +43,7 @@ class BogusDeclaration(pystencils.astnodes.Node): ...@@ -45,7 +43,7 @@ class BogusDeclaration(pystencils.astnodes.Node):
return result return result
class BogusUsage(pystencils.astnodes.Node): class BogusUsage(pystencils.sympyextensions.astnodes.Node):
"""Base class for all AST nodes.""" """Base class for all AST nodes."""
def __init__(self, requires_global: bool, parent=None): def __init__(self, requires_global: bool, parent=None):
......
import numpy as np import numpy as np
from pystencils import get_code_obj from pystencils import get_code_obj
from pystencils.astnodes import Block, KernelFunction, SympyAssignment from pystencils.sympyextensions.astnodes import Block, KernelFunction, SympyAssignment
from pystencils.cpu import make_python_function from pystencils.cpu import make_python_function
from pystencils.field import Field from pystencils.field import Field
from pystencils.enums import Target, Backend from pystencils.enums import Target, Backend
......
import numpy as np import numpy as np
import pystencils as ps import pystencils as ps
from pystencils.astnodes import Block, LoopOverCoordinate, SympyAssignment, TypedSymbol from pystencils.sympyextensions.astnodes import Block, LoopOverCoordinate, SympyAssignment, TypedSymbol
from pystencils.transformations import move_constants_before_loop from pystencils.transformations import move_constants_before_loop
...@@ -47,7 +47,7 @@ def test_keep_order_of_accesses(): ...@@ -47,7 +47,7 @@ def test_keep_order_of_accesses():
new_loops = ps.transformations.cut_loop(loop, [n - 1]) new_loops = ps.transformations.cut_loop(loop, [n - 1])
ps.transformations.move_constants_before_loop(new_loops.args[1]) ps.transformations.move_constants_before_loop(new_loops.args[1])
kernel_func = ps.astnodes.KernelFunction( kernel_func = pystencils.sympyextensions.astnodes.KernelFunction(
block, ps.Target.CPU, ps.Backend.C, ps.cpu.cpujit.make_python_function, None block, ps.Target.CPU, ps.Backend.C, ps.cpu.cpujit.make_python_function, None
) )
kernel = kernel_func.compile() kernel = kernel_func.compile()
......
File moved
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
""" """
import pystencils import pystencils
import pystencils.astnodes import pystencils.sympyextensions.astnodes
import pystencils.config import pystencils.config
...@@ -23,9 +23,9 @@ def test_source_code_comment(): ...@@ -23,9 +23,9 @@ def test_source_code_comment():
config = pystencils.config.CreateKernelConfig(target=pystencils.Target.CPU) config = pystencils.config.CreateKernelConfig(target=pystencils.Target.CPU)
ast = pystencils.create_kernel(assignments, config=config) ast = pystencils.create_kernel(assignments, config=config)
ast.body.append(pystencils.astnodes.SourceCodeComment("Hallo")) ast.body.append(pystencils.sympyextensions.astnodes.SourceCodeComment("Hallo"))
ast.body.append(pystencils.astnodes.EmptyLine()) ast.body.append(pystencils.sympyextensions.astnodes.EmptyLine())
ast.body.append(pystencils.astnodes.SourceCodeComment("World!")) ast.body.append(pystencils.sympyextensions.astnodes.SourceCodeComment("World!"))
print(ast) print(ast)
compiled = ast.compile() compiled = ast.compile()
assert compiled is not None assert compiled is not None
......