Email

colorize

  1. Introduction
  2. Goals
  3. Status
  4. TODO
  5. Installation
  6. Standard programs
    1. env
    2. date
    3. diff
    4. df
    5. file
    6. groups
    7. host
    8. ifconfig
    9. ls
    10. md5sum
    11. mount
    12. ping
    13. rm
    14. route
    15. svn
  7. Implementation
    1. Header
    2. Dependencies
    3. Color sequences
    4. Colorize a string
    5. Standard handlers
      1. Base handler
      2. Color by dictionary
      3. Hashes
      4. Headers
      5. Tables
      6. Chars coloring
      7. Regular Expressions
    6. Wrapping a command
      1. Cleaning the path
      2. Sequence with all lines
      3. Running the wrapped command and colorizing its output

Introduction

Colorize is

  • a Python library making it very easy to modify the output (and error output) of command-line tools in a separate process to provide colors, highlighting relevant things, and
  • a set of Python scripts using this library to color the output of standard GNU/Linux commands.

Colorize is available under the GPLv3 license and depends on nothing other than Python.

Goals

The following are the goals for Colorize:

To make it easy to define new rules for coloring streams and to extend existing rules
Instead of using a new adhoc format for the rules for coloring streams, we decided to use a relatively simple Python API, designed to make the coloring rules reflect their logic and intention as readily as possible. The result is a language that a Python programmer should be able to grasp very quickly, that makes existing coloring rules relatively simple and that makes the entire Python language available, if needed, for unusual complex situations.
To make the colorize wrappers as unintrusive as possible
Other than coloring, the wrappers should alter the behaviour of the wrapped programs (eg. signals handling, process hierarchy, job control, etc.) as little as possible. They should produce their input unmodified (other than coloring). They should only add colorization if their output is a terminal and they should output a line that they colorize before starting to read the next.
To provide a reasonable wealth of colorize scripts for most of the standard commands in GNU/Linux
This should allow users to just fetch the existing rules and easily benefit from this program. The provided scripts should give reasonable rules for most users and make it easy to extend them in particular ways (for subjective preferences).

Status

This is still experimental. The main reason I point that out is that, while I am already using it every day on all my shells, I am still reserving the right to make fundamental changes to the API, breaking backwards compatibility for colorize scripts. If I do so, I will, of course, adjust all the scripts I provide, but scripts that you create may need adjustments. I will remove this notice once I deem the API stable enough.

TODO

  • Get svnwiki to not add a .py extension to the scripts for colorization.
  • Get svnwiki to build a tarball with all the scripts defined here.

Installation

I publish blessed releases of these tools periodically in the following directory:

Fetch the latest tarball from there. Uncompress it. It will create a 'colorize' directory, which you simply need to add to the start of your PATH environment variable, as in:

PATH=/home/alejo/bin/colorize:$PATH

Standard programs

This section contains coloring rules for standard commands in GNU/Linux. If you have ideas for new commands or improvements to the rules, please contact me or, if you think they are general enough for other users to benefit and don't mind rennouncing your copyrights for them, feel free to apply the changes directly.

These scripts are all available under the GNU GPL v3 license.

env

#!/usr/bin/python
# Copyright 2010, Alejandro Forero Cuervo
# http://azul.freaks-unidos.net/colorize

from colorize import *

Run(RegexpGroupsColors('^(\S+)(=.*)$', Bold, ''))

date

Not a very useful script, it should probably be improved.

#!/usr/bin/python
# Copyright 2010, Alejandro Forero Cuervo
# http://azul.freaks-unidos.net/colorize

from colorize import *

Run(CharsDict({':': Blue, '/': Blue}))

diff

#!/usr/bin/python
# Copyright 2010, Alejandro Forero Cuervo
# http://azul.freaks-unidos.net/colorize

from colorize import *

Run(Rules(RegexpGroupsColors('^((?:> |\\+).*)$', Green),
          RegexpGroupsColors('^(<(?: .*|)|-.*)$', Red)),
    Rules(RegexpGroupsColors('^(diff: Try `)(.*)(\' for more information.)$', Red, Red + Bold, Red),
          ConstantColor(Red)))

df

#!/usr/bin/python
# Copyright 2010, Alejandro Forero Cuervo
# http://azul.freaks-unidos.net/colorize

from colorize import *

Run(Header(
    ConstantColor(Cyan),
    Table([RowConstant(c) for c in [Bold, '', '', '', Bold]])))

file

For the file command, we group the types of files into a few different categories and colorize them separatelly. We have more categories than we have colors, which means that we have to map some different categories to the same color. This is not as bad as it sounds, as they tend to be related.

#!/usr/bin/python
# Copyright 2010, Alejandro Forero Cuervo
# http://azul.freaks-unidos.net/colorize

from colorize import *

TEXT = (
    '(?:Non-ISO extended|ISO-8859|ASCII|UTF-8 Unicode) (?:English |mail )?text(?:, with very long lines)?',
    'troff or preprocessor input text',
    'magic text file for file\(1\) cmd',
    '(?:XML 1.0|HTML) document text',
    )

IMAGE = (
    '(?:JPEG|PNG) image data(?:.*)',
    )

VIDEO = (
    'Microsoft ASF',
    )

AUDIO = (
    'MPEG ADTS.*',
    'MP3 file.*',
    )

CODE = (
    '(?:ASCII |ISO-8859 |UTF-8 Unicode )?(?:Lisp/Scheme|C\+\+|C|Java) program text',
    )

EXECUTABLE = (
    'ELF 32-bit LSB (?:shared object|executable(?:.*))',
    '(?:a )?(?:perl|python|Bourne(?:-Again)? shell) script text executable',
    )

DOCUMENT = (
    'PDF document, version [0-9.]+',
    'Microsoft Office Document',
    )

SPREADSHEET = (
    'OpenDocument Spreadsheet',
    )

BINARY_OTHER = (
    'Berkeley DB \(Hash, version 8, native byte-order\)',
    'data',
    'timezone data',
    )

CONTAINER = (
    '(?:bzip2|gzip) compressed data',
    'POSIX tar archive',
    'Zip archive data.*',
    )

SPECIAL = (
    'symbolic link to `.*\'',
    '(?:writable, )?regular file, no read permission',
    'directory',
    )

colors = {
    TEXT: Blue,
    IMAGE: Yellow,
    VIDEO: Yellow,
    AUDIO: Yellow,
    CODE: Green,
    EXECUTABLE: Green,
    DOCUMENT: Cyan,
    SPREADSHEET: Cyan,
    BINARY_OTHER: Magenta,
    CONTAINER: Magenta,
    SPECIAL: Red,
    }

rules = []
for entries, color in colors.items():
  for e in entries:
    rules.append(RegexpGroupsColors('^(%s)$' % e, color))

Run(RegexpGroups('^(.*: *)(.*)$', [ConstantColor(''), Rules(*rules)]))

groups

#!/usr/bin/python
# Copyright 2010, Alejandro Forero Cuervo
# http://azul.freaks-unidos.net/colorize

from colorize import *

Run(Table([RowDict(dict((i, Bold) for i in ['root', 'wheel']))]))

host

#!/usr/bin/python
# Copyright 2010, Alejandro Forero Cuervo
# http://azul.freaks-unidos.net/colorize

