# Sending arguments to Python decorators

Python's decorators are tools for changing the behavior of a function without completely recoding it. When we apply a decorator to a function, we say that the function has been decorated. Strictly speaking, when we decorate a function, we send it to a wrapper that returns another function. It's as simple as that.

I was having trouble understanding exactly to which function, the original or the decorated one, the arguments are sent in a Python decorated function call. I wrote the following script to better understand this process (I use Python 3.4):

def wrapper(inFunction):
def outFunction(**kwargs):
print('The input arguments were:')
for key, value in kwargs.items():
print('%r : %r' % (key, value))

# Return the original function
return inFunction(**kwargs)

return outFunction

def add(x = 1, y = 2):
return x + y


wrapper(inFunction) is a function that accepts another function as an argument. It returns a function that simply prints the keyword arguments of inFunction(), and calls inFunction() like normal.

To decorate the function add(x = 1, y = 2) so that its arguments are printed without recoding it, we normally would place @wrapper before its definition. However, let's make the decorator in a way that's closer to how @ works under the hood:

In [22]: decoratedAdd = wrapper(add)
In [23]: decoratedAdd(x = 1, y = 24)
The input arguments were:
'y' : 24
'x' : 1
Out[23]: 25


When we call decoratedAdd(x = 1, y = 24), the arguments are printed to the screen and we still get the same functionality of add(). What I wanted to know was this: are the keyword arguments x = 1, y = 24 bound in the namespace of wrapper() or in the namespace of outFunction()? In otherwords, does wrapper() at any point know what the arguments are that I send to the decorated function?

The answer, as it turns out, is no in this case. This is because the wrapper() function first returns the decorated function, and then the arguments are passed into the decorated function. If this order of operations were flipped, wrapper() should know that I set x to 1 and y to 24, but really it doesn't know these details at all.

In [24]: wrapper(add)(x = 1, y = 24)
The input arguments were:
'y' : 24
'x' : 1
Out[24]: 25


So, when I call wrapper(add)(x = 1, y = 24), first wrapper(add) is called, which returns outFunction(), and then these arguments are passed to outFunction().

Now what happens when I call wrapper(add(x = 1, y = 24))? When I try this, the arguments are first passed into add, but then outFunction is returned without any arguments applied to it.

This example can give us an idea about the working order of operations in Python. Here, this example reveals that function calls in Python are left-associative.

# Overcoming complexity in biology

I've been sitting in on a short lecture series presented by Prof. John Marko here at the EPFL. The topic is on the biophysics of DNA and covers what is probably at least a 25 year span of research that has emerged on its mechanical and biochemical properties.

DNA amazes me in two different ways: relatively simple physical theories can explain in vitro experiments, but establishing a complete understanding of the behavior of DNA and its associated proteins (collectively known as chromatin) in vivo seems at this point almost hopeless.

So why do I think it's so difficult to establish a complete physical theory of the nucleus?

Certainly a lot of recent research has helped us to understand parts of what happens inside the nucleus. Take for example recent experiments that look at transcription factor (TF) binding and the nuclear architecture. TF binding experiments have helped us understand the mechanism of how a single transcription factor searches'' for a target site on a chromosome. It undergoes diffusion in the crowded nuclear environment, occasionally binding to the DNA non-specifically and sliding along it. We now know that this combined diffusion/sliding mechanism produces an optimum search strategy.

Studies of nuclear architecture attempt to understand how the long DNA polymer, which is on the order of one meter long, is packaged into the nucleus, which is only about five micrometers in diameter. This is nearly six orders of magnitude of compaction. Some current theories treat the DNA as a hierarchically packaged polymer or a fractal structure. Interestingly, the fractal model can explain why TF's may diffuse optimally and when crowding can hinder their search.

Both of these examples represent a generalization of one particular phenomenon that occurs inside the nucleus. And even given the enormity of these works, I think this generalization can be dubious because it may not apply to all cell types and there may be differences between cultured cells and those found in an actual organism.

The problem in capturing complete physical models of the nucleus seems to lie with the philosophy of physics itself: find the most essential parts of the system and include those in your model, discarding all irrelevant details. Unfortunately, in vitro experiments suggest that every detail seems to matter inside the nucleus. Local salt concentrations effect electrostatic interactions and entropic binding between proteins and DNA, the global nuclear architecture has an effect on single TF's diffusing inside the nucleus, there are a huge number of proteins that associate with DNA and control the conformation in toplogically associating domains. The list goes on and on.

