1
1
.. Author: Akshay Mestry <[email protected] >
2
2
.. Created on: Saturday, March 01 2025
3
- .. Last updated on: Monday , March 03 2025
3
+ .. Last updated on: Wednesday , March 05 2025
4
4
5
5
:og: title: Building xsNumpy
6
6
:og: description: Journey of building a lightweight, pure-python implementation
@@ -416,11 +416,145 @@ assumed this would be easy too, and it worked... partly.
416
416
417
417
When I tried a slice like ``array[:, 1] ``, it broke. When I tried with
418
418
higher-dimensional arrays, it fell apart! With each new test case, it became
419
- pretty obvious that there were some significant flaws in my logic.
419
+ pretty obvious that there were some significant flaws in my logic. I wasn't
420
+ just building some way to access data, I was constructing a flexible system
421
+ needed to mirror NumPy's powerful, intuitive `indexing `_.
420
422
421
423
.. image :: ../assets/sigh-meme.jpg
422
424
:alt: Deep sigh meme
423
425
426
+ After days of trial and error, I finally realised, these so-called **"easy
427
+ peasy" ** methods were actually sly little gateways into NumPy's deeper design
428
+ philosophies:
429
+
430
+ - **Consistency. ** Whether you're tinkering with 1D, 2D, or N-D arrays, the
431
+ operations should behave like clockwork, no surprises, Sherlock!
432
+ - **Efficiency. ** Slices and views shouldn't faff about copying data
433
+ willy-nilly, they ought to create references, keeping things lean and mean.
434
+ - **Extensibility. ** Indexing had to be nimble enough to handle both the
435
+ simple stuff (``array[1, 2] ``) and the proper head-scratchers (
436
+ ``array[1:3, ...] ``).
437
+
438
+ What kicked off as a laid-back attempt to rework :py:func: `repr ` and
439
+ other important methods ended up being a right masterclass in designing for
440
+ generality. I wasn't just sorting out the easy bits, I had to step back and
441
+ think like a "library designer", anticipating edge cases and making sure the
442
+ whole thing didn't crumble the moment someone tried something a tad clever.
443
+ As of writing about xsNumPy, a couple of months later, this struggle taught me
444
+ something profound, what seems super duper simple on the surface often hides
445
+ massive complexity underneath.
446
+
447
+ And that's exactly why building xsNumpy has been so powerful for my learning.
448
+
449
+ .. _illusion-of-simplicity :
450
+
451
+ Illusion of simplicity
452
+ ===============================================================================
453
+
454
+ Well, after wrestling with the **"simple" ** things, I naively thought the
455
+ hardest and in all honesty, the boring part of xsNumPy was behind me. I was
456
+ chuffed and excited than ever before for the **"fun" ** stuff |dash |
457
+ element-wise arithmetics, broadcasting, and other random functions. What I
458
+ didn't realise was that my journey was about to get even more mental. If
459
+ implementing the ``ndarray `` class was untangling a knot, matrix operations
460
+ felt like trying to weave my own thread from scratch. Not sure, if that makes
461
+ sense.
462
+
463
+ But the point was, it was hard!
464
+
465
+ If you've read it till this point, you might've noticed a trend in my thought
466
+ process. I assume things to be quite simple, which they bloody aren't and I
467
+ start small. This was nothing different. I started simple, at least that's what
468
+ I thought. Basic arithmetic operations like addition, subtraction, and scalar
469
+ multiplication seemed relatively straight. I figured I could just iterate
470
+ through my flattened data and perform operations element-wise. And it worked...
471
+ for the first few test cases.
472
+
473
+ .. code-block :: python
474
+ :linenos:
475
+ :emphasize- lines: 20 ,27
476
+
477
+ def __add__ (self , other : ndarray | int | builtins.float) -> ndarray:
478
+ """ Perform element-wise addition of the ndarray with a scalar or
479
+ another ndarray.
480
+
481
+ This method supports addition with scalars (int or float) and
482
+ other ndarrays of the same shape. The resulting array is of the
483
+ same shape and dtype as the input.
484
+
485
+ :param other: The operand for addition. Can be a scalar or an
486
+ ndarray of the same shape.
487
+ :return: A new ndarray containing the result of the element-wise
488
+ addition.
489
+ :raises TypeError: If `other` is neither a scalar nor an
490
+ ndarray.
491
+ :raises ValueError: If `other` is an ndarray but its shape
492
+ doesn't match `self.shape`.
493
+ """
494
+ arr = ndarray(self .shape, self .dtype)
495
+ if isinstance (other, (int , builtins.float)):
496
+ arr[:] = [x + other for x in self ._data]
497
+ elif isinstance (other, ndarray):
498
+ if self .shape != other.shape:
499
+ raise ValueError (
500
+ " Operands couldn't broadcast together with shapes "
501
+ f " { self .shape} { other.shape} "
502
+ )
503
+ arr[:] = [x + y for x, y in zip (self .flat, other.flat)]
504
+ else :
505
+ raise TypeError (
506
+ f " Unsupported operand type(s) for +: { type (self ).__name__ !r } "
507
+ f " and { type (other).__name__ !r } "
508
+ )
509
+ return arr
510
+
511
+ But, as always, the system collapsed almost immediately for higher-dimensional
512
+ vectors. What if I added a scalar to a matrix? Or a ``(3,) `` array to a
513
+ ``(3, 3) `` matrix? Could I add floats to ints? I mean this lot works in normal
514
+ math, right? Each new **"simple" ** operation posed a challenge in itself. I
515
+ realised I wasn't just adding or multiplying numbers but recreating NumPy's
516
+ `broadcasting `_ rules.
517
+
518
+ Trust me lads, nothing compared to the chaos caused by the matrix
519
+ multiplication. Whilst coding the initial draft of the ``__matmul__ ``, I
520
+ remember discussing this with my friend, :ref: `cast-sameer-mathad `. I thought
521
+ it'd be just a matter of looping through rows and columns, summing them
522
+ element-wise. Classic high school math, if you ask me. And it worked as well...
523
+ until I tried with higher-dimensional arrays. This is where I realised that
524
+ matrix multiplication isn't just about rows and columns but about correctly
525
+ handling **batch dimensions ** for higher-order tensors. I found myself diving
526
+ into NumPy's documentation, reading about the **Generalised Matrix
527
+ Multiplication (GEMM) ** routines and how broadcasting affects the output
528
+ shapes.
529
+
530
+ .. note ::
531
+
532
+ You can check out the complete implementation of arithmetic operations on
533
+ GitHub.
534
+
535
+ `Learn more
536
+ <https://github.com/xames3/xsnumpy/blob/main/xsnumpy/_core.py> `_ |right |
537
+
538
+ .. _aha-moment :
539
+
540
+ "Aha!" moment
541
+ ===============================================================================
542
+
543
+ This happened during the winter break. I didn't have to attend my uni and was
544
+ working full-time on this project. After days of debugging, I realised that
545
+ all of my vector operations weren't about **"getting the math right" **, but
546
+ they were about thinking like NumPy:
547
+
548
+ - **Shape manipulation. ** How do I infer the correct output shape?
549
+ - **Broadcasting. ** How can I extend the smaller arrays to fit the larger ones?
550
+ - **Efficiency. ** How can I minimise unnecessary data duplication?
551
+
552
+ At this stage, I wasn't just rebuilding some scrappy numerical computing
553
+ doppleganger but rather a flexible and extensible system that could handle both
554
+ the intuitive use cases and the weird edge cases. As I started more along the
555
+ lines of NumPy developers, I started coming up with more broader and general
556
+ solutions.
557
+
424
558
.. _NumPy : https://numpy.org/
425
559
.. _multiplying matrices : https://www.mathsisfun.com/algebra/
426
560
matrix-multiplying.html
@@ -432,3 +566,4 @@ pretty obvious that there were some significant flaws in my logic.
432
566
.. _strides : https://numpy.org/doc/stable/reference/generated/numpy.ndarray.
433
567
strides.html
434
568
.. _broadcasting : https://numpy.org/doc/stable/user/basics.broadcasting.html
569
+ .. _indexing : https://numpy.org/doc/stable/user/basics.indexing.html
0 commit comments