 Good afternoon everyone. I hope you're all enjoying for some. Learning a lot of stuff. Hopefully I'm also enjoying a lot of peace. I'll be talking a bit about extending number. So the goal of my talk is to give you an overview on how you can extend number to better solve your problems. So Let's first start with the beginning. So give a short introduction what number is for those who don't know it yet or need some refreshing. So number is a just in time compiler. You see here an example that I shamelessly stole from the website from number. It's a project supported by Anaconda. So its goal is to accelerate scientific Python. So you see here this is basically how it works. You have a decorator just in time decorator. It says no Python is true, which means that in the generated code no Python is involved. So no Python. The Python interpreter is not called. What's nice about number is that it has very good NumPy support. It is also able to generate code for CUDA and also it is extensible, which is exactly the topic of this talk. Anything else? You notice it. So I'll give you a bit of context on what I've been working on in my day job. I don't want to talk too much about it, but it gives you an idea of what problems you can solve. So what you see here is a photonic integrated circuit. So that's basically just a regular electronics chip, which is only special because you can connect an optical fiber to it. So through this fiber light will come into the chip and will be guided into the chip. We at Lucida, we actually try to build software to be able to build and design those circuits. Part of this is an optical circuit simulator, in which we try to simulate the behavior of those circuits. We want our users to be able to build models of their components. So the people who have a bit of background in electronics might know SPICE, which is a circuit simulator for electronics. So it's very similar to that. So we want our users to build models in Python, so a high-level API, but at the same time it has to be really, really fast. We want to run a lot of simulations in a very short time so that we can figure out how to better build our circuits. So you see already some problems here. So this calculate S-Matic. It has to be called by our solver, which is written in C, C++. We have here sort of dictionary-like structure and we have some C++ objects that we want to use from our simulator. So these are all stuff things that are not directly supported by Namba. So dictionaries are not supported. There's in the latest version some basic support for strings, but even there it's limited. But luckily you can extend Namba. Okay, let's go to the next slide. Namba is a compiler and it's basically a very boring compiler. But it's good in software. We want things to be boring. Boring is good in software. That's basically what we do as software engineers. We take a bunch of boring stuff and we turn it together in something very exciting. But one thing that makes it special is, as I said, extensible. So you have a few extension points here. So one, two, three, four. But with those extension points you can add your own stuff to the compiler pipeline. So let's start with the beginning. So you start with Python source code. This will be translated into byte on byte code. And then Namba will transform this byte on byte code in its own representation. So this is Namba intermediate representation. It's basically an abstraction over byte code. Next you have the opportunity to rewrite this intermediate representation. For example to do parallelization, to do all kinds of optimizations and you can add your own rewriters. So then the next step is type inference, where you can also add your own types and do type inference of your own functions. Then you have another opportunity to do another rewrite phase, but this time with the actual types. And then when all that is finished you can actually get to the point and start generating the code. So that it's important to remark that Namba does not directly generate machine code itself, but it generates LLVM intermediate representation, which is taken by LLVM to actually generate the machine code for your machine. Okay, so also there in the in the lowering phase you have the possibility to add your own stuff. So you can add custom data models, custom code generation. So and after that you have very fast new native speed of your Python code. So I will now go to a bit more detail of all those extension points. Let's start with the beginning. So I was talking about the rewrite phase. So Namba likes decorators very much. So this is basically how all the extensions work. So here we say, okay, we want to register our rewrite and we say, okay, it's before inference. As I said before, you have a step, a rewrite step before inference, before type inference, and then one after inference. Basically a rewrite consists of two steps. So you have a much match where you're going to look for the expressions, the statements, the instructions you want to replace. And then when you return true, the apply method is invoked and in that phase you can actually replace the function block of with a new one in which you do an optimization, for example. Next, you have the type inference. So there you have something, the concept of types. So maybe I have to clarify here. So a Namba type is a bit different than what you would have in then in just a regular Python type. So you can compare it more with what the mypy project offers. So where you have the opportunity to add type annotations to your functions. So you have to compare it with that. So you have the possibility here to add a my point type. So this example, we have a point which has basically an x and y coordinate. Then you can use that to do type inference for your own functions. So here we have a callable, my point constructor, which if you use it in your Python code will create a my point object. And this type callable is basically going to say, okay, I want to infer the types of my point constructor. This is going to generate your type, and for given x and y arguments, you want to say, okay, the return value is going to be of a type, my point type. That's basically how it works. So that's the possibilities you have during type inference. Next one, that is finished. So again, you have a rewrite phase where you are able to reuse the types. And when all that is finished, you can start with actually lowering. So that means generating LLVM intermediate representation. So again, decorator, same principle, we have a my point type. So we're going to register a model to a certain type. In this case, for our point, we want to use a struct like, a C struct like model with a x and y attribute of which we here assume that it's an integer. And that's basically the data layout of your point. So this is telling number, okay, I have here a data structure with this data layout. As you also see, this is a list because the order is important. And then this information can then be used when you're going to actually lower the implementation of your callables. So as you said, as we had before, we had our my point constructor. Which takes two arguments. So we have an integer argument, the x and then the y. So the lower building is a decorator to say, okay, I have a callable that I want to lower. I have an instruction that I want to lower. This can also be a set adder, a get adder, an addition, basically any operation. And you're going to say, okay, for this particle signature, this is the implementation of the LLVM code. But what is important is that for LLVM, for NUMBA, you're never ever going to or very rarely are going to generate LLVM intermediates representation yourself. So what makes NUMBA very nice is that you, they provide a lot of functionality to be able to easily generate LLVM intermediates representation. So this is a nice example. So as I said before, you have we had the point for which we use a struct-like model. And there's this, in the code gen utilities, you have this create struct proxy, which generates like this kind of syntactic sugar that you can use here. So it looks here that we assign the value x to point.x, but what it actually does in the background is using the builder to at the same time generate the correct code for doing this operation. So it looks like you're actually assigning the value, but in reality it's generating the code. So that's basically it. So I'll summarize a bit. So we have first rewrite step then we have type inference and then we have the actual lowering. So these are the extension points you have. Then I'll come back to my problem. So remember we have to integrate with c, c++. So and in reality that's going to be very often the case when you are working on definitely scientific computing. You already have maybe a solver that has been going around university or we need your company for a long time. You don't want to throw all that away because you've put all your experience in it. Starting from skeptics would be very difficult. So it is important to be able to integrate with other languages. But luckily we have this very nice love triangle. We have NumPy and the C programming language and they all love each other. So let me clarify a little bit. So for the people we don't know about NumPy internally it stores its data as a C contiguous array, which means that you can exit this from C and do all operations from C as well and there's this very nice library, the C types library, which you can use to generate pointers to that data. Okay. Same thing a bit for our number. So number has actually quite good integration with C. So the C array for example, construct for example, it allows to wrap a C array and pretend as if it was an umpire array. So that means that you can apply slicing, use all the NumPy, the NumPy operations to be able to use a C array as if it was a NumPy array. So it works actually pretty well. Then we have the C func decorator, which is very similar to the just-in-time decorator that you typically use when using NumPy. But the main difference there is that you have to upfront provide the types that you want your function to invoke with. This means that at that time when you use the decorator it's going to be already compiled to machine codes and then you can get back an address to the actual code that is underneath. So you can get back a pointer to the actual address in your memory where the function is located. That's very nice because now we can take that pointer, give it to a simulator and call the generated code from a C program. So that's basically what we also do in our solver. So we pass the pointer of the function generated by number to the solver. We also pass the pointers to the C++ objects that we want to call. We wrap them and then using CFFI we are able to to call the functions from our solver itself. So that's basically how it works. Also, of course, C, you are able to call NumPy using the Python C API. Okay, that's it. You have also prepared examples. They are on the website. So it will be a bit short to go over them, but if you what is in here is very interesting to you, I highly recommend you to go to there. If something is not clear, contact me. I'll also be around to talk about it. So and of course we are still quite some time for questions. So please, if there are some questions, go ahead. No questions. Okay. I will probably not be able to run on this computer, but So this is basically the example of the rewrite. So as I said, so there's a very very unuseful example, obviously, but so we have here a very meaningful variable that we want to be replaced. So as you see here, so you have the function block and you can search for instances of a sign expression. So this is an sign expression. You will search for any sign expression that has the target name. So this is the target meaningful verb. We return true if we have any matches and then you can replace it with a constant of 42 and then we can run it and this will believe me, it will return 43 because 42 plus 1 is 43. Other example is Well, it's a Mac. Yes, so this is an example of the So the point, basically the point example that I also explained during the presentation. So we have here our my point constructor. So it's a my point. So we have x and y which are supposed to be an integer. And here we have the Python type to that corresponds with this the number type, sorry, that corresponds with the my point constructor. This is a type I was talking about. And then we have Yes, scroll. So this is the binary data layout and then the actual implementation. When you only do that, so this is something I didn't talk about yet, but so you are actually at this point, you can do very little with your point. So we basically, okay, you can create a point. It will be allocated in your memory. You can add it to a list and then you can return the length of the list. That's obviously very not very useful. But what you still need to add is like support for getting the attributes. So this is basically here. Something I didn't explain during the presentation, but you can use something like a template which allows you to do inference for the attributes and also other instructions. So it's basically here if my attribute name is either x or y, I want to return a type of integer 64. So and then I can again lower this. So for this instruction, I can generate again bytecode, use the create struct proxy. Same name. So I do a get adder on the actual struct. And this will, using the builder, generate the LLVM code. And then you use you use the utilities from number to say how it should keep track of the memory. So that once you've done that, you're able to access the attributes of your point. So it will create using the lower. Yes, there's a question. So the question was if you can use structured data structures for your for your fields, the answer is yes. So there's actually on the number documentation, there are some examples, or at least in the development branch of the latest versions of number, there is some explanation on how you can make records and use so use new types. So basically you're able to use any type there. And because Numba supports the numpy arrays, so it has a lot of inbuilt types for numpy arrays. Also for CFFI as native support for CFFI. So it will take the CFFI representations turn those into native number types so that it's all able to directly work with with those types. Yes, another question there at the back. So the question is about the time that it takes and the complexity of understanding numbers, is that correct? Well, I'll be honest, it took me a time to figure out how Numba worked. So it's one of the reasons why I wanted to talk about it today. So it definitely did. So the last half year they've been putting a lot of effort into documentation, but before there were documentation on this was sparse. So but what is very nice about it, so Numba uses all this also to implement Numba itself. So which is also always a very good idea to to to build your and use your own stuff to build your own stuff. So so that means that you can look at the code. So as when I was talking about the numpy support it's advanced because numpy is quite a complex thing. So building adjusting time copilot for it will take a lot of time. But you can reuse all that work and you will by reading that code by reading Numba code you learn how to use all those things as well. So so the Numba project itself is a very good documentation for for Numba on how to use Numba. So it's actually, if you are really interested in using this I propose really that you get into reading the Numba code. So again, my time is up. So thank you all for listening. If there's any follow-up questions, I'll be glad to talk to you.