A simple example of Truchet is Smith tiles. The tiles are designed to fit together seamlessly even when placed randomly:
Christopher Carlson came up with a way to generalize the tiles so they could be placed on top of each other at different sizes. A square can be covered by four half-sized tiles with inverted colors and extra wings, and the pattern will remain seamless.
Here are his tiles:
It can be hard to see how they overlap, but this is a start. This is three different sizes of tile overlaid randomly, with the grid displayed to help see the edges:
I love the randomness of these images, how shapes emerge that were not in the tiles themselves. I’ve been using them as Zoom backgrounds and desktop wallpapers. But I wondered if they could be used to create images.
The set of gray values in the Carlson set is somewhat limited, so I created a new set of tiles with more opportunities for variation:
These produced even more chaos and serendipity when used randomly:
To make images, I used a photo as source and fit tiles onto it to match the gray levels. Larger squares would be subdivided when their sub-squares’ intensities differed more than some threshold:
The algorithm to pick a tile will try to choose a good orientation, to match the colors within the square. Notice the tiles used for my shoulders. Though, on the flip side, both these images clearly exhibit “the forehead problem” because there’s little color variation there.
Looking around for other high-contrast images, I tried a well-known blogger’s avatar:
The subdivision algorithm uses a threshold to decide when a square has enough variation within it to deserve subdivision. What happens if we start that threshold very large, and slide it down to very small, animating the result?
My energy level is not what it used to be, probably because of the Keppra (anti-seizure medication). Psychologically, I am not used to the idea that my brain can just shut off with no notice. I guess over time, I’ll just ignore that possibility?
]]>This happened three weeks ago. I went to the emergency room, had a few CT scans and an MRI. The best theory is that I had a seizure. So now I am on anti-seizure medication, and am legally forbidden to drive a car for six months.
I was wearing a helmet, and was on a bike path, not a street. The physical effects were minimal: a sore shoulder and some road rash.
There was no eye-witness, so doctors guess I fell because I blacked out rather than the other way around. During the time I don’t remember, I called my wife and told her where I was, so maybe I was never truly unconscious? No one knows.
I usually have a low heart rate (a resting pulse of 50 bpm is not unusual), so maybe it was cardiac-related? I’m wearing a heart monitor to collect data.
The first week was hard because I felt completely limited in what I could do. All spring I had been feeling strong and capable in physical activities, and now I was finding a short walk difficult.
At first the anti-seizure meds made me tired and a bit fuzzy-headed. But we’ve adjusted them, and/or I’m adjusting to them, and/or my concussion is wearing off, so I feel more like myself. I’ve gotten back on the bike (though not alone), and have been swimming in the ocean, same as every summer.
I have more visits coming up with more doctors to try to understand what happened, and what might happen. I doubt they will be able to completely rule out a seizure, so I may be on meds for quite some time. Their recommendations are quite cautious (“Don’t take a bath without supervision”), so now we are making absurd trade-offs and considerations of risks and possibilities.
It’s unsettling to have lost time without a clear explanation, and especially unsettling to think that it could happen again at any time. I’m not sure what to do with that.
]]>Of course the angles in an equilateral triangle are 60 degrees. But 60 also appears in solid geometry. There are four Archimedean solids (regular polyhedra made with any mixture of regular polygons) with 60 vertices. You can use Nat Alison’s beautiful polyhedra viewer to explore them:
]]>Now I’ve done 400 walks starting and ending at the same point, totaling 2318 miles:
The cadence of these walks has slowed quite a bit. Not the pace of the walking itself, but the number of walks in a week. Nat is only with us one day a week now, so I don’t have to be out for his sake. I have biking and swimming as exercise options now, and my toes were getting a bit aggravated by walking.
Quantifying the walks was a fun project, and motivated me to get out and go, but it also pushed me to go farther and farther: in February, all the walks were longer than 7 miles, which might not have been wise. The problem with a statistic is the unreasonable expectation that it will constantly increase.
I’m not sure what will happen now. I’m going to continue walking as an at least occasional exercise, but maybe with a different motivator? Or maybe not: it’s hard to drop a project once you start it...
Now I’m going on a bike ride, and I’m not going to measure my speed.
]]>The __setattr__ special method (dunder) is called when an attribute is changed on an object. But like all special methods, it’s not found on the object, only on the class. We didn’t want to track changes to all objects of the class because it would produce too much noise in the logs.
So we wanted a per-object special method. The way we came up with was to define a new class with the special method. The class is derived from the object’s existing class, so that everything would work the way it should. Then we changed the object’s class.
Changing an object’s class sounds kind of impossible, but since in Python everything happens at run-time, you can just assign a new class to obj.__class__, and now that is the object’s class.
Here’s the code, simplified:
>>> class SomeObject:
... ...
>>> class Nothing:
... """Just to get a nice repr for Nothing."""
... def __repr__(self):
... return "<Nothing>"
>>> obj = SomeObject()
>>> obj.attr = "first"
>>> obj.attr
'first'
>>> def spy_on_changes(obj):
... """Tweak an object to show attributes changing."""
... class Wrapper(obj.__class__):
... def __setattr__(self, name, value):
... old = getattr(self, name, Nothing())
... print(f"Spy: {name}: {old!r} -> {value!r}")
... return super().__setattr__(name, value)
... obj.__class__ = Wrapper
>>> spy_on_changes(obj)
>>> obj.attr = "second"
Spy: attr: 'first' -> 'second'
>>> obj.attr
'second'
>>> obj.another = 'foo'
Spy: another: <Nothing> -> 'foo'
One more detail: the Nothing class lets us use repr() to show an object but also get a nice message if there wasn’t a value before.
The real code was more involved, and showed what code was changing the attribute. This is extreme, but helped us debug a problem. As I said in Machete-mode Debugging, Python’s dynamic nature can get us into trouble, so we might as well use it to get us out of trouble.
]]>As an example, I have a custom keyword “pypi”, so I can search PyPI for a package name by typing “pypi some name” in my browser’s address bar, and it takes me directly to https://pypi.org/search/?q=some name.
You can create your own custom keywords to have shortcuts to all kinds of searches. You provide a URL with a placeholder %s in it. When you type the keyword in the address bar, the rest of the entry is plugged into the placeholder, and you jump to the resulting URL.
These are some of the custom search keywords I use. You can use them as-is, adapt them as you need, or create your own. Instructions for creating keywords are further down the page.
Firefox defines custom keywords as bookmarks:
Chrome defines custom keywords as search engines in Settings:
A few years ago I wrote here about how to draw Cairo SVG in a Jupyter notebook. That worked, but wasn’t as convenient as I wanted. Now I have a module that manages the PyCairo contexts for me. It automatically handles the displaying of SVG and PNG directly in the notebook, or lets me write them to a file.
The module is drawing.py.
The code looks like this (with a sample drawing copied from the PyCairo docs):
from drawing import cairo_context
def demo():
with cairo_context(200, 200, format="svg") as context:
x, y, x1, y1 = 0.1, 0.5, 0.4, 0.9
x2, y2, x3, y3 = 0.6, 0.1, 0.9, 0.5
context.scale(200, 200)
context.set_line_width(0.04)
context.move_to(x, y)
context.curve_to(x1, y1, x2, y2, x3, y3)
context.stroke()
context.set_source_rgba(1, 0.2, 0.2, 0.6)
context.set_line_width(0.02)
context.move_to(x, y)
context.line_to(x1, y1)
context.move_to(x2, y2)
context.line_to(x3, y3)
context.stroke()
return context
demo()
Using demo()
in a notebook cell will draw the SVG. Nice.
The key to making this work is Jupyter’s special methods _repr_svg_, _repr_png_, and a little _repr_html_ thrown in also.
The code is at drawing.py. I created it so that I could play around with Truchet tiles:
]]>People ask how I make them. I tried a few “make a code screenshot” sites at first, but wanted more control over how the images looked. Now I create the code shots in vim with a tweaked color scheme (shades-of-purple). This lets me do things like highlight specific words I want to draw attention to. Then I use the Mac screenshot tool to grab an image:
The font is Recursive Mono SemiCasual. Recursive is a lovely and lively variable font with a number of axes to play with, especially the Casual axis, which I’ve set to halfway between Linear and Casual. You can get it now on Google Fonts. I’d like to try using Semi-Mono, but vim only understands monospaced fonts.
Some people have asked about publishing these tidbits here on nedbatchelder.com also, but I’m not sure it’s worth the effort. Would it be helpful to have a page like this that collected them per-month or something? Or one tweet per page? Or just let them live on Twitter?
]]>>>> word = "Hello"
>>> f"{word:/^20}"
'///////Hello////////'
>>> amt = 12345678
>> f"{amt:20,}"
' 12,345,678'
Datetimes can use strftime syntax:
>>> f"{now:%Y-%m on day %d}"
'2022-04 on day 14'
The reason datetime uses different formatting specs than strings is because datetime defines its own __format__ method. Any object can define its own formatting mini-language. F-strings and .format() will use the __format__ method on an object, and pass it the formatting directives being used:
>>> class Confused:
... def __format__(self, fmt):
... return f"What is {fmt}?"
...
>>> c = Confused()
>>> f"{c:xyz12}"
'What is xyz12?'
Of course, __format__ can be used for more useful formatting than Confused is doing...
Geographic latitude and longitude are conventionally presented in a few different formats: degrees; or degrees and minutes; or degrees, minutes and seconds. Then the numbers can have varying number of decimal places, and sometimes the units are represented by symbols.
Here’s an implementation of those possibilities in __format__. The format string starts with “d”, “dm”, or “dms” to indicate the basic format. The number of decimal places can be specified with “.N”. Finally, symbols can be added, either plain or fancy, by adding a quote or minute symbol:
import dataclasses, re
@dataclasses.dataclass
class LatLong:
lat: float
long: float
def __format__(self, fmt):
dms, nfmt, opts = re.fullmatch(r"(dm?s?)([.\d]*)([′']?)", fmt).groups()
formatted = []
for num in [self.lat, self.long]:
parts = []
for ms in dms[1:]:
parts.append(str(int(num)))
num = abs((num - int(num)) * 60)
parts.append(format(num, nfmt + "f"))
syms = None
if "'" in opts:
syms = "°'\""
elif "′" in opts:
syms = "°′″"
if opts:
parts = [p + s for p, s in zip(parts, syms)]
formatted.append(" ".join(parts))
joined = ", ".join(formatted)
return joined
>>> where = LatLong(42.359764937, -71.092068768)
>>> print(f"Location: {where:d'}")
Location: 42.359765°, -71.092069°
>>> print(f"Location: {where:d.4}")
Location: 42.3598, -71.0921
>>> print(f"Location: {where:dm'}")
Location: 42° 21.585896', -71° 5.524126'
>>> print(f"Location: {where:dms.4'}")
Location: 42° 21' 35.1538", -71° 5' 31.4476"
>>> print(f"Location: {where:dms.4}")
Location: 42 21 35.1538, -71 5 31.4476
>>> print(f"Location: {where:dms.6′}")
Location: 42° 21′ 35.153773″, -71° 5′ 31.447565″
>>> print("There: {:dms.6′}".format(where))
There: 42° 21′ 35.153773″, -71° 5′ 31.447565″
>>> print(format(where, "dms.6′"))
42° 21′ 35.153773″, -71° 5′ 31.447565″
This implementation doesn’t handle errors properly, but shows the basic idea. Also, lat/long are often shown with N/S E/W instead of positive and negative values. That’s left as an exercise for the reader.
]]>