Popup YouTube Video
Sheerpower Logo

Processing Complex Numbers


Processing Complex Numbers

A complex number has a real part and an imaginary part — 3 + 4i — and every REAL in Sheerpower can hold one. There is no complex type to declare and no library to load: write the imaginary unit as a suffix on a number, and the arithmetic, the text forms and the functions follow. This page covers the values themselves (Part 1), the precision and the magnitude range they carry (Part 2), three worked stories (Part 3), complex arrays (Part 4), the transforms with their stories (Part 5), what the operations cost (Part 6), and the functions of a complex number (Part 7). The operators page (Mathematical and Logical Operators) introduced the basics; the array pages (Array Math Functions, Index Lists & Slices, Solve(), Sort(), and More) matter for the transforms that come later on this page.

Part 1: Writing and Reading Complex Numbers

4i is a literal, and 3 + 4i is a real plus an imaginary, which the compiler folds into one complex value. From computed parts use complex(re, im); from text, val(). The four arithmetic operators work as you expect, and a real next to a complex is promoted. The functions that take a complex apart: real(), imag(), abs() (the magnitude), conj() (the sign of the imaginary part flipped) and arg() (the angle in radians, from -pi to pi).

z = 3 + 4i print z, typeof$(z) ! 3+4i Name:Z, Dtype:Real, *Complex* print real(z), imag(z), abs(z), conj(z) ! 3 4 5 3-4i print z * (1 - 2i), z / (1 - 2i) ! 11-2i -1+2i print z + 1, 2 * z ! 4+4i 6+8i -- a real is promoted print complex(1.5, -2), val("2.5-0.5i") ! 1.5-2i 2.5-.5i print 4i * 4i ! -16+0i -- i squared is -1 print round(arg(z), 6), round(arg(1i), 6), round(arg(-1), 6) ! .927295 1.570796 3.141593 print z = 3 + 4i, z <> 3 + 4i, (3 + 0i) = 3 ! 1 0 1 -- = and <> compare both parts

Two rules keep this simple. A value that is complex stays complex: z - 4i is 3+0i, not 3, so a program can always tell what it is holding; real(z) is the way back to an ordinary number. And a complex number has no order: z < w raises the catchable exception NOORDER, and so do max, min and clamp; compare abs(), real() or imag(), which is what you meant anyway.

Part 2: Precision and Magnitude

Each part of a complex number is a decimal floating-point value with 30 significant digits — twice a double's 15 — and its own exponent from 10-8191 to 108191. Every + - * / is computed exactly and rounded once, to the nearest 30-digit value (ties to even), so the result of one operation is always the correctly rounded answer, never the sum of several roundings. abs() is a correctly rounded square root of the exact sum of squares. Printing shows 16 digits per part by default, and sprintf$ shows as many as you ask for, up to 30.

third = (1 + 2i) / 3 print third ! .3333333333333333+.6666666666666667i print sprintf$("%.30r", third) ! .333333333333333333333333333333+.666666666666666666666666666667i print sprintf$("%.6r", third) ! .333333+.666667i big = 1e4000 + 2e-4000i print big ! 1e+4000+2e-4000i print big * big ! 1e+8000+4i -- the cross term is exact: 2 * 1e4000 * 2e-4000 print abs(1e8000 + 1e8000i) ! 1.41421356237309504880168872421e+8000 w = 1e20 + 1i print (w + 1) - w ! 1+0i -- a double would have lost the 1 w = 1e30 + 1i print (w + 1) - w ! 0+0i -- past 30 digits, so does this print (1 + 1e-29i) * (1 - 1e-29i) ! 1+0i -- 1 + 1e-58 rounds to 1 at 30 digits

The limits are loud, not silent. A part whose exponent would pass 8191 raises NUMOVER (the same exception an INTEGER overflow raises), and there is no complex NaN or infinity to absorb a mistake: a NaN operand gives NaN, an infinite one raises. Signed zeros are kept and shown, because the branch cuts of the transcendental functions depend on them: -complex(0, 0) prints -0-0i and conj(-4 + 0i) prints -4-0i, whose arg() is -pi where arg(-4 + 0i) is pi.

