*An animation made by adding*

9 straight paths of polar points with

1 spiral path of polar points and

the Cartesian canvas center point

9 straight paths of polar points with

1 spiral path of polar points and

the Cartesian canvas center point

**tl;dr:**In this post, I extend my vector based point classes to include ordered point containers called`Path`. I implement my`Path`class with my`Vector`base class. This extends vector arithmetic to include`Path`containing Polar or Cartesian points. This is also continues my exploration of the use of Structural Pattern Matching within Python programming. Expect to see more`match`statements than are strictly necessary.In my artwork, my focus is paths, corridors and their branches. This is the second post on a series about the Python code that contributes to my artwork. While my mazes are hand drawn, I use my own software written in Python for analysis, animation, and print image processing.

I always prefer to work with iterators but sometimes materialization of sequences into memory is useful. I define an
ordered sequence of n-dimensional point objects to be a `Path` through n-dimensional space. I want to operate
on instances of `Path` as units with simple arithmetic statements. My goal is writing expressions that mix
paths of `CartesianPoints`, paths of `PolarPoints`, individual Cartesian and Polar points, as well as
scalars.

square = Path( CartesianPoint(0, 0), CartesianPoint(0, 10), CartesianPoint(10, 10), CartesianPoint(10, 0), ) # translate square by 50 in both x and y directions translated_square_50 = square + 50 # translate square by 1 in the x direction and 10 in y direction translated_square_1_10 = square + CartesianPoint(1, 10) # scale the square by 2 and 3 in the x and y directions respectively scaled_square_2_3 = square * CartesianPoint(2, 3) # scale each point of the square by each point of a translated square scaled_square = square * translated_square_50

The implementation of the `Path` class is built directly on the `Vector` class. I need only redefine
two private methods: `_judge_candidate_value` and `_operation`. The first restricts members of a
`Path` to be exclusively `Vector` instances. The second defines how arithmetic works with `Path`.
In this latter case, the match statement differentiates the behavior of `Path` instances with scalars, other
`Path` instances, Points, and other iterables like `ndarrays` from numpy.

```
class Path(Vector):
@classmethod
def _judge_candidate_value(cls, a_potential_point):
# accept only instances of Vector as values for a Path
if isinstance(a_potential_point, Vector):
return a_potential_point
raise TypeError(
f'{cls} members must be a Vector instance, "{a_potential_point}" is not'
)
def _operation(self, the_other, a_dyadic_fn):
# return a new instance with members that result from a_dyadic_fn applied to
# this instance zipped with the_other
match the_other:
case Number() as a_number:
# match scalars
return self.__class__(
*starmap(
a_dyadic_fn, zip_longest(self, (a_number,), fillvalue=a_number)
)
)
case self.__class__() as a_path_object:
# match an instance of another Path
return self.__class__(*starmap(a_dyadic_fn, zip(self, a_path_object)))
case CartesianPoint() | PolarPoint():
# match any instance of the two point families
return self.__class__(
*starmap(
a_dyadic_fn,
zip_longest(self, (the_other,), fillvalue=the_other),
)
)
case Iterable() as an_iterable:
# match any other type of iterable
return self.__class__(*starmap(a_dyadic_fn, zip(self, an_iterable)))
case _:
# no idea how to apply this value
raise TypeError(f"{the_other} disallowed")
```

(see this code in situ in the
path.py
file in the
points main directory
)
The method `_operation`, simulates single-dispatch using the `match` statement for only one of the
method's arguments. I'm not sure if the standard decorator implementations of single dispatch are able to do this.
There five different ways the function works based on the type of the method's first argument. The fourth case is
important because it handles Iterables of unknown provenance. It assumes that its content represent point-like
objects, but not necessarily the point classes in this package, for example, `ndarrays`. If they are not
`CartesianPoint` or `PolarPoint`, it assumes the unknown points represent points in Cartesian space. See
the test `test_multiplication_with_an_iterator_of_tuples`
where a `Path` instance is multiplied by an iterator of bare tuples.

The path_demo.py
script that draws the animation above, relies on arithmetic with path objects composed of `PolarPoint`
instances.
The first `Path` object that it creates is a spiral of five loops about the origin (0,0) in polar space. Then
the outer for loop creates nine straight rays emanating from the origin, also in polar space. It creates them one at a
time in the direction specified by theta (*θ*). Each spiraled ray is created by adding the
straight ray path to the spiral path and then the cartesian image center point. The result is nine

`Path`instances in polar space growing from the center of canvas.

