Skip to content

Commit a47ca67

Browse files
committed
docs: add experiences and aha moment
Signed-off-by: Akshay Mestry <[email protected]>
1 parent a8b81f1 commit a47ca67

File tree

1 file changed

+137
-2
lines changed

1 file changed

+137
-2
lines changed

docs/source/projects/xsnumpy.rst

Lines changed: 137 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
.. Author: Akshay Mestry <[email protected]>
22
.. Created on: Saturday, March 01 2025
3-
.. Last updated on: Monday, March 03 2025
3+
.. Last updated on: Wednesday, March 05 2025
44
55
:og:title: Building xsNumpy
66
:og:description: Journey of building a lightweight, pure-python implementation
@@ -416,11 +416,145 @@ assumed this would be easy too, and it worked... partly.
416416
417417
When I tried a slice like ``array[:, 1]``, it broke. When I tried with
418418
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`_.
420422

421423
.. image:: ../assets/sigh-meme.jpg
422424
:alt: Deep sigh meme
423425

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+
424558
.. _NumPy: https://numpy.org/
425559
.. _multiplying matrices: https://www.mathsisfun.com/algebra/
426560
matrix-multiplying.html
@@ -432,3 +566,4 @@ pretty obvious that there were some significant flaws in my logic.
432566
.. _strides: https://numpy.org/doc/stable/reference/generated/numpy.ndarray.
433567
strides.html
434568
.. _broadcasting: https://numpy.org/doc/stable/user/basics.broadcasting.html
569+
.. _indexing: https://numpy.org/doc/stable/user/basics.indexing.html

0 commit comments

Comments
 (0)