huge = 1e8000 + 0i when exception in y = huge * huge use print extype; " "; extext$ ! -4105 Numeric overflow end when print -complex(0, 0), conj(-4 + 0i) ! -0-0i -4-0i

When does 30 digits matter? 30 digits will not make a measurement more accurate than the sensor was. Where it pays is in long chains: a million-point transform touches every value twenty times over, a rotation applied a million times drifts by a million roundings, and with 15 digits the drift reaches the digits you print. With 30 it stays fifteen places below them. The other place is money-like quantities, where a decimal representation makes 0.1 exactly 0.1.

Part 3: Three Stories

An AC circuit. Impedance is a complex number: the resistance is its real part, the reactance (inductive minus capacitive) its imaginary part. Ohm's law then reads exactly as it does for direct current, current = volts / impedance, and the phase angle between voltage and current is arg(current). The apparent power is volts * conj(current): its real part is the real power in watts, its imaginary part the reactive power, its magnitude the volt-amperes.

volts = 120 + 0i resistance = 40 inductive = 30 capacitive = 10 impedance = resistance + (inductive - capacitive) * 1i current = volts / impedance print impedance, current ! 40+20i 2.4-1.2i print round(abs(current), 4), round(arg(current) * 180 / pi, 2) ! 2.6833 -26.57 -- amps, degrees lagging apparent = volts * conj(current) print round(real(apparent), 2), round(imag(apparent), 2), round(abs(apparent), 2) ! 288 144 321.99

A rotation in the plane. Multiplying by a complex number of magnitude 1 rotates a point about the origin, which is how a 2-D graphics or geometry program turns things without a matrix. 1i is a quarter turn, exactly; for any other angle build the unit number from cos and sin.

point = 3 + 1i print point * 1i, point * 1i * 1i ! -1+3i -3-1i -- a quarter turn, then a half print point * complex(cos(pi / 6), sin(pi / 6)) ! 2.098076211353317+2.366025403784439i -- 30 degrees

The Mandelbrot set. The classic loop: square a complex number and add the starting point, over and over, and count how many steps it takes to escape past magnitude 2. A point that never escapes belongs to the set. One routine, one abs() per step, and a line of the picture falls out of a for loop.

routine escape_count with c, returning n zz = 0 + 0i n = 0 do while n < 50 zz = zz * zz + c if abs(zz) > 2 then exit do n = n + 1 loop end routine print escape_count(c = 0 + 0i), escape_count(c = -1 + 0i), escape_count(c = 1 + 1i) ! 50 50 1 line$ = "" for k = -20 to 10 cc = complex(k / 10, 0.45) if escape_count(c = cc) >= 50 then line$ = line$ + "#" else line$ = line$ + "." next k print line$ ! ...............########........

Part 4: Complex Arrays

An array of REALs holds complex elements the way a variable does, and the array pages' machinery carries over: element-wise arithmetic, the maps, masks, filter(), where(), the sorts and the linear algebra. abs(), real() and imag() turn a complex array into a real one, which is how the numeric statistics and comparisons get at it.

dim z(*), m(*), w(*) fill z with 1 + 2i, 3 - 1i, 0.5 + 0.5i print z ! 1+2i 3-1i .5+.5i print z * z ! -3+4i 8-6i 0+.5i print z * 1i ! -2+1i 1+3i -.5+.5i -- every element a quarter turn print abs(z) ! 2.2360679774997897 3.1622776601683793 .7071067811865475 print real(z), imag(z) ! 1 3 .5 2 -1 .5 print conj(z) ! 1-2i 3+1i .5-.5i print filter(z, abs(z) > 2) ! 1+2i 3-1i print where(imag(z) < 0, z, conj(z)) ! 1-2i 3-1i .5-.5i -- fold everything below the axis print stats$sum(z), stats$mean(z) ! 4.5+1.5i 1.5+.5i print reduce(z, *) ! 0+5i dim re(*), im(*) fill re with 1, 2, 3 fill im with 4, 5, 6 print complex(re, im) ! 1+4i 2+5i 3+6i

