An upcoming Circuit Cellar column calls for a way to measure LED light output vs. current input, which means I need some way to hold LEDs directly over a photodiode while excluding ambient light. Fortunately, the M2 had black PLA filament already loaded:

That honkin’ big photodiode is a surplus PIN-10AP that’s been lying in wait for an opportunity just like this. The green filter matches the silicon response to CIE-standard human eye sensitivity, so the output tracks what you’d actually see. That’s irrelevant for testing red LEDs that all have pretty much the same wavelength, but it might come in handy for something.
The main body of the fixture holds the LED about 1 mm from the front of the photodiode, indexed against the LED flange so they’re all at a consistent location. The cap has three locating pins made of 3 mm orange filament, with black foam rubber to push the LED into position and block ambient light.
The business end looks like this:

The most convenient way to mount the thing involves a right-angle BNC adapter in my trusty bench vise:

The circuitry has a voltage-to-current driver for the LED and a zero-bias current-to-voltage converter for the photocell. The zero-bias trick keeps the voltage across the photodiode at zero, so the current varies linearly with illumination.
The solid model laid out for printing along the X axis:

It obviously has some improvements over the as-printed one in the pictures, in the unlikely event I need another fixture. The most important: a rear ring covering the back of the photodiode. Turns out that the PIN-10AP filter cap leaks a surprising amount of light around the body; I covered the gap with black tape to make the measurements, but that’s crude.
I added a few screw holes to hold the parts together, but the cap (with the foam and pegs) must come off easily while swapping LEDs. I’d be tempted to sink studs into the body and use wing nuts to hold the lid in place, but I don’t have any 4-40 wing nuts…
There’s a tiny bit of support under the central hole to support the LED flange recess and the trench for some foam under the leads:

