Friday, October 20, 2017

ADSL2+ Sync Rate

If you have ever called up your ISP support team to complain, you may have heard them quote a number called SNR to describe the quality of your connection.

It's not quite as simple as a single number, though.

ADSL2+ uses multiple "channels" at once to transfer data between the ISP and your home modem. In the case of my ASUS modem, there are 512 channels: there is 2.208 MHz of available spectrum on the copper wire; these are split into equally spaced 4.3125 kHz wide channels.

However, not every channel (or bin) is created equally: there is almost certainly some electrical "noise" or congestion on the cables. Some bins are reserved for normal telephone connections, others for upstream and downstream communication with the server, with a few reserved as guards to keep sections separated.

I pay for a 4 Mb/s connection, so let's assume the ISP allows me 1672 bits' worth of downstream sync rate*. It is up to the modem and ISP to negotiate how to allocate the sync rate across the available bins. My modem surveys the ratio of signal to noise (SNR) across each bin in the downstream set of channels. A rule of thumb is that each bit requires about 3dB of SNR to reliably decode the signal without errors. Bins with higher SNR will therefore have higher bit capacity for bits.

My modem allows me to connect with SSH and look at three files, which are ordinarily used by the administrative web server interface to present me a way to easily change modem settings and view diagnostics:
  • /var/tmp/spectrum-bpc-ds
  • /var/tmp/spectrum-bpc-us
  • /var/tmp/spectrum-snr
Let's do some very simple mathematics with the data in these files that won't be too difficult to follow:

Data taken from the SNR file:
1672 bits to allocate
268 channels into which they must be allocated
10,523.27 dB (the sum of SNR across the available reception channels)

Assumptions:
3 dB/bit required to convert received signal without errors

1) 1672 bits * 3 dB/bit = 5016 dB SNR required to decode the signal

2) 10,523.27 dB - 5016 dB = 5507.27 dB unused SNR

3) 5507.27 dB / 268 channels = 20.54951 dB unused SNR per channel

4) Now, we iterate each of the 268 channels in turn. I'll demonstrate with the first channel, where the SNR was 24.21 dB.

a) 24.21 dB - 20.54951 dB = ~3.66 dB to be used for data reception

b) 3.66 dB / 3 dB/bit = ~1.22 bits

c) round 1.22 bits to a whole number: 1

It turns out that this simple algorithm gets us remarkably close to the numbers of bits per channel (BPC) in the other files, which are arrived at by the modem's own algorithms. The difference could even be accounted for by (lack of, or outdated) "bitswap", where bins are periodically re-checked for SNR and reallocated to make the best use of the spectrum as it evolves over time.


* Ignore - for now - that those numbers are not equivalent, they are effectively the same thing quoted for different units of time
 

Wednesday, August 23, 2017

OpenVPN

This article on Ars Technica inspired me to try and setup a VPN, but it seemed to lack a couple of extra steps that were required to run the server on a Raspberry Pi behind a router. The official OpenVPN documentation was helpful but long-winded, and I resorted to a few askubuntu answers in the end.
  1. Certificate names

    client-no-pass doesn't have to be replaced with the client host name, but can be (pretty much) any identifier for the client. The important thing is to sign all certificates with the same CA key.
  2. VPN topology

    The default topology of net30 is perhaps not as easy to comprehend as subnet, and subnet works with Ubuntu, Windows and iPhone clients.
  3. Default gateway

    On the server, I needed to push redirect-gateway, push "route xxx.xxx.xxx.0 255.255.255.0" and consequently push "dhcp-option DNS xxx.xxx.xxx.1" so that the client would be able to route to the server's local network, and the tun0 device would be able to do DNS lookups.
  4. Firewall rules

    Besides enabling the forwarding of IPv4 so that the Raspberry Pi acted as a router, I need to add three firewall rules in iptables: one for NAT masquerading, and two for accepting new and related established connections.
  5. DNS setup for clients

    The dhcp-options that were pushed to the client were ignored, which resulted in the client being unable to resolve any DNS names to IP addresses. This was only a problem on Ubuntu clients, but the script /etc/openvpn/update-resolv-conf was provided when I installed openvpn; all I had to do was reference it from client-name.config as a pair of lines:
    up /etc/openvpn/update-resolv-conf
    down /etc/openvpn/update-resolv-conf
    

Sunday, August 13, 2017

Grub

