Mon, 31 Dec 2007

I want a deep-freeze to keep large amounts of frozen food in.  But
they're expensive; the cheapest ones cost about $1000 (US$320), and a
125-liter one I wanted cost $1159.  So I've been putting off buying
one.

Our refrigerator has been malfunctioning badly enough that we've been
leaving it off much of the time.  (It sounds like the compressor motor
is about to go.)  So we've been keeping PET bottles full of frozen
water in the freezer to keep it cold while it's off, and putting some
in the refrigerator compartment to keep it cool as well.  This would
work better if the refrigerator were better insulated and didn't lose
all of its cold air every time we opened the door.

So maybe I can freeze some ice in our small freezer and put it in a
big insulated icebox with some frozen food, and just add new colder
ice occasionally to make up for heat loss.  Obviously that's feasible
if the box is arbitrarily well insulated; how feasible is it with real
materials?  When I was learning about thermal storage for indoor
climate control, I learned that thermal insulators like styrofoam and
fiberglass pretty much uniformly conduct heat at about 0.04 W/m/K.
(Straw is twice as bad; silica aerogel is twice as good.).

An Absurdly Demanding Requirement.
----------------------------------

So suppose you want a 2-liter bottle of salted ice to keep the
temperature at -5°C (if you salt ice enough it will melt at as low as
0°F = -18°C; I assume without justification that the associated change
in heat of fusion is small.  The solution needs to be about 10% salt
for the freezing point to depress to -6°C) for 24 hours at a time.
That means the total heat flux out has to be 2 L * 80 kcal/L over
those 24 hours, which is 6.7 kcal/h, pretty fucking low.  Suppose the
freezer is 250 L in a convenient cubical shape; that's a 63-cm cube,
which has .63 * .63 * 6 = 2.3 m² of surface area to lose heat through.
0.04 W/m/K * 2.3 m² = 0.09 m*W/K.  Suppose, pessimistically, that the
outside air temperature is 35°C, so we have a 40 K difference across
the wall; now we have 3.7 m*W.  A kcal/h is fairly close to a watt;
6.7 kcal/h is 7.8 watts.  So I'd need 48 cm of insulation on every
side of this 63-cm cube; 48 cm would lose 3.7 m*W / (.47 m) = 7.7 W.

So that's a pretty annoyingly large box --- a 1.59-meter cube, which
is 4000 liters, of which all but 250 would be made of styrofoam.  You
save a little bit if you round off the corners, maybe.  But remember,
this is what's necessary for a 2-liter bottle of ice to keep 250
liters cold --- more than 100 times as much.

A More Reasonable Requirement
-----------------------------

Our small freezer in the top of the fridge currently contains 16
liters of water in 10 bottles of various sizes.  It's nearly full, so
if we were putting ice from it into the icebox once a day, we couldn't
transfer much more ice than this 16 liters, per day.

The fridge's compressor is 140 watts, so it can probably pump about
280 watts of heat.  Maybe half of this gets lost just from normal use
of the fridge, so only 140 watts or so is available for making ice to
cool the icebox.

So these are two wattage limits on the lossage of the icebox.  Which
is more stringent?

16 liters of ice melted per day is 16 L * 80 kcal/L/day = 1280
kcal/day, which is 62 watts.  So that's the more stringent
requirement.

If the inside of the icebox is the same size and shape, we have our
same 3.7 m*W figure as before.  If we divide that by 62 watts, we get
6.0 cm.

So if I make a styrofoam box that's 6 cm thick, I can use salted ice
from the refrigerator's freezer to keep it at -5°C if I replace the
ice once a day.  The whole box will be 420 L in size, 75 cm on a side,
and be made of 170 L of styrofoam.

(Styrofoam is actually a bit better than this; I've seen numbers of
0.033 W/m/K.)

Materials, Strength, Safety
---------------------------

I'm thinking I could build this thing out of discarded styrofoam cut
into standard-size "bricks" or "tiles" and glued together.  It's
probably important to build the thing so that there aren't any
non-styrofoam paths all the way from the inside to the outside ---
that would be a "thermal bridge", and the heat conducted through that
path could dwarf the heat conducted through the neighboring styrofoam.
I think I can probably get OK thermal performance by the following
approach:

- make the thing out of three or more layers of tiles, and stagger the
  joints between layers so that no joint makes an uninterrupted path
  from from the inside to the outside;
- glue the layers together, but don't put glue in the joints between
  tiles in the same layer; just let air fill whatever space is left
  open by my imprecision.
- make special non-flat "corner tiles" for the corners, like angle
  iron, so that there aren't joints actually at the corners.
- cut the edges of the tiles to size using a sharp knife rather than a
  hot wire to avoid any increase in density.

This approach inevitably leaves the insulating material permeable to
water, since the cracks between tiles inevitably connect with the
cracks between the tiles in the next layer, and water (e.g. from
condensation) could thus flow from one layer to the next.  If water
were to saturate the cracks like this, the path between the inside and
outside wouldn't be particularly short, nor would the cross-sectional
area of the water path be particularly large, nor could the water
convect efficiently through these narrow channels, particularly since
it would be frozen toward the inside; but water's thermal conductivity
is about 0.58 W/m/K, more than fifteen times that of styrofoam, so it
pays to be careful.

If the whole box is sealed on the inside and out with some kind of
impermeable plastic liner, water won't be able to get in and cause
problems.

It's pretty important that the tiles all be the same thickness to
fairly high precision (<1mm?), but I'm not sure how to make that
happen.  In wood construction, apparently you cut wood to a given
thickness using a "thickness planer", which is a huge expensive piece
of machinery.  Maybe there's a less expensive approach available for
styrofoam.

The bottom will need to support most of the weight of the contents;
that means an average pressure of 63 cm of water (63 g/cm²), or about
0.90 psi, and probably commonly pressures of several times that, due
to e.g. bottles that don't have flat bottoms being dropped in.
Unevenness of pressure can be reduced to some extent by a rigid bottom
liner.  I don't think the remaining pressure will be a problem; I took
a small sheet of expanded polystyrene foam sitting around the house
(from an ice cream delivery) and balanced a 2L bottle of water on its
2.55cm-diameter cap on top of the foam, and it left no detectable
dent.  That's 2kg on 5.1cm², or 390 g/cm², 5.5psi.  (Random web pages
suggest compressive strength of 10-30 psi, which is quite strong
enough.)

Sooner or later, no doubt, one of us will trip and fall on the thing,
or a big pile of food will fall over inside, and break it.  If the
outer impermeable plastic liner mentioned earlier is strong enough to
survive this and contain the potential ensuing flood of meltwater, it
will be much less of a pain in the ass to deal with.

Inevitably as we open and close the thing, frost will form on the
inside, which eventually has to be removed.  Probably the best way to
do this is to have a fairly strong removable inner liner that doesn't
stick very strongly to ice, and doesn't admit much air between it and
the nonremovable inner liner.  Then we can remove it periodically,
like a garbage bag from a garbage can, and empty out the excess ice
chunks.

On weight: styrofoam's density is apparently normally specified in the
US in "pcf", pounds per cubic foot, and it ranges from about 0.9 pcf
to about 2.2 pcf.  170 liters at 1.3 pounds per cubic foot would be
3.5 kg.  Probably the necessary plastic liners will weigh more.

How much would this cost if I were to buy the foam new?  I ran across
some random web page with this ad:

    EPS Insulation Products and Prices
    SPECIAL SALE
    2500 Sq. Ft., 2" Thick of Non Standard Sized Insulation Panels
    $700.00 (While Supplies Last)

That works out to 11 800 liters of styrofoam for US$700, about
US$0.059 per liter, which would put the total cost of the styrofoam
for this project at US$10.  The non-sale price in small units seems to
be only a little higher; 1" x 48" x 96" costs US$5.44, and that's 76
liters, US$0.072 per liter.

The Liner
---------

I folded an HDPE shopping bag in half nine times, to 512 layers thick,
and measured it at about 6mm thick, which means each layer is about 12
microns, about 0.5 mils.  I think the liner needs to be considerably
more resistant to tearing than this shopping bag, so I tried a Ziploc
bag as well, which folded 7 times to 128 layers thick at about 5mm ---
about 39 microns per layer, or 1.5 mils.  Probably 80 microns, or pi
mils, should be adequate.  (Now I just have to find a cheap source for
plastic that thick.)

The inside of the box is 6 * .63 * .63 m² = 2.4 m², so 80 microns on
the inside of the box is 190 mL of polyethylene; the outside is
another 6 * .75 * .75 = 3.38 m², so another 270 mL, for 460 mL in all,
plus a small amount around the lip.  LDPE is about 0.92 g/cm³, so 460
mL is about 423 g.

The Lid
-------

Somehow you have to be able to put things in and take them out, but
without constantly losing huge amounts of cold through the lid.  And
the liner is supposed to prevent water or air from getting into the
styrofoam, so it has to be continuous between the inside and the
outside of the box, so it has to run across the lip of the lid.  

How much do we lose through conduction through that liner?

Let's say the lid is the entire top side of the cube.  We have
63 * 4 = 252 cm of circumference around the inside of the top of the
lid.  Low-density polyethylene, the most likely material to make the
liner out of, conducts heat at about 0.33 W/m/K, about an order of
magnitude worse than styrofoam, according to some random web page.  So
if our cross-sectional area is 250 cm * 80 microns * 2 (remember, the
lid is coated in a plastic liner too), we have a cross-sectional area
of about 4.0e-4 m², so we have 1.33e-4 m*W/K.  Earlier I assumed 40 K
temperature difference from the inside to the outside, so we have
5.3e-3 m*W.  The minimal distance across the lip is 6 cm, which would
give us 88 mW of heat loss, which is about thirty times too small to
worry about.