```
def draw_path_demo(a_canvas):
"""Draw 9 looping rays from the center of the canvas"""
canvas_middle_point = CartesianPoint(a_canvas.the_image.size) / 2
# materialize a spiral path of PolarPoints about the origin
spiral_path = Path(
iter_natural_steps_between(PolarPoint(0, 0), PolarPoint(50, 10 * π), 100)
)
collection_of_ray_paths = []
for θ in iter_linear_steps_between(0, 2 * π, 9):
# materialize a path of PolarPoints making a straight ray from the origin
ray_path = Path(
iter_linear_steps_between(PolarPoint(0, θ), PolarPoint(300, θ), 100)
)
# combine the straight rays with the spiral path and translate them
# from the canvas origin to the canvas middle - save the result as
# a path of PolarPoints
collection_of_ray_paths.append(ray_path + spiral_path + canvas_middle_point)
# set up windowing iterators for all the spiraling ray paths
windowed_iter_for_every_ray_path = (
windowed(a_ray_path, 2) for a_ray_path in collection_of_ray_paths
)
# step through the all the spiral ray paths in parallel, round robin style
for a_segment_for_every_ray in zip(*windowed_iter_for_every_ray_path):
for start_point, end_point in a_segment_for_every_ray:
a_canvas.draw_line_segment(start_point, end_point)
```

(see this code in situ in the
path_demo.py
file in the
points demo directory
)
Instead of drawing each ray one at a time, I want all the spiral rays to grow simultaneously. I create nine iterators
of the nine rays `Path` instances using the `windowed` iterator from the `moreitertools` module.
With a size specified as 2, this pairs items from an iterator in a moving window: (0th, 1st), (1st, 2nd), (2nd, 3rd),
... This is perfect for drawing succeeding line segments. Drawing the nine ray iterators simultaneously means zipping
the iterators together, iterating over the result, drawing one segment from each ray in turn, round-robin style.

When creating my ray and spiral paths, I'm using two different iterators that seem to do very similar things:
`iter_linear_steps_between` and `iter_natural_steps_between`. I learned some very interesting things
about `PolarPoint` by writing these iterators.

The first iterator draws from a generalization of how this would be solved in the one dimensional world of scalars:
create a delta value by subtracting the start point from the end point and then dividing by the number of increments
desired. Then add that increment repeatedly to the start point for the number of intermediate points desired. This is
how `iter_linear_steps_between` works. In fact, it was written originally as a version of the Python
`range` iterator for floating point values. It gets used that way for the *θ* value in the
loop for the nine rays. I think it is a triumph of Object Oriented Programming that a method written for floating
point values can be used without modification for my

`CartesianPoint`and

`PolarPoint`classes.

```
def iter_linear_steps_between(
start, stop, number_of_iterations, target_type=lambda n: n
):
# in cartesian space, make a sequence of points representing
# a straight line between the start and stop points
increment = (stop - start) / number_of_iterations
for i in range(number_of_iterations):
yield target_type(start + (increment * i))
```

(see this code in situ in the
iterators.py
file in the
points main directory
)
This iterator works for any object that implements scalar-style arithmetic. Of course, that ability was a design the
goal of my point classes in the first place. This algorithm always results in iterating over the intermediate points
in a straight line, even if the input start and end points are `PolarPoints`.

An alternative algorithm, not relying on the arithmetic interface magic, leads to some very useful effects. It treats
the points as just vectors of values. It applies the delta equation to paired values from each dimension of the vector
independently rather than using the semantics of the point. This is how `iter_natural_steps_between` is
implemented. For `CartesianPoint`s, this produces exactly the same result as the previous implementation: a
straight line. However, for `PolarPoint`s, it behaves very differently. Instead of a straight line, it produces
a smooth curve between the start and end points.

```
def iter_natural_steps_between(
start, stop, number_of_iterations, target_type=lambda n: n
):
# In cartesian space, Vectors and CartesianPoints trace out straight
# lines between the start and end points. PolarPoints trace curves
# based on the full magnitude of the angular components (number of time
# around the circle). I use this iterator for making spiral paths.
base_point_type = start.__class__
for p in zip(
*(
iter_linear_steps_between(first, second, number_of_iterations)
for first, second in zip(start, stop)
)
):
yield target_type(base_point_type(*p))
```

(see this code in situ in the
iterators.py
file in the
points main directory
)
Why? Examining the values of a series of intermediate `PolarPoint`s representing a straight line in Cartesian
space reveals that the * Δρ* and

*are not constant over the range. The first iterator automatically and transparently adjusts based on the definition of arithmetic for my Cartesian and Polar points. The second iterator treats all points as just vectors of values, ignoring the differences between Cartesian and Polar points. The second iterator forces the use of constant*

**Δθ***and*

**Δρ***for*

**Δθ**`PolarPoint`, so a curved line is the result.

Playing round with the differences between these two iterators took me down a rabbit hole. It seems that there should
be several more iterators that perhaps cause both `CartisianPoint`s and `PolarPoint`s to curve, or
perhaps makes `CartesianPoint`s curve, but make `PolarPoint`s straight? Hey, I ought to invent a
celebratory `TransPoint` class (Cartesian interface, Polar implementation) in honor of some very fine people I
know. I'll eventually write about where my explorations took me.

Instead, the next post will be about further specialization of `Path` to create transformable viewports for
the Python PIL package using a modernized version the ancient Cohen-Southerland clipping algorithm.