Two things to know. sort(z), unique() and isin() order complex values by real part and then imaginary part — a bookkeeping order that makes them work, not a size: to sort by magnitude, sort by abs(z) and gather (z[sortindex(abs(z))]). And the statistics that need an order — stats$max, min, median, percentile, argmax and their kin — raise NOORDER on a complex element, while the ones that need a square (stats$var, stddev, pcorr, ...) raise NOCOMPLEX; stats$sum and stats$mean are defined. Matrices with complex coefficients go through matmul(), solve() and determinant() with the same exact-then-round arithmetic, and cluster input reads a cell written as 3+4i into a REAL field as a complex value.

dim a(2, 2), rhs(*) fill a with 1 + 1i, 2, 0, 1 - 1i fill rhs with 1, 1i print determinant(a) ! 2+0i print solve(a, rhs) ! .5-1.5i -.5+.5i

Part 5: Transforms — fft(), ifft() and fftfreq()

The discrete Fourier transform turns a signal sampled in time into the tones it is made of. fft(x) takes an array of numbers (or complex numbers) and gives back an array of the same length whose elements are complex: element k is the amplitude and phase of the tone that repeats k times over the whole sample. ifft(z) goes back. fftfreq(n, spacing) gives the frequency that goes with each element, so you can read a spectrum in hertz instead of bin numbers. Any length works: a power of two is fastest, anything else is handled too.

dim x(*), z(*) fill x with 1, 0, 0, 0 print fft(x) ! 1+0i 1-0i 1+0i 1+0i -- an impulse is every tone equally fill x with 1, 1, 1, 1 print fft(x) ! 4-0i 0+0i 0+0i 0+0i -- a constant is only the zero tone n = 8 redim x(n) for k = 1 to n x(k) = cos(2 * pi * 2 * (k - 1) / n) ! two cycles over the sample next k z = fft(x) print round(abs(z), 6) ! 0 0 4 0 0 0 4 0 -- element 3 (k = 2) and its mirror print fftfreq(8) ! 0 .125 .25 .375 -.5 -.375 -.25 -.125 print fftfreq(8, 0.001) ! 0 125 250 375 -500 -375 -250 -125 -- samples 1 ms apart: hertz print round(real(ifft(z)), 12) ! 1 0 -1 0 1 0 -1 0 -- and back

Inside the transform, doubles. An FFT's round-off is about log2(n) units of its arithmetic — 2e-15 at a million points with doubles — which is eight orders of magnitude below the leakage of any window, and most signals carry 7 digits or fewer. So fft() copies the array into doubles, transforms, and copies back: the copy in is correctly rounded (0.5 is 0.5), the copy back is each double's exact value to 17 digits, the way NumPy prints one. That is why a bin that should be zero reads as 1e-16-ish, and why a -0i can appear: signed zeros are kept. When the digits do matter — exact integer convolution, a transform applied thousands of times, a study of the numerics — fft(x, digits: 30) runs the 30-digit kernel end to end, and ifft(fft(x, digits: 30), digits: 30) returns short decimals exactly.

A vibration sensor. A thousand samples a second for one second from a machine: a 50 Hz hum from the mains, a weaker 120 Hz tone from a bearing, and noise. The power spectrum is abs(fft(x)) ^ 2, the frequency axis is fftfreq(n, 1 / rate), and stats$argmax over the first half (the second half mirrors it) names the strongest tone.