It's nice when the Grub bootloader remembers your last choice and reuses it as the default value next time your machine boots. Using Linux, put the following in /etc/default/grub:
GRUB_DEFAULT=saved
GRUB_SAVEDEFAULT=true
Then run:
sudo update-grub

Wednesday, March 29, 2017

SubscribeOn / ObserveOn

Once you have an instance of an IObservable<T> object, Reactive Extensions (Rx) provides at least two points where you can choose an IScheduler which will determine the threading context under which the later operations will run. Those operations can be broken down into two phases: subscription and observation.

Subscription refers to the operations that modify the behaviours of an IObservable<T> (e.g. Throttle, Merge, Concat) The IScheduler for the subscription phase is assigned by the SubscribeOn() method.

Observation refers to the callback operations that are invoked as the result of observing an IObservable<T> (e.g. OnNext, OnCompleted, OnError) The IScheduler for the observation phase is assigned by the ObserveOn() method.

While not the subject of this post, you can also choose an IScheduler implementation to convert any IEnumerable<T> to an IObservable<T> with the ToObservable method. This is relevant when the IEnumerable<T> has the potential to take a long time to be enumerated.

Saturday, March 04, 2017

6 Simple Rules for Async/Await

  1. If a method A calls another method B that returns a Task (or Task<T>) then the calling method does not block on the completion of that task, unless it either:
    1. awaits that task, or
    2. waits on the result of that task
  2. The calling method cannot await a task unless it is declared with the async modifier, which causes the compiler to build a state machine (similar to the IEnumerable iterator) for that awaitable method.
  3. In point 1 above, the behaviour is independent of whether or not B is marked with the async keyword
  4. It is this non-blocking behaviour of A that allows the calling thread to proceed past the point where B is invoked. It's even possible for the thread's call stack to unwind, and for the thread to go back to reading the Windows message queue if it was the UI thread.
  5. The compiler will only warn you "because this call is not awaited ..." if B was marked with async.
  6. It is expected that A will do something with the Task returned by B; at the very least there should be some code to check that the Task did not throw any exceptions. If - instead of B - we have an async method C that returns void then we do not present A with any opportunity to monitor the completion of C. Unobserved exceptions thrown during the execution of C could indicate corrupted program state and can be configured to terminate the application in much the same way that an unhandled exception in synchronous code can unwind a stack fully and terminate the process.

Wednesday, December 28, 2016

iSCSI Enterprise Target

In the presence of conflicting information (lots of it) this is how I got iSCSI Enterprise Target running on Ubuntu 16.04 (LTS). First, install the required software with
sudo apt-get install iscsitarget
man ietd indicates that the configuration file is located at /etc/ietd.conf but it isn't, so instead:
sudo nano /etc/iet/ietd.conf
If you're following the "Creating an Open Source SAN" chapter of "Pro Ubuntu Server Administration" by Sander van Vugt, be careful when adding your target not to include a space between the comma and the type
Target iqn.2008-08.com.sandervanvugt:mytarget
    Lun 0 Path=/dev/sdb,Type=fileio
    Lun 1 Path=/dev/sdc, Type=fileio
One step that's missing from this chapter is to enable it (why isn't it enabled?).
sudo nano /etc/default/iscsitarget
Ensure this line is present in the configuration
ISCSITARGET_ENABLE=true
Then exit nano, saving changes, and restart the iscsitarget service
sudo /etc/init.d/iscsitarget restart
If everything has gone to plan, you should be able to see these files
cat /proc/net/iet/session
cat /proc/net/iet/volume
Otherwise, have a look at the log
tail /var /log/syslog

Monday, November 02, 2015

Sudoku

It's hard to find a newspaper without one, and I personally find that Sudoku solutions are an interesting problem. Randomly filling a grid of 81 cells with numbers from 1-9 wouldn't take long, but the end result probably wouldn't be a Sudoku solution, where each row, each column, and each box, all need to have the numbers 1-9 exactly once.

Instead of doing it randomly, we could try a constrained approach: only pick numbers from the set that would form an allowable Sudoku solution. We could start off by defining the set of all values in row M, all values in column N, and all values in box MN. The union of all three sets would indicate the values that have already been picked; therefore they're the numbers we couldn't re-use if we were to make a valid Sudoku solution. \( R(m) \cup C(n) \cup B(m,n) \) is the numbers that have been used, \( (R(m) \cup C(n) \cup B(m,n))' \) are the numbers available to us (this last set is the equivalent of \(R(m)' \cap C(n)' \cap B(m,n)'\)).

