Specifying numba classes

I am trying to understand how to specify classes in numba so that I can transform some code written in OOP. Here is what I have, I have written each class into a different file. I am generating the specification of each class one a time. As I complete each, I run a small script where I import the class and generate a single instance. All classes seem to work individually. However as I am working on one class that calls the rest I had done so far I get an error: spec values should be Numba type instances,... and a huge list of numba specs whenever I try to import it.

So far, I have specified all of my classes with numba specs except for the very first one which is a namedtuple that I defined

Point = namedtuple('Point', 'x y z')
Point= nb.types.NamedTuple([nb.u8, nb.u8, nb.f4], Point)

…meaning that this is not a numba class.

What is the 'logic behind the specification part? Are there any rules on how this should be done? What guidelines should I follow to transform my classes into something that I can run with numba? Hope this makes sense.

Do you have a minimal complete example that shows what you’re trying to do and the error you get?

Hi @nelson2005 I don’t but I will try to get something up. I am only copying the ‘constructor’ functions for the sake of brevity. Here are the different classes,

file: point.py

'''point class'''

import numba as nb
from collections import namedtuple

Point3D = namedtuple('Point', 'x y z')
Point3D = nb.types.NamedTuple([nb.u8, nb.u8, nb.f4], Point3D)

file: edge.py

'''edge class'''
import numba as nb
from point import, Point3D
from numba.experimental import jitclass

spec_edge = {'id': nb.u8,
             'p0': Point3D,
             'p1': Point3D,
             'next': nb.optional(nb.u8),
             'inv':  nb.optional(nb.u8),
             'triangle': nb.optional(nb.u8)}

@jitclass(spec_edge)
class Edge:
    def __init__(self, id: int, p0: Point3D, p1: Point3D):
        self.id = id
        self.p0 = p0
        self.p1 = p1
        self.next = None
        self.inv = None
        self.triangle = None

file: triangle.py

''' triangle class'''

import numpy as np
import numba as nb

from point import Point3D
from edge import Edge
from numba.experimental import jitclass

# create edge type
edge_type =  Edge.class_type.instance_type

spec_triangle = {'e01': edge_type, 'e12': edge_type, 'e20': edge_type,
                 'id': nb.u8, 'vertices': nb.types.ListType(Point3D)}

@jitclass(spec_triangle)
class Triangle:
    
    def __init__(self, id: int, e01: Edge, e12: Edge, e20: Edge):
        
        self.id = id
        
        self.e01 = e01
        self.e12 = e12
        self.e20 = e20

        if not self.closed():
            assert(Trianglerror, 'Error!! triangle not closed!')

        self.e01.next = self.e12.id
        self.e12.next = self.e20.id
        self.e20.next = self.e01.id

        self.e01.triangle = self.id
        self.e12.triangle = self.id
        self.e20.triangle = self.id

        self.p0 = self.e01.p0
        self.p1 = self.e01.p1
        self.p2 = self.e12.p1

        self.vertices = [self.p0, self.p1, self.p2]

Here is where I run into an error when I type, from priority import PriorityItemp
file: priority.py

''' priority queue plus class'''

import numba as nb

from point import Point3D
from triangle import Triangle
from numba.experimental import jitclass


triangle_type = Triangle.class_type.instance_type

dc_spec = {'error': nb.f8, 'triangle': triangle_type, 'point': Point3D}

@jitclass(dc_spec)
class PriorityItem:
    def __init__(self, error: float, triangle: triangle_type, point: Point3D): 
        self.error = error
        self.triangle = triangle
        self.point = point

    def __lt__(self, pi):
        return -self.error < -pi.error

pqp_spec = {'pq': nb.types.ListType(triangle_type),
                     '_pq_index': nb.typed.Dict.empty(
                                            key_type = nb.u8,
                                            value_type = triangle_type),
                    'attr': nb.typeof('triangle')}

@jitclass(pqp_spec)
class PQP:
    '''priority queue plus allows for deletion of element'''
    def __init__(self, pq: list =[], attr='triangle'):
        self.pq = pq
        self._pq_index = {}
        self.attr = attr

The error I get is the following (long!):

