Overlapping rectangle puzzle

A couple of weeks ago, Scientific American published this puzzle.

Overlapping rectangle puzzle

There are nine overlapping rectangles, A through I. They overlap one another in a specific pattern using a notation I hadn’t seen before:

A\(D, F)    F\(A, B, I)
B\(F, G)    G\(B, C, I)
C\(G, H)    H\(C, D, E)
D\(A, H)    I\(E, F, G)
E\(H, I)

This means that rectangle A overlaps with two rectangles: D and F; rectangle F overlaps with three rectangles: A, B, and I; and so on. The goal is to correctly label the rectangles in the image.

My sense of the “right” way to handle this puzzle is to think of it as a network and use some powerful result from graph theory to solve it in an instant. But since the only thing I know about graph theory is that it exists, I went about it differently.

First, I labeled each rectangle with a code that indicates the number of rectangles it intersects and the number of rectangles its overlapping rectangles intersect.

Overlapping rectangles labeled with intersection counts

For example, the rectangle at the top is labeled 3–222. That means it intersects three rectangles, and each of those intersecting rectangles intersects with two rectangles. I put the numbers after the dash in increasing order, just as the puzzle puts the rectangle letters in alphabetical order in its notation.

Then I added my notation after the puzzle’s notation:

A\(D, F)     2–23 
B\(F, G)     2–33
C\(G, H)     2–33
D\(A, H)     2–23
E\(H, I)     2–33
F\(A, B, I)  3–223
G\(B, C, I)  3–223
H\(C, D, E)  3–222
I\(E, F, G)  3–233

The H and I rectangles are unique in my notation, so they could be identified immediately. I then went counterclockwise from H and saw that the leftmost rectangle had to be D (the only 2–23 intersecting H), the one at the bottom had to be A (the only 2–23 intersecting D), and then similarly up from the bottom through F, B, G, and C. E came from its connection to H and I.

Labeled rectangles

When I looked at the SciAm solution, I was disappointed. It was faster than mine, but not because they used some clever math. They basically identified H and I using my method (albeit without my notation), figured out B and E from that, and then “the rest is simple.” It is simple, but it didn’t teach me anything new. Oh well, maybe next year.


Tweaking my screenshot macro

In my recent post on the Keyboard Maestro episode of Mac Power Users, I needed a screenshot of a Typinator window and had trouble getting what I wanted. I tried using my SnapClip macro, but I found that when the macro’s user input window appeared,

SnapClip input window

the Typinator window went into the background (as expected) but didn’t come back to the foreground when the KM window was dismissed (not as expected). I think the reason was that the Typinator window isn’t a regular application window, but whatever the reason, I had to take the screenshot “by hand” using ⇧⌘4 instead of using SnapClip. My positioning of the crosshairs was off as I tried to get both the window and a desktop background border, so the image was a little wonky:

Typinator temporary snippet

The borders are clearly of different widths. Also, and this may not be as obvious, the blue border color varies because my ⇧⌘4 screenshot captured some of the window’s shadow.

The answer was to rewrite SnapClip so the screenshot is taken before the Keyboard Maestro user input window appears. Now SnapClip can handle the Typinator window:

Reshot Typinator window

Much better.

The way SnapClip was written, it put up the user input window and used the results of that interaction to send options to a Python script named screenshot, the source code of which is included in the SnapClip post. screenshot called macOS’s built-in screencapture command with the appropriate arguments. To fix the “Typinator window problem,” I had to rewrite both the KM macro and the Python script.

Here’s the new version of SnapClip, which you can also download (you may need to right-click that link to download it instead of displaying its XML).

KM SnapClip

I’m not going to describe the whole thing. It’s basically the same as the old version but with these changes:

As with the old version, I’ve colored the actions to help keep track of the nested Ifs. The outermost, which tests whether a title is given, is light blue-green,      ; the middle one, which tests whether the image is supposed to have a background border, is orange,      ; and the innermost, which tests whether the image is supposed to be uploaded to my server, is purplish-pink,      .

The process-screenshot script is this:

python:
 1:  #!/usr/bin/env python3
 2:  
 3:  import tempfile
 4:  from PIL import Image
 5:  import os
 6:  import subprocess
 7:  import shutil
 8:  from datetime import datetime
 9:  import urllib.parse
