CategoriesPython

Generating a Starry Sky

Years ago, I discovered netpbm’s ppmforge, one option of which is to generate a starry sky, which fascinated me. The author’s website is full of interesting projects and papers, too. t I set out to rewrite it in Python, which I had just recently started learning at the time. Following is some code from 2012. If you want to compare it to ppmforge, the relevant lines are 63, 440-508. You’ll probably find that my younger self didn’t do a great job at translating everything to Python.

import random
import Image
import cStringIO
 
def cast(low, high):
    arand = (2.0**15.0) - 1.0
    return ((low)+(((high)-(low)) * ((random.randint(0, 12345678) & 0x7FFF) / arand)))
 
def make_star_image(width=600, height=600, star_fraction=3, star_quality=0.5, star_intensity=8, star_tint_exp=0.5, bg=None, lambd=0.0025):
    star_data = []
     
    if bg == None:
        star_image = Image.new("RGB", (width, height))
        for i in range(0, width):
            for l in range(0, height):
                if random.expovariate(1.5) < star_fraction:
                    v = int(star_intensity * ((1 / (1 - cast(0, 0.999))**star_quality)))
                    if v > 255:
                        v = 255
                    star_data.append((v, v, v))
                else:
                    star_data.append((0, 0, 0))
    else:
        index = 0
        if bg.mode != "RGB":
            bg = bg.convert("RGB")
        width, height = bg.size
        star_image = Image.new("RGB", (width, height))
        bg = bg.getdata()
        for i in range(0, width):
            for l in range(0, height):
                r, g, b = bg[index]
                average = (r + b + g) / 3
                r = random.expovariate(lambd)
                if r < average or random.random() > 0.9:
                    v = int(star_intensity * ((1 / (1 - cast(0, 0.999))**star_quality)))
                    if v > 255:
                        v = 255
                    if r < average:
                        if v > 40:
                            v = int(v * 1.5)
                            if v > 255:
                                v = 255
                            star_data.append((v, v, v))
                        elif v < 40 and random.random() > 0.5:
                            star_data.append((v, v, v))
                        else:
                            star_data.append((0, 0, 0))
                    else:
                        star_data.append((v, v, v))
                else:
                    star_data.append((0, 0, 0))
                index += 1
    star_image.putdata(star_data)
    return star_image
 
def main():
    make_star_image(width=1280, height=800, star_quality=1.2, lambd=0.0025, star_intensity=1, star_fraction=1).show()
    #bg = Image.open("/home/harrison/Pictures/wallpapers/bp1.jpg")
    #make_star_image(bg=bg, lambd=0.0035).show()
#    io = cStringIO.cStringIO()
#    starry.save(io, "JPEG")
#    data = io.getvalue()
#    
#    print "content-type: image/jpeg"
#    print
#    print data
 
if __name__ == "__main__":
    main()

The temperature calculation is off, the cast function isn’t quite right, and I didn’t even attempt the blackbody radiation calculations from the planck macro. The list could go on.

My most recent iteration of a starry sky generator is available on Github. This version is based on a deeper understanding of the math involved. For example, it turned out that the cast function was unnecessary, as its functionality is basically already built in to Python’s random module. I’m still working on understanding the planck function, so if you know much about blackbody radiation, I’d be happy to talk to you about it!

I like to compare these two versions of basically the same program, because it illustrates, in my mind, the idea of a Pythonic program. The improvements in the latest version are a result of better understanding both the problem and the solution.

The first version required the cast function because it just copied syntax over from the C program and made it work in Python. After taking the time to understand the problem that function was trying to solve, while also learning the best way to solve that problem in Python, it was able to be replaced entirely.

So, what would have been a troublesome function for a reader of the code to puzzle over, was turned into an easily-understood standard library function call.

Another example is in the control flow. The old version is deeply nested and confusing. The new one breaks more of it out into separate functions, uses clearer variable names, and makes better use of white space for grouping. I think it’s a lot easier to follow, besides looking prettier.

This comes up a lot when I’m solving code challenges on PyBites, too. When you compare solutions, it’s easy to see that some are better than others. Sometimes I’m happy with my solution, but sometimes it leaves a lot to be desired. It depends on how much experience I have with that kind of problem and how much time I put into understanding it.

The way to grow is to keep reading good examples of code and practicing. We can’t master every area, but we can keep improving!

There are several performance-related improvements in the new version, as well. For example, this version uses a bytearray to store the pixels before converting to an Image. In another post, I’ll go through the performance measurements I used to determine that this method is significantly faster than the other options.

I’m sure that there is still a lot that could be better about my latest starry sky generator, but it’s nice to be able to compare and see how much I’ve grown so far.

The Django app is currently hosted on Google AppEngine, so go ahead and check it out!

Or, see this page if you want to use text like the featured image.

CategoriesPython

Introduction to Xonsh

Recently, I got started with xonsh (pronounced “conch”) as a replacement shell for Bash.

What is xonsh, you might ask? Well, basically, it’s a version of Python meant for use as a shell. Since it’s a superset of Python, all Python programs are valid xonsh shell scripts, so you can make use of Python’s standard library and any other Python package you have available.

Probably my favorite feature, though, is being able to transfer my Python knowledge to shell scripting. As the feature comparison puts it, xonsh is a “sane language.”

That means we can do math (and a lot more!) directly in the shell, like so:

$ (5 + 5) ** 5
100000

However, we can also write commands, just like in Bash:

$ curl example.com
<!doctype html>
<html>
<head>
    <title>Example Domain</title>
...

Xonsh handles this by having two modes, which it automatically chooses between for each line. Python mode is the default, but any time a line contains only an expression statement, and the names are not all current variables, it will be interpreted as a command in subprocess mode.

When you install xonsh and run it without a .xonshrc file, you’ll be presented with a welcome screen:

            Welcome to the xonsh shell (0.9.13.dev1)                              

            ~ The only shell that is also a shell ~                              

----------------------------------------------------
xonfig tutorial   ->    Launch the tutorial in the browser
xonfig wizard     ->    Run the configuration wizard and claim your shell 
(Note: Run the Wizard or create a ~/.xonshrc file to suppress the welcome screen)

Going through the wizard will present you with a ton of options. Most will not be necessary to mess with, but there are some useful things like changing the prompt, and various plugins.

That’s just the tip of the iceberg. Xonsh has a lot more to offer, but I’m still exploring the possibilities.

For further reading, the guides cover a lot of topics in-depth.