Let's assume we're looking at an empty cell, in an empty grid. \(R(m)=\emptyset, C(n)=\emptyset, B(m,n)=\emptyset\), therefore we can choose anything from \(\emptyset'\). The world is our oyster. Let's pick 1. We could have picked anything, but 1's a good place to start. In the adjacent cell, \(R(m)\) and \(B(m,n)\) would both yield \(\{1\}\) so we'd be forced to pick anything from \(\{1\}'\). Let's pick 2. Continuing in ascending order (and increasing level of constraint) we'd finally reach the 9th cell where \(R(m)=\{1,2,3,4,5,6,7,8\}\) and \(B(m,n)=\{7,8\}\). The only number we can pick is 9. Our top row is complete.

Continuing at the first column of the second row, and proceeding in the same fashion, we keep filling cells. It's too easy, something's got to break.

Ah. On the second row, at the seventh colum, we reach Box(2,7). We've already written \(\{4,5,6,1,2,3\}\) into the row, and \(\{7,8,9\}\) into the box. What can we do now?

The answer is to backtrack. We didn't have to choose the lowest value from \(R(m)' \cap C(n)' \cap B(m,n)'\) every time; in fact - more often than not - we had a wide array of options. So, we backtrack. That involves reverting the operation on the current cell (leaving it blank), moving to the previous cell, making sure it's blank (but we remembered which value(s) we'd already tried), and then trying another value. We don't need to limit ourselves to going back just one cell, either. In fact, in this case, we need to revert the last three cells. A second row that looks like \(\{4,5,6,7,8,9,1,2,3\}\) is a valid partial solution to a Sudoku problem. We use backtracking even more on the third row, producing \(\{7,8,9,1,2,3,4,5,6\}\). Still: a valid partial solution.

It turns out that our set-based constraints, plus the backtracking ability, give us the power to solve any Sudoku problem, even when the starting point is ambiguous (in the case of the empty grid).

1 2 3 4 5 6 7 8 9
4 5 6 7 8 9 1 2 3
7 8 9 1 2 3 4 5 6
2 1 4 3 6 5 8 9 7
3 6 5 8 9 7 2 1 4
8 9 7 2 1 4 3 6 5
5 3 1 6 4 2 9 7 8
6 4 2 9 7 8 5 3 1
9 7 8 5 3 1 6 4 2

Saturday, October 03, 2015

ForEach

var forEach = new TransformManyBlock<IEnumerable<int>, int>(x => x);

Saturday, May 17, 2014

Geostationary

Satellites in a geostationary orbit appear to maintain a fixed position relative to a position on the surface of the Earth. To accomplish this, they need to rotate at the same angular speed that the Earth spins, which makes one full rotation every sidereal day. There are 23.93446122 hours (or 86,164.0604 minutes) in one sidereal day. A full revolution of 2π radians in 86,164.0604 seconds gives us \(\omega = 7.29212 \times 10^{-5} \ radians \cdot s^{-1}\). This is our target angular speed if we want to keep a satellite overhead the same spot.

Earth's standard gravitational parameter is its mass M x G (the gravitational constant), which is roughly μ = 398,600.4418. The standard gravitational parameters for planets are better known than either M or G individually (the best way we have at our disposal to weigh extremely massive objects is by observing their gravitational effect on other bodies like orbiting satellites). Acceleration due to gravity is \(\mu \over r^2\). The equatorial radius of Earth is 6,378.14 km. If we plug in that figure, we'd see acceleration on the surface is the familiar 0.009798285 km/s. 422 km above the surface (in the orbital sphere of the ISS), it's just 0.008619904 km/s (87.97%). The further out we go, the lower the acceleration and the longer the orbital period.

To find out how far away we have to place a geostationary satellite, consider that it will need to be in a circular orbit with an orbital period of one sidereal day. In one second, it will move \(7.29212 \times 10^{-5} radians\); therefore \(r \times (1 - cos(7.29212 \times 10^{-5}))\) will be the distance that it falls towards the Earth in that time. We know from the SUVAT equation \(s = ut + {1 \over 2}at^2\) that in a second, with no initial velocity, that the distance travelled is half the acceleration. We now have an identity \(r (1 - \cos(7.29212 \times 10^{-5})) = {\mu \over r^2}\). Rearrange it and solve: \[r = \sqrt[3]{\mu \over {2 - 2 cos(7.29212 \times 10^{-5})}}\] It turns out that the altitude of 42164.15974 km above the center of the Earth (35786.02274 km above the equatorial surface) is where you'll find geostationary satellites - and they can only exist in that one orbital plane.