from colorize import *

Run(Rules(
    RegexpGroupsColors('^(.* has address )([0-9.]*)$', '', Bold),
    RegexpGroupsColors('^(.* mail is handled by )(.*)$', '', Bold)))

ifconfig

#!/usr/bin/python
# Copyright 2010, Alejandro Forero Cuervo
# http://azul.freaks-unidos.net/colorize

from colorize import *

Run(Rules(
    RegexpGroupsColors('^([^ ]+)( .*)$', Bold, ''),
    RegexpGroupsColors('^( *inet addr:)([0-9.]+) (.*)$', '', Bold, '')))

ls

#!/usr/bin/python
# Copyright 2010, Alejandro Forero Cuervo
# http://azul.freaks-unidos.net/colorize

from colorize import *

Run(None,
    Rules(RegexpGroupsColors('^(ls: (?:cannot (?:access|open) )?)(.*)(: (?:Permission denied|No such file or directory))$',
                             Red, Red + Bold, Red),
          ConstantColor(Red)))

md5sum

#!/usr/bin/python
# Copyright 2010, Alejandro Forero Cuervo
# http://azul.freaks-unidos.net/colorize

from colorize import *

Run(Table([RowHash(), RowConstant('')]), ConstantColor(Red))

mount

#!/usr/bin/python
# Copyright 2010, Alejandro Forero Cuervo
# http://azul.freaks-unidos.net/colorize

from colorize import *

Run(RegexpGroupsColors('^(\S+)( on )(\S+)( type )(\S+)( \\()(.*)(\\))$', Bold, '', Bold, '', Bold, '', Bold, ''),
    Rules(RegexpGroupsColors(
              '^(mount: can\'t find )(.*)( in /etc/fstab or /etc/mtab)$',
              Red, Red + Bold, Red),
          RegexpGroupsColors(
              '^(mount: according to )(.*)(, )(.*)( is already mounted on )(.*)$',
              Red, Red + Bold, Red, Red + Bold, Red, Red + Bold),
          ConstantColor(Red)))

ping

#!/usr/bin/python
# Copyright 2010, Alejandro Forero Cuervo
# http://azul.freaks-unidos.net/colorize

from colorize import *

Run(Header(
    RegexpGroupsColors('^(PING )([^ ]*)( \\()([0-9.]+)(\\) .*)$', Underline, Bold + Underline, Underline, Bold + Underline, Underline),
    Rules(RegexpGroupsColors('^([0-9]+ bytes from .+(?: ?\\([0-9.]+\\))?: icmp_seq=)([0-9]+)( ttl=)([0-9]+)( time=)([0-9.]+ [a-z]+)$',
                             '', Bold, '', Bold, '', Bold),
          RegexpGroupsColors('^(From )(.*)( icmp_seq=)([0-9]+)( )(Destination Host Unreachable|Time to live exceeded)$',
                             '', Bold, '', Bold, '', Red + Bold))))

rm

#!/usr/bin/python
# Copyright 2010, Alejandro Forero Cuervo
# http://azul.freaks-unidos.net/colorize

from colorize import *

Run(None,
    Rules(RegexpGroupsColors(
              '^(rm: cannot remove (?:directory )?`)(.*)(\': .*)$',
              Red, Red + Bold, Red),
          ConstantColor(Red)))

route

#!/usr/bin/python
from colorize import *

Run(Header(ConstantColor(Cyan + Underline),
           Header(ConstantColor(Cyan + Underline),
                  Table([RowConstant(c) for c in [Bold, Bold, '', '', '', '', '', Bold]]))))

svn

#!/usr/bin/python
# Copyright 2010, Alejandro Forero Cuervo
# http://azul.freaks-unidos.net/colorize

from colorize import *
import sys

def Stdout(args):
  if len(args) == 1:
    return None
  op = args[1]
  if op == 'diff':
    return Rules(
        RegexpGroupsColors('^((?:> |\\+).*)$', Green),
        RegexpGroupsColors('^(<(?: .*|)|-.*)$', Red))
  if op in ['commit', 'ci']:
    return Rules(
        RegexpGroupsColors('^(Committed revision )([0-9]+)(\\.)$', '', Bold, ''),
        RegexpGroupsColors('^(Sending\s*)(.*)$', '', Bold))
  if op in ['up', 'update']:
    return Rules(
        RegexpGroupsColors('^(At revision |Updated to revision )([0-9]+)(\\.)$', '', Bold, ''),
        RegexpGroupsColors('^(A\s+.*)$', Green),
        RegexpGroupsColors('^(D\s+.*)$', Red))
  if op in ['stat']:
    return Rules(
        RegexpGroupsColors('^(A\s+.*)$', Green),
        RegexpGroupsColors('^(D\s+.*)$', Red))
  if op in ['add']:
    return RegexpGroupsColors('^(A\s+.*)$', Green)
  if op in ['ps', 'propset']:
    return RegexpGroupsColors('^(Property \')(.*)(\' set on \')(.*)(\')$', '', Bold, '', Bold, '')
  if op in ['pd', 'propdel']:
    return RegexpGroupsColors('^(Property \')(.*)(\' )(deleted)( on \')(.*)(\')$', '', Bold, '', Green, '', Bold, '')
  return None

def Stderr(args):
  if len(args) == 1:
    return RegexpGroupsColors('^(Type \')(.*)(\' for usage.)$', '', Bold, '')
  return None
 
Run(Stdout(sys.argv), Stderr(sys.argv))

Implementation

Header

#!/usr/bin/python
# Copyright 2010, Alejandro Forero Cuervo
# http://azul.freaks-unidos.net/colorize
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program.  If not, see <http://www.gnu.org/licenses/>.

Dependencies

import fcntl
import math
import os
import re
import struct
import sys
import termios

Color sequences

This section defines particular color sequences. Note that you may combine font types (eg. Bold) with particular colors simply by appending them, as in Green + Bold.

Bold = '\033[1m'
Underline = '\033[2m'
Reverse = '\033[3m'

Black = '\033[0;30m'
Red = '\033[0;31m'
Green = '\033[0;32m'
Yellow = '\033[0;33m'
Blue = '\033[34m'
Magenta = '\033[35m'
Cyan = '\033[36m'
White = '\033[37m'

BgBlack = '\033[0;40m'
BgRed = '\033[0;41m'
BgGreen = '\033[0;42m'
BgYellow = '\033[0;43m'
BgBlue = '\033[44m'
BgMagenta = '\033[45m'
BgCyan = '\033[46m'
BgWhite = '\033[47m'

Reset = '\033[0m'

Colorize a string

The primitive that applies a particular color to a string. It returns a list of strings that should be appended to print the original string with a given color.

def Colorize(string, color):
  if color:
    return [color, string, Reset]
  return [string]

Standard handlers

A handler will be a function that receives a sequence with all the lines that need to be colorized (corresponding to either the standard output or the standard error of a program) and prints them, colorized, to the current process's standard output.

This section defines the standard handlers with which coloring rules are defined. The programming interface was designed with the goal of making the coloring rules reflect their intention as much as possible.

Base handler

A handler instance has a Run method that receives a generator of lines to be colorized (corresponding to the output or error output of a program), colors them and prints them.