A common theme to this list that I have described above is that phenomena at one length scale tend to have a direct impact on those at another, like the global nuclear architecture affecting a single TF trajectory. These are hallmarks of complexity: a dependence on the details of a system and multiscale behavior.

I am currently of the opinion that a complete model of the nucleus, and probably of other biological systems, must therefore necessarily abandon one important part of physical theories: reduction to the simplest possible set of parameters to describe a system. We need to incorporate all the details across all length scales to reproduce what exactly is going on.

And if classical physics falls short of this goal, what other approaches do we then require?

# Learning Python's Multiprocessing Module

I've been doing a bit of programming work lately that would greatly benefit from a speed boost using parallel/concurrent processing tools. Essentially, I'm doing a parameter sweep where the values for two different simulation parameters are input into the simulation and allowed to run with the results being recorded to disk at the end. The point is to find out how the simulation results vary with the parameter values.

In my current code, a new simulation is initialized with each pair of parameter values inside one iteration of a for loop; each iteration of the loop is independent of the others. Spreading these iterations over the 12 cores on my workstation should result in about a 12x decrease in the amount of time the simulation takes to run.

I've had good success using the parfor loop construct in Matlab in the past, but my simulation was written in Python and I want to learn more about Python's multiprocessing tools, so this post will explore that module in the context of performing parameter sweeps.

## Profile the code first to identify bottlenecks

First, I profiled my code to identify where any slowdowns might be occurring in the serial program. I used a great tutorial at the Zapier Engineering blog to write a decorator for profiling the main instance method of my class that was doing most of the work. Surprisingly, I found that a few numpy methods were taking the most time, namely norm() and cross(). To address this, I directly imported the Fortran BLAS nrm2() function using scipy's get_blas_funcs() function and hard-coded the cross product in pure Python inside the method; these two steps alone resulted in a 4x decrease in simulation time. I suspect the reason for this was because the overhead of calling functions on small arrays outweighs the increase in speed using Numpy's optimized C code. I was normalizing single vectors and taking cross products between two vectors at a time many times during each loop iteration.

## A brief glance at Python's multiprocessing module

PyMOTW has a good, minimal description of the main aspects of the multiprocessing module. They state that the simplest way to create tasks on different cores of a machine is to create new Process objects with target functions. Each object is then set to execute by calling its start() method.

The basic example from their site looks like this:

import multiprocessing

def worker():
"""worker function"""
print 'Worker'
return

if __name__ == '__main__':
jobs = []
for i in range(5):
p = multiprocessing.Process(target=worker)
jobs.append(p)
p.start()


In this example, it's important to create the Process instances inside the

if __name__ == '__main__':


section of the script because child processes import the script where the target function is contained. Placing the object instantiation in this section prevents an infinite, recursive string of such instantiations. A workaround to this is to define the function in a different script and import it into the namespace.

To send arguments to the function (worker() in the example above), we can use the args keyword in the Process object instantiation like

p = multiprocessing.Process(target=worker, args=(i,))


A very important thing to note is that the arguments must be objects that can be pickled using Python's pickle module. If an argument is a class instance, this means that every attritube of that class must be pickleable.

An important class in the multiprocessing module is a Pool. A Pool object controls a pool of worker processes. Jobs can be submitted to the Pool, which then sends the jobs to the individual workers.

The Pool.map() achieves the same functionality as Matlab's parfor construct. This method essentially applies a function to each element in an iterable and returns the results. For example, if I wanted to square each number in a list of integers between 0 and 9 and perform the square operation on multiple processors, I would write a function for squaring an argument, and supply this function and the list of integers to Pool.map(). The code looks like this:

import multiprocessing

def funSquare(num):
return num ** 2

if __name__ == '__main__':
pool = multiprocessing.Pool()
results = pool.map(funSquare, range(10))
print(results)


## Design the solution to the problem

In my parameter sweep, I have two classes: one is an object that I'm simulating and the other acts as a controller that sends parameters to the structure and collects the results of the simulation. Everything was written in a serial fashion and I want to change it so the bulk of the work is performed in parallel.

After the bottlenecks were identified in the serial code, I began thinking about how the the problem of parameter sweeps could be addressed using the multiprocessing module.

The solution requirements I identified for my parameter sweep are as follows:

1. Accept two values (one for each parameter) from the range of values to test as inputs to the simulation.
2. For each pair of values, run the simulation as an independent process.
3. Return the results of the simulation as as a list or Numpy array.