So, unless I fucked up my figures, the main thing to worry about is
that the lid fits well and doesn't have much airflow.  It doesn't need
to have fancy interlocking shapes to lengthen the thermal path,
although some degree of interlockingness may be helpful to ensure that
the silly thing actually shuts properly.

Energy Cost of Ice
------------------

Suppose you wanted to sell a bunch of iceboxes like this to some
people who didn't have refrigerators at home.  How much will they
spend on salted ice from the iceman?

They can give the iceman their melted bottles of salted ice each
morning, so there shouldn't be any significant material cost --- only
the energy cost.  If it costs 60 watts of chilling to keep one of
these things cold, that will probably be about 30 watts of electricity
to run an icemaker somewhere nearby.  That allows you to serve 30
households for under a kilowatt, which is 720 kWh per month ---
US$72.00 at prevailing US electricity rates, about AR$33 at prevailing
residential Argentine electricity rates, or about AR$330 at prevailing
business Argentine electricity rates.  So if you can run the icemaker
in somebody's house, and they have an electric bill that they pay, it
will cost the customers about AR$1.10 a month, plus labor and profit,
which seems like a pretty reasonable cost.  If you have to run it out
of a commercial property, it will cost the customers about AR$11.00
per month, which probably makes it completely infeasible in that form.
(Maybe if you tripled the thickness of the styrofoam.)

Materials Cost of Ice
---------------------

So if we have two 16kg "charges" of, say, 10% salted ice, we'll need
32 liters' worth of bottles (say, 11 3-L bottles), 32 liters of water,
and about 3.2 kg of salt.  (I think.  Is that 10% by molecules or by
mass?  "For every mole of foreign particles dissolved in a kilogram of
water, the freezing point goes down by roughly 1.8°C", says
http://www.worsleyschool.net/science/files/saltandfreezing/ofwater.html,
down to about -21.1°C.)  This should be pretty cheap.

Sat, 29 Dec 2007

Storing high-volume digital data so that it will last for decades or centuries
without any intervention or maintenance --- the way the Archimedes Palimpsest
partly survived being ignored for centuries in a variety of untoward places ---
is an important problem.  One problem is that the very clever ways we have of
encoding data with error-correction coding, data compression, interleaving bits
from different codewords so that localized damage only takes out a few from
each code word, and so on --- make the data formats very difficult to
reverse-engineer.

Here's a two-dimensional image data format that's designed to be easy to
reverse-engineer, even though it contains a certain amount of redundancy.  In
theory, it should allow you to store about three megabytes per side on a
letter-size sheet of paper printed in a 1200dpi laser printer, and if you use
archival-quality paper, the laser priner toner on the paper should be able to
survive many centuries of benign neglect.

It's also cool to look at.

I haven't written a decoder, although I have decoded some of the data by hand
to verify that it's working properly.

#!/usr/bin/python
# -*- encoding: utf-8 -*-
# Fractal barcodes, by Kragen Sitaker, 2007.  8-of-9 variant.

# This program converts a stream of data into a self-similar
# two-dimensional matrix barcode, with the earliest bits of data being
# encoded as the largest features.  Each level of detail is is encoded
# in a 3x3 square, which encodes 4 bits of data, and 8 of the 9
# squares are then used for the next level of detail.

# The reason this is interesting is the following:
# - It looks really cool.
# - It should be really easy to get a fix on the positioning of the
#   resulting image and compensate for any distortions.
# - It's an "archival-friendly" encoding mechanism in the sense that
#   there are visual clues to the encoding mechanism.
# - If the image is scanned with insufficient resolution to capture
#   all of its details, whatever scale gets captured will convey some
#   prefix of the data.

# So:

# with 9 pixels (8 live) you get 4 bits (4 new)
# with 81 pixels (64 live) you get 36 bits (32 new)
# with 729 pixels (512 live) you get 292 bits (256 new)
# with 6561 pixels (4096 live) you get 2340 bits (2048 new)
# with 59049 pixels (32768 live) you get 18724 bits (16384 new)
# with 531441 pixels (262144 live) you get 149796 bits (131072 new)
# with 4782969 pixels (2097152 live) you get 1198372 bits (1048576 new)
# (that's the limit at 300 dpi)
# with 43046721 pixels (16777216 live) you get 9586980 bits (8388608 new)

# (Actually you could get one more bit at the top level, but I don't.)

# The bits b0-b3 and their complements are laid out like this:
# !b2 !b1 !b0
#  b3  K  !b3
#  b0  b1  b2
# K is the bit from the next higher size of block.  b0 is the MSB,
# i.e. the 4's bit of the nibble.  White means 1; black means 0.

# The symmetry of the pattern should simplify reverse-engineering its
# interpretation.

# (There are actually 70 8-bit patterns that are half ones and half
# zeroes, not just 16, so you could encode 6 bits or a little more per
# 3x3 region, which would boost your coding efficiency by 50%.  This
# has some disadvantages for an archival format, though; it destroys
# byte-alignment of the bits being encoded, and the mapping between
# the balanced 8-bit patterns and the "cleartext" 6-bit patterns is
# necessarily a bit arbitrary and thus difficult to discover.)

# This is a little trickier than the 2x2-based one I did earlier,
# because laser printer resolutions tend to increase by factors of 2,
# not 3: 150dpi, 300dpi, 600dpi, 1200dpi.  So you get some benefit
# from not being resolution-agnostic.  But you could produce:
# * at 300dpi, a 7.29" x 7.29" 2187x2187 matrix containing 4782969
#   pixels and 1.2 megabits (25% coding efficiency) according to the
#   above.  (That's the size this program prints.)
# * at 600dpi, four 3.645" square matrices (unfortunately you may not be
#   able to fit six on a sheet) for a total of 4.8 megabits or 600kB.
# * at 1200dpi, you could have a single 6561x6561 matrix at 5.47"
#   square, encoding about 9.6 megabits (22% coding efficiency); you
#   might be able to fit two of them. Or you could have a bunch of
#   1.8" square 2187x2187 matrices --- say, 20 of them, in a 4x5
#   arrangement, encoding just under 24 megabits or three megabytes.

# Three megabytes of ASCII text is somewhere between 400 and 1000
# printed pages. If you just make 80x66 page-images out of 5x8 pixels,
# you get up to 25x22 = 550 pages out of a 1200dpi letter-size page.
# So this method has comparable overhead, just distributed
# differently.

# I think this makes for a reasonable-cost inactive long-term archival
# format for digitally-encoded data; although it's still a little more
# expensive than archival-quality CD-Rs, it should last many centuries
# longer on 3.5-cent-per-page archival paper.

# So our basic scale is 2187 pixels, divided by 300 pixels per inch.

import sys

bytes = lambda infile: iter(lambda: infile.read(1), '') # read(1) until ''
def nibbles(infile):
    for char in bytes(infile):
        yield ord(char) >> 4
        yield ord(char) & 15

def zoomed(array):
    return [[v for v in row for _ in range(3)]
            for row in array for _ in range(3)]

class Symbol:
    def __init__(self):
        self.full = [[False]]  # no-go zones
        self.colors = [[1]]    # an optimization to avoid redundant painting
    def zoom_in(self):
        self.full, self.colors = (zoomed(self.full), zoomed(self.colors))
        print "zoom_in"
    def put_square(self, xx, yy, color):
        if self.colors[xx][yy] != color:
            print "  %s %s %s sq" % (xx, yy, color)
            self.colors[xx][yy] = color
    def blocks(self):
        return ((xx, yy) for yy in range(0, len(self.full), 3)
                         for xx in range(0, len(self.full), 3)
                if not self.full[xx][yy])
    def fill(self, xx, yy): self.full[xx][yy] = True

print """%!
/rl { rlineto } bind def  % abbreviation to shorten 'sq' (draw a square)
/sq { setgray  moveto  1 0 rl 0 1 rl -1 0 rl  closepath fill } bind def
/inch { 72 mul } def  /center { sub 2 div } def
/sz 2187 300 div 72 mul def  % 2187 pixels (3^7) divided by 300 dpi
8.5 inch sz center  11 inch sz center  translate  % close enough on A4
sz dup scale  % now the square is (0, 0) to (1, 1)
/zoom_in { 1 3 div dup scale } def  gsave
"""
symbol, input_nibbles = Symbol(), nibbles(sys.stdin)
try:
    while 1:
        symbol.zoom_in()
        for (xx, yy) in symbol.blocks():
            symbol.fill(xx+1, yy+1)  # center of block
            nibble = input_nibbles.next()
            for ((dx, dy), mask) in [((0, 0), 8), ((1, 0), 4),
                                     ((2, 0), 2), ((0, 1), 1)]:
                if nibble & mask: color = 1  # white
                else: color = 0              # black
                symbol.put_square(xx+dx, yy+dy, color)
                symbol.put_square(xx+(2-dx), yy+(2-dy), 1-color)
except StopIteration: pass

print "grestore showpage"

Thu, 27 Dec 2007

Why This Is Important
---------------------

According to the EPA's figures, 75% of residential energy use in the
US in 2001 was for heating things up and cooling them down in fairly
brute-force fashions --- of 9.86 quadrillion BTU, only 2.40
quadrillion were spent on anything else. [0] And essentially all of
that is unnecessary.

Only about a fifth of total energy consumption in the US is
residential [1], so this 75% is only about 16% of the total; but I
think it is only slightly exaggerated from usage patterns in the US
economy as a whole.  I estimate that 50% of overall US energy
consumption is spent on indoor climate control.