rate = 1000 n = 1000 dim sample(*), spectrum(*), freqs(*), power(*) redim sample(n) for k = 1 to n t = (k - 1) / rate sample(k) = 1.0 * sin(2 * pi * 50 * t) + 0.3 * sin(2 * pi * 120 * t) + 0.05 * (rnd(1000) - 500) / 500 next k spectrum = fft(sample) freqs = fftfreq(n, 1 / rate) power = abs(spectrum) ^ 2 half = int(n / 2) peak = stats$argmax(power[1:half]) print "strongest tone at "; freqs(peak); " Hz" ! strongest tone at 50 Hz power(peak) = 0 print "next at "; freqs(stats$argmax(power[1:half])); " Hz" ! next at 120 Hz

Touch tones. A telephone key is two tones at once: the 1 key is 697 Hz and 1209 Hz. Sampled at 8 kHz for a tenth of a second (800 samples), the two strongest bins are 700 and 1210 Hz — not 697 and 1209, because 800 samples at 8 kHz resolve frequencies to the nearest 10 Hz (rate / n). A longer sample resolves finer; that trade between length and resolution is the whole craft of reading a spectrum.

rate = 8000 n = 800 dim tone(*), tf(*), tm(*) redim tone(n) for k = 1 to n t = (k - 1) / rate tone(k) = sin(2 * pi * 697 * t) + sin(2 * pi * 1209 * t) next k tm = abs(fft(tone)) tf = fftfreq(n, 1 / rate) first = stats$argmax(tm[1:400]) tm(first) = 0 print "the two tones: "; tf(first); " Hz and "; tf(stats$argmax(tm[1:400])); " Hz" ! the two tones: 1210 Hz and 700 Hz

The weekly cycle in daily sales. Twelve weeks of daily sales have a level, a weekend bump that repeats every seven days, and a slow trend. The transform finds the seven-day cycle at once (the strongest bin, DC aside, sits at 1/7 cycles per day), and zeroing that bin and its mirror before transforming back removes the cycle and leaves the level and the trend — a seasonal adjustment in four statements.

n = 84 dim sales(*), sz(*), sf(*), sp(*), flat(*) redim sales(n) for d = 1 to n sales(d) = 1000 + 300 * cos(2 * pi * (d - 1) / 7) + 2 * d next d sz = fft(sales) sf = fftfreq(n) sp = abs(sz) sp(1) = 0 ! ignore the level (the DC bin) print "the strongest cycle repeats every "; round(1 / sf(stats$argmax(sp[1:42])), 2); " days" ! the strongest cycle repeats every 7 days sz(13) = 0 + 0i ! bin 12 (7-day) and its mirror sz(n - 11) = 0 + 0i flat = real(ifft(sz)) print round(sales[8:14], 0) ! 1316 1205 953 752 754 959 1215 print round(flat[8:14], 0) ! 1018 1022 1024 1022 1020 1022 1026

Smoothing by convolution. A moving average is a convolution, and a convolution is a multiplication of spectra: transform both, multiply, transform back. (This is the circular convolution — the ends wrap — which is what the kernel of zeros padding is for.)

dim noisy(*), kernel(*), smooth(*) fill noisy with 5, 9, 4, 8, 6, 10, 5, 9 fill kernel with 0.25, 0.25, 0.25, 0.25, 0, 0, 0, 0 smooth = real(ifft(fft(noisy) * fft(kernel))) print round(smooth, 2) ! 7.25 7 6.75 6.5 6.75 7 7.25 7.5

What the transform costs, this machine (the 2026-09-09 build):

pointsfft(x), doublesfft(x, digits: 30)
65,5360.031 s0.42 s
1,048,5760.34 s (about 3 million points a second)about 8 s

The refusals are loud: a NaN or infinity in the input raises NUM_OUTOFRANGE naming the element (fill holes first with where(isnan(x), 0, x)), a value beyond a double's range raises NUMOVER and names digits: 30, which takes it, and a string array is a compile error.

Part 6: What the Operations Cost

