DesignValidator.java
package org.fresnel.optics;
import java.util.ArrayList;
import java.util.List;
/**
* Computes design metrics and printability warnings for a Fresnel zone plate.
*
* <p>Outermost zone width approximation: {@code Δr ≈ λ·f / D}.
*
* <p>Printability thresholds (printer pixels per outermost zone):
* <ul>
* <li>< 2 px: critical (ERROR)</li>
* <li>< 5 px: suboptimal (WARNING)</li>
* <li>≥ 5 px: good (no warning)</li>
* </ul>
*/
public final class DesignValidator {
/** Average transmission of a binary amplitude zone plate. */
public static final double BINARY_AMPLITUDE_TRANSMISSION = 0.5;
/** First-order diffraction efficiency of a binary amplitude zone plate (1/π² ≈ 0.1013). */
public static final double BINARY_AMPLITUDE_FIRST_ORDER_EFFICIENCY = 1.0 / (Math.PI * Math.PI);
private DesignValidator() {}
/** Compute metrics for a single zone plate design. */
public static DesignMetrics computeMetrics(SingleZonePlateParameters p) {
double lambdaMm = Units.nmToMm(p.wavelengthNm());
double outerZoneMm = lambdaMm * p.focalLengthMm() / p.apertureDiameterMm();
double printerPixelMm = Units.pixelSizeMm(p.dpi());
double pixelsPerOuterZone = outerZoneMm / printerPixelMm;
// Number of zones across the radius: n = R^2 / (λ·f)
double radiusMm = p.apertureDiameterMm() / 2.0;
int numberOfZones = (int) Math.floor((radiusMm * radiusMm) / (lambdaMm * p.focalLengthMm()));
// Chromatic shift table at sample R/G/B wavelengths (paraxial f ∝ 1/λ).
java.util.List<DesignMetrics.ChromaticShift> chromatic = java.util.List.of(
new DesignMetrics.ChromaticShift(450.0,
focalLengthAtWavelength(p.focalLengthMm(), p.wavelengthNm(), 450.0)),
new DesignMetrics.ChromaticShift(532.0,
focalLengthAtWavelength(p.focalLengthMm(), p.wavelengthNm(), 532.0)),
new DesignMetrics.ChromaticShift(p.wavelengthNm(), p.focalLengthMm()),
new DesignMetrics.ChromaticShift(630.0,
focalLengthAtWavelength(p.focalLengthMm(), p.wavelengthNm(), 630.0)));
// Defocus blur table at a few wall distances scaled to the design focal length.
double f = p.focalLengthMm();
double[] sampleDistances = { 0.5 * f, 0.75 * f, f, 1.5 * f, 2.0 * f };
java.util.List<DesignMetrics.DefocusEntry> defocus = new ArrayList<>(sampleDistances.length);
for (double z : sampleDistances) {
defocus.add(new DesignMetrics.DefocusEntry(z,
defocusBlurMm(p.apertureDiameterMm(), f, z)));
}
return new DesignMetrics(
outerZoneMm * 1000.0,
printerPixelMm * 1000.0,
pixelsPerOuterZone,
BINARY_AMPLITUDE_TRANSMISSION,
BINARY_AMPLITUDE_FIRST_ORDER_EFFICIENCY,
numberOfZones,
chromatic,
defocus);
}
/** Validate a single zone plate design and produce warnings. */
public static ValidationResult validate(SingleZonePlateParameters p) {
DesignMetrics m = computeMetrics(p);
List<ValidationResult.Warning> warnings = new ArrayList<>();
boolean valid = true;
if (m.pixelsPerOuterZone() < 2.0) {
warnings.add(new ValidationResult.Warning(
"OUTER_ZONE_TOO_SMALL",
String.format(
"The outer zone width is only %.2f printer pixels (need ≥ 2).",
m.pixelsPerOuterZone()),
ValidationResult.Warning.Severity.ERROR));
valid = false;
} else if (m.pixelsPerOuterZone() < 5.0) {
warnings.add(new ValidationResult.Warning(
"OUTER_ZONE_SUBOPTIMAL",
String.format(
"The outer zone covers %.2f printer pixels; ≥ 5 px is recommended for good print fidelity.",
m.pixelsPerOuterZone()),
ValidationResult.Warning.Severity.WARNING));
}
if (m.numberOfZones() < 5) {
warnings.add(new ValidationResult.Warning(
"FEW_ZONES",
String.format(
"Only %d Fresnel zones fit in the aperture; focus quality will be poor.",
m.numberOfZones()),
ValidationResult.Warning.Severity.WARNING));
}
return new ValidationResult(valid, warnings, m);
}
/**
* Estimate the focal length at a different wavelength (paraxial diffractive approximation,
* f(λ) ∝ 1/λ).
*/
public static double focalLengthAtWavelength(double designFocalMm, double designWavelengthNm,
double otherWavelengthNm) {
if (otherWavelengthNm <= 0) throw new IllegalArgumentException("wavelength must be > 0");
return designFocalMm * (designWavelengthNm / otherWavelengthNm);
}
/**
* Defocus blur diameter for a wall at distance {@code zMm} when the design focal length is
* {@code fMm} and the aperture is {@code dMm}: {@code c ≈ D·|f-z|/f}.
*/
public static double defocusBlurMm(double apertureDiameterMm, double focalLengthMm, double wallDistanceMm) {
return apertureDiameterMm * Math.abs(focalLengthMm - wallDistanceMm) / focalLengthMm;
}
}