In part 9 we got our first glimpse of how abstraction can make programming tasks a lot easier. Generally, abstraction will save a lot of wear and tear on your fingers at the cost of some brain hurt.

To explore a generic process, let’s create a very simple filter;

	p = z-1('1 p * pole)
	Pole-Response = p

As the name suggests, it is a pole response, basically the impulse response of a one pole filter.

To examine its output, let’s drop back to the interactive mode of k2cli, familiar from part 1.

PS C:\Users\Vesa\code\Kronos.2.HG\Debug> .\k2cli.exe --loop 8 .\part10-generics.k
K2CLI 0.1
(c) 2011 Vesa Norilo

Pole-Response(0.5) => 1
Pole-Response(0.5) => 0.5
Pole-Response(0.5) => 0.25
Pole-Response(0.5) => 0.125
Pole-Response(0.5) => 0.0625
Pole-Response(0.5) => 0.03125
Pole-Response(0.5) => 0.015625
Pole-Response(0.5) => 0.0078125

The ‘–loop’ command line switch causes every evaluation to be repeated a number of times, useful when we want to examine the output of a stateful process.

Now, suppose we would like a complex pole.

Knowing complex math, we might come up with:

Complex-Pole(pr pi)
	(re im) = z-1('(1 0) (re * pr - im * pi re * pi + im * pr))

	Complex-Pole = (re im)
EXPR>Complex-Pole(0.7 0.7)
Complex-Pole(0.7 0.7) => (1 0)
Complex-Pole(0.7 0.7) => (0.7 0.7)
Complex-Pole(0.7 0.7) => (-0 0.98)
Complex-Pole(0.7 0.7) => (-0.686 0.686)

All right, so that wasn’t hard. We just provide real and imaginary parts separately, pass them both to the unit delay and spell out a piecewise complex multiplication as the recursive process.

However, operating on complex numbers is very tedious if everything has to be written out in terms of real and imaginary parts. A custom type can be used to automate all of this.

Defining a Type

The first thing we need is a type tag. For a best practice I recommend naming the type tag descriptively and providing all functions related to the type in a package of the same name.

Type Complex
Package Complex{
	Cons(re im) { Cons = Make(Complex re im) }
	Real/Img(Z) { (Real Img) = Break(Complex Z) }

These functions provide a constructor and accessors. ‘Complex:Cons’ can be called to create a tagged complex number out of two parts. ‘Complex:Real’ and ‘Complex:Img’ can be used to break up a complex number in its constituent parts. Like so;

EXPR>Complex:Cons(1 42)
Complex:Cons(1 42) => <Complex(1 42)>
EXPR>Complex:Real(Complex:Cons(1 2))
Complex:Real(Complex:Cons(1 2)) => 1
EXPR>Complex:Img(Complex:Cons(4 99))
Complex:Img(Complex:Cons(4 99)) => 99

We will obviously want to provide arithmetics as well. This can be done by adding a form to the global functions ‘Add’ and ‘Mul’ that can be used to compute on complex numbers.

Add(a b)
	Add = Complex:Cons(Complex:Real(a) + Complex:Real(b) Complex:Img(a) + Complex:Img(b))

Mul(a b)
	Mul = Complex:Cons(
		Complex:Real(a) * Complex:Real(b) - Complex:Img(a) * Complex:Img(b)
		Complex:Real(a) * Complex:Img(b) + Complex:Img(a) * Complex:Real(b)

The application of these forms is governed by the use of accessors ’Complex:Real’ and ‘Complex:Img’. Any type that doesn’t conform to these accessors will not be processed by these forms of ‘Add’ and ‘Mul’. It works as desired;

EXPR>Complex:Cons(3 3) * Complex:Cons(0 1)
Complex:Cons(3 3) * Complex:Cons(0 1) => <Complex(-3 3)>
EXPR>Complex:Cons(1 2) + Complex:Cons(10 20)
Complex:Cons(1 2) + Complex:Cons(10 20) => <Complex(11 22)>

Looking back at the pole response, multiplication with the coefficient is actually the only thing ‘Pole-Response’ needs to be able to perform. Should it then work with our complex number type? Almost.

The compiler will spit out an error message, shortened here for brevity.

EXPR>Pole-Response(Complex:Cons(0.7 0.7))
Pole-Response(Complex:Cons(0.7 0.7)) => ** Specialization Error E-9995 **


:Mul (f Complex)
<< E-9996:No valid forms >>
< ... SNIP ... >
<< E:-9977:Exception (Multiplication failed for f Complex) >>

The compiler complains that there are no forms of ‘Mul’ that can accept arguments of type ‘f’ for float and ‘Complex’. Looking at the only multiplication in the filter, ‘p * pole’, we can deduce that the type of ‘p’ must be a float since ‘pole’ is something we pass directly as a complex number.

Indeed, our unit delay is initialized with ’1, a function that returns a float. That is the source of the stray type in our program.

We could, of course, replace the expression with ‘z-1(‘Complex:Cons(1 0) p * pole)’. However, then our function would only accept a complex number.

We want to pick the initializer dynamically, according to the type of the coefficient. This can be accomplished by yet another function;

	zero = When(Type-Of(a) == Float '1
		    Type-Of(a) == Complex 'Complex:Cons(1 0))

Now, we can complete the generic pole response.

	out = z-1(unity(p) p * out)
	Generic-Pole = out

And enjoy the results…

>.\k2cli.exe --loop 4 .\part10-generics.k
K2CLI 0.1
(c) 2011 Vesa Norilo

Generic-Pole(0.8) => 1
Generic-Pole(0.8) => 0.8
Generic-Pole(0.8) => 0.64
Generic-Pole(0.8) => 0.512
EXPR>Generic-Pole(Complex:Cons(0.707 0.707))
Generic-Pole(Complex:Cons(0.707 0.707)) => <Complex(1 0)>
Generic-Pole(Complex:Cons(0.707 0.707)) => <Complex(0.707 0.707)>
Generic-Pole(Complex:Cons(0.707 0.707)) => <Complex(-0 0.999698)>
Generic-Pole(Complex:Cons(0.707 0.707)) => <Complex(-0.706787 0.706787)>

This may seem like more work than stirctly necessary, but keep in mind the implications. If everywhere in our code, we initialize delays with ‘unity’ and a similar ‘zero’ function, we can make our filters dynamically configurable to whatever signal and coefficients are being fed to them.

In addition, we can add further types without touching old code. All that would be needed is to implement the necessary arithmetic along with the agreed-upon initializer routines, and suddenly all our previous signal processing code is able to handle the newly minted type.

As a final note,

> .\k2cli.exe --audio-out "Audio:Clock(Complex:Real(Generic-Pole(Complex:Cons(Sqrt(0.99) 0.1))))" .\part10-generics.k

Is the sound of a pole very near to the unit circle.

No Comment.

Add Your Comment

6 + one =