I often choose to return the results as Numpy arrays since I can easily pickle them when saving to a disk. This may change depending on your specific problem.

## Implementation of the solution

I'll now give a simplified example of how this solution to the parameter sweep can be implemented using Python's multiprocessing module. I won't use objects like in my real code, but will first demonstrate an example where Pool.map() is applied to a list of numbers.

import multiprocessing

def runSimulation(params):
"""This is the main processing function. It will contain whatever
code should be run on multiple processors.

"""
param1, param2 = params
# Example computation
processedData = []
for ctr in range(1000000):
processedData.append(param1 * ctr - param2 ** 2)

return processedData

if __name__ == '__main__':
# Define the parameters to test
param1 = range(100)
param2 = range(2, 202, 2)

# Zip the parameters because pool.map() takes only one iterable
params = zip(param1, param2)

pool = multiprocessing.Pool()
results = pool.map(runSimulation, params)


This is a rather silly example of a simulation, but I think it illustrates the point nicely. In the main portion of the code, I first define two lists for each parameter value that I want to 'simulate.' These parameters are zipped together in this example because Pool.map() takes only one iterable as its argument. The pool is opened using with multiprocessing.Pool().

Most of the work is performed in the function runSimulation(params). It takes a tuple of two parameters which are unpacked. Then, these parameters are used in the for loop to build a list of simulated values which is eventually returned.

Returning to the main section, each simulation is run on a different core of my machine using the Pool.map() function. This applies the function called runSimulation() to the values in the params iterable. In other words, it calls the code described in runSimulation() with a different pair of values in params.

All the results are eventually returned in a list in the same order as the parameter iterable. This means that the first element in the results list corresponds to parameters of 0 and 2 in this example.

## Iterables over arbitrary objects

In my real simulation code, I use a class to encapsulate a number of structural parameters and methods for simulating a polymer model. So long as instances of this class can be pickled, I can use them as the iterable in Pool.map(), not just lists of floating point numbers.

import multiprocessing

class simObject():
def __init__(self, params):
self.param1, self.param2 = params

def runSimulation(objInstance):
"""This is the main processing function. It will contain whatever
code should be run on multiple processors.

"""
param1, param2 = objInstance.param1, objInstance.param2
# Example computation
processedData = []
for ctr in range(1000000):
processedData.append(param1 * ctr - param2 ** 2)

return processedData

if __name__ == '__main__':
# Define the parameters to test
param1 = range(100)
param2 = range(2, 202, 2)

objList = []
# Create a list of objects to feed into pool.map()
for p1, p2 in zip(param1, param2):
objList.append(simObject((p1, p2)))

pool = multiprocessing.Pool()
results = pool.map(runSimulation, objList)


Again, this is a silly example, but it demonstrates that lists of objects can be used in the parameter sweep, allowing for easy parallelization of object-oriented code.

Instead of runSimulation(), you may want to apply an instance method to a list in pool.map(). A naïve way to do this is to replace runSimulation with with the method name but this too causes problems. I won't go into the details here, but one solution is to use an instance's __call__() method and pass the object instance into the pool. More details can be found here.

## Comparing computation times

The following code makes a rough comparison between computation time for the parallel and serial versions of map():

import multiprocessing
import time

def runSimulation(params):
"""This is the main processing function. It will contain whatever
code should be run on multiple processors.

"""
param1, param2 = params
# Example computation
processedData = []
for ctr in range(1000000):
processedData.append(param1 * ctr - param2 ** 2)

return processedData

if __name__ == '__main__':
# Define the parameters to test
param1 = range(100)
param2 = range(2, 202, 2)

params = zip(param1, param2)

pool = multiprocessing.Pool()

# Parallel map
tic = time.time()
results = pool.map(runSimulation, params)
toc = time.time()

# Serial map
tic2 = time.time()
results = map(runSimulation, params)
toc2 = time.time()

print('Parallel processing time: %r\nSerial processing time: %r'
% (toc - tic, toc2 - tic2))


On my machine, pool.map() ran in 9.6 seconds, but the serial version took 163.3 seconds. My laptop has 8 cores, so I would have expected the speedup to be a factor of 8, not a factor of 16. I'm not sure why it's 16, but I suspect part of the reason is that measuring system time using the time.time() function is not wholly accurate.

## Important points

I can verify that all the cores are being utilized on my machine while the code is running by using the htop console program. In some cases, Python modules like Numpy, scipy, etc. may limit processes in Python to running on only one core on Linux machines, which defeats the purpose of writing concurrent code in this case. (See for example this discussion.) To fix this, we can import Python's os module to reset the task affinity in our code:

os.system("taskset -p 0xff %d" % os.getpid())


## Conclusions

I think that Matlab's parfor construct is easier to use because one doesn't have to consider the nuances of writing concurrent code. So long as each loop iteration is independent of the others, you simply write a parfor instead of for and you're set.

In Python, you have to prevent infinite, recursive function calls by placing your code in the main section of your script or by placing the function in a different script and importing it. You also have to be sure that Numpy and other Python modules that use BLAS haven't reset the core affinity. What you gain over Matlab's implementation is the power of using Python as a general programming language with a lot of tools for scientific computing. This and the multiprocessing module is free; you have to have an institute license or pay for Matlab's Parallel Computing Toolbox.

# Measurements as processes

I work in microscopy, which is one form of optical sensing. In the sensing field, we are often concerned with making measurements on some structure so as to learn what it is. Often, I think the word measurement refers to the dataset that's produced.

I think it can be more effective to think of a measurement as a process that transforms the structure into the dataset. Why is this so? Well, to understand what the original structure was, we have to look at our data and make an inference. If we understand the steps in the process that took the original structure and turned it into data, we can apply the inverse of those steps in reverse order to get the original structure.

Of course, our dataset may only capture some limited aspects of the original structure, so we may only be able to make probabilistic statements about what the original structure was.

Take, for example, super-resolution microscopy experiments (SR). In SR, some feature of a cell is labeled with a discrete number of fluorescent molecules, then these molecules are localized to a high precision. The centroids of all the molecules are then convolved with a Gaussian function (or something similar) with a width equal to the localization precision to produce a rendered, super-resolved image of the structure. The measurement process can be thought of like this:

1. Attach fluorescent molecules to every macromolecule (or randomly to a subset of macromolecules) in the structure of interest.
2. For every molecule that emits photons during the time of acqusition by one camera frame, record its true coordinate positions and the number of detected photons. This can create multiple localizations that correspond to the same molecule in multiple camera frames.
3. Remove molecules from the lis that emitted less than some threshold number of photons. These correspond to molecules with a signal-to-noise ratio that is too low to be detected.
4. Randomly bump the molecule positions according to a Gaussian distribution with width equal to the localization precision in each direction.

This process results in a list of molecule positions that originally were located on the structure of interest, but were eventually displaced randomly and filtered out due to various sources of noise. To understand what the original structure was, we have to "undo" each of these steps to the best of our abilities.

I think it's interesting to note that everytime a random change to the original molecule positions occurred, we lose some information about the structure.

# Parallelized confocal microscopy with multiply scattered light

My PhD work dealt with the topic of sensing and imaging using light that had been transmitted through a random medium. This topic is often applied to practical problems such as imaging through a turbulent atmosphere or detecting objects, like a tumor, buried beneath an opaque substance, like skin and muscle tissue. Though I don't necessarily work in this field anymore, I still follow its developments occasionally.

A few articles appeared within the last week in journals like Nature Photonics]] about the problem of imaging through walls. This problem has been studied since the late 1980's in a number of papers, including the notable Feng, Kane, Lee, and Stone paper and Freund's discussion of using cross-correlating two speckle patterns, one of which is a reference wave, to do a sort of holographic imaging. The recent work continues from these original ideas and applies them to microscopy.