10:  import argparse
11:  
12:  # Handle the arguments
13:  desc = 'Process a screenshot saved to a file.'
14:  ep = '''If a title is given, it saves the image to a file on the
15:  Desktop with a filename of the form yyyymmdd-title.png.
16:  If no title is given, the image is put on the clipboard and the
17:  upload option is ignored.'''
18:  parser = argparse.ArgumentParser(description=desc, epilog=ep,
19:            formatter_class=argparse.RawDescriptionHelpFormatter)
20:  parser.add_argument('-b', '--background', help='add desktop background border', action='store_true')
21:  parser.add_argument('-u', '--upload', help='upload to images directory and print URL', action='store_true')
22:  parser.add_argument('-t', '--title', help='image title', type=str)
23:  parser.add_argument('ssfile', help='path to screenshot file')
24:  args = parser.parse_args()
25:  
26:  # Parameters
27:  type = "png"
28:  bgcolor = (85, 111, 137)
29:  border = 32
30:  optimizer = '/Applications/ImageOptim.app/Contents/MacOS/ImageOptim'
31:  
32:  # Add a desktop background border if asked for
33:  if args.background:
34:    snap = Image.open(args.ssfile)
35:    # Make a solid-colored background bigger than the screenshot.
36:    snapsize = tuple([ x + 2*border for x in snap.size ])
37:    bg = Image.new('RGBA', snapsize, bgcolor)
38:    bg.alpha_composite(snap, dest=(border, border))
39:    bg.save(args.ssfile)
40:  
41:  # Optimize the file
42:  subprocess.run([optimizer, args.ssfile], stderr=subprocess.DEVNULL)
43:  
44:  # Save it to a Desktop file if a title was given; otherwise,
45:  # save it to the clipboard
46:  if args.title:
47:    sdate = datetime.now().strftime("%Y%m%d")
48:    desktop = os.environ['HOME'] + "/Desktop/"
49:    fname = f'{desktop}{sdate}-{args.title}.{type}'
50:    shutil.copyfile(args.ssfile, fname)
51:    bname = os.path.basename(fname)
52:  
53:    # Upload the file and print the URL if asked
54:    if args.upload:
55:      year = datetime.now().strftime("%Y")
56:      server = f'user@server.com:path/to/images{year}/'
57:      port = '123456789'
58:      subprocess.run(['scp', '-P', port, fname, server])
59:      bname = urllib.parse.quote(bname)
60:      print(f'https://leancrew.com/all-this/images{year}/{bname}')
61:  else:
62:    subprocess.call(['impbcopy', args.ssfile])
63:  
64:  # Delete the temporary file
65:  os.remove(args.ssfile)

The main differences between it and the screenshot script are:

Otherwise, it follows the logic of screenshot.

Will this be my last version of SnapClip? Of course not. But it gets better every time I rewrite it, and I learn more about both my workflow and the ins and outs of macOS every time I change it.


Final (?) version of my LLM proofreading macro

I’ve written two posts on using LLMs to proofread my posts before publishing. In the first post, I had a fairly rudimentary prompt and spent most of my time discussing the feedback ChatGPT gave me. By the second post, I had refined the prompt and was using a simple Keyboard Maestro macro to prepare the post text with line numbers to help me quickly get to the errors ChatGPT and Claude found.

The workflow described in the second post has served me pretty well for the past couple of months, but it did involve some unnecessary copying and jumping between apps, so I’ve improved the KM macro to take on what I used to do by hand.

Before I get into the specifics, I’ll start by saying that overall I’m happy with the results. Between the two of them, ChatGPT and Claude seem to ferret out almost all of my typos and poorly constructed sentences. Unfortunately, neither of them has emerged as the clear winner—one almost always finds mistakes that the other missed. So I’m still running my posts through both and don’t expect that to change, at least not for a while. And because this is the only use I have for either of them, I’m still using the free versions of each.

Being happy with the results doesn’t mean I trust them. Both flag “errors” that aren’t. My favorite recent error was this from Claude:

“watchOS 26” should be “watchOS 11” (or another actual watchOS version number - watchOS 26 doesn’t exist)

The free version of Claude is a little behind the times—or in denial.

Let’s move on to my workflow. Here’s my setup and how I get and use the LLM proofreading suggestions:

It doesn’t really make any difference whether I do ChatGPT or Claude first, but I’ve kind of settled into running ChatGPT before Claude.

In Keyboard Maestro, I have an LLM group whose macros run only when the ChatGPT or Claude SSB app is active.

LLM group in Keyboard Maestro

This is the group in which my Proofread Post macro lives. Here it is:

KM Proofread Post

If you’re interested, you can download it (you may need to right-click on that link) and adjust it to your needs.

The first step gets the text of the post by running this AppleScript:

applescript:
1:  tell application "BBEdit"
2:    get text of front text document
3:  end tell