I don't have good numbers for the rest of the world.  I assume they're
roughly similar.

As global warming increases the amount of severe weather people must
cope with, the importance of effective indoor climate control will
increase; and fighting global warming will require the reduction of
energy consumption.  Additionally, sufficiently effective indoor
climate control can render uninhabitable regions habitable and, if
scalable to large buildings, enable the cultivation of a wider range
of crops than the local climate would normally allow.

Argentine Air Conditioning
--------------------------

Here in Argentina, air conditioners are rated in "frigorías".  One
frigoría is one kilocalorie per hour, which is about four BTUs per
hour.  They recommend 50 frigorías per cubic meter.  (This is a little
strange, since you'd think you would pick your air conditioner
capacity by the amount of heat that comes into your house per hour,
rather than the amount of heat your house can hold.  But I digress.)

Our apartment is 80 m², and about 2.75 meters in height, which means
it contains about 220 m³, and therefore should need about 11000 kcal/h
of cooling capacity.  This is a bit of a problem for two reasons:
- Air conditioners are expensive more or less in proportion to their
  cooling capacity, and a 2250-frigoría unit costs $1550 ($0.69 per
  frigoría), while a 3000-frigoría unit costs $2040 ($0.68 per
  frigoría).  The largest off-the-shelf unit I found was 8000
  frigorías, and it cost $5000 ($0.63 per frigoría).  At this rate
  11000 frigorías would cost us $6930 (US$2200), which seems like a
  lot of money to me.  For example, it's more than three months' rent.
- Air conditioners also use energy more or less in proportion to their
  cooling capacity, with variations in the neighborhood of 30%.  The
  8000-frigoría air conditioner uses 3540 watts of electrical power,
  which would be 16 amps if it's properly power-factor corrected.  (I
  know that's not the amount of heat it's removing, because a kcal/h
  is only about 17% bigger than a watt.  I'm a little confused because
  I thought that it normally cost more than a watt to remove a watt of
  heat, but all the air-conditioning units' datasheets claim that they
  remove a little over two watts of heat per watt of power used.)  So
  11000 frigorías should be 4868 watts of electrical power, or a
  little over 22 amps, more than half of the total of our main circuit
  breaker.  If we assume a duty cycle of 40% during the summer, that's
  1947 watts on average during the summer.  If we were paying US$0.10
  per kilowatt-hour, which is in the neighborhood of what the price
  would be if residential power weren't heavily subsidized, 4206 kWh
  would cost us US$420.60, or AR$1324.  The current residential
  electrical rates are about $0.04 per kWh, which would bring the
  price to only $168.20, but that's still enough of a cost to want to
  minimize it.

Water Tanks to Sink the Heat During the Day: What Size?
-------------------------------------------------------

So I've been thinking some more about my 'desert cool and water
economics' kragen-tol post from more than a year ago.  Suppose we
wanted to be able to sink 11000 kcal/h of electrical power for 40% of
each day during the summer into some kind of heat storage tank (say,
full of water) and then radiate it into space at night.  How big of a
tank would we need, and how much area?  How much surface area would we
need for heat-exchanger coils to suck 11000 kcal/h of heat out of the
air during the day?

The first question is the easiest one.  In the last few days, the
highs have been around 28°C and the lows have been around 18°C.
Suppose we want to lower the temperature of the air from 32°C to 22°C,
which is 10 K of difference.  So the water must start out cooler than
22°C and end up cooler than 32°C.  Suppose we can start with water at
16°C and end with it at 30°C.  Then we can get 14 kcal of heat out of
the air per liter of water that flows from the cold tank through the
heat exchanger into the hot tank.  So 11000 kcal/h would be 790 liters
per hour.  I've hypothesized needing this cooling power for 9.6 hours
(40%) out of each day, which means that each tank would need to hold
7580 liters of water, which is 7.58 metric tons and 7.58 cubic meters.

If we kept these 7.58 metric tons of water indoors, and didn't need
any extra space for insulation, we would need 5.5 square meters of
floor space for them, or about 2.3 meters square.  That's about 7% of
our total floor space, and that 7% should be no more and no less
constant across dwellings than the figure of 50 frigorías per cubic
meter.

Water Tank Size Is Less Sensitive To Temperature
------------------------------------------------

The same amount of water can sink more heat, and sink it more quickly,
when the difference between the air temperature and the cool-water
temperature is greater.  If the cool-water temperature stays at 16°C
and I want to cool the air from 28°C, I can only sink, at most, 12
kcal per liter of water.  But if the air temperature is 40°C, I can
sink 24 kcal/L, twice as much heat for the same volume of water.

So while a conventional air conditioner needs to be sized for the
amount of heat it needs to extract on the hottest days, and therefore
cares a lot about how hot they are, the design of such a reservoir
device should be relatively insensitive to the maximum temperature.

So, even if the 16°C number is correct, I might be able to get by with
a much smaller amount of water, simply because the 11000-frigorías
number is only relevant on the very hottest days --- if even then.

The Exhaust Heat Exchanger
--------------------------

Cooling the water at night can happen in essentially three ways:
evaporation, conduction, and radiation.  I think evaporation is
limited in its applicability (it won't help colonize the Grand Erg
Occidental, and it will get expensive if water does) although being
able to use brackish water might broaden its applicability.

