colorize
Sat Jul 3 18:47:01 2010
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.
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.
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)))
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)]))
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)))
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))))
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/>.
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'
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
icfp-2010
Sat Jun 26 11:50:41 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.
simple-logging
Thu May 27 07:51:37 2010
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
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.
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)
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* '()))))
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
amanda-pacifica
Fri Apr 30 11:36:49 2010
the-house-keeper-and-the-professor
Wed Apr 28 09:22:24 2010
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.
God's Debris
Mon Apr 26 22:00:44 2010
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).
weekend-in-florence
Mon Feb 22 00:04:18 2010
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:
- Buy your train tickets from SBB. I had to pay 278 CHF for a round-trip ticket going through Milan, which takes short of 6 hours. I left early on Friday to arrive in time for lunch and came back on Sunday's late afternoon, to arrive late in the evening.
- For landmarks, I'd recommend not missing the following, in order of importance:
- Duomo and it's Battistero. I've heard that the climb to the tower is well worth it, but I haven't done it myself. I don't find the church that impressive on the inside, but I find it extremely beautiful on the outside.
- Ponte Vecchio. I took a photo of it.
- Basilica di Santa Croce.
- Piazza della Signoria.
- Piazza della Repubblica, on which, by the way, the Edison bookstore —that, with a relatively good offer of books in English in the top-most floor, I find rather charming— is located.
- Supposedly, the Piazzale Michelangelo, has a very nice view of the city. I haven't been there.
- Giardino di Boboli, a large park relatively close to the city center, with nice views of the city.
- I don't have much recommendations in the way of restaurants. We had very good pizzas in Yellow and an OK meal (good albeit pricey) in Paszkowski. The Ora D'Aria restaurant seems potentially good, but you'll probably need a reservation (which we didn't have). Don't forget to have a good ice cream.
- For a hotel, I recommend the Gran Duomo, which I think is well worth its price. The service is excellent, the rooms spacious and the building in perfect shape. If you're considering a room with a view over the Duomo, it won't disappoint you: it looks exactly as in the photos on their website. You can have breakfast in your room right across the street from the Duomo or, as we did, buy a bottle of wine, some hams, cheeses, bread and olives and enjoy a nice dinner.
- In addition to the many gelaterias, Florence seems to have a lot of leather and relatively good stationery shops. You may want to buy stationery or bags here, if you're into that.
- Make reservations for the Galleria degli Uffizi and the Galleria dell'Accademia in their official site. There seem to be many other websites that seem to sell the reservations for a slightly higher price. In summer, having a reservation may save you from having to make long queues (this wasn't an issue when I visited in February, but it certainly was when I visited in both August and October).
- In the Galleria degli Uffizi you'll see very beautiful Renaissance works by painters such as Botticelli, Raffaello and Lippi, for which I'd encourage you to reserve at least 2 hours, possibly 3. You may have a light lunch at the museum's café, overlooking the rooftops.
- In the Galleria dell'Accademia you'll watch mainly Michelangelo's David and his unfinished sculptures. There are also a lot of religious paintings (and a collection of religious art from Russia) and a small yet interesting museum of musical instruments. I find the Uffizi museum a lot more significant than this one, but the David is impressive.
- There's also the potentially interesting Museo dell'Opera del Duomo, telling the history of the construction of the Duomo. You can just buy the tickets for that one on site.
- If your time allows, you'll probably want to visit one of the nearby cities in the Tuscany. I've been to San Gimignano for a day and to Siena for three. I found Siena significantly more charming, so I'd generally recommend that one. Whichever you pick, you'll probably need at least one full day. Another option, which I haven't taken, would be Pisa.
cain
Tue Feb 16 10:35:12 2010
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.
no-estar-solo
Wed Feb 3 08:50:54 2010
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.
visual-explanations
Thu Jan 21 23:36:07 2010
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:
- the cholera epidemic in London in 1854, where John Snow performed and outstanding job in finding the source of the disease, and
- the failed launch of the space shuttle Challenger in 1986, which exploded shortly after launch because the temperatures in the day it was launched were outside of the normal operating parameters of some of its parts.
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:
- lacks an underlying structure or theory, being just a random assortment of images and comments which, while compelling at first, quickly becomes boring, and
- just plain isn't very convincing.
I wouldn't recommend it.
Last update: 2008-01-27 (Rev 13448)