That’s another improvement; the as-printed one has foam on only one side of the leads.
The OpenSCAD source code:
// LED test fixture for PIN-10AP photodiode // Ed Nisley KE4ZNU May 2013 // Layouts: Adapter AdapterSupport Cap Shield Build Show Layout = "Build"; Gap = 8; // between parts in Show //- Extrusion parameters must match reality! // Print with +1 shells and 3 solid layers ThreadThick = 0.25; ThreadWidth = 0.40; HoleWindage = 0.2; function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit); Protrusion = 0.1; // make holes end cleanly Spacing = 5; // between parts on build platform inch = 25.4; Tap2_56 = 0.070 * inch; Clear2_56 = 0.082 * inch; Head2_56 = 0.156 * inch; Head2_56Thick = 0.055 * inch; //---------------------- // Dimensions PhotoDiodeOD = 31.3; PhotoDiodeStemOD = 16.0; PhotoDiodeStemLength = 8.0; PhotoDiodeWindowDia = 17.7; PhotoDiodeHeight = 14.0; FixtureOD = PhotoDiodeOD + 2*7.0; LEDDia = 5.0; // LED body LEDFlangeOD = 6.0; // flange at base of LED LEDFlangeThick = IntegerMultiple(1.5,ThreadThick); LEDLength = 10.0; // overall length LEDRecess = 4.0; // tube to fit LED body LEDSides = 8; FixtureLength = PhotoDiodeHeight + LEDLength + IntegerMultiple(1.0,ThreadThick); CapLength = 15.0; // LED cover FoamOD = FixtureOD/2; FoamThick = IntegerMultiple(2.0,ThreadThick); TrenchDepth = 2*FoamThick; TrenchWidth = LEDDia; ShieldThick = 5.0; ShieldScrewCircle = PhotoDiodeOD + (FixtureOD - PhotoDiodeOD)/2; PinOD = 3.0; // alignment pin (filament) PinLength = 10.0; // ... total length PinCircle = FixtureOD/2; GrubScrewOD = Tap2_56; $fn=4*6; // default cylinder sides //---------------------- // Useful routines module PolyCyl(Dia,Height,ForceSides=0) { // based on nophead's polyholes Sides = (ForceSides != 0) ? ForceSides : (ceil(Dia) + 2); FixDia = Dia / cos(180/Sides); cylinder(r=(FixDia + HoleWindage)/2, h=Height, $fn=Sides); } module ShowPegGrid(Space = 10.0,Size = 1.0) { RangeX = floor(95 / Space); RangeY = floor(125 / Space); for (x=[-RangeX:RangeX]) for (y=[-RangeY:RangeY]) translate([x*Space,y*Space,Size/2]) %cube(Size,center=true); } //----------------------- // Parts module Adapter() { difference() { cylinder(r=FixtureOD/2,h=FixtureLength); translate([0,0,-Protrusion]) { PolyCyl(LEDDia,2*FixtureLength,LEDSides); PolyCyl(PhotoDiodeWindowDia,(FixtureLength - LEDRecess + Protrusion)); PolyCyl(PhotoDiodeOD,(PhotoDiodeHeight + Protrusion)); } translate([0,0,(FixtureLength - LEDFlangeThick)]) PolyCyl(LEDFlangeOD,2*LEDFlangeThick,LEDSides); translate([FixtureOD/2,0,(FixtureLength + FoamThick/2 - LEDFlangeThick)]) { cube([FixtureOD,TrenchWidth,FoamThick],center=true); } for (angle = [90:90:270]) rotate(angle) translate([0.75*PinCircle,0,(FixtureLength - PinLength/2)]) PolyCyl(PinOD,PinLength,6); for (angle = [0:120:240]) rotate(angle) translate([ShieldScrewCircle/2,0,-Protrusion]) rotate(45) PolyCyl(Tap2_56,(ShieldThick - 6*ThreadThick + Protrusion)); if (0) translate([0,0,FixtureLength/4]) rotate([0,90,0]) PolyCyl(GrubScrewOD,FixtureOD); } } module AdapterSupport() { spiderthick = IntegerMultiple(LEDFlangeThick - ThreadThick,ThreadThick); color("Yellow") union() { for (leg = [0:LEDSides/2 - 1]) rotate(leg*360/LEDSides) translate([0,0,spiderthick/2]) cube([(LEDFlangeOD - 0.5*ThreadWidth), 2.5*ThreadWidth, spiderthick], center=true); cylinder(r=LEDDia/2,h=spiderthick,$fn=LEDSides); for (bar = [-1:1]) translate([LEDDia/3,(bar*3*ThreadWidth - ThreadWidth),0]) cube([FixtureOD/2,2*ThreadWidth,IntegerMultiple(LEDFlangeThick - ThreadThick)]); } } module Cap() { difference() { cylinder(r=FixtureOD/2,h=CapLength); translate([(FixtureOD/2 - LEDDia/2),0,CapLength]) { cube([FixtureOD,TrenchWidth,2*TrenchDepth],center=true); } translate([0,0,(CapLength - FoamThick)]) PolyCyl(FoamOD,(FoamThick + Protrusion)); for (angle = [90:90:270]) rotate(angle) translate([0.75*PinCircle,0,(CapLength - PinLength/2)]) PolyCyl(PinOD,PinLength,6); } } module Shield() { difference() { cylinder(r=FixtureOD/2,h=ShieldThick); translate([0,0,-Protrusion]) PolyCyl(PhotoDiodeStemOD,(ShieldThick + 2*Protrusion)); for (angle = [0:120:240]) rotate(angle) { translate([ShieldScrewCircle/2,0,-Protrusion]) rotate(180/5) PolyCyl(Clear2_56,(ShieldThick + 2*Protrusion)); if (0) translate([ShieldScrewCircle/2,0,(ShieldThick - 1.5*Head2_56Thick)]) rotate(180/6) PolyCyl(Head2_56,4*Head2_56Thick); } } } //------------------- // Build it... ShowPegGrid(); if (Layout == "Adapter") Adapter(); if (Layout == "Cap") Cap(); if (Layout == "Shield") Shield(); if (Layout == "Show") { translate([0,0,(ShieldThick + Gap)]) { translate([0,0,FixtureLength + CapLength + Gap]) rotate([180,0,0]) Cap(); Adapter(); color("Orange") for (angle = [90:90:270]) rotate(angle) translate([0.75*PinCircle,0,(FixtureLength + Gap - PinLength/2)]) PolyCyl(PinOD,PinLength,6); } Shield(); } if (Layout == "AdapterSupport") { translate([0,0,FixtureLength]) rotate([180,0,0]) %Adapter(); AdapterSupport(); } if (Layout == "Build") { translate([(Spacing + FixtureOD),0,0]) { translate([0,0,FixtureLength]) rotate([180,0,0]) Adapter(); AdapterSupport(); } translate([0,0,0]) Cap(); translate([-(Spacing + FixtureOD),0,0]) Shield(); }