The base class uses the ColorLine function that receives a single line, colors it and returns it.

class Handler(object):
  def Run(self, lines):
    for line in lines:
      print self.ColorLine(line)

  def ColorLine(self, line):
    return line

  def GetSize(self):
    return struct.unpack('hh', fcntl.ioctl(1, termios.TIOCGWINSZ, ' ' * 4))

We provide a subclass that uses a ColorTokens function similar to ColorLine but which may return multiple strings, that will be appended and printed together. This simplifies things for most subclasses.

class HandlerList(Handler):
  def ColorLine(self, line):
    tokens = self.ColorTokens(line)
    if tokens is None:
      return line
    return ''.join(tokens)

  def ColorTokens(self, line):
    return None
Color by dictionary

We provide a handler that receives a dictionary of colors. All lines are looked up in the dictionary, which should contain the color to use for them.

class ColorDict(HandlerList):
  def __init__(self, dict, default=None):
    self.dict = dict
    self.default = default

  def ColorTokens(self, line):
    return Colorize(line, self.dict.get(line, self.default))

Based on this we provide a very simple handler: one that colorizes all lines with the same color.

def ConstantColor(color):
  return ColorDict({}, color)
Hashes

For lines or tokens that are hashes, we want to split them into substrings and colorize each in different ways, based on their hashes. That gives each hash a color signature, that may make it easier to spot identical hashes.

class HashColors(HandlerList):
  def __init__(self, colors):
    self.colors = colors

  def ColorTokens(self, line):
    for option in [4, 3, 5, 6, 2]:
      if len(line) % option == 0:
        splits = option
        break
    else:
      option = 4
    substring_length = int(math.ceil(len(line) / splits))
    assert 0 < substring_length
    for i in xrange(splits):
      substring = line[substring_length * i:substring_length * (i + 1)]
      for i in Colorize(substring, self.colors[hash(substring) % len(self.colors)]):
        yield i

The constructor for the HashColors class receives a list of the colors to use for the signatures. We provide a wrapper with a default list of colors.

def Hash():
  return HashColors([Red, Green, Yellow, Blue, Magenta, Cyan])
Headers

Some commands (such as df) provide headers: lines before the body of the input that just describe the contents. We provide a constructor of handlers receives two handlers, header and body, which will be applied to the first line in the input and to all the other lines, respectively.

class Header(Handler):
  def __init__(self, header, body):
    self.header = header
    self.body = body

  def Run(self, lines):
    try:
      header_line = lines.next()
    except StopIteration:
      return
    self.header.Run([header_line])
    self.body.Run(lines)
Tables
class Row(object):
  def __init__(self, token_handler):
    self.token_handler = token_handler

  def Colorize(self, space, text):
    yield space
    yield self.token_handler.ColorLine(text)

Some constructors:

def RowConstant(color):
  return Row(ConstantColor(color))

def RowDict(dict, default=None):
  return Row(ColorDict(dict, default))

def RowHash():
  return Row(Hash())
class Table(HandlerList):
  def __init__(self, rows):
    self.rows = rows

  TOKEN = re.compile('^( *)([^ ]+)(.*)')

  def _ConsumeToken(self, line):
    match = self.TOKEN.match(line)
    if match:
      return match.group(1), match.group(2), match.group(3)
    return '', line, ''

  def ColorTokens(self, line):
    index = 0
    while line:
      space, token, line = self._ConsumeToken(line)
      for s in self.rows[index].Colorize(space, token):
        yield s
      index = min(index + 1, len(self.rows) - 1)
Chars coloring
class CharsFunction(HandlerList):
  def __init__(self, color_function):
    self.color_function = color_function

  def ColorTokens(self, line):
    for char in line:
      for s in Colorize(char, self.color_function(char)):
        yield s
def CharsDict(dict, default=None):
  return CharsFunction(lambda x: dict.get(x, default))
Regular Expressions
class RegexpGroups(HandlerList):
  def __init__(self, regexp_text, handlers):
    self.expr = re.compile(regexp_text)
    self.handlers = handlers

  def ColorTokens(self, line):
    match = self.expr.match(line)
    if not match:
      return None
    output = []
    index = 0
    for text in match.groups():
      output.append(self.handlers[index].ColorLine(text))
      index = min(index + 1, len(self.handlers) - 1)
    return output
def RegexpGroupsColors(expr, *colors):
  return RegexpGroups(expr, [ConstantColor(c) for c in colors])
class Rules(HandlerList):
  def __init__(self, *rules):
    self.rules = rules

  def ColorTokens(self, line):
    for rule in self.rules:
      result = rule.ColorTokens(line)
      if result:
        return result
    return line

Wrapping a command

To wrap a command, the user should create a Python script which imports colorize and calls the Run method. In practice, we expect a directory to contain all the colorize scripts and nothing more, to be inserted at the beginning of the PATH environment variable. The scripts in that directory should be named after the commands they wrap: the Run function will modify the PATH environment variable internally to remove the directory with the wrapper and execute the actual program.

Cleaning the path

The _CleanPath internal function modifies the PATH environment variable to remove the directory in which the current binary was found.

def _CleanPath(args):
  path = os.getenv('PATH').split(':')
  path_current = os.path.dirname(os.path.abspath(os.path.expanduser(args[0])))
  path_clean = [p for p in path if os.path.abspath(os.path.expanduser(p)) != path_current]
  os.environ['PATH'] = ':'.join(path_clean)
Sequence with all lines

We can't just use the implicit iteration in the file objects, as those provide buffering. We provide a function that creates a sequence without any buffering. This is important for commands such as ping which produce their output gradually as they run.

We ignore KeyboardInterrupt: the wrapped process will receive it and handle it (probably terminating) but we don't want the colorizing processes to terminate prematurely. This is important, for example, for commands like ping which produce some output when they receive the interrupt signal.

def _YieldLines(file):
  while True:
    try:
      line = file.readline()
      if not line:
        return
      yield line[:-1]
    except KeyboardInterrupt:
      pass
Running the wrapped command and colorizing its output

The Run function receives two handlers for coloring the standard output and the standard error of the wrapped command. If either handler is None, the output will not be altered.

The implementation checks if the output is going to a terminal and a handler was provided. If so, it forks a process and uses pipe and dup2 to make it receive the output of the command. The child process will apply the handler to its input and print it to the right file descriptor.

def Run(handler_out, handler_err=None):
  for file, handler in ((sys.stdout, handler_out), (sys.stderr, handler_err)):
    if handler and os.isatty(file.fileno()):
      read_end, write_end = os.pipe()
      if os.fork():
        os.close(write_end)
        if file == sys.stderr:
          os.dup2(2, 1)
        handler.Run(_YieldLines(os.fdopen(read_end)))
        sys.exit(0)
      os.close(read_end)
      os.dup2(write_end, file.fileno())

  args = sys.argv
  _CleanPath(args)
  program = [os.path.basename(args[0])] + args[1:]
  os.execvp(program[0], program)
  sys.exit(-1)  # execvp failed

Loading... Vote up! Vote down! Save to del.icio.usSubmit Story to Digg Discussion

icfp-2010

Introduction