Saturday, May 10, 2014

Gravity / ISS

Let's say we're interested in finding how fast the International Space Station has to move in order not to fall back to earth (i.e. to stay in orbit). The earth has an equatorial radius of about 6384km and the satellite is about 422km above the equator when it's overhead on its inclined orbital path. For the sake of simplicity we will ignore the oblate spheroid shape of the earth, and the replace the elliptical orbit with a circular one, 6806km from the center of the earth.

On Earth's surface the acceleration due to gravity is \(9.8 m \cdot s^{-2}\). The further you go away, the lower the force of gravity. 422km above the surface we're told that the acceleration is 89% of surface gravity - it's \(8.722 m \cdot s^{-2}\).

Using the SUVAT equation \(s = ut + {1 \over 2}at^2\) we can see that an object dropped from 422km (i.e. \(g = 8.772 m \cdot s^{-2}\)) would fall 4.361m in the first second.

In the same second, we know that the ISS traces out the circular orbit (i.e. it doesn't crash into the Earth). The angular distance it travels (we could use the unit circle for visual confirmation) is \(\arccos({{r - s} \over r})\) or \(\arccos({{6806000 - 4.361} \over 6806000})\) or \(\arccos({6805995.639 \over 6806000})\) or simply 0.001132041 radians.

If it travels 0.001132041 radians in a second, it will take 5550.316851 seconds to perform a complete revolution of 2π. 5550 seconds is 92½ minutes, a figure that's very close to the published figure (on Wikipedia) and that's quite amazing given the rough estimates we've made to get this far. A circle of radius 6806km has a circumference of 13612km. Divide that circumference by the orbital period in seconds and you get \(7704 m \cdot s^{-1}\). You could also try \(r \sin(\theta)\) or \(6806000 \sin(0.001132041)\) which gives the same result. It's moving pretty quickly, but would have to be even quicker if the orbit was any closer to the surface!

Wednesday, April 16, 2014

Newtonian Reflector

Newtonian reflector telescopes use two mirrors – a figured primary mirror (the objective) which focuses incoming light, and a flat secondary mirror which redirects that light at an angle so that it can be viewed without getting your head stuck in the tube. The secondary mirror is only practically important – a CCD sensor could be placed directly into the path of the light focused by the objective mirror and it would capture the light without requiring an additional reflection. However, the subject of this post is the primary mirror, particularly: it’s shape.

A 2D parabola is defined by the quadratic equation \(y = ax^2 + bx + c\). If we make some simplifying assumptions (such as: its open side faces up; it “rests” on the x-axis; the focus is on the y-axis at height p) then our equation reduces to \(y = 4px^2\). The first order derivative (e.g. slope) of this equation is \({dy \over dx} = 2px\). That means, for a given focal length (and radius from the centre of the mirror), we can estimate the height of the reflective surface from the x-axis, as well as the slope at that point. These two computed values show where light is reflected when it encounters the surface of the objective mirror. Remember that incident light is reflected about the normal vector to the surface:
\(R = D – (2D \cdot N)N\)

To enable a 3D version (a paraboloid) we can take advantage of the knowledge that we only need rotate the parabola (i.e. the shape is rotationally symmetric about the vertical axis). In 3D, I chose to rotate around the z-axis.

It then becomes rather straight forward to take a solid angle of light, to compute the angles at which each incident ray encounters the parabolic reflector surface (parallax will affect closer light sources more than further away sources) and then note the points at which two or more rays converge to a single point. It turns out that the parabolic shape is ONLY able to focus light that is travelling parallel to the reflector’s primary axis. Closer sources converge further back than the stated focal length; sources at infinity converge exactly at the parabola’s focal point. Off-axis sources result in a complex out-of-focus shape when we attempt to capture them on a flat focal plane like a CCD. Looking at the distribution of focal points for a given field of view it was difficult to imagine a single transformation that might enable coma to be completely (and correctly) removed, though clearly a retail market for coma-correcting lenses abounds.

Thursday, January 24, 2013

Day-glo

Ever wonder what makes those striking day-glo colors shine brighter than everything around them? Our eyes can only see visible light (the colors of the rainbow) but the sun emits electromagnetic waves across a vastly wider spectrum (which we cannot see with our eyes, of course, and not just because it's not a good idea to stare at the sun). But just because we cannot see them, doesn't mean they aren't there. When photons from the ultraviolet region of the spectrum bump into an object, some of the energy can be reflected. Because we can't see them, they go unnoticed. However, certain chemical combinations transform higher-energy ultraviolet photons into lower-energy visible light photons (a process called fluorescence). When those fluorescent objects start emitting more visible light than the non-fluorescent objects around them, they give the perception of being brighter: day-glo. Interesting fact: whiter-than-white washing detergent is known to employ fluorescence to make clothes appear brighter.

Wednesday, October 17, 2012

Scalar

If you're reading this then it's already too late you've probably come across scalars and vectors and pondered their differences. The pair of terms is used in many contexts as diverse as operations performed by a CPU. I think the names go back as far as matrix mathematics, and - if you'll listen - I'll tell you why...

Matrices can be "multiplied" in two ways:

  1. two matrices are multiplied and the result is an arrangement of dot products;
  2. one matrix and one scalar are multiplied and every value in the matrix is scaled by the scalar.

Now, every time an algorithm requires to scale a collection of values by some number, will I name that variable "factor" or "scalar"?

Which would you choose?

Tuesday, October 09, 2012

Rectilinear

In my quest for a better panorama builder, I stumbled upon a few odd facts about cameras. Mine (the Canon G1X) for example, has a rectilinear lens. Or almost rectilinear. What this means is that for every pixel left or right (or indeed up and down) from the center of a photograph represents a constant number of degrees. I've not been able to measure the angle of view too closely, but I can report that it's slightly over 60 degrees (left to right) judging by my initial results. On a photograph 4352 pixels wide this is about 0.0138 degrees per pixel. (I've assumed the same scale factor is present on the vertical but this is not necessary - just an assumption).

Now, imagine you're standing in the middle of a massive sphere, pointing the camera outwards. If you took a photograph, each line of pixels (up/down and left/right) would represent a curved line segment of a great circle on that sphere. And if a pixel is defined by a row and column (or X and Y value) then the position of the light source of that pixel will be the intersection of those two great circles.

Mathematically it's relatively simple to calculate the intersection point of two great circles (we only consider two points and then throw one of those away - an optimisation) and thus the position in space of the pixel's light source. We could map two or more photographs of the same scene (taken from the same place, but at different angles) onto our sphere (from the inside) and with the right manipulations, we'd be able to build a 360 degree * 180 degree panoramic view (i.e. the full sphere).

And what are the right manipulations? They involve matrices, or more specifically, singular value decompositions. If every photograph shares two overlapping points with another photograph (or photographs) then we can test our inner-sphere projection by comparing the angle between the point-pairs in one photograph with the same point-pair in another. They need to be equal angles or else something's likely wrong with our assumption of the lens projection. Given two point-pairs, it's simply a case of using Procrustes analysis (or actually, just a sub-solution of it) to determine a rotation that can be applied to all pixels of one image to align it perfectly with the other image. Once the aligned images wrap around the entire sphere, you might find that the computed scale factor obtained when calibrating the lens was slightly out. So... why bother calibrating in the first place if you already know the lens is rectilinear! Readjust the scale factor and realign the images as necessary. Then fill the remainder of the sphere with your photographs. Simples!

Saturday, March 24, 2012

West-East

Nothing too technical here: I noticed (when viewing the current night sky in Cape Town) that West and East on the map were swapped around but North and South were still oriented as I'd expect. I puzzled for a few seconds, then held the laptop up to the sky. Eureka! When you're lying on your back (outside, on the grass, an unfamiliar concept to anyone in the UK) staring at the stars with your head and toes aligned North to South, West is your right and East is on your left. All of a sardine the map makes sense...

Thursday, October 27, 2011

Anaglyph

Red/cyan 3D anaglyphs are a mix of lo and hi-technology. Let me explain: red objects are red because that's the color of light they reflect; all other colors are absorbed. There are three "primary colors" of light - red, green and blue. So when a transparent sheet of red film absorbs all "other" colors of light, we really just mean it absorbs green and blue.

When we mix 100% green with 100% blue we get cyan. A transparent sheet of cyan film therefore allows green and blue light, yet absorbs red light. If we take a digital photograph (composed of red, green and blue light) and view it through a transparent cyan film, the film absorbs the red light allowing only green and blue to pass. In theory, if I have two photographs and I remove all the red from one, then I view both through transparent cyan film, I won't be able to tell the images apart. One's image emits no red light; the other's red light is blocked by the "filter" - in neither case does any red light reach the viewer.

What if we view an image with only red through our cyan filter? There is no green and no blue light in the image, and all red light gets absorbed by the filter. The image will appear black.


LEFT RIGHT
IMAGE (red only) (green/blue only)
FILTER (red) (cyan)
EYE (left only) (right only)



Now, put on an imaginary pair of red/cyan glasses and look at two digital photographs side by side. Close your left eye (red filter) and look with your right eye (cyan filter). Remember, an image with no green or blue appears black through cyan, so we remove these colors from the left image. An image with only green and blue appears in green and blue (there's no point in having red in the image as it would be absorbed by the filter anyway - plus it would destroy what comes next). With only your right eye, the left image is black and right image is visible. Now, close your right eye (cyan filter) and look with your left eye. The right image has gone black because it doesn't have any red - the green and blue have been absorbed by the red filter. The left image springs to life now, because it has red in it. We have found a way to show each eye a different image - and our brains do the rest.

Sunday, September 18, 2011

Diskette

I was emptying out a bunch of junk from my apartment and came across an old 3.5" floppy disk. My mind - being as it is - immediately wondered what the unit tests would look like if I were writing a virtual floppy disk (because nobody's got real floppy drives these days.)

On my Diskette class, I would expose the following methods to modify/access state:
  • bool ToggleDiskAccessSlot()
  • bool ToggleWriteProtect()
  • void RotateDisk(int numSectors)
  • int Read(int cylinder, int side)
  • void Write(int cylinder, int side, int value)

Using Roy Osherove's naming strategy (a la accepted answer to this question), I'd add a unit test class named DisketteTests. Unit tests - IMHO - are intended to ensure that public methods modify the internal state of an object as expected. A test first sets up the object under test to a known state, then invokes a method, and finally makes an assertion about the state.

I might want to test the ability to read or write while the disk access slot is closed (I'd expect some exception to be thrown, so there's no explicit calls to Assert in the method body, the assertion is made by the test framework based on the test's custom attributes):
[ExpectedException(typeof(InvalidOperationException))]
[TestMethod]
public void Read_DiskAccessSlotIsClosed_ThrowsException()
{
Diskette d = new Diskette();
// intentionally missing the step to ToggleDiskAccessSlot();
Ignore(d.Read(1, 1));
}

A more standard test might look like this:
[TestMethod]
public void Read_DiskAccessSlotIsOpen_GetsCurrentValue()
{
Diskette d = new Diskette();
if (!d.ToggleDiskAccessSlot())
{
Assert.Fail("Unable to open disk access slot");
}
int value = d.Read(1, 1);
Assert.AreEqual(value, 0);
}

Saturday, September 10, 2011

Memoize

The first time I saw this word I thought someone had mispelled it. Instead - as I later found out - it's the name for a common pattern in functional programming. Memoize caches the result of a function based on its input parameters. That may not sound particularly special, but in cases where a solution is recursive and frequently calls itself with the same parameters, memoize can turn an algorithm that takes billions of years to run[1], into one that takes a fraction of a second.

Consider this problem outlined on ROT13(Cebwrpg Rhyre). Every node in the triangle has an associated maximum cost. For the elements at the base level of the triangle, that value is the value of the element. For the element at the next highest level, the maximum cost is the value of the element itself, plus the greater value of the two elements below it. This property holds recursively throughout the entire graph, and we can use the memoize pattern to our advantage: instead of working out the cost of each element every time our algorithm requires it, we simply fetch it from the cache (if it doesn't exist, we add it to the cache, and use it on all subsequent calls.)

The original brute force C# code looked like this:
static int MaxCost(Triangle triangle, int x, int y)
{
if (y == triangle.Depth)
{
return 0;
}
else
{
int left = MaxCost(triangle, x, y + 1);
int right = MaxCost(triangle, x + 1, y + 1);
return Math.Max(left, right) + triangle[x, y];
}
}

We need to rewrite the method body slightly because it's self-recursive, and we can't take advantage of the memoize cache if our function just calls back into itself directly.

If we're pressed for time, we might simply code the cache into the method body:
static int MaxCost(Triangle triangle, int x, int y)
{
var key = new Tuple<Triangle,int,int>(triangle, x, y);
if (cache.ContainsKey(key))
{
return cache[key];
}

if (y == triangle.Depth)
{
return 0;
}
else
{
int left = MaxCost(triangle, x, y + 1);
int right = MaxCost(triangle, x + 1, y + 1);
int result = Math.Max(left, right) + triangle[x, y];
cache.Add(key, result);
return result;
}
}

With more time on our hands, we might separate the function into two distinct logic functions, one that deals with memoization, and the other that deals with the problem at hand:
static int MemoizedMaxCost(Triangle triangle, int y, int x)
{
var key = new Tuple<Triangle, int, int>(triangle, x, y);
if (cache.ContainsKey(key))
{
cache[key];
}
int result = InternalMaxCost(triangle, y, x);
cache.Add(key, result);
return result;
}

static int InternalMaxCost(Triangle triangle, int x, int y)
{
if (y == triangle.Depth)
{
return 0;
}
else
{
int left = MemoizedMaxCost(triangle, x, y + 1);
int right = MemoizedMaxCost(triangle, x + 1, y + 1);
return Math.Max(left, right) + triangle[x, y];
}
}

Note that we now have a pair of mutually recursive functions: the memoized function calls the internal one to evaluate any uncached values, and the internal one calls the memoized one to gain any of the performance benefits from memoizing. A match made in heaven?

Monday, September 05, 2011

Covariance / Contravariance

This is a little experiment to get your head around co- and contravariance, as applied to generic interfaces in C#. Covariance allows the result of a computation to vary, while Contravariance allows the input to vary. There are three interfaces in the code sample. The first demonstrates covariance by emulating a function that only returns a result. The second demonstrates contravariance by emulating an action that only takes an input. The third demonstrates both co- and contravariance. Let's look at the code:
#region Covariance
interface IFunction<out TResult>
{
TResult Invoke();
}

class Function<TResult> : IFunction<TResult>
{
public TResult Invoke()
{
throw new NotImplementedException();
}
}
#endregion

#region Contravariance
interface IAction<in T>
{
void Invoke(T arg);
}

class Action<T> : IAction<T>
{
public void Invoke(T arg)
{
throw new NotImplementedException();
}
}
#endregion

#region Both
interface IFunction<in T, out TResult>
{
TResult Invoke(T arg);
}

class Function<T, TResult> : IFunction<T, TResult>
{
public TResult Invoke(T arg)
{
throw new NotImplementedException();
}
}
#endregion

None of the methods are implemented, because a) I'm lazy and b) you don't need to see an implementation to witness the co- or contravariance first hand. The next code block shows the phenomena at work:
IFunction<object> f1 = new Function<string>();
IAction<string> a1 = new Action<object>();
IFunction<string, object> f2 = new Function<object, string>();

Notes:

  1. In the case of covariance, the generic argument type of the class is more specific than the interface (covariance: we can always implicitly cast from string to object when reading from the container).
  2. In the case of contravariance, the generic argument type of the interface is more specific than the class (contravariance: we can always implicitly cast from string to object when writing to the container).
  3. In the mixed case, we can see covariance for the output and contravariance for the input.

Monday, August 15, 2011

Real World Scalability

Armed with [edit: never quite] all there is to know about solving scalability problems, it's just short of impossible to walk onto a client site and fix their poorly performing application. Changes to the system require an understanding of what the system does and it's intended interactions with other systems. This complex hierarchy of dependencies starts with lines of code (hopefully encapsulated in object-oriented classes if it's C#) and quickly jumps from other processes on the same machine to distributed processes owned by other teams (especially if SOA has been employed). How do we know when we've broken some undocumented feature? Unit tests? Hardly anybody uses them. Integration tests? Does your team even know what they are? Who writes these tests, and when?

If the tests don't already exist, it will be much more difficult to determine that a scalability fix hasn't negatively altered the required behaviour of a process. But in a time when the bottom line is the final word and employers are encouraged to differentiate developers by price (for example: choosing a large off shore team) it's less common to see the tests written up front.