There’s some obvious fragility to this, but I’ve yet to run it when the frontmost BBEdit document wasn’t the Markdown source of the post I’m writing. The text is saved to the Keyboard Maestro variable MarkdownPost.

The second step passes the text through a shell filter,

nl -ba | sed -E 's/^ +//'

which adds line numbers to the beginning of each line. How this pipeline works is discussed in my earlier post. The result is saved back into MarkdownPost.

The third step combines the prompt and the line-numbered Markdown text and pastes it into the LLM’s text field for processing. The prompt is

Find typographical errors and grammatical mistakes in the following Markdown text. Do not make any changes, just tell me what you think should be changed. Ignore all embedded URLs, quoted text, and programming code. Only report actual errors, not style suggestions. I am using the linefeed character to end each line and have put line numbers at the beginning of each line; use those line numbers to report where the errors are. The text to analyze starts after a line with ten asterisks.

This is followed by a couple of blank lines, a line with ten asterisks, and the line-numbered Markdown text.

Because the third step creates an entry in Keyboard Maestro’s clipboard history that I don’t want, I added the final step to delete it.

I still read through my posts after the LLM proofreading, and I usually have my Mac read it aloud to me, too. But I’m not good at finding my own typos. ChatGPT and Claude find mistakes that my eyes and ears don’t.


Powerball in Python

Yesterday morning, before Christmas got going, I read about the big Powerball win Wednesday night. I figured that was as good an excuse as any to practice my new lotto math skills and wrote a short Python script to work out the game’s odds.

Powerball’s rules and odds differ from the UK Lotto’s, but they’re similar. In Powerball, there are 69 numbered white balls and 26 numbered red balls. Players select five numbers from 1 through 69 and one number from 1 through 26. In the drawing, five white balls and one red ball (the Powerball) are drawn. Your winnings depend on how many white balls you match and whether you matched the Powerball.

The script I wrote, powerball.py, prints the odds for every type of win. Here’s the output:

⚪⚪⚪⚪⚪ 🔴    1 in 292,201,338.00
⚪⚪⚪⚪⚪       1 in 11,688,053.52
  ⚪⚪⚪⚪ 🔴    1 in 913,129.18
  ⚪⚪⚪⚪       1 in 36,525.17
    ⚪⚪⚪ 🔴    1 in 14,494.11
    ⚪⚪⚪       1 in 579.76
      ⚪⚪ 🔴    1 in 701.33
        ⚪ 🔴    1 in 91.98
           🔴    1 in 38.32

The alignment may not look great in your browser. I fiddled with the CSS to get it looking OK in Safari on my computer, but that doesn’t mean it’ll work for you. I suppose I could change the script to have it output an HTML table, but the idea was to have the columns align well in Terminal, which they do:

Powerball output in Terminal

Again, this is on my computer with my Terminal settings, but the alignment looks good in all the fixed-width fonts I have.

Of course, the main point of the script was the calculation of odds, not formatting. Here’s powerball.py:

python:
 1:  #!/usr/bin/env python3
 2:  
 3:  from math import comb
 4:  
 5:  def powerball(matches, power):
 6:    'Odds (n for "1 in n") of the Powerball game'
 7:  
 8:    if power:
 9:      return 26 * comb(69, 5)/(comb(5, matches)*comb(69 - 5, 5 - matches))
10:    else:
11:      return 26/(26 - 1) * comb(69, 5)/(comb(5, matches)*comb(69 - 5, 5 - matches))
12:  
13:  def mstring(matches):
14:    'Regular match output string'
15:  
16:    return f'{"  "*(5-matches)}{"⚪"*matches}'
17:  
18:  def pstring(power):
19:    'Powerball output string'
20:  
21:    if power:
22:      return f'🔴'
23:    else:
24:      return f'  '
25:  
26:  for m in range(5, -1, -1):
27:    for p in (True, False):
28:      if p or m > 2:
29:        print(f'{mstring(m)} {pstring(p)}    1 in {powerball(m, p):,.2f}')

The powerball function takes two arguments: the number of white ball matches and a boolean for whether the Powerball was matched. It returns the inverse of the probability of getting the given results, which is

26C569Cm5C5m64

when the Powerball is matched and

2625C569Cm5C5m64

when it isn’t. These are inverses of probabilities because the lottery presents odds in “1 in n” form.

The mstring and pstring functions return the strings with emoji circles for the number of white and red balls that are matched. The circles take up two regular monospace character widths.

The nested loop in Lines 26–29 prints out the results. The if statement prevents the printing of certain combinations (two white ball matches without the Powerball match, for example) because those combinations don’t pay off. The odds are printed to two decimal places because that’s how they’re presented on the Powerball site.