A complex operation is exact-then-round-once at 30 digits, and it is still fast: the kernel does its products in 64-bit pieces and rounds with a reciprocal multiply, no division instruction anywhere. Measured in an ordinary for loop (200,000 iterations, one statement per iteration, including the statement's own overhead:

statementoperations per second
a = a * b (complex)6.5 million
a = a + b (complex)6.5 million
m = abs(a)1.4 million
a = a / b (complex)700,000
plain = plain * 1.0001 (a REAL, for scale)12.5 million

So a complex multiply or add costs about two real multiplies. Division is the exception at under a million a second — it runs the general exact path — so in a hot loop divide once and multiply by the result where the algebra allows. A million complex multiply-adds is about a third of a second. For scale, a radix-2 FFT written in Sheerpower itself runs about 700,000 butterflies a second (65,536 points in 0.75 s); the built-in transforms of the parts that follow do the same arithmetic at about 3.3 million butterflies a second, a million points in about three seconds.

Part 7: The Functions of a Complex Number

sqr(), exp(), log() (and log10, log2), the trig family (sin, cos, tan, csc, sec, cot, asin, acos, atn), the hyperbolic one (sinh, cosh, tanh) and a non-whole power all take a complex number. They are computed in doubles, like their REAL versions, with the standard branch cuts of the C language (Annex G): the principal square root, the logarithm's cut along the negative real axis, and the signed zero deciding which side of a cut you are on. A whole-number power stays exact.

z = 3 + 4i print sqr(z), exp(1i), log(z) ! 2+1i .5403023058681398+.8414709848078965i 1.6094379124341+.9272952180016122i print sin(z), cos(1 + 1i), tanh(1 + 1i) ! 3.853738037919377-27.01681325800394i .8337300251311491-.9888977057628651i 1.083923327338695+.2717525853195117i print asin(2 + 0i), atn(2i) ! 1.570796326794897+1.316957896924817i 1.570796326794897+.5493061443340549i print z ^ 0.5, (-8 + 0i) ^ (1 / 3), 2 ^ (1i) ! 2+1i 1+1.732050807568877i .7692389013639721+.6389612763136348i print sqr(-4 + 0i), sqr(conj(-4 + 0i)) ! 0+2i 0-2i -- the cut: from above, from below print log(-1 + 0i), log(conj(-1 + 0i)) ! 0+3.141592653589793i 0-3.141592653589793i print exp(log(z)), sin(z) ^ 2 + cos(z) ^ 2 ! 3+3.999999999999999i .9999999999998803+4.162852422011e-15i

The last line shows the precision: these are double-precision results (15 to 16 digits), so an identity comes back to that many digits and no more — exactly what exp(log(x)) does for a REAL x, whose exp and log also run in doubles. Two rules follow. A result that is not finite raises NUM_OUTOFRANGE naming the function (log(0 + 0i), csc(0 + 0i)), and a zero base to a negative or complex power raises EXPZERONEG. And a real argument keeps its real rules: sqr(-4) still raises, because promoting it silently would turn a typo into an imaginary number; write sqr(-4 + 0i) when you mean the complex root. The functions map over arrays like every other built-in.

zc = 0 + 0i when exception in y = log(zc) use print extype; " "; extext$; " -- "; _string$ ! -4106 Numeric value out of range for this function -- log() of 0+0i: the result is not finite end when dim v(*) fill v with 1 + 1i, -1 + 0i, 1i print sqr(v) ! 1.09868411346781+.4550898605622273i 0+1i .7071067811865476+.7071067811865475i

A literal folds at compile time. Sheerpower evaluates a function of literals when it compiles, so log(0 + 0i) written out is a compile error, not a runtime exception; through a variable, as above, it is a catchable one. The same is true of sqr(-4).

Summary: any REAL holds a complex number (3 + 4i, complex(re, im), val("3+4i")); each part carries 30 significant digits and an exponent to 108191; every operation is exact then rounded once; real / imag / abs / conj / arg take one apart; there is no order, only = and <>; a multiply costs about three real multiplies.
Hide Description

    

       


      

Enter or modify the code below, and then click on RUN

Looking for the full power of Sheerpower?
Check out the Sheerpower website. Free to download. Free to use.