ypeError: spec values should be Numba type instances, got DictType[uint64,instance.jitclass.Triangle#1e8a0e20a50<e01:instance.jitclass.Edge#1e8a095d750<id:uint64,p0:Point(uint64, uint64, float32),p1:Point(uint64, uint64, float32),next:OptionalType(uint64),inv:OptionalType(uint64),triangle:OptionalType(uint64)>,e12:instance.jitclass.Edge#1e8a095d750<id:uint64,p0:Point(uint64, uint64, float32),p1:Point(uint64, uint64, float32),next:OptionalType(uint64),inv:OptionalType(uint64),triangle:OptionalType(uint64)>,e20:instance.jitclass.Edge#1e8a095d750<id:uint64,p0:Point(uint64, uint64, float32),p1:Point(uint64, uint64, float32),next:OptionalType(uint64),inv:OptionalType(uint64),triangle:OptionalType(uint64)>,id:uint64,vertices:ListType[Point(uint64, uint64, float32)]>]<iv=None>({})```

Hey @MLLO,
if you want to use a custom class which is not supported by Numba you can use the jitclass decorator to help numba infer the field types of your class. The “spec” argument specifies the types of each field within this class. see:
https://numba.readthedocs.io/en/stable/user/jitclass.html#numba.experimental.jitclass

Fortunately namedtuples are supported by Numba.
In this case there is no need for another jitclass.
Here is an example how you could generate a list of points as namedtuples using njit:

import random
import numba as nb
from numba import njit
from numba.typed import List
from collections import namedtuple

# Define the named tuple for a point with 'x' and 'y' coordinates as floats
Point = namedtuple('Point', ['x', 'y'], defaults=(0.0, 0.0))

# Let Numba infer the type of a Point instance
typePoint = nb.typeof(Point(0.0, 0.0))

@njit
def generate_random_points(num_points=3):
    points = List.empty_list(typePoint)  # Create an empty typed List for points
    for _ in range(num_points):
        x = random.uniform(0, 1)
        y = random.uniform(0, 1)
        points.append(Point(x, y))
    return points

# Example usage:
for point in generate_random_points():
    print(point)
# Example of random output:
# Point(x=0.43478165367537835, y=0.340281906910353)
# Point(x=0.16272863174704166, y=0.23146773413181754)
# Point(x=0.39842225949152255, y=0.3782255738005008)

I am adding to the thread as it is related but and I cannot edit my previous message above. Still having problems with my classes. My classes are a bit larger than what I show here but I think with the following there is plenty. Here is what I have,
point file

import numba as nb
from collections import namedtuple

Point3D = namedtuple('Point3D','x y z', defaults=(0, 0, 0.0))
Point2D = namedtuple('Point2D','x y', defaults=(0, 0))

# Let Numba infer the type of a Point instance
Point3DType = nb.typeof(Point3D(0, 0, 0.0))
Point2DType = nb.typeof(Point3D(0, 0))

edge file

import numba as nb
from point import Point3D, Point3DType
from numba.experimental import jitclass

spec_edge ={
            'id': nb.u8,
            'p0': Point3DType,
            'p1': Point3DType,
            'next': nb.optional(nb.u8),
            'inv': nb.optional(nb.u8),
            'triangle': nb.optional(nb.u8),
}
EdgeType = nb.deferred_type()

@jitclass(spec_edge)
class Edge:
    def __init__(self, id: int, p0: Point3D, p1: Point3D):
        self.id = id
        self.p0 = p0
        self.p1 = p1
        self.next = None
        self.inv = None
        self.triangle = None
EdgeType.define(Edge.class_type.instance_type)

triangle file


import numpy as np
from point import Point3DType, Point3D, Point2D
from edge import EdgeType, Edge
from numba.experimental import jitclass
import numba as nb

spec_triangle = {'e01': EdgeType,
                 'e12': EdgeType,
                 'e20': EdgeType,
                 'id': nb.u8,
                 'p0': Point3DType,
                 'p1': Point3DType,
                 'p2': Point3DType,
                 'vertices': nb.types.ListType(Point3DType),
                }

@jitclass(spec_triangle)
class Triangle:
    
    def __init__(self, id: int, e01: Edge, e12: Edge, e20: Edge):
        
        self.id = id
        
        self.e01 = e01
        self.e12 = e12
        self.e20 = e20

        if not self.closed():
            Exception('Triangle not closed!')

        self.e01.next = e12.id
        self.e12.next = e20.id
        self.e20.next = e01.id

        self.e01.triangle = id
        self.e12.triangle = id
        self.e20.triangle = id

        self.p0 = e01.p0
        self.p1 = e01.p1
        self.p2 = e12.p1

        self.vertices = [self.p0, self.p1, self.p2]

I tried running the following code

from point import Point3D
from edge import Edge
from triangle import Triangle
p0= Point3D(0, 3, 0.5)
p1= Point3D(1, 0, 1.5)
p2= Point3D(2, 0, 0.5)
t0 = Triangle(0 ,Edge(1, p0, p1), Edge(2, p1,p2), Edge(3, p2, p0))

but got the following error

---------------------------------------------------------------------------
TypingError                               Traceback (most recent call last)
c:\Python\Notebooks\active\tin\testing.ipynb Cell 12 line 1
----> 1 t0 = Triangle(0 ,Edge(1, p0, p1), Edge(2, p1,p2), Edge(3, p2, p0))

File c:\Python\Miniconda3\envs\tin\Lib\site-packages\numba\experimental\jitclass\base.py:124, in JitClassType.__call__(cls, *args, **kwargs)
    122 bind = cls._ctor_sig.bind(None, *args, **kwargs)
    123 bind.apply_defaults()
--> 124 return cls._ctor(*bind.args[1:], **bind.kwargs)

File c:\Python\Miniconda3\envs\tin\Lib\site-packages\numba\core\dispatcher.py:468, in _DispatcherBase._compile_for_args(self, *args, **kws)
    464         msg = (f"{str(e).rstrip()} \n\nThis error may have been caused "
    465                f"by the following argument(s):\n{args_str}\n")
    466         e.patch_message(msg)
--> 468     error_rewrite(e, 'typing')
    469 except errors.UnsupportedError as e:
    470     # Something unsupported is present in the user code, add help info
    471     error_rewrite(e, 'unsupported_error')

File c:\Python\Miniconda3\envs\tin\Lib\site-packages\numba\core\dispatcher.py:409, in _DispatcherBase._compile_for_args.<locals>.error_rewrite(e, issue_type)
    407     raise e
    408 else:
--> 409     raise e.with_traceback(None)

TypingError: Failed in nopython mode pipeline (step: nopython frontend)
Internal error at <numba.core.typeinfer.CallConstraint object at 0x000001CEC061BB10>.
Failed in nopython mode pipeline (step: native lowering)
No definition for lowering DeferredType#1987427294352.next = OptionalType(uint64)

File "triangle.py", line 37:
    def __init__(self, id: int, e01: Edge, e12: Edge, e20: Edge):
        <source elided>

        self.e01.next = e12.id
        ^

During: lowering "($144load_attr.3).next = $132load_attr.1" at c:\Python\Notebooks\active\tin\triangle.py (37)
During: resolving callee type: jitclass.Triangle#1cebe633010<e01:DeferredType#1987427294352,e12:DeferredType#1987427294352,e20:DeferredType#1987427294352,id:uint64,p0:Point3D(int64, int64, float64),p1:Point3D(int64, int64, float64),p2:Point3D(int64, int64, float64),vertices:ListType[Point3D(int64, int64, float64)]>
During: typing of call at <string> (3)

Enable logging at debug level for details.

File "<string>", line 3:
<source missing, REPL/exec in use?>

It seems that it does not recognize self.e01.next as an attribute of an edge???

Does this help?

  • set the numba field types (spec)
  • generate the class content
  • infer the class type to be used in the next jitclass
import numba as nb
from numba.experimental import jitclass

# =============================================================================
# Definition of Point
# =============================================================================
@jitclass([('x', nb.f8),
           ('y', nb.f8)])
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
typePoint = Point.class_type.instance_type

# Example
p0 = Point(0.0, 0.0)
print(f'Point => x:{p0.x}, y:{p0.y}')

# =============================================================================
# Definition of Edge
# =============================================================================
@jitclass([('id', nb.i8),
           ('p0', typePoint),
           ('p1', typePoint),
           ('next', nb.i8),
           ('inv', nb.i8),
           ('triangle', nb.i8)])
class Edge:
    def __init__(self, id: int, p0, p1):
        self.id = id
        self.p0 = p0
        self.p1 = p1
        self.next = -1
        self.inv = -1
        self.triangle = -1
typeEdge = Edge.class_type.instance_type

# Example
e01 = Edge(id=1,
           p0=Point(1.0, 0.0),
           p1=Point(0.0, 1.0))
print(f'Edge => id: {e01.id}, p0: ({e01.p0.x}, {e01.p0.y}), p1: ({e01.p1.x}, {e01.p1.y})')

# =============================================================================
# Definition of Triangle
# =============================================================================
@jitclass([('e01', typeEdge),
           ('e12', typeEdge),
           ('e20', typeEdge),
           ('id', nb.u8),
           ('p0', typePoint),
           ('p1', typePoint),
           ('p2', typePoint),
           ('vertices', nb.types.ListType(typePoint))])
class Triangle:
    def __init__(self, id: int, e01, e12, e20):
        self.id = id
        self.e01 = e01
        self.e12 = e12
        self.e20 = e20
        self.e01.next = e12.id
        self.e12.next = e20.id
        self.e20.next = e01.id
        self.e01.triangle = id
        self.e12.triangle = id
        self.e20.triangle = id
        self.p0 = e01.p0
        self.p1 = e01.p1
        self.p2 = e12.p1
        self.vertices = nb.typed.List([self.p0, self.p1, self.p2])
typeTriangle = Triangle.class_type.instance_type

# Example
p0= Point(0, 0)
p1= Point(1, 0)
p2= Point(0, 1)
t = Triangle(1 , Edge(1, p0, p1), Edge(2, p1,p2), Edge(3, p2, p0))
print(f'Triangle => id: {t.id}')
for v in t.vertices:
    print(f'            x:{v.x}, y:{v.y}')

Thank you very much @Oyibo !

This works. It does raise some questions however (hope you do not mind). The change from a nametuple to a class for the point3D appears to have gotten rid off the error I was getting. So here my question is how should I define a numba namedtuple??? ( I was using a tuple because it can easily be converted into a numpy array)

I did check whether keeping nb.optional() had an impact or not (I noticed you removed that and that instead of none you gave it -1. It did not!

Either I have missed something or there is no documentation (guidelines) about the logic on how to specify a numba class. For instance, I am not certain when one has to use nb.vertices.ListType() .vs. when nb.typed.List()???

Moreover, when should we use numba types inside our original functions? (as opposed to only as part of the specification)??

Finally, if I include within the Triangle constructor the following check,

   if not self.closed():
      Exception('Triangle not closed!')

which is the following,

    def closed(self) -> bool:
        return (self.e01.p1 == self.e12.p0) and \
               (self.e12.p1 == self.e20.p0) and \
               (self.e20.p1 == self.e01.p0)

It all blows us… and I get the following error message ( I only include a part of the error here).

ypingError: Failed in nopython mode pipeline (step: nopython frontend)
Failed in nopython mode pipeline (step: nopython frontend)
- Resolution failure for literal arguments:
Failed in nopython mode pipeline (step: nopython frontend)
No implementation of function Function(<built-in function eq>) found for signature:

 >>> eq(instance.jitclass.Point3D#1b33d5b4d90<x:uint64,y:uint64,z:float32>, instance.jitclass.Point3D#1b33d5b4d90<x:uint64,y:uint64,z:float32>)

There are 32 candidate implementations:
    - Of which 28 did not match due to:
    Overload of function 'eq': File: <numerous>: Line N/A.
      With argument(s): '(instance.jitclass.Point3D#1b33d5b4d90<x:uint64,y:uint64,z:float32>, instance.jitclass.Point3D#1b33d5b4d90<x:uint64,y:uint64,z:float32>)':
     No match.
    - Of which 2 did not match due to:
    Operator Overload in function 'eq': File: unknown: Line unknown.
      With argument(s): '(instance.jitclass.Point3D#1b33d5b4d90<x:uint64,y:uint64,z:float32>, instance.jitclass.Point3D#1b33d5b4d90<x:uint64,y:uint64,z:float32>)':
     No match for registered cases:
      * (bool, bool) -> bool
      * (int8, int8) -> bool
      * (int16, int16) -> bool
      * (int32, int32) -> bool
      * (int64, int64) -> bool
      * (uint8, uint8) -> bool
      * (uint16, uint16) -> bool
      * (uint32, uint32) -> bool
      * (uint64, uint64) -> bool
      * (float32, float32) -> bool
      * (float64, float64) -> bool
      * (complex64, complex64) -> bool
      * (complex128, complex128) -> bool
    - Of which 2 did not match due to:
    Overload in function 'set_eq': File: numba\cpython\setobj.py: Line 1653.
      With argument(s): '(instance.jitclass.Point3D#1b33d5b4d90<x:uint64,y:uint64,z:float32>, instance.jitclass.Point3D#1b33d5b4d90<x:uint64,y:uint64,z:float32>)':
     Rejected as the implementation raised a specific error:
       TypingError: All arguments must be Sets, got (instance.jitclass.Point3D#1b33d5b4d90<x:uint64,y:uint64,z:float32>, instance.jitclass.Point3D#1b33d5b4d90<x:uint64,y:uint64,z:float32>)
  raised from c:\Python\Miniconda3\envs\tin\Lib\site-packages\numba\cpython\setobj.py:108

During: typing of intrinsic-call at c:\Python\Notebooks\active\tin\triangle.py (51)

File "triangle.py", line 51:
    def closed(self) -> bool:
        return (self.e01.p1 == self.e12.p0) and \
        ^

It appears that it is checking every permutation of variable types for the edges of a triangle when these were defined as uint64 ??? How does one deal with this???

I thank you again! (really) !! and apologize for all the questions.

I think I know why it is blowing up.!

Changing from a nametupled to a class the equality operator does not work as it did before !!! Presumably I need to specify it as ___eq__() in the class definition but then that raises the issue of having to pass a deferred type?? further complicating things.

Even after changing the point class to the following,

typePoint3D = nb.deferred_type()

@jitclass([('x', nb.u8),
           ('y', nb.u8),
           ('z', nb.f8)])
class Point3D:
    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z
    
    def __eq__(self, p: typePoint3D):
        return (self.x == p.x) and (self.y == p.y) and (self.z == p.z)

typePoint3D.define(Point3D.class_type.instance_type)

I still get a similar error message (partially shown here),

TypingError: Failed in nopython mode pipeline (step: nopython frontend)
Failed in nopython mode pipeline (step: nopython frontend)
- Resolution failure for literal arguments:
Failed in nopython mode pipeline (step: nopython frontend)
No implementation of function Function(<built-in function eq>) found for signature:

 >>> eq(DeferredType#2134585288080, DeferredType#2134585288080)

There are 32 candidate implementations:
    - Of which 28 did not match due to:
    Overload of function 'eq': File: <numerous>: Line N/A.
      With argument(s): '(DeferredType#2134585288080, DeferredType#2134585288080)':
     No match.
    - Of which 2 did not match due to:
    Operator Overload in function 'eq': File: unknown: Line unknown.
      With argument(s): '(DeferredType#2134585288080, DeferredType#2134585288080)':
     No match for registered cases:
      * (bool, bool) -> bool
      * (int8, int8) -> bool
      * (int16, int16) -> bool
      * (int32, int32) -> bool
      * (int64, int64) -> bool
      * (uint8, uint8) -> bool
      * (uint16, uint16) -> bool
      * (uint32, uint32) -> bool
      * (uint64, uint64) -> bool
      * (float32, float32) -> bool
      * (float64, float64) -> bool
      * (complex64, complex64) -> bool
      * (complex128, complex128) -> bool
    - Of which 2 did not match due to:
    Overload in function 'set_eq': File: numba\cpython\setobj.py: Line 1653.
      With argument(s): '(DeferredType#2134585288080, DeferredType#2134585288080)':
     Rejected as the implementation raised a specific error:
       TypingError: All arguments must be Sets, got (DeferredType#2134585288080, DeferredType#2134585288080)
  raised from c:\Python\Miniconda3\envs\tin\Lib\site-packages\numba\cpython\setobj.py:108

During: typing of intrinsic-call at c:\Python\Notebooks\active\tin\triangle.py (51)

File "triangle.py", line 51:
    def closed(self) -> bool:
        return (self.e01.p1 == self.e12.p0) and \
        ^

- Resolution failure for non-literal arguments:
None

During: resolving callee type: BoundFunction((<class 'numba.core.types.misc.ClassInstanceType'>, 'closed') for instance.jitclass.Triangle#1f08132f310<id:uint64,e01:instance.jitclass.Edge#1f0812c70d0<id:uint64,p0:DeferredType#2134585288080,p1:DeferredType#2134585288080,next:OptionalType(uint64),inv:uint64,triangle:uint64>,e12:instance.jitclass.Edge#1f0812c70d0<id:uint64,p0:DeferredType#2134585288080,p1:DeferredType#2134585288080,next:OptionalType(uint64),inv:uint64,triangle:uint64>,e20:instance.jitclass.Edge#1f0812c70d0<id:uint64,p0:DeferredType#2134585288080,p1:DeferredType#2134585288080,next:OptionalType(uint64),inv:uint64,triangle:uint64>,p0:DeferredType#2134585288080,p1:DeferredType#2134585288080,p2:DeferredType#2134585288080,vertices:ListType[DeferredType#2134585288080]>)
During: typing of call at c:\Python\Notebooks\active\tin\triangle.py (33)


File "triangle.py", line 33:
    def __init__(self, id: int, e01: Edge, e12: Edge, e20: Edge):
        <source elided>

        if not self.closed():
        ^

No need to apologize.
Do you need deferred_type()?

import numba as nb
from numba.experimental import jitclass

# =============================================================================
# Definition of Point
# =============================================================================
@jitclass([('x', nb.f8),
           ('y', nb.f8)])
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __eq__(self, other):
        return self.x == other.x and self.y == other.y
typePoint = Point.class_type.instance_type

# Example
p = Point(0.0, 0.0)
print(f'Point => x:{p.x}, y:{p.y}')
print(Point(0.0, 0.0) == Point(0.0, 0.0))
print(Point(0.0, 0.0) == Point(1.0, 0.0))
# Point => x:0.0, y:0.0
# True
# False

Hi @Oyibo! Yes, I believe I do need to introduced the deferred_type() if I then re-do the __eq__() function. Otherwise I would not be able to check whether two points are the same.

Regardless of the change, it still blows up when checking whether the triangle is closed or not???

Hey @MLLO, have you checked the example? There is no TypingError exception.
Can you remove deferred_type?

import numba as nb
from numba.experimental import jitclass

# =============================================================================
# Definition of Point
# =============================================================================
@jitclass([('x', nb.f8),
           ('y', nb.f8)])
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __eq__(self, other):
        return self.x == other.x and self.y == other.y
typePoint = Point.class_type.instance_type

# Example
p = Point(0.0, 0.0)
print(f'Point => x:{p.x}, y:{p.y}')
print(Point(0.0, 0.0) == Point(0.0, 0.0))
print(Point(0.0, 0.0) == Point(1.0, 0.0))
# Point => x:0.0, y:0.0
# True
# False

# =============================================================================
# Definition of Edge
# =============================================================================
@jitclass([('id', nb.i8),
           ('p0', typePoint),
           ('p1', typePoint),
           ('next', nb.i8),
           ('inv', nb.i8),
           ('triangle', nb.i8)])
class Edge:
    def __init__(self, id: int, p0, p1):
        self.id = id
        self.p0 = p0
        self.p1 = p1
        self.next = -1
        self.inv = -1
        self.triangle = -1
typeEdge = Edge.class_type.instance_type

# Example
edge = Edge(1, Point(1.0, 0.0), Point(0.0, 1.0))
print(f'\nEdge => id: {edge.id}, p0: ({edge.p0.x}, {edge.p0.y}), p1: ({edge.p1.x}, {edge.p1.y})')

# =============================================================================
# Definition of Triangle
# =============================================================================
@jitclass([('e01', typeEdge),
           ('e12', typeEdge),
           ('e20', typeEdge),
           ('id', nb.u8),
           ('p0', typePoint),
           ('p1', typePoint),
           ('p2', typePoint),
           ('vertices', nb.types.ListType(typePoint))])
class Triangle:
    def __init__(self, id: int, e01, e12, e20):
        self.id = id
        self.e01 = e01
        self.e12 = e12
        self.e20 = e20
        self.e01.next = e12.id
        self.e12.next = e20.id
        self.e20.next = e01.id
        self.e01.triangle = id
        self.e12.triangle = id
        self.e20.triangle = id
        self.p0 = e01.p0
        self.p1 = e01.p1
        self.p2 = e12.p1
        self.vertices = nb.typed.List([self.p0, self.p1, self.p2])
        if self.closed():
            print('I am a triangle.')
        else:
            print('I am not a triangle.')

    def closed(self) -> bool:
        return (self.e01.p1 == self.e12.p0) and \
               (self.e12.p1 == self.e20.p0) and \
               (self.e20.p1 == self.e01.p0)
typeTriangle = Triangle.class_type.instance_type

# Example 1 (closed)
p0 = Point(0, 0)
p1 = Point(1, 0)
p2 = Point(0, 1)
print()
t = Triangle(1, Edge(1, p0, p1), Edge(2, p1, p2), Edge(3, p2, p0))
print(f'Triangle => id: {t.id} (closed)')
for v in t.vertices:
    print(f'            x:{v.x}, y:{v.y}')
print(f'    Is triangle closed? => {t.closed()}')

# Example 2 (not closed)
p0 = Point(0, 0)
p1 = Point(1, 0)
p2 = Point(0, 1)
print()
t = Triangle(2, Edge(1, p0, p1), Edge(2, p1, p2), Edge(3, p0, p2))
print(f'Triangle => id: {t.id} (not closed)')
for v in t.vertices:
    print(f'            x:{v.x}, y:{v.y}')
print(f'    Is triangle closed? => {t.closed()}')
# I am a triangle.
# Triangle => id: 1 (closed)
#             x:0.0, y:0.0
#             x:1.0, y:0.0
#             x:0.0, y:1.0
#     Is triangle closed? => True

# I am not a triangle.
# Triangle => id: 2 (not closed)
#             x:0.0, y:0.0
#             x:1.0, y:0.0
#             x:0.0, y:1.0
#     Is triangle closed? => False
1 Like

@Oyibo Oyibo You are absolutely right! it works!

But now, I am thoroughly confused as to when I should try to specify a type of an instance method when using numba or not!!! :frowning:

What am I missing? Am I missing some documentation to help me navigate this? I feel that every time I get stuck (though I try many things) I ending up sending my questions to the discussion forum (which is quite often).

So if there are any additional information or some source of examples that I need to consult I will be happy to learn about it.

Many thanks

Are you confused about the usage of ‘deferred_type’?
The ‘deferred_type’ was proposed as a workaround for your circular referencing problem by another group member. It is no requirement for a general jitclass.
In Numba, the deferred_type is a feature that allows you to define a type that doesn’t have to be fully specified at compile time. It’s often used in cases where you want to create a type that depends on runtime information or where the type is not known until runtime.
=> simply avoid it, if possible

The documentation has some pretty good examples for implementations of jitclasses.
Check the latest one here:
https://numba.readthedocs.io/en/stable/user/jitclass.html#jitclass

Thanks @Oyibo

No, I understand the purpose of the deferred type. What I am confused about are some of the following (which have come up throughout this thread)

  1. When should one specify the type, as for instance in a function parameter? As I mentioned earlier I had resorted to the deferred_type() because I thought I had to specify the output of the __lt__() function above as being of a type_Point3D but you did not do so we did not need the deferred_type() after all.
  2. When does one use nb.vertices.ListType() .vs. nb.typed.List()??? This happened above when returning a list of vertices of the triangle. It is not clear to me when I should use one or the other?
  1. In Numba, when you override a supported dunder method like __eq__ in a jitclass, Numba can automatically infer the types for self and other because they are both instances of the same class. Numba probably uses the class definition to infer these types.
    Compiling Python classes with @jitclass — Numba 0+untagged.4320.g54c0cf8.dirty documentation
  2. typed.List can be used in case of type-homogeneous lists.
    Supported Python features — Numba 0+untagged.4320.g54c0cf8.dirty documentation
    Where has nb.vertices.ListType() been used?

Thanks @Oyibo !

nb.vertices.ListType() was used here (last line)

class Triangle:
    def __init__(self, id: int, e01, e12, e20):
        self.id = id
        self.e01 = e01
        self.e12 = e12
        self.e20 = e20
        self.e01.next = e12.id
        self.e12.next = e20.id
        self.e20.next = e01.id
        self.e01.triangle = id
        self.e12.triangle = id
        self.e20.triangle = id
        self.p0 = e01.p0
        self.p1 = e01.p1
        self.p2 = e12.p1
        self.vertices = nb.typed.List([self.p0, self.p1, self.p2])

That was something you introduced. But I was not certain why you did so… why couldn’t it had been left as

self.vertices = [self.p0, self.p1, self.p2]

as it was originally. Which is why I asked about when one should be introducing numba types in the body of the class ???

Both implementations are possible.
Using numba.typed.List provides improved performance, better type inference, avoids reflection issues, and offers a typed container for lists.
https://numba.readthedocs.io/en/stable/reference/deprecation.html#deprecation