This leaves conduction and radiation.  Radiation has the major
advantage that, on dry nights, the heat sink is the cosmic background
radiation at a temperature of about 3 kelvins, so it might be possible
to cool the water quite cold, perhaps even to freeze it.  (This
probably involves using an intermediate heat transfer fluid with a
lower freezing point, but it's tractable.)

The basic radiation law is Stefan's Law or the Stefan-Boltzmann law:

    Q/t = e{sigma}A(T_hot^4 - T_cold^4)

    e is the emissivity, a number between 0 and 1 indicating how close
    to an ideal black-body radiator the object in question is (at the
    relevant wavelengths).  I think it's (1 - albedo) at the relevant
    wavelengths.
    Sigma is the Stefan-Boltzmann constant: 5.67e-8 W/m²/K^4
    Q is the heat transferred.
    t is time.
    T_hot and T_cold are the temperatures of the hot and cold bodies,
    measured in kelvins.

So if our e is, say, 0.75; T_hot is at least 16°C (289 K); T_cold is
insignificantly small; t is, say, 8 hours; and we want the Q to be
100Mcal (11000 kcal/h * 9.6 h); how much emitting area do we need?

100Mcal is 420 MJ, so the required heat dumping rate is 14.5kW;
T_hot^4 is 7.0e9 K^4; so we have

   14.5kW / (0.75 * (5.67e-8 W/m²/K^4) * 7.0e9 K^4) = A
   m² * 14.5e3 / (0.75 * 5.67e-8 * 7.0e9) = A
   m² * 14.5e3 / (0.75 * 5.67 * 70) = A = 49 m²

So I'd need about 7 meters square of roof space to beam the heat from
my little apartment back out into space at night, plus enough pipes
and aluminum to keep it all warm.  And on cloudy nights, it wouldn't
work.  Note that 49m² is more than half the floor area of the
apartment.

Conduction, on the other hand, requires little extra equipment (just a
fan and some flaps to direct air from the outdoors through the same
heat exchanger used to cool the indoors) and is known to be feasible.

Cost of Space
-------------

We can estimate the "cost" of losing the space at 7% of our rent: for
us, $150 per month, or $1850 per year.  So it costs us $40 per month
during the life of the thing, and as long as the electrical rates are
so deeply subsidized, it would actually cost us almost all of the $150
amount.  We're not in a particularly expensive area of Buenos Aires,
but areas outside the Capital Federal are cheaper still.  So for
people in areas that cost less per month per square meter, it might
save them money each month.  Even for us, it might cost less up front
than a $7000 refrigerative cooler, at least if it doesn't break the
floor and can be adequately insulated without building anything really
expensive.

Heat Exchanger Area
-------------------

I don't really have a clue what size of heat exchanger I would need.
Presumably it would be a few times bigger than the heat exchanger that
an air conditioner would use, because the temperature difference
between the coolant and the air is smaller.

Tank Insulation
---------------

The hot water tank must retain its heat until it can be exhausted into
the great outdoors at night, if it is inside; and the cold water tank
must retain its cool until it can be exhausted into the indoors during
the hot day, if it is outside.  In the winter, their roles would be
reversed.  (Beatrice suggested reversing their roles during the
winter.  I need to think more about this.)  Insulating materials like
fiberglass typically have thermal conductivities around 0.04 W/m/K.
If a cubical tank contains 5.28 cubic meters, it's 1.74 meters on a
side, and therefore has 18.2 m² of surface area, so that's around 0.72
m W/K, and I've been hypothesizing about a 20 K temperature
difference, so that's 14.4 m W.  We need to divide that by enough
centimeters of insulation that the resulting uncontrolled heat flux is
small compared to the average 4400 kcal/h (=5100W) heat flux that the
thing is designed to control.  If "small" means 5%, that's 510W, which
means we need 2.8 cm of insulation.

3 cm of insulation around cubical tanks seems like it might be a much
more reasonable proposition than welding up some giant dewar flasks
for the thing, which would presumably need to be round instead of some
more convenient shape.  (Right?)  That's roughly a cubic meter of
fiberglass (or whatever: cork, cotton, felt, "mineral wool",
styrofoam, are all in the same ballpark; silica aerogel could cut the
space by half, and straw would double it) to insulate all ten cubic
meters of reservoir.

Phase Change Materials Instead of Water
---------------------------------------

Water has a very high specific heat, so using most other materials for
the hot and cool reservoirs in place of water (air, say, or iron, or
concrete) would increase the weight required, rather than decrease it.
(And solid materials have the additional problem that they're kind of
inconvenient to move between the warm reservoir and the cool
reservoir.)

However, materials that change phase in the relevant temperature range
--- say, solid to liquid, or liquid to gas --- have a much higher
effective specific heat than any substance.  If we were trying to cool
the apartment from by heating water to 1°C from -1°C, for example, we
could dump half a calorie per kilogram of ice going from -1°C to 0°C,
then 80 calories per kilogram as it melted, then a calorie per
kilogram of water going from 0°C to 1°C.  This allows you to reduce
the weight of water you need by more than a factor of 40.  If
previously you would have needed five metric tons, now you only need
123 kg.

(Air conditioners in the US are often rated in "tons", which are tons
of ice melted per day.  Before refrigerative air conditioners were
invented, you would cut ice out of frozen lakes in the winter and
store it in an "icehouse", insulated by straw, all year round.  So
this idea of using phase-change materials as heat reservoirs is
nothing new.)

(Note that this works even if you want to cool the air from 25°C to
20°C.)

But this only works if the relevant reservoir can be induced to keep
its temperature at the melting or boiling point of the reservoir
fluid, or oscillate back and forth across it.  If you can dependably
get access to a reservoir of below-freezing cold, you can use this
approach with water; otherwise, you might have to use a material whose
phase change temperature is somewhere more convenient.  (If it can be
actually inside the range where you want to maintain the air
temperature, you don't need tanks and stuff.  You can just put the
stuff out where it can conduct heat to the air, like in the wall or on
the coffee table or whatever.)

There are all kinds of research going on on phase-change materials as
lower-mass heat reservoirs.  See, for example, J.R.Gates's
phase-change material home page:
> http://freespace.virgin.net/m.eckert/index.htm

They also have this nice thermostatic property, which has been used to
calibrate thermometers for centuries, that they tend to heat up things
that are colder than their phase-change temperature, and cool down
things that are warmer.

This is Nothing New; Why Was It Rejected Before?
------------------------------------------------

Obviously, indoor climate control with insulated heat reservoirs,
passive solar design, phase-change materials for reservoirs, and so
on, is nothing new.  Adobe houses, stone castles, dugout houses,
soddies, caves, iceboxes using ice stored since the winter in the
icehouse, walls shared between houses to prevent heat loss, and so on,
all go back generations if not millennia or longer.  But they were all
abandoned in the developed world over the last few decades.  Why were
they abandoned, and what makes me think they're worth recovering?

They were abandoned for several reasons:
* Fashion.  In "Pioneer Pride", I seem to recall reading my
  great-uncle abandoned his Western New Mexico dugout because his new
  wife didn't feel that it was a "proper house."  It wasn't what she
  was used to, what she'd grown up with.  This is the problem the
  Viridians are tackling; green design has to be hip in order to get
  uptake.
* High cost of labor.  The traditional techniques involve a lot of
  heavy material hauling, and most of it isn't easily automatable.
  (Adobe bricks are still mixed by hand, made by hand in wooden
  frames, then stacked by hand to dry.)
* Inflexible construction techniques.  You can build a wooden
  balloon-frame house anywhere you can haul the wood, but you can only
  build a cave house where there's a cave.
* Low cost of energy.  As we've eagerly depleted the Earth's
  fossil-fuel endowment without much concern for the environmental
  effects, we've kept the cost of that energy quite low, so it's
  seemed sensible to use cheaper construction techniques and save
  space rather than spend money to save electricity.  And electricity
  subsidies like those in Argentina, while they might be helpful for
  development, make this worse.

I think there are a number of new developments that make these
approaches relevant again:
* Fashion.  Energy conservation, and green design in general, are
  trendy in much of the world.  Hopefully they'll stay that way for a
  few decades.
* More extreme weather.  As global warming increases the extremes of
  heat and cold we must survive, even people who cling to 1950s
  climate-control technologies will benefit by complementing them with
  more efficient approaches.
* Better materials.  Even glass and steel are major improvements (in
  cost and flexibility) over many traditional materials, but we also
  have stainless steel (making large Dewar flasks possible), silica
  aerogel, PVC, fiberglass, styrofoam, fiberglass-epoxy composites
  (making large transparent water tanks possible), low-emissivity
  glass, stainless steel, aluminum, and copper pipes and fins (making
  heat exchangers efficient), rammed earth, straw bales, and on and
  on.  In this case, the large amount of thermal mass (in the form of
  water in a PVC tank insulated with styrofoam) occupies a tiny
  fraction of the size of adobe walls.
* Better designs.  Dewar flasks, window overhangs, double-paned
  windows, and so on.
* Less-labor-intensive construction techniques.  We can now excavate
  and build earth berms with small-scale earthmoving equipment.
* Inexpensive computer control.  It's now inexpensive and reliable to
  have a microcontroller turn a water pump or two on and off a few
  times a day, monitor some pressure sensors, control some fans, open
  or close some air control flaps, open or close curtains or louvers
  to regulate the amount of solar heat gain, and raise an alarm in
  case of pump failure or pipe leakage.  This means that it should be
  straightforward to control air and radiant temperature indoors as
  effectively as with a traditional climate-control system.
* A possibly rising cost of energy.  Certainly the cost will rise
  here; it may double or quadruple or more.

Related Buzzwords
-----------------

Traditional adobe construction.  Traditional dugout construction.
Earth berms.  Trombe walls.  The German Passivhaus program.  Seasonal
thermal stores.  Earth-berm construction.  Straw-bale construction.
Dewar flasks.  Earthships.  Thermal energy storage in general, in
particular, "full storage systems" that run the air conditioner
chillers only at night.  John Hait's Passive Annual Heat Storage.
Water walls.  Isolated solar gain.  Annualized geo-solar.
Superinsulation.  Heat recovery ventilation.  Ground source heat
pumps.  The SHPEGS Solar Heat Pump Electrical Generation System.

References
----------

[0] "Energy Consumption and Expenditurs RECS 2001", from the US
Department of Energy Residential Energy Consumption Survey:
> http://www.eia.doe.gov/emeu/recs/recs2001/detailcetbls.html#total
In particular, see page 3 of the total consumption PDF:
> ftp://ftp.eia.doe.gov/pub/consumption/residential/2001ce_tables/enduse_consump2001.pdf

Of the 9.86 quadrillion BTU, 2.21 quadrillion are spent on air
conditioners and refrigerators, which someone might argue aren't
really part of "climate control".

[1] US Department of Energy Energy Information Administration (DOE
EIA) Monthly Energy Review, December, 2007
> http://www.eia.doe.gov/emeu/mer/consump.html
In particular, the third page of section 2, "Energy Consumption by
Sector", p.27:
> http://www.eia.doe.gov/emeu/mer/pdf/pages/sec2.pdf

Currently this shows a 9-month total for 2007 of 16.43 quadrillion BTU
for the residential sector, out of 76.16 quadrillion overall; there's
another 13.84 quadrillion attributed to the "commercial" sector, which
has roughly similar seasonal and source energy usage patterns,
suggesting that a large part of its energy consumption is also
devoted to indoor climate control.

However, that total also includes 30.87 quadrillion BTU of energy used
by the "electric power sector" --- 40% of the total --- and presumably
that usage is roughly proportional to total usage of electricity.

So my best estimate is that (0.75 * (16.43 + 13.84) / (76.16 - 30.87))
= 50% of US energy consumption is devoted to climate control.

Sat, 22 Dec 2007

This gets 20fps at 320x240 with one wave on my 700MHz laptop.

#!/usr/bin/python

# Simple wave mechanics in PyGame, by Kragen Javier Sitaker, 2007-12-07.

# Needs Python, SDL, PyGame, and Numeric Python installed.

# Notes on speed:

# Sadly at first I was only able to render <17fps with a single wave,
# which means it's doing under 1.3 million pixels per second on my
# 700MHz PIII-Coppermine, and 11fps with two waves.  I thought it was
# absurd that it takes more than 500 clock cycles per pixel,
# especially given that it's not doing any significant amount of
# Python (it's doing about 80 Python bytecodes in the redraw function,
# which adds up to 960 pixels per Python bytecode) but I wrote a C
# version (just doing all the math inside the loop, instead of in big
# arrays) and it was only 50% faster.  After some experimentation, I
# switched the C version to avoid floating-point math in the inner
# loop, to approximate the square root with linear interpolation, and
# to replace the sine function and scaling to palette index operations
# with a table lookup, and quadrupled its speed, making it about six
# times as fast as this Python version at the time.  I tried the same
# optimizations on this program, and it got slower.

# I finally got to 20fps (with one wave) (19% of the C version's
# speed) by switching to single-precision float math.  The tricky
# parts were that single-precision isn't precise enough to express the
# current time (so you have to take it mod 2*pi) and that you have to
# manually convert each scalar to a single-precision float.

import pygame, sys, Numeric, time, math

twopi = 2 * math.pi

def grayscale_for_masks(masks, level):
    "Compute a grayscale pixel from bit masks and a floating-point level [0,1)"
    return sum([int(mask * level) & mask for mask in masks])

class World:
    "The stuff that gets drawn on the screen."
    def __init__(self, screen):
        self.screen = screen
        width, height = self.screen.get_size()
        # This is a bit hard to explain, but this makes arrays 'xs'
        # and 'ys' that contain the x and y coordinates of each pixel.
        # So every row of the 'xs' array is [0, 1, 2, 3...], and row 0
        # of the 'ys' array is [0, 0, 0, 0...], while row 1 is [1, 1,
        # 1, 1...].  This is somewhat confused by the default Python
        # display of these guys being transposed if you print them out.
        (self.xs, self.ys) = (xs, ys) = Numeric.indices((width, height))
        # Now we want an array of radii (hi Andy).  So we 
        from_center_x = xs - width / 2
        from_center_y = ys - height / 2
        self.r = (Numeric.sqrt(from_center_x ** 2 + from_center_y ** 2)
                        /
                  (width/64)).astype(Numeric.Float32)
        self.tmp = self.r.copy()  # temp space for later (to reduce per-frame allocation)

        masks = self.screen.get_masks()[0:3]
        # Lookup table for grayscale levels.
        self.palette = Numeric.array([grayscale_for_masks(masks, level/256.0)
                                        for level in range(256)])
    def add_second_wave(self, to_what): pass
    def peak(self): return 1.01  # was getting occasional overflow errors on y1
    def redraw(self):
        # This function is written in a fairly assembly-language style
        # in order to cut down on the number of intermediate result
        # spaces that must be allocated.
        tmp = self.tmp                  # to make code briefer
        N = Numeric
        f32 = lambda x: N.array(x, N.Float32)
        # tmp gets -time.time() + self.r
        N.add(f32(-time.time() % twopi), self.r, tmp)
        # tmp gets sin(tmp), i.e. sin(r - time)
        N.sin(tmp, tmp)
        self.add_second_wave(tmp)  # add a second wave in the subclass
        # tmp gets tmp + peak, i.e. peak + sin(r - time)
        N.add(tmp, f32(self.peak()), tmp)
        # tmp gets tmp * (256/ (2*peak)), i.e. (1 + sin(r-time))/2 * 256
        N.multiply(tmp, f32(256 / (self.peak()*2)), tmp)
        # round floats to Int8 so we can look things up in palette
        ints = tmp.astype(N.Int8)
        # Look up the pixel value for each grayscale level in the palette
        grayscale = N.take(self.palette, ints)
        # I tried using surfarray.pixels2d and blitting from there,
        # but that made things like 10% slower.  So here we blit_array
        # onto the screen.
        pygame.surfarray.blit_array(self.screen, grayscale)

class World2Waves(World):
    def __init__(self, screen):
        World.__init__(self, screen)
        # Center our second set of waves at the upper left-hand corner
        # of the screen instead of the middle, and give it twice as
        # long a wavelength
        self.r2 = (Numeric.sqrt(self.xs ** 2 + self.ys ** 2)
                              /
                   (screen.get_width()/32)).astype(Numeric.Float32)
        self.tmp2 = self.r.copy()
    def add_second_wave(self, to_what):
        # our second wave travels slower by a factor of e
        Numeric.add(Numeric.array(-time.time() / Numeric.e % twopi,
                                  Numeric.Float32),
                    self.r2, self.tmp2)
        Numeric.sin(self.tmp2, self.tmp2)
        Numeric.add(self.tmp2, to_what, to_what)
    def peak(self): return 2

def main(argv):
    pygame.init()
    screen = pygame.display.set_mode((320, 240), pygame.FULLSCREEN)

    world = World2Waves(screen)  # alternatively just World(screen)
    frames = 0
    start = time.time()
    while 1:
        ev = pygame.event.poll()
        if ev.type == pygame.NOEVENT:
            frames += 1
            world.redraw()
            pygame.display.flip()
        elif ev.type == pygame.MOUSEBUTTONDOWN: break
        elif ev.type == pygame.QUIT: break
    end = time.time()
    print "%.2f seconds, %.2f fps" % ((end - start), frames / (end - start))

if __name__ == '__main__': main(sys.argv)

Mon, 17 Dec 2007

I've been trying to puzzle out the libart2 API for antialiased
rendering in art_svp_render_aa.h, without having access to the source.
I think I understand it now.

Basically, the problem it's trying to solve is that rendering a bunch
of antialiased shapes that share borders is kind of a pain.  

The Alpha-Blending Approach
---------------------------

Rendering one antialiased shape is pretty straightforward: you just
alpha-blend the edges with whatever the background happens to be at
the moment.  Rendering multiple antialiased shapes that are supposed
to share borders, well, you end up with these "cracks" where the
background shows through, as follows.

Suppose you have a black background and two light gray polygons, say
80% white and 90% white, and we're looking at three adjacent pixels,
A, B, and C.  If the border between the two polygons falls neatly
between, say, B and C, then A is 80% white, B is 80% white, and C is
90% white.  No problem.

But suppose the border runs down the middle of B.  Now A is still 80%
white and C is still 90% white, and ideally you'd like B to be
somewhere in between, say 85% white.  But with the alpha-blending
approach, here's what you get.  First, you render the 80% white
polygon, which takes B (currently at 0%) and alpha-blends 80% white
into it with a 50% alpha corresponding to its 50% coverage of B,
leaving B as (B * (1 - 50%) + 80% * 50%) = 40% white.  Then, you
render the 90% white polygon, which also covers B to a 50% extent, and
so B gets ((B = 40%) * (1 - 50%) + 90% * 50%), or 65%.  65% is not
between 80% and 90%.  B's color ought to be composed half and half of
the two polygons' colors, but instead it's composed of three-eighths
of each of their colors and one-quarter the background color.

This is, for example, the approach the <canvas> tag in Firefox and
Safari take, and it results in transparency artifacts whenever you try
to do 3-D rendering on top of it.

Other Approaches
----------------

There are other simple approaches you can take.  You can decline to do
antialiasing, or you can do antialiasing by rendering non-antialiased
to a much larger pixel buffer and then downsampling.  You can randomly
sample one or several points inside each pixel rather than just using
the center point.  And art_svp_render_aa has a different approach.

art_svp_render_aa
-----------------

The most transparent function in art_svp_render_aa.h is the following:

void
art_svp_render_aa (const ArtSVP *svp,
		   int x0, int y0, int x1, int y1,
		   void (*callback) (void *callback_data,
				     int y,
				     int start,
				     ArtSVPRenderAAStep *steps, int n_steps),
		   void *callback_data);

An ArtSVP is a "sorted vector path", which is the kind of thing you
reduce everything else to in libart as the last step before you try to
render it into pixels.

An ArtSVPRenderAAStep is this:

struct _ArtSVPRenderAAStep {
  int x;
  int delta; /* stored with 16 fractional bits */
};

So I made an ArtSVP from an ArtVpath representing a diamond between
(160, 0), (240, 120), (160, 240), and (80, 120), and I called
art_svp_render_aa with a callback that just dumped out the arguments
it got, scaling the delta argument down as suggested by the comment
(and scaling down the 'start' argument in the same way).  The results
looked like this:

    y=0 start=0.5 n_steps=3
      x=159 delta=-85
      x=160 delta=-1.52588e-05
      x=161 delta=85
    y=1 start=0.5 n_steps=5
      x=158 delta=-21.25
      x=159 delta=-212.5
      x=160 delta=-1.52588e-05
      x=161 delta=212.5
      x=162 delta=21.25
    y=2 start=0.5 n_steps=4
      x=158 delta=-170
      x=159 delta=-85
      x=161 delta=85
      x=162 delta=170
...
    y=25 start=0.5 n_steps=6
      x=142 delta=-21.25
      x=143 delta=-212.5
      x=144 delta=-21.25
      x=176 delta=21.25
      x=177 delta=212.5
      x=178 delta=21.25
...


And so on until the last scan line in the region.  It happened that
the x-coordinates were roughly where the boundaries of my shape were
on those scan lines.

It turns out that libart wants your Vpaths to be counterclockwise, and
that's why the values are negative; and the start value is 0.5 to make
rounding work properly.

So this gives you a sort of map of how inside the shape each pixel is.
The start value tells you how inside the shape you are at the start of
the scan line, with 0.5 being "completely outside" and 255.5 being
"completely inside", and the ArtSVPRenderAAStep items tell you where
the degree of insideness changes along that scan line.  You'll notice
that, except on the first line where the scan line kind of gently
touches my shape, the negative numbers always total to 255, as do the
positive numbers, because each scan line goes all the way into the
inside-out shape (once) and all the way back out of it.

So this is sufficient to do antialiased rendering of a single shape in
the dumb alpha-blending approach explained at the top: the start and
delta values give you your alpha, although on kind of a funny scale.
Maybe this is how art_rgb_svp_alpha works; I don't have access to the
libart source at the moment.  But you can do it like this:

struct alpha_blending_shape_info {
  art_u8 *buffer;
  art_u8 r, g, b;  /* foreground! */
  int x0, x1;
  int rowstride;
  enum { even_odd_rule, art_rgb_svp_alpha_rule, interesting_rule } rule;
};

// callback for art_svp_render_aa for the antialiased polygon rendering
void alpha_blending_shape_callback(void *callback_data, int y, int start,
                                   ArtSVPRenderAAStep *steps, int n_steps) {
  struct alpha_blending_shape_info *info = callback_data;
  int value = start;
  int x = info->x0;
  int ii = 0;
  int new_x, alpha;
  for (;;) { // N + 1 fills for N steps
    new_x = (ii < n_steps) ? steps[ii].x : info->x1;
    switch (info->rule) {
    case even_odd_rule:
      alpha = 256 - abs((((unsigned)value >> 16) % 512) - 256);
      break;
    case art_rgb_svp_alpha_rule:
      // same winding rule as art_rgb_svp_alpha.  if I were literate I
      // would probably know what it's called.
      alpha = abs(value >> 16);
      if (alpha > 255) alpha = 255;
      break;
    case interesting_rule:
      // This shows more clearly what's going on under the covers with value.
      alpha = value >> 18;
      break;
    }
    if (alpha) {
      art_rgb_run_alpha(info->buffer + y * info->rowstride + x * 3, 
                        info->r, info->g, info->b, alpha, new_x - x);
    }
  if (ii >= n_steps) break;
    x = new_x;
    value += steps[ii].delta;
    ii++;
  }
}

The Iterator Interface
----------------------

But there are other functions in art_svp_render_aa.h as well:

    ArtSVPRenderAAIter *
    art_svp_render_aa_iter (const ArtSVP *svp,
                            int x0, int y0, int x1, int y1);

    void
    art_svp_render_aa_iter_step (ArtSVPRenderAAIter *iter, int *p_start,
                                 ArtSVPRenderAAStep **p_steps, int *p_n_steps);

    void
    art_svp_render_aa_iter_done (ArtSVPRenderAAIter *iter);

Those look like a classic iterator pattern in C.  First you have a
function to initialize the iterator; then you have a function to step
the iterator and get some output values from it (although no way to
tell when it's exhausted); and a function to discard the iterator when
you're done.

And, as you'd expect, it turns out you can reimplement
art_svp_render_aa on top of the iterator interface as follows:

void svp_render_aa(ArtSVP *svp, 
                   int x0, int y0, int x1, int y1,
                   void (*callback) (void *callback_data,
                                     int y,
                                     int start,
                                     ArtSVPRenderAAStep *steps, int n_steps),
                   void *callback_data) {
  ArtSVPRenderAAIter *iter = art_svp_render_aa_iter(svp, x0, y0, x1, y1);
  int y;
  int start;
  ArtSVPRenderAAStep *steps;
  int n_steps;
  for (y = y0; y < y1; y++) {
    art_svp_render_aa_iter_step(iter, &start, &steps, &n_steps);
    callback(callback_data, y, start, steps, n_steps);
  }
  art_svp_render_aa_iter_done(iter);
}

And it seems to work the same as the original.  (I wish I had the
source of libart handy; I imagine it's not very different.)

But this iterator interface is useful because it allows you to iterate
through the scan lines of several different shapes, possibly in
different colors, in parallel.  Which means that you can calculate,
for each boundary pixel, what percentage of it is occupied by each
shape.  So you can get results without cracks between them in the case
where the shapes share some border, unlike the alpha-blending
approach, but without using a humongous amount of memory or processor
time, as in the supersampled-rendering approach.

However, the interface still doesn't tell you whether a pixel is 50%
occupied by shape A and 50% occupied by shape B because the two are on
opposite sides of a shared border that runs through the middle of the
pixel, or because shape A has a diagonal border running from upper
left to lower right, while shape B has a diagonal border running from
lower left to upper right, both through the center of the pixel.  So
at least one of those two cases will be rendered incorrectly.  Perhaps
more seriously, if you have two shapes in different colors but with
the same border, the straightforward way of using this information
will give you a jaggy border where the color of the bottom shape leaks
through.  I think you can avoid these cases by cleverly manipulating
the geometry of the shapes so that they do not overlap before you try
to render them.

Additionally, I'd think that if you were using it this way, you would
want some kind of iterator in the x direction over the various
ArtSVPRenderAAStep[]s that describe the scanline you're currently on.
But there doesn't seem to be such an iterator facility defined in
libart.

Sat, 15 Dec 2007

This program was a sketch for the little bursts in the music sequencer,
but the background makes for an interesting effect.

#!/usr/bin/python

# Draw a cool burst on the screen.

# Has the following interesting features:
# - The area covered by any individual color remains constant as the
#   burst expands, once it's out far enough that there's an empty part
#   in the middle. I think.
# - Nicely alpha-blends with background.
# - Starts expanding rapidly and then slows down a bit.
# - Renders at about 60fps on my laptop in 320x240, or 30fps in 640x480

# Currently has the following problems:
# - the burst surface is bigger than needed
# - I don't understand how to adjust the clock to do the right thing.
# - I'd like the palette to make a nice gradual transition from white,
#   down to red, down to yellow, and then down to transparent.
# - Too symmetrical.
# - The code is too wordy.

import pygame, Numeric, time, math

burstsize = 40
fuzz = 20

def scale(mask, component):
    "Encode a floating-point color component in [0, 1] with a bitmask."
    mask = mask % (2**32)               # make unsigned
    rv = int(mask * component) & mask   # scale mask by weight
    if rv >= 2**31: rv -= 2**32         # convert back to signed
    rv = int(rv)                        # convert back to non-long
    return rv

def pixel(masks, components):
    "Encode an (r, g, b, alpha) tuple according to given masks."
    return sum(map(scale, masks, components))

class World:
    def __init__(self, screen):
        self.screen = screen
        # make a simple, pretty background.
        bxs, bys = Numeric.indices(self.screen.get_size())
        self.background = bxs * bys * bys / 128

        surfsize = (burstsize+fuzz)*2
        self.burstsurface = pygame.Surface((surfsize,surfsize)).convert_alpha()
        xs, ys = Numeric.indices(self.burstsurface.get_size())
        (dx, dy) = (xs - surfsize/2, ys - surfsize/2)  # distances from center
        self.rsq = dx*dx + dy*dy        # square of distances from center

        # Compute where to blit the surface
        cx, cy = self.screen.get_rect().center
        self.blitdest = (cx - surfsize/2, cy - surfsize/2)

        # Red and green stay at 100%; blue fades from 80% down to 0,
        # and alpha fades from 90% down to 0.  Then the last palette
        # value is entirely transparent.
        masks = self.burstsurface.get_masks()
        self.palette = Numeric.array([pixel(masks, (1, 1,
                                                    0.8 * float(fuzz**2-ii)/fuzz**2,
                                                    0.9 * float(fuzz**2-ii)/fuzz**2))
                                      for ii in range(fuzz**2)] + [0])
        self.palette = self.palette.astype(Numeric.Int32)  # just to make sure
    def redraw(self):
        pygame.surfarray.blit_array(self.screen, self.background)
        # The threshold value of r-squared with maximum brightness.  I
        # fiddled with this until it was kind of OK, but I don't
        # really like it.
        clock = burstsize**2 - ((-time.time() / 3 % 2 - 1) * burstsize)**2
        # the per-pixel distance from that threshold
        burst = Numeric.absolute(self.rsq - clock)
        # mapped to fit the palette
        burst_clamped = Numeric.clip(burst, 0, fuzz**2).astype(Numeric.Int)
        burst_image = Numeric.take(self.palette, burst_clamped)
        pygame.surfarray.blit_array(self.burstsurface, burst_image)
        self.screen.blit(self.burstsurface, self.blitdest)

def main():
    pygame.init()
    # It gets really trippy if you specify certain bit depths in place
    # of 0.  Like 22.  I assume this is some kind of SDL bug, but it's
    # really cool.
    screen = pygame.display.set_mode((320, 240), pygame.FULLSCREEN, 0)
    world = World(screen)
    frames = 0
    start = time.time()
    while 1:
        ev = pygame.event.poll()
        if ev.type == pygame.NOEVENT:
            world.redraw()
            pygame.display.flip()
            frames += 1
        elif ev.type == pygame.QUIT: break
        elif ev.type == pygame.MOUSEBUTTONDOWN: break
    dur = time.time() - start
    print "%d frames; %.2f/sec" % (frames, frames/dur)

if __name__ == '__main__': main()

Sun, 09 Dec 2007

Here's a simple sequencer that has some interesting features in PyGame.
See http://pobox.com/~kragen/sw/pygmusic/ for more details.

#!/usr/bin/python
# -*- encoding: utf-8 -*-
"""Pygmusic: A simple music sequencer in PyGame.  By Kragen Sitaker,
2007-12-07, 08, and 09.

Drag things around with the mouse; the right mouse button exits.

I, the copyright holder of this work, hereby release it into the
public domain. This applies worldwide.

In case this is not legally possible, I grant any entity the right
to use this work for any purpose, without any conditions, unless
such conditions are required by law.
"""

import pygame, time, os, sys, math

### basic utility functions

def gray(x):
    "Return a grayscale color tuple given an argument in [0,256)"
    return (x, x, x)
black = gray(0)
white = gray(255)

def padd((x1, y1), (x2, y2)):
    "Add an offset to a 2D point."
    return (x2 + x1, y2 + y1)
def diff((x1, y1), (x2, y2)):
    "Take the offset between two 2D points."
    return (x2 - x1, y2 - y1)

### basic drawable objects

class Visible:
    """Things that get drawn on the screen and maybe handle mouse clicks
    or make sounds.

    These objects can be added to the world with world.add and are
    expected to be able to do the following:
    - obj.contains((x, y)): tell whether a mouse position is in the object
    - obj.handle_click(world, event): handle a mouse button going down
    - obj.play(world): make any appropriate sounds
    - obj.center(): return (x, y) center to decide when to be played
    - obj.draw(self, world, screen): display itself; called every frame.
    - obj.is_drop_target_for(self, object, world): handle object being
      dropped on us

    Additionally, if an object is draggable, it needs to support these:
    - obj.move(delta): move by delta x and delta y
    - obj.handle_drop(world): finish being dragged

    In order to use the default implementations of contains and
    center, the object needs to have a rect property that acts like a
    pygame.Rect.

    """
    def contains(self, (x, y)): return self.rect.collidepoint(x, y)
    def center(self): return self.rect.center
    def is_drop_target_for(self, obj, world): "Default is to do nothing."
    def handle_click(self, world, ev): "Default is to do nothing."
    def play(self, world): "Default is to do nothing."

class Timer(Visible):
    "A horizontal strip on the screen that plays things in it when triggered."
    def __init__(self, rect, cycletime, color, active=True, divisions=8):
        "cycletime is the time to go through the whole cycle."
        self.rect = rect
        self.cycletime = cycletime
        self.color = color
        self.active = active
        self.start = time.time()
        self.lastoffset = 0
        self.divisions = divisions
    def drawvbar(self, screen, offset, color):
        "Draws a timing mark at a given offset."
        screen.fill(color, ((self.rect.left + offset, self.rect.top),
                            (1, self.rect.h)))
    def drawvbars(self, screen, n, color):
        "Draw timing marks for odd intervals of every nth of a cycle."
        for ii in range(1, n):
            self.drawvbar(screen, ii * self.rect.w / float(n), color)
    def draw(self, world, screen):
        screen.fill(gray(self.color), self.rect)
        self.drawvbars(screen, self.divisions*2, gray(136))
        self.drawvbars(screen, self.divisions, gray(144))
        self.drawvbars(screen, 2, gray(192))
        if self.active: self.draw_cursor(world, screen)
    def time(self):
        "The time since the beginning of the current play cycle."
        age = time.time() - self.start
        if age > self.cycletime: return self.cycletime
        return age
    def offset(self):
        "The current position of the cursor for this timer."
        return int(self.time() / self.cycletime * self.rect.w + 0.5)
    def handle_click(self, world, ev): self.trigger()
    def trigger(self):
        "Start playing the sounds within."
        self.active = True
        self.lastoffset = 0
        self.start = time.time()
    def cursor_rect(self, start, end):
        "Returns a Rect from pixel offset 'start' to 'end' for a cursor."
        return pygame.Rect((self.rect.left + start, self.rect.top),
                           (end - start, self.rect.h))
    def cursor_rects(self, offset):
        "Returns the Rects showing the currently playing period on the screen."
        assert offset >= self.lastoffset
        return [self.cursor_rect(self.lastoffset, offset)]
    def draw_cursor(self, world, screen):
        """Draws the white box that represents the currently playing period,
        and also plays the sounds found within."""
        offset = self.offset()
        for rect in self.cursor_rects(offset):
            screen.fill(white, rect)
            # If we .play() things immediately, there could be
            # surprising effects (e.g. if we're playing ourselves).
            # So we enqueue the playing for later.
            for obj in world.objects_in(rect):
                world.defer(lambda obj=obj: obj.play(world))
        self.lastoffset = offset

class RepeatingTimer(Timer):
    "A horizontal strip on the screen that plays things in it repeatedly."
    def time(self):
        return (time.time() - self.start) % self.cycletime
    def handle_click(self, world, ev):
        "Turns the timer on and off when clicked."
        if not self.active: self.lastoffset = self.offset()
        self.active = not self.active
    def cursor_rects(self, offset):
        if offset >= self.lastoffset:
            return [self.cursor_rect(self.lastoffset, offset)]
        else:
            return [self.cursor_rect(self.lastoffset, self.rect.w),
                    self.cursor_rect(0, offset)]

class ImageDisplay(Visible):
    "A visible object that displays by merely blitting an image."
    def __init__(self, pos, image):
        self.image = image
        self.rect = pygame.Rect(pos, image.get_size()) # to satisfy Visible
    def draw(self, world, surface):
        "ImageDisplay.draw just blits the object's image."
        surface.blit(self.image, self.rect.topleft)

class UglyHalo(ImageDisplay):
    """A white square that fades to nothing over 0.6 seconds to show
    that something has happened."""
    def __init__(self, rect):
        "rect is the area to draw the halo around."
        halosize = rect.h * 2
        # I wanted to do a circle, but it turns out that in SDL you can't
        # have both a per-pixel alpha and a per-surface alpha, and I
        # figured that fading the per-pixel alpha in a Python nested loop
        # would be too slow, so for now, it's a white square.  See the
        # NumericHalo class for an alternative.
        image = pygame.surface.Surface((halosize, halosize))
        image.fill(white)
        ImageDisplay.__init__(self, diff(image.get_rect().center, rect.center),
                              image)
        self.start = time.time()
    def draw(self, world, surface):
        "Blits the halo image with the current opacity; possibly suicides."
        age = time.time() - self.start
        opacity = int(255 * (0.6 - age))
        if opacity <= 0: world.delete(self)
        else:
            self.image.set_alpha(opacity)
            ImageDisplay.draw(self, world, surface)

### rendering haloes with Numeric

def scale(mask, component):
    "Encode a floating-point color component in [0, 1] with a bitmask."
    mask = mask % (2**32)               # make unsigned
    rv = int(mask * component) & mask   # scale mask by weight
    if rv >= 2**31: rv -= 2**32         # convert back to signed
    rv = int(rv)                        # convert back to non-long
    return rv

def pixel(masks, components):
    "Encode an (r, g, b, alpha) tuple according to given masks."
    return sum(map(scale, masks, components))

class NumericHaloMovie:
    "Renders frames of a halo on demand, then caches them."
    def __init__(self, args):
        framelength, size, max_age, fuzz = args
        self.framelength = framelength
        self.size = size                # size of the object halo is around
        self.max_age = max_age
        self.fuzzsq = fuzz**2
        self.frames = {}
        self.shape = (size*2, size*2)
        cx = cy = size                  # x and y at center
        xs, ys = Numeric.indices(self.shape) # x and y coords of each pixel
        (dx, dy) = (xs - cx, ys - cy)   # distances from center for each pixel
        self.rsq = dx*dx + dy*dy        # squared distance from center
    def render_frame(self, ii):
        age = ii * self.framelength    # age in seconds when to show this frame
        # create a surface that has an alpha channel, to render to and return
        mysurf = pygame.Surface(self.shape).convert_alpha()
        masks = mysurf.get_masks()      # get bitmasks for r, g, b, alpha
        global_alpha = (1.2 * self.max_age - age)/self.max_age
        fsq = self.fuzzsq
        # palette is the colors in delta-rsquared, from densest to
        # most rarefied.  Most rarefied is transparent, 0.  Densest is
        # nearly white.
        palette = Numeric.array(
            [pixel(masks, (1, 1,        # r and g are always 100%
                           0.8 * float(fsq-ii)/fsq, # b is 0-80%
                           0.9 * float(fsq-ii)/fsq * global_alpha))
             for ii in range(fsq)] + [0])
        # This is the r-squared where that maximum density is found.
        # The formula is just voodoo --- I whacked on it until the
        # effect looked OK.  It doesn't scale properly with max_age.
        max_level = self.size**2/2 * (1 - (1 - age*2)**2)
        # Take absolute difference of r-squared from the point of the
        # current maximum, clamp it between 0 and fuzz**2, convert to
        # integer so we can use it to index the palette.
        density = Numeric.clip(Numeric.absolute(self.rsq - max_level),
                               0, fsq).astype(Numeric.Int)
        colors = Numeric.take(palette, density)
        pygame.surfarray.blit_array(mysurf, colors)
        return mysurf
    def __getitem__(self, framenum):
        "Get a frame."
        try: return self.frames[framenum]
        except KeyError:
            frame = self.render_frame(framenum)
            self.frames[framenum] = frame
            return frame

halo_movies = {}
def get_halo_movie(*args):
    "Find a requested halo movie."
    # This way multiple haloes of the same size (and other attributes)
    # can share the same rendered frames.
    if halo_movies.has_key(args): return halo_movies[args]
    halo_movies[args] = NumericHaloMovie(args)
    return halo_movies[args]

class NumericHalo(Visible):
    "Draws a fading halo computed with Numerical Python."
    def __init__(self, rect):
        "rect is the area to draw the halo around."
        fuzz = 10
        self.framelength = 1/120.0      # of a second.
        self.max_age = 0.4              # of a second
        self.frames = get_halo_movie(self.framelength, rect.h,
                                     self.max_age, fuzz)
        pos = diff(self.frames[0].get_rect().center, rect.center)
        self.rect = pygame.Rect(pos, self.frames[0].get_size())
        self.start = time.time()
    def draw(self, world, surface):
        "Blits the best frame for the halo's current age; possibly suicides."
        age = time.time() - self.start
        if age > self.max_age: world.delete(self)
        else:
            surface.blit(self.frames[int(age / self.framelength + 0.5)],
                         self.rect.topleft)

try:
    # test to see if Numeric and surfarray are available
    import Numeric
    pygame.surfarray.blit_array
except:
    make_halo = UglyHalo
else:
    make_halo = NumericHalo

### Sound-making objects

class DragSource(ImageDisplay):
    "Here is a source you can drag new Visibles from."
    def __init__(self, pos, image, instance):
        "instance is a callable to call with (x, y) to make the new thing."
        ImageDisplay.__init__(self, pos, image)
        self.instance = instance
    def handle_click(self, world, ev):
        "Start a drag with a new instance of, say, a Sound."
        # The (15, 15) offset has two purposes:
        # - it makes it clear that the new thing is a new thing, and
        #   not a change of color of the old thing;
        # - it happens to put that thing by default into one of the
        #   timers, so that if you don't drag it anywhere, it will still
        #   get played.
        new = self.instance(padd(self.rect.topleft, (15, 15)))
        world.add(new)
        new.start_drag(world, ev)
        new.play(world)

class Sound(ImageDisplay):
    "A draggable sound that you can put in the tracks."
    def __init__(self, pos, image, sound):
        ImageDisplay.__init__(self, pos, image)
        self.sound = sound
    def move(self, delta): self.rect = self.rect.move(delta)
    def start_drag(self, world, ev):
        world.raise_to_top(self)
        world.grab(self, ev.pos)
    def handle_drop(self, world):
        "Handle being dropped; called by the world."
        # we cheat and look a pixel up and left
        droptarget = world.object_at(padd(self.rect.topleft, (-1, -1)))
        if droptarget: droptarget.is_drop_target_for(self, world)
    def handle_click(self, world, ev):
        self.start_drag(world, ev)
        self.play(world)
    def play(self, world):
        "Plays the object's sound and kicks off a halo; called by timer."
        self.sound.play()
        world.add_nonclickable(make_halo(self.rect))

class Trigger(Sound):
    """A draggable object that you can put in a timer to trigger some
    other thing, such as another timer."""
    # XXX currently inherits from Sound so as to be draggable.
    def __init__(self, pos, image, gun):
        ImageDisplay.__init__(self, pos, image)
        self.gun = gun
    def play(self, world):
        world.add_nonclickable(make_halo(self.rect))
        self.gun.trigger()

### miscellaneous including the world

class Trash(ImageDisplay):
    """The trashcan deletes things dropped on it."""
    def is_drop_target_for(self, object, world):
        "Delete the dropped thing and make a halo."
        world.delete(object)
        world.add_nonclickable(make_halo(self.rect))

class Profiler:
    def __init__(self):
        self.times = {}
    def __str__(self): return str(self.times)
    def start(self): self.last_time = time.time()
    def note(self, what):
        now = time.time()
        dur = now - self.last_time
        self.last_time = now
        if not self.times.has_key(what): self.times[what] = 0
        self.times[what] += dur

class World:
    """Manages the set of stuff you see on the screen and routes
    events.  I think this is basically the Smalltalk MVC Controller."""
    def __init__(self, screen):
        "screen is an SDL/PyGame surface to draw on."
        self.screen = screen
        self.objects = []               # clickable visible objects
        self.nonclickable_objects = []  # halos and such
        self.grab(None, None)           # initialize drag state
        self.redraw_profiler = Profiler()
        self.deferreds = []
    def redraw(self):
        "This gets called whenever there's idle time, i.e. each frame."
        self.screen.fill(black)
        self.redraw_profiler.start()
        for obj in self.objects + self.nonclickable_objects:
            obj.draw(self, self.screen)
            self.redraw_profiler.note(obj.__class__.__name__)
        for task in self.deferreds: task()
        self.deferreds = []
    def defer(self, task):
        "Enqueue a task to be done as soon as possible after redraw."
        self.deferreds.append(task)
    def add(self, obj):
        "Call to put a new visible, clickable object on the screen."
        self.objects.append(obj)
    def add_nonclickable(self, obj):
        """Like add, but for nonclickable objects above everything.

        I added this for halos because they were accidentally getting
        drawn underneath other screen objects, and they aren't
        supposed to impede the clickability of the objects they're
        haloing around.
        """
        self.nonclickable_objects.append(obj)
    def object_at(self, pos):
        "Return the topmost object at pos, or None."
        # iterate backwards so objects drawn "on top" get first choice
        for ii in range(len(self.objects)-1, -1, -1):
            if self.objects[ii].contains(pos): return self.objects[ii]
    def handle_click(self, ev):
        "Route a click event to the relevant object."
        obj = self.object_at(ev.pos)
        if obj: obj.handle_click(self, ev)
        else: self.ungrab()
    def handle_release(self, ev):
        "Handle a button release event."
        self.ungrab()
    def handle_motion(self, ev):
        "Handle mouse motion, by doing a mouse drag if needed."
        if self.dragobj is None: return
        self.dragobj.move(diff(self.dragpos, ev.pos))
        self.dragpos = ev.pos
    def ungrab(self):
        "Terminate any drag."
        if self.dragobj is not None: self.dragobj.handle_drop(self)
        self.grab(None, None)
    def grab(self, obj, pos):
        "Start dragging some object."
        (self.dragobj, self.dragpos) = (obj, pos)
    def delete(self, obj):
        "Remove a visible object (clickable or not) from the world."
        if obj in self.nonclickable_objects:
            self.nonclickable_objects.remove(obj)
        else:
            self.objects.remove(obj)
    def raise_to_top(self, obj):
        "Move an object to the top of the drawing stack."
        self.delete(obj)
        self.add(obj)
    def objects_in(self, arect):
        """Find all the objects whose centers are in a rectangle.

        This is used by the timer to figure out what to play.
        """
        return [obj for obj in self.objects
                if arect.contains(pygame.Rect(obj.center(), (1, 1,)))]
            
def main(argv):
    "Main program."
    mydir = os.path.split(argv[0])[0]
    pygame.init()
    screen = pygame.display.set_mode((640, 480), pygame.FULLSCREEN)

    world = World(screen)
    timerwidth = 440
    def timer(y, cycletime, color, active=True):
        world.add(RepeatingTimer(rect=pygame.Rect((100, y), (timerwidth,30)),
                        cycletime=cycletime*0.16667, color=color, active=active,
                        divisions=cycletime))

    font = pygame.font.Font(None, 24)   # use default font, 24 pixels high
    def getsound(soundname):
        path = os.path.join(mydir, soundname + '.wav')
        return pygame.mixer.Sound(path)
    def renderletter(letter, color): return font.render(letter, 1, color)
    def addsource(xpos, letter, soundname):
        image = renderletter(letter, white)
        sound = getsound(soundname)
        make = lambda pos: Sound(pos=pos, image=image, sound=sound)
        source = DragSource(pos=(xpos, 80),
                            image=renderletter(letter, (255, 128, 128)),
                            instance=make)
        world.add(source)
        return lambda pos: world.add(source.instance(pos))

    yy = addsource(100, 'Y', 'score')
    zz = addsource(120, 'Z', 'extraball')
    aa = addsource(140, 'A', 'reflect_paddle')
    bb = addsource(160, 'B', 'reflect_brick')
    addsource(180, 'C', 'menu_click')
    addsource(200, 'D', 'shrink')

    timer(100, 3, 128)
    bb((95, 100))
    timer(130, 5, 120, active=False)
    zz((95, 130))
    timer(160, 7, 128, active=False)
    yy((95, 160))
    timer(190, 8, 120)
    aa((95, 190))
    aa((95 + timerwidth/2, 190))

    mytimer = Timer(rect=pygame.Rect((100, 250), (timerwidth,30)),
                    cycletime=1, color=128, active=True,
                    divisions=6)
    world.add(mytimer)
    world.add(DragSource((75, 250), image=renderletter("E", (255, 128, 128)),
                         instance = lambda pos:
                             Trigger(pos, renderletter("E", white), mytimer)))

    trashf = os.path.join(mydir, 'trashcan_empty.png')
    world.add(Trash((100, 300), pygame.image.load(trashf).convert()))

    # Some basic instructions.
    world.add(ImageDisplay((200, 300),
        font.render("Drag things around with the mouse.", 1, white)))
    world.add(ImageDisplay((200, 320),
        font.render("The right mouse button exits.", 1, white)))

    frames = 0
    start = time.time()
    while 1:
        ev = pygame.event.poll()
        if ev.type == pygame.NOEVENT:
            world.redraw()
            frames += 1
            pygame.display.flip()
        elif ev.type == pygame.MOUSEMOTION:
            world.handle_motion(ev)
        elif ev.type == pygame.MOUSEBUTTONDOWN:
            if ev.button == 3: break
            world.handle_click(ev)
        elif ev.type == pygame.MOUSEBUTTONUP:
            world.handle_release(ev)
        elif ev.type == pygame.QUIT: break
    end = time.time()
    print "%.2f seconds, %.2f fps" % ((end - start), frames / (end - start))
    print 'redraw times', world.redraw_profiler

if __name__ == '__main__': main(sys.argv)

Sat, 01 Dec 2007

# For i386 Linux.  You'll have to run this through cpp and gas;
# easiest way is to put it in a file whose name ends in .S and say
# gcc -m32 -nostdlib -static -o printstring printstring.S
#include <asm/unistd.h>
	.section .rodata
mystr:  .asciz "ABCDE is a string"
        .data
byte:   .byte 0
	
	.text
	
        .globl _start
_start: mov $mystr, %eax
        call printchars
        mov $'\n, %eax
        call printchar
        mov $__NR_exit, %eax
        mov $0, %ebx
        int $0x80
	
printchars:                     # args:  %eax points to string
        xor %ebx, %ebx
loop:   mov (%eax), %bl
        inc %eax
	
        push %eax
        push %ebx
        movzbl %bl, %eax
        call printnumspc
        pop %ebx
        pop %eax

	and %ebx, %ebx          # nul-terminated string
	jnz loop
        ret
printnumspc:                    # args: %eax is number to print
	call printnum           # number must be nonnegative
        mov $32, %eax           # space char
        jmp printchar           # tail-call
printnum:                       # args: %eax is number to print
        mov $10, %ebx
	mov $'0, %ecx
        and %eax, %eax
        jnz printnonzero
        mov $'0, %eax
        jmp printchar           # tail-call
	
printnonzero:                   # args: %eax is number to print
                                # number must be nonnegative
        xor %edx, %edx          # %ebx is base (nonclobbered)
        idiv %ebx               # %ecx is zero digit
	                        # (nonclobbered)
        and %eax, %eax
        jz done
	
        push %edx
        call printnonzero
        pop %edx
	
done:   add %ecx, %edx
	
        push %eax
        push %ebx
        push %ecx
        mov %edx, %eax
        call printchar
        pop %ecx
        pop %ebx
        pop %eax
        ret
	
printchar:                      # args: %al is char to print
        mov %al, byte
        mov $__NR_write, %eax
        mov $1, %ebx            # stdout
        mov $byte, %ecx
        mov $1, %edx            # length
        int $0x80
        ret