One article appeared on the arXiv and is from the Mosk and Lagendijk camp. In this article, they exploit the optical memory effect, whereby a speckle pattern generated by the multiple scattering of a plane wave by a random slab is simply translated as the angle of incidence of the plane wave is varied. If a thin fluorescent target is placed directly on the opposite face of the scattering slab, it will be excited by the speckle pattern and emit a fluorescence signal that can be captured by an objective. Changing the angle of incidence of the plane wave then allows for multiple points to scan the sample in parallel. Ultimately, a number of images are taken with the sample illuminated by several transversally shifted speckle patterns and the resulting 4D data cube (corresponding to the sample's x-y dimensions and the two tilt angles of the incident plane wave) is used to render an image with improved resolution.

As stated in the article's title, the resolution improvement is relative to that of a widefield microscope. They obtain an effective point spread function of about 140 nm and a field of view of 10 microns by 10 microns.

So how does the technique work? My first thought was that the memory effect is simply another way of saying that the speckle pattern acts as a number of confocal-like point sources, which essentially means this is something of a parallelized confocal microscope. However, I'm not sure this is correct for two reasons: 1) there is no detection pin hole, and 2) the angle of incidence of the plane wave is scanned over a small angular range so that the speckle pattern is simply translated. If the angle of incidence is so great that the linear change in phase of the plane wave is greater than roughly the size of the scattering slab, the speckle pattern is no longer simply translated but changes completely.

In reality, the ultimate resolution of this technique is the average speckle grain size, which can't be less than about half the wavelength. This suggests that the angular spectrum of the speckle pattern is what determines the resolution improvement.

The speckle pattern in a region bounded by the maximum extent of the memory effect has a fixed angular spectrum and translating the speckle pattern only changes the phase of the spectrum. So, scanning a target with a speckle pattern produces beat patterns containing high spatial frequency information that can propgate on the low spatial frequency waves that reach the objective. Translating the speckle pattern then performs a sort of phase-shifting interferometry that allows for both intensity and phase retrieval of the object.

Importantly, if the speckle pattern is scanned outside the range of the memory effect, the speckle's angular spectrum within the region changes completely so that the original reference wave is lost. The fact that the object is fluorescent and not simply scattering the light shouldn't matter if the fluorescence intensity is linear with the excitation light intensity. However, if the fluorescence has saturated at the average intensity of the speckle pattern, then I'm not exactly sure that this technique will work (though maybe some sort of nonlinear structured illumination microscopy could be achieved).

Overall it's a neat demonstration and worth the exercise to understand it, though I'm doubtful that at this point it would be useful for applications in the life sciences. This is because the resolution isn't that much better than a spinning disk confocal microscope, which has a larger field of view and would arguably be easier to use by biologists.

# An accidental half-wave plate

Recently in the lab we made a seemingly minor change to one of our microscopes that led to a minor problem that lasted for about a week. Briefly, we introduced a mirror following a periscope that raises the plane that the laser beams travel in parallel to the table. The periscope is necessary to bringthe beams out of the plane containing the beam-combining optics and into the TIRF module port on our Zeiss inverted microscope for epi-illumination.

We had introduced the mirror to give us one more degree of freedom for steering the beam, but to do so we had to move the periscope and turn one of the mirrors by 90 degrees. Unfortunately, after doing this we found that the optical power leaving the microscope objective had dropped by a factor of 10, which was insufficient to do STORM imaging.

We eventually determined that the Zeiss TIRF module contained a polarizing beam splitter that combined the laser beams with white light from another port at 90 degrees to the beam path. In the newsetup, we placed a broadband halfwave plate before the TIRF module, rotated it while watching a power meter, and were finally able to get even more power leaving the objective than with the old setup.

So what had happened to cause the decrease in power by adding another mirror, and why did introducing the halfwave plate fix the problem? As it turns out, you can rotate the polarization by 90 degrees using only a pair of mirrors to redirect the beam into a plane parallel to its original plane of travel but into a direction perpendicular to its original one. This was what we were doing before we introduced the mirror. After the new mirror was put in place, we rotated a mirror of the periscope, which effectively rotated the laser beam's polarization back into its original orientation. And, since there was a polarizing beam splitter inside the TIRF module, the new beam polarization was transmitted through the module instead of being reflected towards the objective.

The picture below describes how this can happen. Note that the final beam trajectory is in the direction pointing into the screen.

# The philosophy of -omics studies

As a physcist, I'm a relative newcomer to the biological sciences. As a result, many approaches to doing science within these fields are new and really interesting to me.

One such idea is the -omics approach to science. Sonja Prohaska and Peter Stadler provide an insightful and sometimes amusing intrepetation of -omics studies in an article in the book Bioinformatics for Omics Data. (The PubMed link to the article is here, but you can find a pdf by Googling "The use and abuse of -omes.") According to them, -omics refers to the research field of digesting the pile of data coming from measurements of a particular -ome, such as the "genome" or "transcriptome." The goal is to relate the collection of parts within the -ome to biological function, or at least to determine function by comparing -omes of two different organisms.

The authors explain that -omics approaches have three components, which I quote from their article:

1. A suite of (typically high-throughput) technologies address a well-defined collection of biological objects, the pertinent -ome.
2. Both technologically and conceptually there is a cataloging effort to enumerate and characterize the individual objects–hopefully coupled with an initiative to make this catalog available as a database.
3. Beyond the enumeration of objects, one strives to cover the quantitative aspects of the -ome at hand.

While grand in scope, these approaches carry difficulties that to me appear unique. As stated above, -omic information is acquired through high-throughput techniques, which means that they generate very large amounts of data. Of major concern is actually linking this data to biological function. In other words, scientists must answer whether the correlations in the data actually allow us to predict the behavior of a cell or tissue. As might be expected, generating a complex data set such as is found in -omics studies can be quite impressive at first glance. But this complexity may hide the fact that no biologically relevant conclusions can be drawn from it.

The authors specifically enumerate four limitations that could adversely affect -omics studies:

1. Technical limitations
2. Limitations in the experimental design (such as heavy reliance on assumptions)
3. Conceptual limitations
4. Limitations in the analysis

Particularly, the conceptual limitations struck me as intriguing. The authors offered the example of genomics studies in which the notion of a "gene" is not currently a well-defined concept. When the concepts underpinning an entire collection of measurements is unclear, we should question whether the measured data has the meaning that we think it does.

Overall, I found that this commentary provided an interesting and sobering view of one particular approach to studying biology. It's interesting because I suspect that many important biological questions in the near future will come from taking an integrated and systems perspective. This perspective will require high-throughput techniques that carry the same limitations that -omics studies have.

# Customized Wikibooks are awesome

Last night the internet failed in our apartment and is still down. Because of this, I ran into the office today (a Saturday) to download a bunch of files to do some much-needed background reading on some topics.

Here's how it works. (I'll skip the actual details and let you read it straight from Wikipedia's help files. However, I will explain the process of making your own book.) After you activate the book creater, you simply navigate to any Wikipedia page and click the button on the top of the page that asks if you want to add the current page to your book. Do this for as many pages as you'd like.

When you're ready to access your book, you can do it online, or export it to a number of formats for offline viewing, such as PDF or an Open Document format. I thought that the PDF was beautufilly rendered. It even included all reference information.

There are other options with this tool too, such as sorting your articles into chapters or naming your book. I believe that you can share it with others as well.

Cheers to you, Wikipedia, for creating an awesome and useful feature!

# The art of alignment

I've been working on realigning one of our STORM microscopy setups and moving the other over the past couple weeks. As a result, I've been doing a lot more alignment lately than I normally do, which has got me thinking about alignment in general.

I've worked in optics labs for nearly ten years now, but I was never systematically taught how to do alignments. Eventually, I just figured it out from asking and watching lots of others do it. This part of experimental optics seems to be something that's passed down and shared across generations of PhD students and post-docs, like some sort of cultural heritage preserved by spoken word.

Unfortunately, this fact means that written tutorials or alternative resources on alignment are scarce. This in turn leads to frustration for new students who can't easily find someone to show them how to align a system. I was lucky in my training because both my undergraduate and graduate programs were in centers dedicated to the study of optics and optical engineering. Resources were plentiful. But for a biologist working with a custom microscope using multiple laser lines, finding someone who is competent in optics to help realign their system can be highly unlikely.

So how can people who don't have a background in optics be trained in alignment? I'm certain that written tutorials are not the best tool, since aligning an optics setup is a very hands-on job. But, a tutorial that makes explicit the principles of alignment might make a good starting point for others.

What are these principles? Below is my rough, initial list that applies to aligning laser beams. Aligning incoherent systems is another beast altogether.

1. The angle of the beam propagation is just as important as the position of the beam in a plane perpendicular to the table. In other words, just because a beam is going through a stopped-down iris doesn't mean it's traveling where you think it is.
2. You need to measure the beam position in two planes to determine its angle of propagation.
3. Use "layers of abstraction" to divide sections of the setup into independent units.
4. Use paired mirrors to get enough degrees of freedom to establish these abstraction layers.
5. Design feedback mechanisms into the system. In other words, keep some irises or alignment marks on the walls in place to aid in future realignments.

# Hello, world!

Well, I've finally got this site off the ground… more or less.

This website is the continuation of my blog, My Quad-Ruled Life. On that site, I explored a number of topics as a graduate student in optics, while improving my writing as well. As the site progressed and I matured academically, I found that I was blogging less often, but still used the blog as a means of exploring ideas. Additionally, I desired a place to present notes and code to the public in an easily accessible and open manner.

So, after discovering Nikola and GitHub Pages, I quickly hashed together a fully homemade site that allowed me to keep writing blog posts and to present some of the fruits of my research freely on the internet.

I hope you'll find this site useful as a learning resource and as a means to get to know me better as a scientist.

-Kyle