All work

2014–present

IAT Software platform

The full product: a point-and-click authoring environment, a server that administers IATs, Excel exports of Greenwald D-scores, and a public site.

C# WinFormsWPFJavaXSLTKnockoutJSWebSocketsLinux

The problem

Implicit Association Tests are easy to fake if the participant can see what is being measured. The product had to stay faithful to the research method while remaining usable by non-engineers — graduate students, high-school psychology classes, academic labs.

Approach

  • Original client: 600+ classes, 50+ custom UI objects, nontrivial multithreading.
  • Image generation off the UI thread with Bitmap reuse — memory from nearly 1 GB to 55 MB.
  • XSLT for Excel workbooks (150+ sheets) and for HTML/JS test administration.
  • Public site in KnockoutJS, documented in Adobe InDesign, deployed behind nginx on Linux.
  • The WPF rewrite is how the platform survives the next decade.

Results

  • Adopted by researchers at Oxford and Princeton, and by high-school psychology classes.
  • Single founder owning architecture, implementation, operations, documentation, and support.
  • Live repositories: IAT-Design-WPF, IAT-Design, IAT-Server, IAT-Website.

ProductOriginal clientServerWPF rewriteWebsite source

From the source

The 7-block IAT, as code

IAT Design/C7BlockIATGenerator.cs

csharp
private void CopyItemsToBlock(CIATBlock dest, CIATBlock src, bool reverse)
{
    for (int ctr = 0; ctr < src.NumItems; ctr++)
    {
        if (src[ctr].GetKeyedDirection(src.URI) == KeyedDirection.None)
            dest.AddItem(src[ctr], KeyedDirection.None);
        else if (src[ctr].GetKeyedDirection(src.URI) == KeyedDirection.Left)
            dest.AddItem(src[ctr], reverse ? KeyedDirection.Right : KeyedDirection.Left);
        else if (src[ctr].GetKeyedDirection(src.URI) == KeyedDirection.Right)
            dest.AddItem(src[ctr], reverse ? KeyedDirection.Left : KeyedDirection.Right);
        else if (src[ctr].GetKeyedDirection(src.URI) == KeyedDirection.DynamicLeft)
            dest.AddItem(src[ctr], reverse ? KeyedDirection.DynamicRight : KeyedDirection.DynamicLeft);
        else if (src[ctr].GetKeyedDirection(src.URI) == KeyedDirection.DynamicRight)
            dest.AddItem(src[ctr], reverse ? KeyedDirection.DynamicLeft : KeyedDirection.DynamicRight);
    }
}

public bool Generate(bool bAlternate)
{
    CIATBlock b3 = new CIATBlock(IAT);
    b3.Key = GenerateResponseKeyForBlock(3);
    CopyItemsToBlock(b3, IAT.Blocks[0], false);
    CopyItemsToBlock(b3, IAT.Blocks[1], false);
    b3.AddToIAT(insertionNdx++);

    CIATBlock b5 = new CIATBlock(IAT);
    b5.Key = GenerateResponseKeyForBlock(5);
    CopyItemsToBlock(b5, IAT.Blocks[1], true);
    b5.AddToIAT(insertionNdx++);

    CIATBlock b6 = new CIATBlock(IAT);
    b6.Key = GenerateResponseKeyForBlock(6);
    CopyItemsToBlock(b6, IAT.Blocks[0], false);
    CopyItemsToBlock(b6, IAT.Blocks[1], true);
    b6.AddToIAT(insertionNdx++);

    if (bAlternate)
    {
        new AlternationGroup(b3, b6);
        new AlternationGroup(b4, b7);
    }
    return true;
}

Greenwald’s procedure is a generator, not a wizard. Blocks 3–4 combine the two practice keys; block 5 reverses the target; 6–7 recombine. CopyItemsToBlock flips keyed direction when the target is reversed. Optional AlternationGroup swaps compatible/incompatible blocks across participants.

Bitmaps that don’t own the process

IAT Design/ImageManager.cs

csharp
public void Start()
{
    CCompositeImageGenerator.StartGeneration();
    StartResizer();
    StartThumbnailGenerator();
    StartNonUserImageGenerator();
    this.Running = true;
}

public void Halt(bool bIsHaltingForSave)
{
    CCompositeImageGenerator.EndGeneration();
    var resizer = new ManualResetEvent(false);
    var thumbs = new ManualResetEvent(false);
    var generated = new ManualResetEvent(false);
    CIATImage.HaltResizer(resizer);
    CThumbnail.HaltThumbnailGenerator(thumbs);
    HaltNonUserImageGenerator(generated);
    resizer.WaitOne();
    thumbs.WaitOne();
    generated.WaitOne();
    this.Running = false;
}

private void CompactImageDictionary()
{
    lock (dictionaryLock)
    {
        foreach (var id in UserImages.Keys.ToList())
            if (UserImages[id]?.NumInstances == 0)
            {
                UserImages[id].Dispose();
                UserImages.Remove(id);
            }
        foreach (var id in NonUserImages.Keys.ToList())
            if (NonUserImages[id]?.NumInstances == 0)
            {
                NonUserImages[id].Dispose();
                NonUserImages.Remove(id);
            }
    }
}

Thumbnails, resizes, and generated stimuli run on timers off the UI thread. CompactImageDictionary drops images with zero remaining instances. This is how peak memory went from nearly 1 GB to 55 MB — reuse the bitmap, don’t keep every size around forever.

Generate off the UI thread, ref-count the result

IAT Design/CCompositeImageGenerator.cs

csharp
static public void StartGeneration()
{
    halted = new ManualResetEvent(false);
    halting = false;
    ImageGenerationTimer = new System.Threading.Timer((n) =>
    {
        if (!Monitor.TryEnter(generatorLock))
            return;
        try
        {
            List<CCompositeImage> stale;
            lock (listLockObj)
                stale = ImageDictionary.Keys
                    .Where(ci => !ci.IsValid).ToList();
            foreach (var ci in stale)
                ci.TryGenerate(false);
            if (halting)
                halted.Set();
        }
        finally { Monitor.Exit(generatorLock); }
    }, null, 0, 100);
}

static public void AddCompositeImage(CCompositeImage ci)
{
    lock (listLockObj)
    {
        ImageDictionary[ci] = ImageDictionary.ContainsKey(ci)
            ? ImageDictionary[ci] + 1 : 1;
        ci.Invalidate();
    }
}

Composite keys and instruction screens are regenerated on a 100 ms timer. TryEnter so a slow generate never piles up. AddCompositeImage increments a count; RemoveImage drops the entry at zero. The UI binds to the cached bitmap, not the generator.