Motivation
Less than two months before Christmas 2025, I got myself a gift: an R47 from the first production batch.
But what to do with it?
Hey look, it has first-class complex arithmetic, it’s programmable, it allows free-form plotting on its dot-matrix screen… What does that remind me of?
Oh…that!
Initialization
To allow easier adjustments, I have broken out the program’s initialization routine as separate:
LBL ‘MbInit’
-3
STO ‘Xmin’
0.75
STO ‘Xmax’
1.12
STO ‘Ymax’
CHS
STO ‘Ymin’
400
STO ‘SXMax’
240
STO ‘SYMax’
25
STO ‘IterMx’
2
√𝑥
STO ‘MBound’
RTN
This must be called once before you execute the program for the first time. After that, these global variables may either be treated as constants or adjusted to suit:
- Ymin, Ymax: the vertical limits of the Mandelbrot set, straight from Wikipedia
- Xmin, Xmax: the horizontal limits, extended from the recommended [-2, 0.47] to account for the R47’s screen aspect ratio, to avoid distortion; incidentally makes the program run faster as more pixels end up outside the set
- SXMax, SYMax: screen dimensions; saved as constants because they’re needed twice in the program, once during setup and once again inside the main loop
- IterMx: maximum number of inner loop iterations before a point is considered inside the Mandelbrot set; the default is fast-ish but a bit sloppy; additional detail can be seen up to 100 or so, at a cost to run time; you can halve it without losing recognizability, but 10 is too far
- MBound: an empirical constant to determine whether the z function is escaping the set or not; worst practical case is a 1-1-sqrt(2) right triangle where the z vector is the hypotenuse; can be as high as 2 before it gives no more detail; waiting until it hits ±Inf is theoretically correct but vastly overkill
The Program
I now present the actual program, with commentary:
LBL ‘Mbrot’
LocR 07 ; local numbered registers for the innermost (00) loop; speed and scoping
RCL ‘MBound’
STO R.02 ; mnemonic: it's set to sqrt(2) above in MbInit
0 ; clear entire screen
ENTER ; nonzero (X,Y) means leave part alone
CLLCDₓᵧ
RCL ‘Xmax’ ; calculate X axis range from MbInit values
RCL ‘Xmin’
-
STO ‘Xrange’
RCL ‘Ymax’ ; ditto Y axis range
RCL ‘Ymin’
-
STO ‘Yrange’
RCL ‘SYMax’ ; R47 screen coordinate system puts 0 on the bottom, not the top
DECR X ; convert 1-based screen Y dim to 0-based pixel coord
STO R.03 ; mnemonic = Y = 3-armed letter; outer screen loop variable
TICKS ; remember starting time, for benchmarking output
STO R.06
LBL j ; outer screen iteration loop: j = "Y"
RCL ‘SXMax’ ; plot from rightmost pixel for simpler "countdown" loop logic
DECR X ; convert 1-based screen X dim to 0-based pixel coord
STO R.04 ; mnemonic = X = 4-armed letter; inner screen loop variable
LBL i ; inner screen loop; i = "X"
0 ; Mandelbrot z starts at 0
STO R.00 ; mnemonic: z = zero
RCL ‘Xrange’ ; scale X from screen coords into Mandelbrot range
RCL÷ ‘SXMax’
RCL× R.04
RCL+ ‘Xmin’
RCL ‘Yrange’ ; ditto Y
RCL÷ ‘SYMax’
RCL× R.03
RCL+ ‘Ymin’
COMPLEX ; construct c = Y + Xi
STO R.01 ; mnemonic: second element in expression
RCL ‘IterMx’ ; innermost loop to determine if z escapes set
STO R.05 ; local numbered copy to avoid repeated by-name lookup
LBL 00 ; the “zero” loop calculates z = .00
RCL R.00 ; core rule: z ↦ z² + c
𝑥²
RCL R.01
+
STO R.00
|𝑥| ; the vector length of z in rectangular coordinate space
𝑥>? R.02 ; test against local copy of MBound constant = sqrt(2)
GTO e ; it escaped the set!
DSZ R.05 ; keep looping until we determine z's disposition
GTO 00
RCL R.03 ; did not GTO e, so it's in the Mandelbrot set!
RCL R.04
PIXEL
LBL e ; either falling thru from above or skipped the PIXEL
DSL R.04 ; next X iteration, inner LBL i
GTO i
PAUSE 00 ; let firmware update the LCD per row; needed on HW, not sim
DSL R.03 ; next Y iteration, outer LBL j
GTO j
GTO f ; finish up
RTN ; end of 'e', purely for 'rejig --fmt' purposes
RTN ; end of 00 loop, same reason
RTN ; end of inner 'i' screen loop, ditto
RTN ; end of outer 'j' screen loop, ditto
LBL f ; finishing steps
SNAP ; save a screenshot of the resulting image
TICKS ; get the current time, in ticks (0.1s)
RCL- R.06 ; subtract starting time from it
36000. ; produce seconds in HP decimal format
÷
H→𝕋 ; convert to R47 typed time value to show run time
RTN
END
You may now wish to download this separately in R47 program form, suited to copying into PROGRAMS folder on the R47’s USB storage. Eject the disk, then 🟦 I/O READP it into the calculator.
Alternately, call 🟦 I/O SAVEST to back up your calculator’s state, then LOADST this version. Beware! This will RESET your R47’s running state, not quite to factory condition, but leaving only the contents of the flash storage untouched. Thus the backup.
Making It Faster
This program takes about 23 seconds to run on my main macOS desktop machine, on the r47 simulator.1 This is under version 00.109.04.00b0, where several speed improvements to the program interpreter landed.
You might guess that the remaining speed hits come down to all the high-precision complex number arithmetic, but let’s measure instead.
On profiling it, what we actually find is that the single biggest hot spot is the per-instruction runFunction() call. In other words, it is spending nearly all its time interpreting our program, not actually doing math.
In prior versions of this program, the primary hot spot was findNamedVariable() which looks up the register index for a named variable, but that was because we were doing this multiple times inside the innermost loop, which runs roughly 100k times in this program: 400 px wide × 240 px high × average iteration count of “z is escaping the set” test per pixel. The LocR call at the top let me to move the six variables/constants accessed inside the 00 loop out into a local scope where all accesses are by index, which is much faster. As a bonus, we avoid polluting the global namespace with these innermost variables.
Measurement shows that the remaining by-name variable lookups take ~6% of the program’s running time, but one must ask how important saving that fraction is versus keeping the program readable. I believe what I have now is a good balance.
We could wish for more speed without hand-optimization, but that would require adding a bytecode compiler to the R47, at minimum. I would expect low-hanging fruit resulting from such a project to produce speedups in the 10× range before one had to begin resorting to crazy micro-optimizations to make progress. Beyond that, you could get into JIT and such, but that is highly unlikely to happen.
To-Do
This is just a start. Ideas for future expansion:
- interactive center point movement and zooming
- freeze the display until either user presses a key or we
SNAPa BMP - automatic dive mode, saving BMP at each step; try seahorse valley!
- Julia set mode
Closing Thought
“Pathological monsters!” cried the terrified mathematician. “Every one of them is a splinter in my eye.”
— Jonathan Coulton, Mandelbrot Set
(You may now wish to return to my R47 article index.)
License
This work is © 2025-2026 by Warren Young and is licensed under CC BY-NC-SA 4.0
- ^
I cheated: leaving the the
PAUSE 00call in adds another 7 seconds for no particularly good reason, but since it is only necessary on the hardware, I removed it.