I participated in the ICFP programming contest of 2010, in a team with Codrin Grajdeanu and Mihaly Barasz. I had participated in previous years, though not since 2006, and this was the first for Codrin and Mihaly. We finished among the top 5 teams. I enjoyed the contest very much; while the server problems were frustrating and discouraging, we think the task was very well crafted and interesting.

The task

At the core of the task description lies a very tough problem. We suspected from the beginning that we would encounter a well-known unsolved problem, which was confirmed by the organizers at the end of the contest.

In typical ICFP fashion, to get to this core teams had to work through several layers of obfuscation which, while not “scientifically” hard —more like reverse-engineering— made the contest really funny. They required experience, knowledge and intuition to crack and turned out to be quite challenging given the constrains of time.

Fortunately, we managed to solve them and they were diverse enough to keep the whole team busy for a while. Without them the contest would have been a very different experience.

We really liked the task.

Our solutions

Format conversion

We wrote two programs for format conversion.

factory
Transforms a factory of fuels described in a higher level language into the actual format in the contest. In this language one can create individual machines (with as many inputs as desired) and compose them to create new ones.
convert
Our general program for converting formats, from human-readable to trits and back, both for cars, fuels and their individual components. It takes care of normalizing cars when outputting them as trits. We tested it on all the cars by converting them to human-readable and back.

Generating factories of fuels

The following is a list of programs we wrote for converting a fuel into a factory that produces it:

prog
A few interesting machines, created in the language implemented by factory.
fuel
Given a string of trits, output a machine that produces it (followed by an infinite stream of 0). The machine is described in the language implemented by factory and uses some of the machines from prog. These machines have 9 + 3n gates, where n is the length of the string of trits produced.
fuel-v2
An improvement on fuel, this produces machines with around 2 + 2n gates on average. Sadly, we only wrote this less than two hours before the end of the contest.
fuel-v3
A further improvement on fuel, producing machines with around 1 + 1.67n gates, which we finished just as the contest did.

We realized rather late, just before the end of the contest, that we could significantly improve the algorithms to produce factories for a given fuel string (to produce shorter factories).

The fuel-v3 will produce a factory with n sub-machines, one for each character, each with one input and one output. Given an even distribution of trits, this will require 1 * 1/3 + 2 * 1/3 + 2 * 1/3 = 1.67 gates per trit. The first sub-machine is picked based on the first character in the output that needs to be produced:

0
The machine is a single gate, with it's outputs connected to the output gate and to it's right input (X 0R).
1
The machine has two gates, with output's connected to 0L 1R and X 0R. The input goes to 1L.
2
The machine has two gates, with output's connected to 0L 1L and X 0R. The input goes to 1R.

To compute the rest of the factory, we check what output needs to be fed as the input to the first sub-machine so that, after the first cycle, it will produce the expected output (minus the first character, which it produced in the first cycle). Once that is determined, we apply this procedure recursively.

Solving cars

Simplex

We came up with a very nice solution for solving those cars for which a solution with a single ingredient existed using the Simplex linear programming method. At the beginning we didn't know how many cars would be solvable this way, but it turned out to be a large amount (we estimate it to be around 68% of the cars submitted). An advantage of this approach is that it takes very little time to run (compared with the brute forcer).

Suppose that a car has 4 tanks and call the coefficients in the 1 by 1 matrix of its fuel a, b, c and d. Each chamber in the car leads to an inequality such as aaabb > aac, which can be rewritten as Log(aaabb) > Log(aac), or Log(aaaaabbc) > 0, or 5Log(a) + 2Log(b) + Log(c) > 0. From the last representation, we use linear programming to find the values for the logarithms that minimize their sum. Once known, we just use exponentiation to produce the actual fuel.

Interestingly, we noticed that for some cars the solutions would yield incredibly large numbers. For example, our solution for car 100957 has a number with 1814 digits (in base 10). Our code to encode some of these numbers as trits, in Python, was way too slow to convert these solutions into trits and the resulting sequences would yield factories too large to be feasible.

Brute Force

Since we noticed a non-trivial amount of cars that the previous approach couldn't crack, we programmed a simple brute-force approach. Unfortunately, this approach took a lot more time to run than the Simplex method (so much that we had to constrain the set of cars that we allowed it to run on), and we didn't want to spend much time obtaining a significant amount of resources to run it on. We set this to skip cars already solved by the Simplex method (even though it's likely that in many cases this approach would yield smaller solutions). It did solve some cars (we estimate it to be around 10% of the cars for which the Simplex approach failed).

The approach was, simply, to generate all possible lists of matrices of zeroes and ones given the dimensions required by a car, to find the one that solves it.

Creating cars

Our approach for creating new cars was simple yet, we think, effective:

  • Produce a random fuel.
  • Produce many pipes randomly.
  • Pick all pairs of pipes (upper, lower) from the pool that lead to valid chambers.
  • Pick the 20 chambers with smallest ratio of the dot product of (upper - lower) with itself and the dot product of lower with itself. In other words, the most demanding possible chambers.
  • Make sure that the car can't be solved by any of our approaches.

When we last looked, close to the end of the contest, only one other team managed to provide fuels for our cars.

Cars DB

We quickly came to the conclusion that we needed a cars database allowing multiple programs to communicate. Creating one turned out to be a very good idea.

We decided to standarize on a plain-text files database, which turned out to be a very good idea, making it trivial to write new code that interacted with the database (for example, to count the number of cars one simply had to ls cars | wc -l, to count the number of solved cars ls cars/solution* | wc -l sufficed, etc.).

We also decided to use Subversion to synchronize the database across multiple machines and even across the same machine to avoid concurrency problems. This turned out to be a very bad idea: towards the end of the contest, as the rate at which new cars were being created increased significantly, Subversion did not scale to the rate of changes and became the bottleneck. Like the contest organizers, we didn't expect the rate at which cars would be created towards the end. We had to cut down to two the number of processes doing periodic syncs/commits and we had to disable writing all but the most essential information to the database.

We had a couple of programs operating on the cars DB, most written as a couple of shell scripts calling our actual programs. They all operated independently, often in different machines.

fuel-submitter and fuel-resubmitter
These programs would periodically scan the database for new solutions that had not been submitted yet. The fuel-submitter was pretty simple, it received the ID of a car and it's solution and would log in, submit the solution and log out. As it became evident that this wouldn't scale, given the rate at which we were producing solutions towards the end (and also in the face of fuel-v2, which meant we would have to submit a new solution for every car we had already solved), we created the fuel-resubmitter, which actually scanned the cars DB for new cars and would submit all the solutions it found in a single session. Uploading solutions (along with keeping the Subversion repository functioning with a high rate of commits) became the bottleneck. When, a few hours before the end of the contest, we found that we had more solutions than we would be able to submit, we split the set of solutions in 8 and ran a separate fuel-resubmitter process for each. In the end, we had a significant pool of fuels that we didn't get to submit.
downloader
The downloader would periodically poll the list of cars for new ones. The first version parsed the original HTML, obviously breaking when this was changed. The second version used the new page provided by the contest organizers. Upon finding new cars, it would just add their code to the cars DB.
solvers
The solvers look for new cars in the DB and try to find fuels for them. These are actually two, one that uses the brute-force solver and one that uses the linear-programming approach.
visualization
We wrote a simple tool to produce an HTML page with a sortable table with all the cars and relevant information about them (number of teams that solved them, size, have we solved them?, etc.).
score
We wrote a simple script to download the highscore every 10 minutes. We wanted to do some charts of the scores but never got around to it.

We think having this set of tools operating automatically greatly helped us. It freed us from having to do this manually and, more importantly, it meant that we reacted very quickly to new cars appearing, even while we slept.

Every car in the cars DB was represented as a directory. For each, we created the following files:

code and code-human
The code of the car (in trits and as a Python expression).
solution, solution-brute, solution-manual
The solution to a car, in human readable form, if known. The actual file used depends on the method used to find it.
last-revision
The last version of the factory-generating procedure used to generate a factory to submit as a solution (based on the solution files). This corresponds to a revision in the repository.
date
The date in which the car was created.
failed-brute
An empty file whose existance indicates that the brute-force algorithm has been run for this car.
upload-text
A log with data obtained from the server upon uploading this solution.
fuel-count
The number of teams that have submitted fuel for this car.

Near the end of the context we stopped adding many of these files to the repository, in order to speed up things.

Statistics

Our cars DB has 3.649 cars. That's 96% of the 3.784 that the contest's about page reports.

The contest page says we solved 2.375 cars. That's 65% of the cars in our DB. We don't know how many of those solutions were submitted after the contest ended, as we only stopped our processes that were submitting solutions some time after the end.

Cars DB has solutions (fuels) for 2.599. That's 71% of all the cars in cars DB. That means that, even if all the solutions we submitted were submitted before the end of the contest, we only submitted 91% of all the solutions we knew. This is caused by two factors:

  • The fact that some of the solutions we found use numbers way too big to be usable, and
  • The difficulties in uploading solutions to the overloaded contest servers.

Cars DB has solutions from the normal solver for 2507 (68%) cars.

Cars DB has solutions from the Brute Force solver for 103 (3%) cars. We believe the brute force approach would solve much more than 3% of the cars, as:

  • we only applied it to cars for which the linear-programming solver failed to find a solution (ie. we didn't apply it to “easy” cars) and
  • we only got to run it in a small subset of all the cars (though it's hard to quantify how small, as we made it stop submitting the failed-brute files).

We submitted all 72 cars we were allowed. We submitted 68 of them in a pretty small timespan roughly 15 hours before the end of the contest.

Our tools

We used the following tools:

GNU/Linux
We used the standard tools in GNU/Linux environments, such as wget, bash or head. It would have been hard to achieve what we did without them.
Subversion
Worked quite well as a source code management system, but broke down on the fast rate of updates to Cars DB towards the end of the contest.
Python
We picked Python without much discussion, as it was the language we all three mostly knew. There are both a Haskell and a Scheme head in our team, but neither of the languages were used.
lp_solve
Used for solving cars with linear programming.

Conclusions

The following are my personal opinions, which may not match those of the other members of our team:

  • I learned a lot from participating in this contest. Many of the things I learned are not easy to put in words, but I do feel that I am a better programmer than I was before the contest started; these just fall into a general “experience” bag. They are definitely real, even if they are hard to state in words. The ICFP programming contest is definitely worth the while.
  • Having good tools makes a huge difference. Having tools to submit solutions from the command line, to instantly see the relevant part of the response from the server, helped greatly to debug our programs and to decode the representation of cars and fuels. It pays a great deal to spend time working on these tools.
  • We shouldn't have let the server problems discourage us as much as we did. We wasted time in which we could have made a lot of progress just watching football out of frustration. During some time, the server refused to accept our cars because they were larger than 100 trits. This frustrated us a great deal, as we hadn't submitted more than 4 (and, indeed, we noticed that for the few hours on which this restriction was in place, some unfortunate teams submitted small cars).
  • Standarizing a bit more would have been helpful. Some of our tools received inputs from stdin whereas others expected them as command-line arguments, which caused us a bit of confusion.
  • We could probably have done a lot more with one or two more teammates.
  • I have mixed feelings about the idea of having used Python as a REPL and reusing our code as libraries. Most of our programs received their input from stdin (or command-line arguments) and produce their output to their standard output, sometimes indicating failure with their return codes. This mostly worked —allowing us to specify the interfaces to the programs and to split the problem into chunks that different team members could work on— but I suspect that having a Python REPL with all the functions we defined could have helped in some situations.
  • We should have recognized that our algorithm for factories was suboptimal much sooner than we did. This is obvious in retrospect but it's hard to say what we could have done to notice it sooner.
  • Subversion is very slow when you have a lot of files. We should have used a different technology for the cars DB, as the computers we were using started wasting a lot of time just synchronizing the repository and committing to it (and they put an unusually high load on the server with the repository, it just couldn't keep up with so many clients). This, again, is obvious in retrospect, but it's understandable that, like the organizers, we didn't anticipate the incredible rate of reads and writes towards the end of the contest.

Loading... Vote up! Vote down! Save to del.icio.usSubmit Story to Digg Discussion (5)

simple-logging

  1. Introduction
  2. Goals
  3. Programming interface
    1. Enabling logging
    2. Logging a simple condition
    3. Logging information about an expression
  4. User interface
    1. Enabling logging of debug severity
    2. Disabling logging altogether
    3. Log directory
    4. Size of logs
    5. Log file names
    6. Number of logs to keep
  5. Bugs
  6. Implementation
    1. Chicken Header
    2. Dependencies
    3. Global variables
    4. Log files
    5. with-logging
    6. Initializing global state
    7. Closing all logs
    8. Paths
    9. Open logging files
    10. Log string with severity
    11. Log information about an evaluation
    12. Log a string
    13. Logging entry points

Introduction

This is an extension for Chicken Scheme implementing a simple interface for logging information about the execution of a program.

Goals

The design goals for this system are the following:

  • Provide a good tradeoff between making the system easy to use for a programmer —making it straight forward to add logging statements to programs— while still capturing enough information about the execution of a program to be useful for a system administrator or user to debug issues.
  • Support multiple severity levels for the conditions logging. In particular, we support the following: fatal, error, warning, info and debug. We log everything but debug by default.
  • Let the system only retain the most recent log files so long-running processes won't consume all available disk space. The logging system includes functionality for automatically erasing logs it has produced as they grow old but also allows users to implement their own log-collection policies in separate programs.

Our target user, the consumer of the logs, is a power user with full access to the source code of the program and reasonable understanding of its logic.

Programming interface

Enabling logging

All calls to the logging function (described below) should happen within the evaluation of a procedure passed to a call to with-logging. In other words, you need to wrap your entire program in a lambda form and pass it to with-logging. with-logging also receives the default name of the current program, which is used to determine where the information will be logged.

Here is one example:

(with-logging
  "svnwiki-post-commit-hook"
  (lambda ()
    (update-svn-repository)
    (generate-static-files)
    (report-success)))

A program should only call with-logging once.

Logging a simple condition

To log a simple condition, use an expression such as:

(log-info "Loading file: ~A" path)

This will result in a line such as the following added to your log file:

info: Tue May 25 18:45:20 2010: 18257: Loading file: moon.jpeg

All the escape sequences supported by the format-modular procedure, which is used internally, are supported.

The following macros are provided, one for each severity level. They all have the same interface:

  • log-fatal
  • log-error
  • log-warning
  • log-info
  • log-debug

Additionally, a logging macro is provided. It has the same interface but receives a severity (as a symbol, such as 'warning) as its first argument, before the format string.

Logging information about an expression

Typically, you will want to log information about the start and finish of the evaluation of an expression. In this case, wrap the expression in a lambda form and pass it before the format string.

For example, instead of:

(load-image path)

Use:

(log-info (lambda () (load-image path)) "Loading file: ~A" path)

This will result in two lines being logged, one at the start of the evaluation of the expression and one at the end:

info: Tue May 25 23:51:36 2010: 11782: > g1: Loading file: mars.jpeg
info: Tue May 25 23:51:39 2010: 11782: < g1: Loading file: mars.jpeg

The > and < signs indicate the beginning and the end of the evaluation. g1 is a unique ID for each call, which is useful to match these lines into pairs. If additional calls to the logging procedures happen during the evaluation of the procedure, these will be logged between the > and the < lines.

The result of the evaluation of the calls to these logging macros is, of course, the value returned by the wrapped procedure.

User interface

Enabling logging of debug severity

By default, conditions of type debug will not be logged, as they tend to be significantly verbose. To enable them, set the LOG_DEBUG environment variable.

Disabling logging altogether

If you don't want any logs to be generated, just set the LOG_DIR environment variable to the empty string. The performance impact of the logging calls will be very small.

Log directory

Logs will be stored in the directory pointed to by any of the following environment variables (in order of preference):

  • LOG_DIR
  • TMPDIR
  • TEMP
  • TMP

If none is set, they will be stored in /tmp.

A sub-directory of this directory will be created, named log-PROGRAM-DATE-PID, where

  • PROGRAM is the string set in the call to with-logging (in the source code of the program) or the value of the LOG_PROGRAM environment variable if set,
  • DATE is the date in which with-logging was called (which usually corresponds to the start of the execution of the program), and
  • PID is the process ID.

All the logs will be stored in this directory.

Size of logs

The system tries to keep all log files under a certain size. If a log file is not empty and adding a certain entry to it would cause this size to be exceeded, the log will be closed and a new one will be opened. This lets us keep the total size of all the logs (for a given run) under a certain limit by erasing older logs.

The maximum size defaults to 1MB but maybe overriden by setting the LOG_SIZE environment variable (to the desired number of bytes).

Note that larger sizes mean larger files and less flexibility for when to erase which logs, but smaller sizes increase the number of times the logging facility has to close a log and start a new one.

Log file names

Logs will be stored in files called SEVERITY-VERSION-DATE, where

  • SEVERITY is one of the severities of the system.
  • VERSION is an increasing number. Every time a log reaches its desired size, the logging facility will increase the version number and create a new log.
  • DATE is the date in which the log was created, which corresponds (roughly) with the timestamp on its first entry.

Number of logs to keep

By default, the system will only keep the 2 most recent logs for each of the severities. As soon as the third log is started, the system will erase the oldest log.

You can set the number of logs to keep by setting the LOG_COLLECT environment variable.

Bugs

  • Dates in the paths and in the log entries themselves should probably be in YYYY-MM-DD-HH-MM-SS form. They are currently represented as number of seconds since beginning of epoch in paths and in some other format in the log files.
  • We don't currently log the source file and line number of the logging expression. This should be fixed by turning the logging functions into macros.

Implementation

Chicken Header

Scheme Library: simple-logging

Author
Alejandro Forero Cuervo <azul@freaks-unidos.net>
Category
io
License
GPL-3
Exports
(logging logging-proc) with-logging get-logging-dir (log-fatal logging-proc) (log-error logging-proc) (log-warning logging-proc) (log-info logging-proc) (log-debug logging-proc)

Dependencies

(import scheme chicken)
(use posix extras srfi-1 data-structures format-compiler)

Global variables

We define a few global variables to keep our state. They are initialized by initialize-global-state.

*logging-directory* will point to the directory in which all logs will be generated. This is initialized by with-logging. We use this to indicate if logging is enabled: if no logging should take place, with-logging should leave this set to #f. In this case, the semantics of the other variables are undefined.

(define *logging-directory* #f)

*severities* will contain a list of all the severities supported, in descending priority. Each element in the list is a pair with the priority and a boolean indicating if messages for it should be logged.

(define *severities* #f)

*logs-to-keep* counts the number of logs to keep (for any given severity). Older logs will be erased as new ones are created. This comes from the LOG_COLLECT environment variable.

(define *logs-to-keep* #f)

*log-size* contains the desired maximum size for the logs. Logs will not be allowed to grow beyond this size (assuming that no single logged entry is larger than this size). The value comes from the environment variable LOG_SIZE.

(define *log-size* #f)

Now we need an assoc list with information about the current logs for each severity. We use an assoc list since the number of severities is fairly small. The entries in the list are lists with a severity and a log-file record (defined below).

(define *log-files* #f)

Log files

We keep information about the current log for each severity (in the *log-files* global list) in the form of a log-file record. This record has the following fields:

file
The file handle for the log (as returned by open-output-file).
version
A count for the number of logs that have been generated for this severity. The current log will have this number as the VERSION part of it's path. The first log generated starts with version set to 0 and, as it fills up (see LOG_SIZE) and a new log has to be created, the version is increased.
length
A count of the number of bytes that have been written to the current log file.
path
The full path to the log file.
(define-record log-file file version length path)

with-logging

The with-logging function, one of the entry points, is pretty simple: it initializes the global variables, evaluates a procedure of no arguments, closes any logs opened during it's evaluation and returns whatever the procedure returned.

(define (with-logging program-name thunk)
  (initialize-global-state program-name)
  (let ((result (thunk)))
    (close-all-open-logs)
    result))

Initializing global state

initialize-global-state sets the values of the global variables based on the program name passed to the with-logging procedure.

If the LOG_DIR environment variable is set to the empty directory (signifying that no logging should take place), we make sure to set *logging-directory* to #f, to avoid any logging from happening. This only matters if with-logging is called multiple times, which programs should avoid.

We reset *log-files* so that multiple calls to with-logging will work. However, programs should only make one call to with-logging and perform all their logic in the thunk they pass to it.

(define (initialize-global-state program-name)
  (cond
    ((equal? (getenv "LOG_DIR") "")
     (set! *logging-directory* #f))
    (else
     (set! *severities*
       `((fatal #t)
         (error #t)
         (warning #t)
         (info #t)
         (debug ,(getenv "LOG_DEBUG"))))
     (set! *logging-directory*
       (format #f "~A/log-~A-~A-~A"
         (get-logging-dir)
         (or (getenv "LOG_PROGRAM") program-name)
         (current-seconds)
         (current-process-id)))
     (set! *logs-to-keep* (string->number (or (getenv "LOG_COLLECT") "2")))
     (set! *log-size* (string->number (or (getenv "LOG_SIZE") "1048576")))
     (set! *log-files* '()))))

Closing all logs

Now we provide a simple function to close all open logs:

(define (close-all-open-logs)
  (when *logging-directory*
    (for-each (compose close-output-port log-file-file cadr) *log-files*)))

Paths

We provide get-logging-dir, a procedure with the logic for extracting the path in which the logs' directory should be created:

(define (get-logging-dir)
  (or (getenv "LOG_DIR")
      (getenv "TMPDIR")
      (getenv "TEMP")
      (getenv "TMP")
      "/tmp"))

Open logging files

The open-log-file receives a severity and it's corresponding log-file record as the file parameter (or #f if no log has not been created for the severity), opens a new log file for the severity and returns its corresponding log-file record.

  • Verifies that the logging directory exists, creating it otherwise.
  • If a log was already opened for the severity, it closes it.
  • If a log old enough to be erased exists (which is determined by checking if the current version is greater than *logs-to-keep*), erases it.
  • Returns a new log-file record with the updated information.

This function should only be called if *logging-directory* is (and, therefore, all the other global variables are) set.

(define (open-log-file severity file)
  (assert *logging-directory*)
  (condition-case (create-directory *logging-directory*) (e (exn) #f))
  (when (and file (log-file-file file))
    (close-output-port (log-file-file file))
    (when (and (positive? *logs-to-keep*)
               (>= (log-file-version file) *logs-to-keep*))
      (condition-case (delete-file (log-file-path file)) (e (exn) #f))))
  (let* ((version (if file (+ (log-file-version file) 1) 0))
         (path (format #f "~A/~A-~A-~A"
                       *logging-directory*
                       severity
                       version
                       (current-seconds))))
    (make-log-file (open-output-file path) 0 version path)))

Log string with severity

Now the basic plumbing function, which logs a string to the log of a given severity. It does the following:

  • Stores the (severity file) pair (where file is a log-file record) for the current severity as the file variable.
  • If the file didn't exist (which happens for the first entry to be logged for a severity), creates it (calling open-log-file). Updates *log-files* to keep track of it.
  • If the file existed (and, therefore, is not empty), checks if the new line would cause it to exceed the desired log size. If so, opens a new log file (also calling open-log-file).
  • Writes the string to the file and updates the byte count.

We use an auxiliary get-new-length function which returns the byte count that the file will have once the string str has been appended to it. We have to make this a function (as opposed to just keeping the returned value) as the file may change during the course of the execution (when a log is rotated).

This function should only be called if *logging-directory* is (and, therefore, all the other global variables are) set.

(define (logging-with-severity severity str)
  (assert *logging-directory*)
  (let ((file (assoc severity *log-files*)))
    (define (get-new-length)
      (+ (log-file-length (cadr file))
         (string-length str)
         (string-length "\n")))
    (cond
      ((not file)
       (set! file `(,severity ,(open-log-file severity #f)))
       (set! *log-files* (cons file *log-files*)))
      ((> (get-new-length) *log-size*)
       (set-car! (cdr file) (open-log-file severity (cadr file)))))
    (assert file)
    (assert (log-file? (cadr file)))
    (assert (port? (log-file-file (cadr file))))
    (write-line str (log-file-file (cadr file)))
    (log-file-length-set! (cadr file) (get-new-length))))

Log information about an evaluation

When a procedure is passed to the logging-proc procedure (defined below), we want to log information about the time its execution starts and the time it finishes. We expect this to be quite a common way of generating logging information, as it captures a bit more information about the stack than single logging entries do.

This does the following:

  • Generate a unique symbol, by means of gensym, to identify this evaluation.
  • Log the start of the evaluation, a string starting with >, by means of log-str.
  • Call the procedure and capture its return value, to return it.
  • Log the end of the evaluation, a string starting with <, also by means of log-str.
(define (log-call position severity proc fmt . args)
  (if *logging-directory*
    (let* ((id (gensym)) (str (format #f " ~A ~?" id fmt args)))
      (log-str position severity ">~A" str)
      (let ((result (proc)))
        (log-str position severity "<~A" str)
        result))
    (proc)))

Log a string

When no procedure is passed to the logging function, we need to call logging-with-severity once for each severity lesser than or equal to the requested severity. We take care of assembling the string to be logged and then just do the required calls to logging-with-severity.

If the requested severity is invalid (not found in *severities*), we default to a recursive call with severity set to warning (and with some additional information about how the severity was improperly set).

(define (log-str position severity fmt . args)
  (when *logging-directory*
    (let ((str (format #f
                       "~A: ~A: ~A: ~A: ~?"
                       severity
                       (or position "[unknown]")
                       (seconds->string (current-seconds))
                       (current-process-id)
                       fmt
                       args))
          (severities (find-tail (lambda (s) (eq? (car s) severity))
                                 *severities*)))
      (cond
        (severities
         (for-each
           (lambda (s)
             (when (cadr s)
               (logging-with-severity (car s) str)))
           severities))
        ((assoc 'warning *severities*)
         (log-str position 'warning "Invalid logging severity: ~A: ~?" severity fmt args))))))

Logging entry points

We now provide the logging macro and the logging-proc procedure. We use a macro simply to capture the position in the source code on which the original expression occurred.

(define (logging-proc position severity fmt . args)
  (let ((result (apply (if (procedure? fmt) log-call log-str)
                       position severity fmt args)))
    (when (eq? severity 'fatal)
      (exit 1))
    result))

(define-syntax (logging x r c)
  (cons* (r 'logging-proc) (get-line-number x) (cdr x)))

We provide the convenience wrappers:

(define-syntax (log-fatal x r c) (cons* (r 'logging-proc) (get-line-number x) ''fatal (cdr x)))
(define-syntax (log-error x r c) (cons* (r 'logging-proc) (get-line-number x) ''error (cdr x)))
(define-syntax (log-warning x r c) (cons* (r 'logging-proc) (get-line-number x) ''warning (cdr x)))
(define-syntax (log-info x r c) (cons* (r 'logging-proc) (get-line-number x) ''info (cdr x)))
(define-syntax (log-debug x r c) (cons* (r 'logging-proc) (get-line-number x) ''debug (cdr x)))

) ; end of module definition

Loading... Vote up! Vote down! Save to del.icio.usSubmit Story to Digg Discussion

amanda-pacifica

Loading... Vote up! Vote down! Save to del.icio.usSubmit Story to Digg Discussion

the-house-keeper-and-the-professor

I read Ogawa's The Housekeeper and the Professor, which my brother recommended, in the Paris to San Francisco flight. I thought it was alright, with some interesting and beautiful scenes. I find the topic of experiencing life and building relationships being unable to generate new memories interesting —and I've written a bit in the past about related topics— and I think the novel could have been a bit more thought-provoking but I still think it was alright, being thought-provoking, in a western sense, was, probably, not one of Ogawa's goals. As I enjoyed it, I recommend it.

Loading... Vote up! Vote down! Save to del.icio.usSubmit Story to Digg Discussion

God's Debris

I read Scott Adams's God's Debris, which Christoph lend me, while I was in Mountain View. I didn't like it: I found it too pretentious and neither interesting nor entertaining.

Half of the ideas are too obvious and simple —just basic reflections about free will and determinism— and the other half are not supported in any way... and not that plausible, in my opinion —the internet is god. The parts with dating advice were probably the most ridiculous of all. Adams should probably stick to drawing comics.

The author warns that the book is not meant for old people, that old people, set in their ways of thinking, won't like it. I don't think the reason I didn't like it is that I'm old. That said, people who haven't thought much about the implications of determinism and chaos may find it interesting. I think I didn't like the book because, well, it's not that insightful, really. Maybe I didn't like it because I thought it was meant to be taken seriously?

I think the only positive things I can say are that the book is relatively unique and short (which, in this case, is probably a very good thing).

Loading... Vote up! Vote down! Save to del.icio.usSubmit Story to Digg Discussion

weekend-in-florence

I just visited Florence, a city I really like, for a third time. Here are some recommendations for my friends in Zurich, should they plan a similar trip:

Loading... Vote up! Vote down! Save to del.icio.usSubmit Story to Digg Discussion (2)

cain

Lucifer sabía bien lo que hacía cuando se rebeló contra dios.

Leí el Caín de Saramago en el avión de Zürich a Niza y me gustó mucho.

Me parece que el libro, —bastante entretenido, interesante, breve y de fácil lectura, que si no fuera así un proyecto de estos podría resultar aburridísimo—, hace un muy buen trabajo en señalar varias de las contradicciones morales de la Biblia. Andaba yo en estos días tratando de ser un poco más tolerante con las payasadas de personas que le rinden culto al dios corrupto y de dobles morales que la Biblia, —a la que Saramago de entrada se refiere como el “Libro de los disparates”—, describe, pero haber leido este libro no me ha ayudado: me ha hecho muy difícil ignorar el absurdo de esta clase de creencias y la gran capacidad de ver sólo lo que se quiere ver y no ver lo que no conviene que se requiere para hacer caso omiso de ellas y adorar al dios que pintan.

Algunas de las historias en las que repara, como la de Abraham e Isaac (que, increiblemente, alguien en algún momento trató de justificarme con el hecho de que un ángel detuvo la mano paterna), siempre me han repugnado mucho, pero había otras que no conocía muy bien, —como la ridícula historia del libro Job, en la que Dios autoriza a Satanás para matar todos los hijos de Job, y causarle muchas otras desgracias, simplemente para mostrarle, a Satanás, la fidelidad de Job—, o en los que no había reparado en ciertos detalles, —como lo que sucede al regreso del monte Sinaí de Moisés con las tablas de los mandamientos, cuando se encuentra con la adoración del “dios falso”, del becerro de oro:

32.25 Y viendo Moisés que el pueblo estaba desenfrenado, porque Aarón lo había permitido, para vergüenza entre sus enemigos,

32:26 se puso Moisés a la puerta del campamento, y dijo: ¿Quién está por Jehová? Júntese conmigo. Y se juntaron con él todos los hijos de Leví.

32:27 Y él les dijo: Así ha dicho Jehová, el Dios de Israel: Poned cada uno su espada sobre su muslo; pasad y volved de puerta a puerta por el campamento, y matad cada uno a su hermano, y a su amigo, y a su pariente.

32:28 Y los hijos de Leví lo hicieron conforme al dicho de Moisés; y cayeron del pueblo en aquel día como tres mil hombres.

— Biblia, Éxodo

Tres mil hombres muertos sólo por adorar otro dios, hay que ser muy hijueputas. Cuesta creer que haya en la tierra tantas personas capaces de adorar, aún hoy, un dios de semejante calaña y de describirme la Biblia como un libro “lleno de sabiduría”.

En fin, como dije al comienzo, Caín me gustó bastante: es agradable seguir la historia que Saramago le compone a Caín e interesante recorrer, a cierta distancia, los disparates que la Biblia propone.

Loading... Vote up! Vote down! Save to del.icio.usSubmit Story to Digg Discussion (19)

no-estar-solo

Al amor lo encontré hace unos años poco después de que dejara de llover en una tarde de primavera paseando entre los árboles a la orilla de un río cuando menos lo esperaba.

Cuando era chiquito mi familia iba a la playa cada año, en enero. A mi me sentaban en la arena a jugar cerca del mar. Las olas iban y venían, iban y venían, estrepitosas e incansables, ocasionalmente empapándome las piernas e inundando los pozos de mis castillos de arena.

Cuando era chiquito solía dormir la siesta con mi cabecita recostada en el pecho de mi papá. Había muchos sonidos, —el palpitar de su corazón, el tic tac del reloj en su muñeca y los infrecuentes gemidos de su estómago—, pero el que más recuerdo era el ruido incansable de su respiración.

Mi papá era un gigante enorme. Su pecho se elevaba y se hundía lentamente, se elevaba y se hundía, y mi cabecita se movía de arriba abajo con cada exhalación. Escuchando los silbidos del aire al entrar en su cuerpo y los silbidos del aire al salir yo cerraba los ojos y evocaba el mar. Trataba con frecuencia de respirar al unísono con él, pero me era difícil, mis pulmones entonces pequeño remedo de los suyos, y pronto me cansaba y desistía.

Mi papá, mucho tiempo después, me dijo que uno de los momentos más felices de su vida fue cuando durmió por primera vez conmigo, poco después de mi nacimiento, en su pecho.

Ahora que soy adulto, cuando me despierto en las noches, escucho al amor respirar a mi lado. En la oscuridad su respiración es el viento que se enredaba en las ramas húmedas cuando la conocí. No estar solo, he notado, es tener a alguien que al respirar te acaricie. Ahora, cuando me despierto, intento respirar al unísono con ella, pero mis pulmones son demasiado grandes y me cuesta respirar tan rápido.

Loading... Vote up! Vote down! Save to del.icio.usSubmit Story to Digg Discussion (6)

visual-explanations

I read the second Tufte book I bought, Visual Explanations. I previously wrote my thoughts on The Visual Display of Quantitative Information.

It's second chapter, Visual and Statistical Thinking: Displays of Evidence for Making Decisions, is probably the best. It illustrates how having good displays of evidence can be vital for making good decisions and thinking about problems, with two particular case studies:

While the analysis of both cases is interesting and well researched, it didn't really convince me. It's overly simplistic to conclude that Snow succeeded and the NASA officials failed simply because of the use or lack of use of good graphs, rather than because of solic scientific thinking. I think the author is somewhat aware of that, but that doesn't stop him from implicitly attributing success and failure to the use of “good” graphs.

I also was annoyed to see the author criticising Feynman's presentation —it describes it as “deeply flawed”— totally missing its point, but I suppose this is not very important.

Then we find a chapter coauthored with a magician, going over diagrams of books describing magical tricks. While at first this may seem an interesting idea, I found it also relatively unconvincing. I see little point to this, which I found pompous rather than rigorous.

There are other chapters, all unremarkable. The last one, Visual Confections, is specially bad, just a potpourri of different drawings from different centuries —from the beginning of the XVII century to some computer screens— with some almost-arbitrary positive or negative criticism with very little point or underlying structure.

Even though the book is well written and the effort that the author put in making the form of the book friendly (such as repeating a drawing so you won't have to go back to the previous page to see it again) does show, I struggled to finish it. In the end, I think it:

I wouldn't recommend it.

Loading... Vote up! Vote down! Save to del.icio.usSubmit Story to Digg Discussion (1)

Loading... Vote up! Vote down! Save to del.icio.usSubmit Story to Digg

Last update: 2008-01-27 (Rev 13448)

svnwiki $Rev: 15576 $